diff --git a/.ai/instructions.md b/.ai/instructions.md index 6c002f9617..681829bae6 100644 --- a/.ai/instructions.md +++ b/.ai/instructions.md @@ -9,7 +9,7 @@ This document provides essential context for AI models interacting with this pro ## 2. Core Technologies & Stack -* **Languages:** Python (>=3.10), C++ (gnu++20) +* **Languages:** Python (>=3.11), C++ (gnu++20) * **Frameworks & Runtimes:** PlatformIO, Arduino, ESP-IDF. * **Build Systems:** PlatformIO is the primary build system. CMake is used as an alternative. * **Configuration:** YAML. @@ -38,7 +38,7 @@ This document provides essential context for AI models interacting with this pro 5. **Dashboard** (`esphome/dashboard/`): A web-based interface for device configuration, management, and OTA updates. * **Platform Support:** - 1. **ESP32** (`components/esp32/`): Espressif ESP32 family. Supports multiple variants (S2, S3, C3, etc.) and both IDF and Arduino frameworks. + 1. **ESP32** (`components/esp32/`): Espressif ESP32 family. Supports multiple variants (Original, C2, C3, C5, C6, H2, P4, S2, S3) with ESP-IDF framework. Arduino framework supports only a subset of the variants (Original, C3, S2, S3). 2. **ESP8266** (`components/esp8266/`): Espressif ESP8266. Arduino framework only, with memory constraints. 3. **RP2040** (`components/rp2040/`): Raspberry Pi Pico/RP2040. Arduino framework with PIO (Programmable I/O) support. 4. **LibreTiny** (`components/libretiny/`): Realtek and Beken chips. Supports multiple chip families and auto-generated components. @@ -51,7 +51,79 @@ This document provides essential context for AI models interacting with this pro * **Naming Conventions:** * **Python:** Follows PEP 8. Use clear, descriptive names following snake_case. - * **C++:** Follows the Google C++ Style Guide. + * **C++:** Follows the Google C++ Style Guide with these specifics (following clang-tidy conventions): + - Function, method, and variable names: `lower_snake_case` + - Class/struct/enum names: `UpperCamelCase` + - Top-level constants (global/namespace scope): `UPPER_SNAKE_CASE` + - Function-local constants: `lower_snake_case` + - Protected/private fields: `lower_snake_case_with_trailing_underscore_` + - Favor descriptive names over abbreviations + +* **C++ Field Visibility:** + * **Prefer `protected`:** Use `protected` for most class fields to enable extensibility and testing. Fields should be `lower_snake_case_with_trailing_underscore_`. + * **Use `private` for safety-critical cases:** Use `private` visibility when direct field access could introduce bugs or violate invariants: + 1. **Pointer lifetime issues:** When setters validate and store pointers from known lists to prevent dangling references. + ```cpp + // Helper to find matching string in vector and return its pointer + inline const char *vector_find(const std::vector &vec, const char *value) { + for (const char *item : vec) { + if (strcmp(item, value) == 0) + return item; + } + return nullptr; + } + + class ClimateDevice { + public: + void set_custom_fan_modes(std::initializer_list modes) { + this->custom_fan_modes_ = modes; + this->active_custom_fan_mode_ = nullptr; // Reset when modes change + } + bool set_custom_fan_mode(const char *mode) { + // Find mode in supported list and store that pointer (not the input pointer) + const char *validated_mode = vector_find(this->custom_fan_modes_, mode); + if (validated_mode != nullptr) { + this->active_custom_fan_mode_ = validated_mode; + return true; + } + return false; + } + private: + std::vector custom_fan_modes_; // Pointers to string literals in flash + const char *active_custom_fan_mode_{nullptr}; // Must point to entry in custom_fan_modes_ + }; + ``` + 2. **Invariant coupling:** When multiple fields must remain synchronized to prevent buffer overflows or data corruption. + ```cpp + class Buffer { + public: + void resize(size_t new_size) { + auto new_data = std::make_unique(new_size); + if (this->data_) { + std::memcpy(new_data.get(), this->data_.get(), std::min(this->size_, new_size)); + } + this->data_ = std::move(new_data); + this->size_ = new_size; // Must stay in sync with data_ + } + private: + std::unique_ptr data_; + size_t size_{0}; // Must match allocated size of data_ + }; + ``` + 3. **Resource management:** When setters perform cleanup or registration operations that derived classes might skip. + * **Provide `protected` accessor methods:** When derived classes need controlled access to `private` members. + +* **C++ Preprocessor Directives:** + * **Avoid `#define` for constants:** Using `#define` for constants is discouraged and should be replaced with `const` variables or enums. + * **Use `#define` only for:** + - Conditional compilation (`#ifdef`, `#ifndef`) + - Compile-time sizes calculated during Python code generation (e.g., configuring `std::array` or `StaticVector` dimensions via `cg.add_define()`) + +* **C++ Additional Conventions:** + * **Member access:** Prefix all class member access with `this->` (e.g., `this->value_` not `value_`) + * **Indentation:** Use spaces (two per indentation level), not tabs + * **Type aliases:** Prefer `using type_t = int;` over `typedef int type_t;` + * **Line length:** Wrap lines at no more than 120 characters * **Component Structure:** * **Standard Files:** @@ -60,7 +132,7 @@ This document provides essential context for AI models interacting with this pro ├── __init__.py # Component configuration schema and code generation ├── [component].h # C++ header file (if needed) ├── [component].cpp # C++ implementation (if needed) - └── [platform]/ # Platform-specific implementations + └── [platform]/ # Platform-specific implementations ├── __init__.py # Platform-specific configuration ├── [platform].h # Platform C++ header └── [platform].cpp # Platform C++ implementation @@ -100,8 +172,7 @@ This document provides essential context for AI models interacting with this pro * **C++ Class Pattern:** ```cpp - namespace esphome { - namespace my_component { + namespace esphome::my_component { class MyComponent : public Component { public: @@ -117,8 +188,7 @@ This document provides essential context for AI models interacting with this pro int param_{0}; }; - } // namespace my_component - } // namespace esphome + } // namespace esphome::my_component ``` * **Common Component Examples:** @@ -150,7 +220,8 @@ This document provides essential context for AI models interacting with this pro * **Configuration Validation:** * **Common Validators:** `cv.int_`, `cv.float_`, `cv.string`, `cv.boolean`, `cv.int_range(min=0, max=100)`, `cv.positive_int`, `cv.percentage`. * **Complex Validation:** `cv.All(cv.string, cv.Length(min=1, max=50))`, `cv.Any(cv.int_, cv.string)`. - * **Platform-Specific:** `cv.only_on(["esp32", "esp8266"])`, `cv.only_with_arduino`. + * **Platform-Specific:** `cv.only_on(["esp32", "esp8266"])`, `esp32.only_on_variant(...)`, `cv.only_on_esp32`, `cv.only_on_esp8266`, `cv.only_on_rp2040`. + * **Framework-Specific:** `cv.only_with_framework(...)`, `cv.only_with_arduino`, `cv.only_with_esp_idf`. * **Schema Extensions:** ```python CONFIG_SCHEMA = cv.Schema({ ... }) @@ -185,6 +256,11 @@ This document provides essential context for AI models interacting with this pro └── components/[component]/ # Component-specific tests ``` Run them using `script/test_build_components`. Use `-c ` to test specific components and `-t ` for specific platforms. + * **Testing All Components Together:** To verify that all components can be tested together without ID conflicts or configuration issues, use: + ```bash + ./script/test_component_grouping.py -e config --all + ``` + This tests all components in a single build to catch conflicts that might not appear when testing components individually. Use `-e config` for fast configuration validation, or `-e compile` for full compilation testing. * **Debugging and Troubleshooting:** * **Debug Tools:** - `esphome config .yaml` to validate configuration. @@ -215,6 +291,146 @@ This document provides essential context for AI models interacting with this pro * **Component Development:** Keep dependencies minimal, provide clear error messages, and write comprehensive docstrings and tests. * **Code Generation:** Generate minimal and efficient C++ code. Validate all user inputs thoroughly. Support multiple platform variations. * **Configuration Design:** Aim for simplicity with sensible defaults, while allowing for advanced customization. + * **Embedded Systems Optimization:** ESPHome targets resource-constrained microcontrollers. Be mindful of flash size and RAM usage. + + **STL Container Guidelines:** + + ESPHome runs on embedded systems with limited resources. Choose containers carefully: + + 1. **Compile-time-known sizes:** Use `std::array` instead of `std::vector` when size is known at compile time. + ```cpp + // Bad - generates STL realloc code + std::vector values; + + // Good - no dynamic allocation + std::array values; + ``` + Use `cg.add_define("MAX_VALUES", count)` to set the size from Python configuration. + + **For byte buffers:** Avoid `std::vector` unless the buffer needs to grow. Use `std::unique_ptr` instead. + + > **Note:** `std::unique_ptr` does **not** provide bounds checking or iterator support like `std::vector`. Use it only when you do not need these features and want minimal overhead. + + ```cpp + // Bad - STL overhead for simple byte buffer + std::vector buffer; + buffer.resize(256); + + // Good - minimal overhead, single allocation + std::unique_ptr buffer = std::make_unique(256); + // Or if size is constant: + std::array buffer; + ``` + + 2. **Compile-time-known fixed sizes with vector-like API:** Use `StaticVector` from `esphome/core/helpers.h` for fixed-size stack allocation with `push_back()` interface. + ```cpp + // Bad - generates STL realloc code (_M_realloc_insert) + std::vector services; + services.reserve(5); // Still includes reallocation machinery + + // Good - compile-time fixed size, stack allocated, no reallocation machinery + StaticVector services; // Allocates all MAX_SERVICES on stack + services.push_back(record1); // Tracks count but all slots allocated + ``` + Use `cg.add_define("MAX_SERVICES", count)` to set the size from Python configuration. + Like `std::array` but with vector-like API (`push_back()`, `size()`) and no STL reallocation code. + + 3. **Runtime-known sizes:** Use `FixedVector` from `esphome/core/helpers.h` when the size is only known at runtime initialization. + ```cpp + // Bad - generates STL realloc code (_M_realloc_insert) + std::vector txt_records; + txt_records.reserve(5); // Still includes reallocation machinery + + // Good - runtime size, single allocation, no reallocation machinery + FixedVector txt_records; + txt_records.init(record_count); // Initialize with exact size at runtime + ``` + **Benefits:** + - Eliminates `_M_realloc_insert`, `_M_default_append` template instantiations (saves 200-500 bytes per instance) + - Single allocation, no upper bound needed + - No reallocation overhead + - Compatible with protobuf code generation when using `[(fixed_vector) = true]` option + + 4. **Small datasets (1-16 elements):** Use `std::vector` or `std::array` with simple structs instead of `std::map`/`std::set`/`std::unordered_map`. + ```cpp + // Bad - 2KB+ overhead for red-black tree/hash table + std::map small_lookup; + std::unordered_map tiny_map; + + // Good - simple struct with linear search (std::vector is fine) + struct LookupEntry { + const char *key; + int value; + }; + std::vector small_lookup = { + {"key1", 10}, + {"key2", 20}, + {"key3", 30}, + }; + // Or std::array if size is compile-time constant: + // std::array small_lookup = {{ ... }}; + ``` + Linear search on small datasets (1-16 elements) is often faster than hashing/tree overhead, but this depends on lookup frequency and access patterns. For frequent lookups in hot code paths, the O(1) vs O(n) complexity difference may still matter even for small datasets. `std::vector` with simple structs is usually fine—it's the heavy containers (`map`, `set`, `unordered_map`) that should be avoided for small datasets unless profiling shows otherwise. + + 5. **Detection:** Look for these patterns in compiler output: + - Large code sections with STL symbols (vector, map, set) + - `alloc`, `realloc`, `dealloc` in symbol names + - `_M_realloc_insert`, `_M_default_append` (vector reallocation) + - Red-black tree code (`rb_tree`, `_Rb_tree`) + - Hash table infrastructure (`unordered_map`, `hash`) + + **When to optimize:** + - Core components (API, network, logger) + - Widely-used components (mdns, wifi, ble) + - Components causing flash size complaints + + **When not to optimize:** + - Single-use niche components + - Code where readability matters more than bytes + - Already using appropriate containers + + * **State Management:** Use `CORE.data` for component state that needs to persist during configuration generation. Avoid module-level mutable globals. + + **Bad Pattern (Module-Level Globals):** + ```python + # Don't do this - state persists between compilation runs + _component_state = [] + _use_feature = None + + def enable_feature(): + global _use_feature + _use_feature = True + ``` + + **Good Pattern (CORE.data with Helpers):** + ```python + from esphome.core import CORE + + # Keys for CORE.data storage + COMPONENT_STATE_KEY = "my_component_state" + USE_FEATURE_KEY = "my_component_use_feature" + + def _get_component_state() -> list: + """Get component state from CORE.data.""" + return CORE.data.setdefault(COMPONENT_STATE_KEY, []) + + def _get_use_feature() -> bool | None: + """Get feature flag from CORE.data.""" + return CORE.data.get(USE_FEATURE_KEY) + + def _set_use_feature(value: bool) -> None: + """Set feature flag in CORE.data.""" + CORE.data[USE_FEATURE_KEY] = value + + def enable_feature(): + _set_use_feature(True) + ``` + + **Why this matters:** + - Module-level globals persist between compilation runs if the dashboard doesn't fork/exec + - `CORE.data` automatically clears between runs + - Typed helper functions provide better IDE support and maintainability + - Encapsulation makes state management explicit and testable * **Security:** Be mindful of security when making changes to the API, web server, or any other network-related code. Do not hardcode secrets or keys. @@ -222,3 +438,45 @@ This document provides essential context for AI models interacting with this pro * **Python:** When adding a new Python dependency, add it to the appropriate `requirements*.txt` file and `pyproject.toml`. * **C++ / PlatformIO:** When adding a new C++ dependency, add it to `platformio.ini` and use `cg.add_library`. * **Build Flags:** Use `cg.add_build_flag(...)` to add compiler flags. + +## 8. Public API and Breaking Changes + +* **Public C++ API:** + * **Components**: Only documented features at [esphome.io](https://esphome.io) are public API. Undocumented `public` members are internal. + * **Core/Base Classes** (`esphome/core/`, `Component`, `Sensor`, etc.): All `public` members are public API. + * **Components with Global Accessors** (`global_api_server`, etc.): All `public` members are public API (except config setters). + +* **Public Python API:** + * All documented configuration options at [esphome.io](https://esphome.io) are public API. + * Python code in `esphome/core/` actively used by existing core components is considered stable API. + * Other Python code is internal unless explicitly documented for external component use. + +* **Breaking Changes Policy:** + * Aim for **6-month deprecation window** when possible + * Clean breaks allowed for: signature changes, deep refactorings, resource constraints + * Must document migration path in PR description (generates release notes) + * Blog post required for core/base class changes or significant architectural changes + * Full details: https://developers.esphome.io/contributing/code/#public-api-and-breaking-changes + +* **Breaking Change Checklist:** + - [ ] Clear justification (RAM/flash savings, architectural improvement) + - [ ] Explored non-breaking alternatives + - [ ] Added deprecation warnings if possible (use `ESPDEPRECATED` macro for C++) + - [ ] Documented migration path in PR description with before/after examples + - [ ] Updated all internal usage and esphome-docs + - [ ] Tested backward compatibility during deprecation period + +* **Deprecation Pattern (C++):** + ```cpp + // Remove before 2026.6.0 + ESPDEPRECATED("Use new_method() instead. Removed in 2026.6.0", "2025.12.0") + void old_method() { this->new_method(); } + ``` + +* **Deprecation Pattern (Python):** + ```python + # Remove before 2026.6.0 + if CONF_OLD_KEY in config: + _LOGGER.warning(f"'{CONF_OLD_KEY}' deprecated, use '{CONF_NEW_KEY}'. Removed in 2026.6.0") + config[CONF_NEW_KEY] = config.pop(CONF_OLD_KEY) # Auto-migrate + ``` diff --git a/.clang-tidy.hash b/.clang-tidy.hash index 30cf982649..3ade00f0cd 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -6af8b429b94191fe8e239fcb3b73f7982d0266cb5b05ffbc81edaeac1bc8c273 +3d46b63015d761c85ca9cb77ab79a389509e5776701fb22aed16e7b79d432c0c diff --git a/.coveragerc b/.coveragerc index f23592be24..c15e79a31b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,4 +1,5 @@ [run] omit = esphome/components/* + esphome/analyze_memory/* tests/integration/* diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 28437e6302..41dd02458e 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -7,6 +7,7 @@ - [ ] Bugfix (non-breaking change which fixes an issue) - [ ] 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 diff --git a/.github/actions/build-image/action.yaml b/.github/actions/build-image/action.yaml index 403b9d8c2a..9c7f051e05 100644 --- a/.github/actions/build-image/action.yaml +++ b/.github/actions/build-image/action.yaml @@ -47,7 +47,7 @@ runs: - name: Build and push to ghcr by digest id: build-ghcr - uses: docker/build-push-action@v6.18.0 + uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 env: DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_RECORD_UPLOAD: false @@ -73,7 +73,7 @@ runs: - name: Build and push to dockerhub by digest id: build-dockerhub - uses: docker/build-push-action@v6.18.0 + uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 env: DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_RECORD_UPLOAD: false diff --git a/.github/actions/restore-python/action.yml b/.github/actions/restore-python/action.yml index 3a7b301b60..c4ac3d1a9e 100644 --- a/.github/actions/restore-python/action.yml +++ b/.github/actions/restore-python/action.yml @@ -17,12 +17,12 @@ runs: steps: - name: Set up Python ${{ inputs.python-version }} id: python - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: ${{ inputs.python-version }} - name: Restore Python virtual environment id: cache-venv - uses: actions/cache/restore@v4.2.3 + uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: venv # yamllint disable-line rule:line-length diff --git a/.github/workflows/auto-label-pr.yml b/.github/workflows/auto-label-pr.yml index 36086579fc..d09072d814 100644 --- a/.github/workflows/auto-label-pr.yml +++ b/.github/workflows/auto-label-pr.yml @@ -22,17 +22,17 @@ jobs: if: github.event.action != 'labeled' || github.event.sender.type != 'Bot' steps: - name: Checkout - uses: actions/checkout@v4.2.2 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Generate a token id: generate-token - uses: actions/create-github-app-token@v2 + uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2 with: app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }} private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} - name: Auto Label PR - uses: actions/github-script@v7.0.1 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: github-token: ${{ steps.generate-token.outputs.token }} script: | @@ -53,6 +53,7 @@ jobs: 'new-target-platform', 'merging-to-release', 'merging-to-beta', + 'chained-pr', 'core', 'small-pr', 'dashboard', @@ -67,6 +68,7 @@ jobs: 'bugfix', 'new-feature', 'breaking-change', + 'developer-breaking-change', 'code-quality' ]; @@ -105,7 +107,9 @@ jobs: // Calculate data from PR files const changedFiles = prFiles.map(file => file.filename); - const totalChanges = prFiles.reduce((sum, file) => sum + (file.additions || 0) + (file.deletions || 0), 0); + const totalAdditions = prFiles.reduce((sum, file) => sum + (file.additions || 0), 0); + const totalDeletions = prFiles.reduce((sum, file) => sum + (file.deletions || 0), 0); + const totalChanges = totalAdditions + totalDeletions; console.log('Current labels:', currentLabels.join(', ')); console.log('Changed files:', changedFiles.length); @@ -138,6 +142,8 @@ jobs: labels.add('merging-to-release'); } else if (baseRef === 'beta') { labels.add('merging-to-beta'); + } else if (baseRef !== 'dev') { + labels.add('chained-pr'); } return labels; @@ -231,16 +237,21 @@ jobs: // Strategy: PR size detection async function detectPRSize() { const labels = new Set(); - const testChanges = prFiles - .filter(file => file.filename.startsWith('tests/')) - .reduce((sum, file) => sum + (file.additions || 0) + (file.deletions || 0), 0); - - const nonTestChanges = totalChanges - testChanges; if (totalChanges <= SMALL_PR_THRESHOLD) { labels.add('small-pr'); + return labels; } + const testAdditions = prFiles + .filter(file => file.filename.startsWith('tests/')) + .reduce((sum, file) => sum + (file.additions || 0), 0); + const testDeletions = prFiles + .filter(file => file.filename.startsWith('tests/')) + .reduce((sum, file) => sum + (file.deletions || 0), 0); + + const nonTestChanges = (totalAdditions - testAdditions) - (totalDeletions - testDeletions); + // Don't add too-big if mega-pr label is already present if (nonTestChanges > TOO_BIG_THRESHOLD && !isMegaPR) { labels.add('too-big'); @@ -357,6 +368,7 @@ jobs: { pattern: /- \[x\] Bugfix \(non-breaking change which fixes an issue\)/i, label: 'bugfix' }, { pattern: /- \[x\] New feature \(non-breaking change which adds functionality\)/i, label: 'new-feature' }, { pattern: /- \[x\] Breaking change \(fix or feature that would cause existing functionality to not work as expected\)/i, label: 'breaking-change' }, + { pattern: /- \[x\] Developer breaking change \(an API change that could break external components\)/i, label: 'developer-breaking-change' }, { pattern: /- \[x\] Code quality improvements to existing code or addition of tests/i, label: 'code-quality' } ]; @@ -375,7 +387,7 @@ jobs: const labels = new Set(); // Check for missing tests - if ((allLabels.has('new-component') || allLabels.has('new-platform')) && !allLabels.has('has-tests')) { + if ((allLabels.has('new-component') || allLabels.has('new-platform') || allLabels.has('new-feature')) && !allLabels.has('has-tests')) { labels.add('needs-tests'); } @@ -406,26 +418,29 @@ jobs: } // Generate review messages - function generateReviewMessages(finalLabels) { + function generateReviewMessages(finalLabels, originalLabelCount) { const messages = []; const prAuthor = context.payload.pull_request.user.login; // Too big message if (finalLabels.includes('too-big')) { - const testChanges = prFiles + const testAdditions = prFiles .filter(file => file.filename.startsWith('tests/')) - .reduce((sum, file) => sum + (file.additions || 0) + (file.deletions || 0), 0); - const nonTestChanges = totalChanges - testChanges; + .reduce((sum, file) => sum + (file.additions || 0), 0); + const testDeletions = prFiles + .filter(file => file.filename.startsWith('tests/')) + .reduce((sum, file) => sum + (file.deletions || 0), 0); + const nonTestChanges = (totalAdditions - testAdditions) - (totalDeletions - testDeletions); - const tooManyLabels = finalLabels.length > MAX_LABELS; + const tooManyLabels = originalLabelCount > MAX_LABELS; const tooManyChanges = nonTestChanges > TOO_BIG_THRESHOLD; let message = `${TOO_BIG_MARKER}\n### 📦 Pull Request Size\n\n`; if (tooManyLabels && tooManyChanges) { - message += `This PR is too large with ${nonTestChanges} line changes (excluding tests) and affects ${finalLabels.length} different components/areas.`; + message += `This PR is too large with ${nonTestChanges} line changes (excluding tests) and affects ${originalLabelCount} different components/areas.`; } else if (tooManyLabels) { - message += `This PR affects ${finalLabels.length} different components/areas.`; + message += `This PR affects ${originalLabelCount} different components/areas.`; } else { message += `This PR is too large with ${nonTestChanges} line changes (excluding tests).`; } @@ -453,8 +468,8 @@ jobs: } // Handle reviews - async function handleReviews(finalLabels) { - const reviewMessages = generateReviewMessages(finalLabels); + async function handleReviews(finalLabels, originalLabelCount) { + const reviewMessages = generateReviewMessages(finalLabels, originalLabelCount); const hasReviewableLabels = finalLabels.some(label => ['too-big', 'needs-codeowners'].includes(label) ); @@ -518,8 +533,8 @@ jobs: const apiData = await fetchApiData(); const baseRef = context.payload.pull_request.base.ref; - // Early exit for non-dev branches - if (baseRef !== 'dev') { + // Early exit for release and beta branches only + if (baseRef === 'release' || baseRef === 'beta') { const branchLabels = await detectMergeBranch(); const finalLabels = Array.from(branchLabels); @@ -614,6 +629,7 @@ jobs: // Handle too many labels (only for non-mega PRs) const tooManyLabels = finalLabels.length > MAX_LABELS; + const originalLabelCount = finalLabels.length; if (tooManyLabels && !isMegaPR && !finalLabels.includes('too-big')) { finalLabels = ['too-big']; @@ -622,7 +638,7 @@ jobs: console.log('Computed labels:', finalLabels.join(', ')); // Handle reviews - await handleReviews(finalLabels); + await handleReviews(finalLabels, originalLabelCount); // Apply labels if (finalLabels.length > 0) { diff --git a/.github/workflows/ci-api-proto.yml b/.github/workflows/ci-api-proto.yml index f51bd84186..2bee5ed211 100644 --- a/.github/workflows/ci-api-proto.yml +++ b/.github/workflows/ci-api-proto.yml @@ -21,9 +21,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4.2.2 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Set up Python - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: "3.11" @@ -47,7 +47,7 @@ jobs: fi - if: failure() name: Review PR - uses: actions/github-script@v7.0.1 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: script: | await github.rest.pulls.createReview({ @@ -62,7 +62,7 @@ jobs: run: git diff - if: failure() name: Archive artifacts - uses: actions/upload-artifact@v4.6.2 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: generated-proto-files path: | @@ -70,7 +70,7 @@ jobs: esphome/components/api/api_pb2_service.* - if: success() name: Dismiss review - uses: actions/github-script@v7.0.1 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: script: | let reviews = await github.rest.pulls.listReviews({ diff --git a/.github/workflows/ci-clang-tidy-hash.yml b/.github/workflows/ci-clang-tidy-hash.yml index 1c7a62e40b..1826ed27cf 100644 --- a/.github/workflows/ci-clang-tidy-hash.yml +++ b/.github/workflows/ci-clang-tidy-hash.yml @@ -6,6 +6,7 @@ on: - ".clang-tidy" - "platformio.ini" - "requirements_dev.txt" + - "sdkconfig.defaults" - ".clang-tidy.hash" - "script/clang_tidy_hash.py" - ".github/workflows/ci-clang-tidy-hash.yml" @@ -20,10 +21,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4.2.2 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Set up Python - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: "3.11" @@ -41,7 +42,7 @@ jobs: - if: failure() name: Request changes - uses: actions/github-script@v7.0.1 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: script: | await github.rest.pulls.createReview({ @@ -54,7 +55,7 @@ jobs: - if: success() name: Dismiss review - uses: actions/github-script@v7.0.1 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: script: | let reviews = await github.rest.pulls.listReviews({ diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index d6dac66359..c76d9cf2a5 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -43,13 +43,13 @@ jobs: - "docker" # - "lint" steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Set up Python - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: "3.11" - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.11.1 + uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 - name: Set TAG run: | diff --git a/.github/workflows/ci-memory-impact-comment.yml b/.github/workflows/ci-memory-impact-comment.yml new file mode 100644 index 0000000000..6ca58e252e --- /dev/null +++ b/.github/workflows/ci-memory-impact-comment.yml @@ -0,0 +1,111 @@ +--- +name: Memory Impact Comment (Forks) + +on: + workflow_run: + workflows: ["CI"] + types: [completed] + +permissions: + contents: read + pull-requests: write + actions: read + +jobs: + memory-impact-comment: + name: Post memory impact comment (fork PRs only) + runs-on: ubuntu-24.04 + # Only run for PRs from forks that had successful CI runs + if: > + github.event.workflow_run.event == 'pull_request' && + github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.head_repository.full_name != github.repository + env: + GH_TOKEN: ${{ github.token }} + steps: + - name: Get PR details + id: pr + run: | + # Get PR details by searching for PR with matching head SHA + # The workflow_run.pull_requests field is often empty for forks + # Use paginate to handle repos with many open PRs + head_sha="${{ github.event.workflow_run.head_sha }}" + pr_data=$(gh api --paginate "/repos/${{ github.repository }}/pulls" \ + --jq ".[] | select(.head.sha == \"$head_sha\") | {number: .number, base_ref: .base.ref}" \ + | head -n 1) + + if [ -z "$pr_data" ]; then + echo "No PR found for SHA $head_sha, skipping" + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + pr_number=$(echo "$pr_data" | jq -r '.number') + base_ref=$(echo "$pr_data" | jq -r '.base_ref') + + echo "pr_number=$pr_number" >> "$GITHUB_OUTPUT" + echo "base_ref=$base_ref" >> "$GITHUB_OUTPUT" + echo "Found PR #$pr_number targeting base branch: $base_ref" + + - name: Check out code from base repository + if: steps.pr.outputs.skip != 'true' + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + with: + # Always check out from the base repository (esphome/esphome), never from forks + # Use the PR's target branch to ensure we run trusted code from the main repo + repository: ${{ github.repository }} + ref: ${{ steps.pr.outputs.base_ref }} + + - name: Restore Python + if: steps.pr.outputs.skip != 'true' + uses: ./.github/actions/restore-python + with: + python-version: "3.11" + cache-key: ${{ hashFiles('.cache-key') }} + + - name: Download memory analysis artifacts + if: steps.pr.outputs.skip != 'true' + run: | + run_id="${{ github.event.workflow_run.id }}" + echo "Downloading artifacts from workflow run $run_id" + + mkdir -p memory-analysis + + # Download target analysis artifact + if gh run download --name "memory-analysis-target" --dir memory-analysis --repo "${{ github.repository }}" "$run_id"; then + echo "Downloaded memory-analysis-target artifact." + else + echo "No memory-analysis-target artifact found." + fi + + # Download PR analysis artifact + if gh run download --name "memory-analysis-pr" --dir memory-analysis --repo "${{ github.repository }}" "$run_id"; then + echo "Downloaded memory-analysis-pr artifact." + else + echo "No memory-analysis-pr artifact found." + fi + + - name: Check if artifacts exist + id: check + if: steps.pr.outputs.skip != 'true' + run: | + if [ -f ./memory-analysis/memory-analysis-target.json ] && [ -f ./memory-analysis/memory-analysis-pr.json ]; then + echo "found=true" >> "$GITHUB_OUTPUT" + else + echo "found=false" >> "$GITHUB_OUTPUT" + echo "Memory analysis artifacts not found, skipping comment" + fi + + - name: Post or update PR comment + if: steps.pr.outputs.skip != 'true' && steps.check.outputs.found == 'true' + env: + PR_NUMBER: ${{ steps.pr.outputs.pr_number }} + run: | + . venv/bin/activate + # Pass PR number and JSON file paths directly to Python script + # Let Python parse the JSON to avoid shell injection risks + # The script will validate and sanitize all inputs + python script/ci_memory_impact_comment.py \ + --pr-number "$PR_NUMBER" \ + --target-json ./memory-analysis/memory-analysis-target.json \ + --pr-json ./memory-analysis/memory-analysis-pr.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 992918a035..9cfc02d5cf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,18 +36,18 @@ jobs: cache-key: ${{ steps.cache-key.outputs.key }} steps: - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Generate cache-key id: cache-key run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v4.2.3 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: venv # yamllint disable-line rule:line-length @@ -70,7 +70,7 @@ jobs: if: needs.determine-jobs.outputs.python-linters == 'true' steps: - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Restore Python uses: ./.github/actions/restore-python with: @@ -91,7 +91,7 @@ jobs: - common steps: - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Restore Python uses: ./.github/actions/restore-python with: @@ -105,6 +105,7 @@ jobs: script/ci-custom.py script/build_codeowners.py --check script/build_language_schema.py --check + script/generate-esp32-boards.py --check pytest: name: Run pytest @@ -113,7 +114,6 @@ jobs: matrix: python-version: - "3.11" - - "3.12" - "3.13" os: - ubuntu-latest @@ -125,18 +125,14 @@ jobs: # version used for docker images on Windows and macOS - python-version: "3.13" os: windows-latest - - python-version: "3.12" - os: windows-latest - python-version: "3.13" os: macOS-latest - - python-version: "3.12" - os: macOS-latest runs-on: ${{ matrix.os }} needs: - common steps: - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Restore Python id: restore-python uses: ./.github/actions/restore-python @@ -156,12 +152,12 @@ jobs: . venv/bin/activate pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/ - name: Upload coverage to Codecov - uses: codecov/codecov-action@v5.4.3 + uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1 with: token: ${{ secrets.CODECOV_TOKEN }} - name: Save Python virtual environment cache if: github.ref == 'refs/heads/dev' - uses: actions/cache/save@v4.2.3 + uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: venv key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }} @@ -174,12 +170,20 @@ jobs: outputs: integration-tests: ${{ steps.determine.outputs.integration-tests }} clang-tidy: ${{ steps.determine.outputs.clang-tidy }} + clang-tidy-mode: ${{ steps.determine.outputs.clang-tidy-mode }} python-linters: ${{ steps.determine.outputs.python-linters }} changed-components: ${{ steps.determine.outputs.changed-components }} + changed-components-with-tests: ${{ steps.determine.outputs.changed-components-with-tests }} + directly-changed-components-with-tests: ${{ steps.determine.outputs.directly-changed-components-with-tests }} component-test-count: ${{ steps.determine.outputs.component-test-count }} + changed-cpp-file-count: ${{ steps.determine.outputs.changed-cpp-file-count }} + memory_impact: ${{ steps.determine.outputs.memory-impact }} + cpp-unit-tests-run-all: ${{ steps.determine.outputs.cpp-unit-tests-run-all }} + cpp-unit-tests-components: ${{ steps.determine.outputs.cpp-unit-tests-components }} + component-test-batches: ${{ steps.determine.outputs.component-test-batches }} steps: - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: # Fetch enough history to find the merge base fetch-depth: 2 @@ -188,6 +192,11 @@ jobs: with: python-version: ${{ env.DEFAULT_PYTHON }} cache-key: ${{ needs.common.outputs.cache-key }} + - name: Restore components graph cache + uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: .temp/components_graph.json + key: components-graph-${{ hashFiles('esphome/components/**/*.py') }} - name: Determine which tests to run id: determine env: @@ -201,9 +210,23 @@ jobs: # Extract individual fields echo "integration-tests=$(echo "$output" | jq -r '.integration_tests')" >> $GITHUB_OUTPUT echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT + echo "clang-tidy-mode=$(echo "$output" | jq -r '.clang_tidy_mode')" >> $GITHUB_OUTPUT echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT + echo "changed-components-with-tests=$(echo "$output" | jq -c '.changed_components_with_tests')" >> $GITHUB_OUTPUT + echo "directly-changed-components-with-tests=$(echo "$output" | jq -c '.directly_changed_components_with_tests')" >> $GITHUB_OUTPUT echo "component-test-count=$(echo "$output" | jq -r '.component_test_count')" >> $GITHUB_OUTPUT + echo "changed-cpp-file-count=$(echo "$output" | jq -r '.changed_cpp_file_count')" >> $GITHUB_OUTPUT + echo "memory-impact=$(echo "$output" | jq -c '.memory_impact')" >> $GITHUB_OUTPUT + echo "cpp-unit-tests-run-all=$(echo "$output" | jq -r '.cpp_unit_tests_run_all')" >> $GITHUB_OUTPUT + echo "cpp-unit-tests-components=$(echo "$output" | jq -c '.cpp_unit_tests_components')" >> $GITHUB_OUTPUT + 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@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: .temp/components_graph.json + key: components-graph-${{ hashFiles('esphome/components/**/*.py') }} integration-tests: name: Run integration tests @@ -214,15 +237,15 @@ jobs: if: needs.determine-jobs.outputs.integration-tests == 'true' steps: - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Set up Python 3.13 id: python - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: "3.13" - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v4.2.3 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }} @@ -241,7 +264,34 @@ jobs: . venv/bin/activate pytest -vv --no-cov --tb=native -n auto tests/integration/ - clang-tidy: + cpp-unit-tests: + name: Run C++ unit tests + runs-on: ubuntu-24.04 + needs: + - common + - determine-jobs + if: github.event_name == 'pull_request' && (needs.determine-jobs.outputs.cpp-unit-tests-run-all == 'true' || needs.determine-jobs.outputs.cpp-unit-tests-components != '[]') + steps: + - name: Check out code from GitHub + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + + - name: Restore Python + uses: ./.github/actions/restore-python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + cache-key: ${{ needs.common.outputs.cache-key }} + + - name: Run cpp_unit_test.py + run: | + . venv/bin/activate + if [ "${{ needs.determine-jobs.outputs.cpp-unit-tests-run-all }}" = "true" ]; then + script/cpp_unit_test.py --all + else + ARGS=$(echo '${{ needs.determine-jobs.outputs.cpp-unit-tests-components }}' | jq -r '.[] | @sh' | xargs) + script/cpp_unit_test.py $ARGS + fi + + clang-tidy-single: name: ${{ matrix.name }} runs-on: ubuntu-24.04 needs: @@ -259,22 +309,6 @@ jobs: name: Run script/clang-tidy for ESP8266 options: --environment esp8266-arduino-tidy --grep USE_ESP8266 pio_cache_key: tidyesp8266 - - id: clang-tidy - name: Run script/clang-tidy for ESP32 Arduino 1/4 - options: --environment esp32-arduino-tidy --split-num 4 --split-at 1 - pio_cache_key: tidyesp32 - - id: clang-tidy - name: Run script/clang-tidy for ESP32 Arduino 2/4 - options: --environment esp32-arduino-tidy --split-num 4 --split-at 2 - pio_cache_key: tidyesp32 - - id: clang-tidy - name: Run script/clang-tidy for ESP32 Arduino 3/4 - options: --environment esp32-arduino-tidy --split-num 4 --split-at 3 - pio_cache_key: tidyesp32 - - id: clang-tidy - name: Run script/clang-tidy for ESP32 Arduino 4/4 - options: --environment esp32-arduino-tidy --split-num 4 --split-at 4 - pio_cache_key: tidyesp32 - id: clang-tidy name: Run script/clang-tidy for ESP32 IDF options: --environment esp32-idf-tidy --grep USE_ESP_IDF @@ -287,7 +321,7 @@ jobs: steps: - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: # Need history for HEAD~1 to work for checking changed files fetch-depth: 2 @@ -300,14 +334,14 @@ jobs: - name: Cache platformio if: github.ref == 'refs/heads/dev' - uses: actions/cache@v4.2.3 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 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@v4.2.3 + uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: ~/.platformio key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }} @@ -355,122 +389,556 @@ jobs: # yamllint disable-line rule:line-length if: always() - test-build-components: - name: Component test ${{ matrix.file }} + clang-tidy-nosplit: + name: Run script/clang-tidy for ESP32 Arduino runs-on: ubuntu-24.04 needs: - common - determine-jobs - if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0 && fromJSON(needs.determine-jobs.outputs.component-test-count) < 100 + if: needs.determine-jobs.outputs.clang-tidy-mode == 'nosplit' + env: + GH_TOKEN: ${{ github.token }} + steps: + - name: Check out code from GitHub + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + with: + # Need history for HEAD~1 to work for checking changed files + fetch-depth: 2 + + - name: Restore Python + uses: ./.github/actions/restore-python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + cache-key: ${{ needs.common.outputs.cache-key }} + + - name: Cache platformio + if: github.ref == 'refs/heads/dev' + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: ~/.platformio + key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }} + + - name: Cache platformio + if: github.ref != 'refs/heads/dev' + uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: ~/.platformio + key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }} + + - name: Register problem matchers + run: | + echo "::add-matcher::.github/workflows/matchers/gcc.json" + echo "::add-matcher::.github/workflows/matchers/clang-tidy.json" + + - name: Check if full clang-tidy scan needed + id: check_full_scan + run: | + . venv/bin/activate + if python script/clang_tidy_hash.py --check; then + echo "full_scan=true" >> $GITHUB_OUTPUT + echo "reason=hash_changed" >> $GITHUB_OUTPUT + else + echo "full_scan=false" >> $GITHUB_OUTPUT + echo "reason=normal" >> $GITHUB_OUTPUT + fi + + - name: Run clang-tidy + run: | + . venv/bin/activate + if [ "${{ steps.check_full_scan.outputs.full_scan }}" = "true" ]; then + echo "Running FULL clang-tidy scan (hash changed)" + script/clang-tidy --all-headers --fix --environment esp32-arduino-tidy + else + echo "Running clang-tidy on changed files only" + script/clang-tidy --all-headers --fix --changed --environment esp32-arduino-tidy + fi + env: + # Also cache libdeps, store them in a ~/.platformio subfolder + PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps + + - name: Suggested changes + run: script/ci-suggest-changes + if: always() + + clang-tidy-split: + name: ${{ matrix.name }} + runs-on: ubuntu-24.04 + needs: + - common + - determine-jobs + if: needs.determine-jobs.outputs.clang-tidy-mode == 'split' + env: + GH_TOKEN: ${{ github.token }} strategy: fail-fast: false max-parallel: 2 matrix: - file: ${{ fromJson(needs.determine-jobs.outputs.changed-components) }} - steps: - - name: Install dependencies - run: | - sudo apt-get update - sudo apt-get install libsdl2-dev + include: + - id: clang-tidy + name: Run script/clang-tidy for ESP32 Arduino 1/4 + options: --environment esp32-arduino-tidy --split-num 4 --split-at 1 + - id: clang-tidy + name: Run script/clang-tidy for ESP32 Arduino 2/4 + options: --environment esp32-arduino-tidy --split-num 4 --split-at 2 + - id: clang-tidy + name: Run script/clang-tidy for ESP32 Arduino 3/4 + options: --environment esp32-arduino-tidy --split-num 4 --split-at 3 + - id: clang-tidy + name: Run script/clang-tidy for ESP32 Arduino 4/4 + options: --environment esp32-arduino-tidy --split-num 4 --split-at 4 + steps: - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + with: + # Need history for HEAD~1 to work for checking changed files + fetch-depth: 2 + - name: Restore Python uses: ./.github/actions/restore-python with: python-version: ${{ env.DEFAULT_PYTHON }} cache-key: ${{ needs.common.outputs.cache-key }} - - name: test_build_components -e config -c ${{ matrix.file }} - run: | - . venv/bin/activate - ./script/test_build_components -e config -c ${{ matrix.file }} - - name: test_build_components -e compile -c ${{ matrix.file }} - run: | - . venv/bin/activate - ./script/test_build_components -e compile -c ${{ matrix.file }} - test-build-components-splitter: - name: Split components for testing into 20 groups maximum - runs-on: ubuntu-24.04 - needs: - - common - - determine-jobs - if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) >= 100 - outputs: - matrix: ${{ steps.split.outputs.components }} - steps: - - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 - - name: Split components into 20 groups - id: split + - name: Cache platformio + if: github.ref == 'refs/heads/dev' + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: ~/.platformio + key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }} + + - name: Cache platformio + if: github.ref != 'refs/heads/dev' + uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: ~/.platformio + key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }} + + - name: Register problem matchers run: | - components=$(echo '${{ needs.determine-jobs.outputs.changed-components }}' | jq -c '.[]' | shuf | jq -s -c '[_nwise(20) | join(" ")]') - echo "components=$components" >> $GITHUB_OUTPUT + echo "::add-matcher::.github/workflows/matchers/gcc.json" + echo "::add-matcher::.github/workflows/matchers/clang-tidy.json" + + - name: Check if full clang-tidy scan needed + id: check_full_scan + run: | + . venv/bin/activate + if python script/clang_tidy_hash.py --check; then + echo "full_scan=true" >> $GITHUB_OUTPUT + echo "reason=hash_changed" >> $GITHUB_OUTPUT + else + echo "full_scan=false" >> $GITHUB_OUTPUT + echo "reason=normal" >> $GITHUB_OUTPUT + fi + + - name: Run clang-tidy + run: | + . venv/bin/activate + if [ "${{ steps.check_full_scan.outputs.full_scan }}" = "true" ]; then + echo "Running FULL clang-tidy scan (hash changed)" + script/clang-tidy --all-headers --fix ${{ matrix.options }} + else + echo "Running clang-tidy on changed files only" + script/clang-tidy --all-headers --fix --changed ${{ matrix.options }} + fi + env: + # Also cache libdeps, store them in a ~/.platformio subfolder + PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps + + - name: Suggested changes + run: script/ci-suggest-changes + if: always() test-build-components-split: - name: Test split components + name: Test components batch (${{ matrix.components }}) runs-on: ubuntu-24.04 needs: - common - determine-jobs - - test-build-components-splitter - if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) >= 100 + if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0 strategy: fail-fast: false - max-parallel: 4 + max-parallel: ${{ (startsWith(github.base_ref, 'beta') || startsWith(github.base_ref, 'release')) && 8 || 4 }} matrix: - components: ${{ fromJson(needs.test-build-components-splitter.outputs.matrix) }} + components: ${{ fromJson(needs.determine-jobs.outputs.component-test-batches) }} steps: + - name: Show disk space + run: | + echo "Available disk space:" + df -h + - name: List components run: echo ${{ matrix.components }} - - name: Install dependencies - run: | - sudo apt-get update - sudo apt-get install libsdl2-dev + - name: Cache apt packages + uses: awalsh128/cache-apt-pkgs-action@acb598e5ddbc6f68a970c5da0688d2f3a9f04d05 # v1.5.3 + with: + packages: libsdl2-dev + version: 1.0 - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Restore Python uses: ./.github/actions/restore-python with: python-version: ${{ env.DEFAULT_PYTHON }} cache-key: ${{ needs.common.outputs.cache-key }} - - name: Validate config + - name: Validate and compile components with intelligent grouping run: | . venv/bin/activate - for component in ${{ matrix.components }}; do - ./script/test_build_components -e config -c $component - done - - name: Compile config - run: | - . venv/bin/activate - mkdir build_cache - export PLATFORMIO_BUILD_CACHE_DIR=$PWD/build_cache - for component in ${{ matrix.components }}; do - ./script/test_build_components -e compile -c $component - done + + # Check if /mnt has more free space than / before bind mounting + # Extract available space in KB for comparison + root_avail=$(df -k / | awk 'NR==2 {print $4}') + mnt_avail=$(df -k /mnt 2>/dev/null | awk 'NR==2 {print $4}') + + echo "Available space: / has ${root_avail}KB, /mnt has ${mnt_avail}KB" + + # Only use /mnt if it has more space than / + if [ -n "$mnt_avail" ] && [ "$mnt_avail" -gt "$root_avail" ]; then + echo "Using /mnt for build files (more space available)" + # Bind mount PlatformIO directory to /mnt (tools, packages, build cache all go there) + sudo mkdir -p /mnt/platformio + sudo chown $USER:$USER /mnt/platformio + mkdir -p ~/.platformio + sudo mount --bind /mnt/platformio ~/.platformio + + # Bind mount test build directory to /mnt + sudo mkdir -p /mnt/test_build_components_build + sudo chown $USER:$USER /mnt/test_build_components_build + mkdir -p tests/test_build_components/build + sudo mount --bind /mnt/test_build_components_build tests/test_build_components/build + else + echo "Using / for build files (more space available than /mnt or /mnt unavailable)" + fi + + # Convert space-separated components to comma-separated for Python script + components_csv=$(echo "${{ matrix.components }}" | tr ' ' ',') + + # Only isolate directly changed components when targeting dev branch + # For beta/release branches, group everything for faster CI + # + # WHY ISOLATE DIRECTLY CHANGED COMPONENTS? + # - Isolated tests run WITHOUT --testing-mode, enabling full validation + # - This catches pin conflicts and other issues in directly changed code + # - Grouped tests use --testing-mode to allow config merging (disables some checks) + # - Dependencies are safe to group since they weren't modified in this PR + if [[ "${{ github.base_ref }}" == beta* ]] || [[ "${{ github.base_ref }}" == release* ]]; then + directly_changed_csv="" + echo "Testing components: $components_csv" + echo "Target branch: ${{ github.base_ref }} - grouping all components" + else + directly_changed_csv=$(echo '${{ needs.determine-jobs.outputs.directly-changed-components-with-tests }}' | jq -r 'join(",")') + echo "Testing components: $components_csv" + echo "Target branch: ${{ github.base_ref }} - isolating directly changed components: $directly_changed_csv" + fi + echo "" + + # Show disk space before validation (after bind mounts setup) + echo "Disk space before config validation:" + df -h + echo "" + + # Run config validation with grouping and isolation + python3 script/test_build_components.py -e config -c "$components_csv" -f --isolate "$directly_changed_csv" + + echo "" + echo "Config validation passed! Starting compilation..." + echo "" + + # Show disk space before compilation + echo "Disk space before compilation:" + df -h + echo "" + + # Run compilation with grouping and isolation + python3 script/test_build_components.py -e compile -c "$components_csv" -f --isolate "$directly_changed_csv" pre-commit-ci-lite: name: pre-commit.ci lite runs-on: ubuntu-latest needs: - common - if: github.event_name == 'pull_request' && github.base_ref != 'beta' && github.base_ref != 'release' + if: github.event_name == 'pull_request' && !startsWith(github.base_ref, 'beta') && !startsWith(github.base_ref, 'release') steps: - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Restore Python uses: ./.github/actions/restore-python with: python-version: ${{ env.DEFAULT_PYTHON }} cache-key: ${{ needs.common.outputs.cache-key }} - - uses: pre-commit/action@v3.0.1 + - uses: esphome/action@43cd1109c09c544d97196f7730ee5b2e0cc6d81e # v3.0.1 fork with pinned actions/cache env: SKIP: pylint,clang-tidy-hash - - uses: pre-commit-ci/lite-action@v1.1.0 + - uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0 if: always() + memory-impact-target-branch: + name: Build target branch for memory impact + runs-on: ubuntu-24.04 + needs: + - common + - determine-jobs + if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.memory_impact).should_run == 'true' + outputs: + ram_usage: ${{ steps.extract.outputs.ram_usage }} + flash_usage: ${{ steps.extract.outputs.flash_usage }} + cache_hit: ${{ steps.cache-memory-analysis.outputs.cache-hit }} + skip: ${{ steps.check-script.outputs.skip }} + steps: + - name: Check out target branch + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + with: + ref: ${{ github.base_ref }} + + # Check if memory impact extraction script exists on target branch + # If not, skip the analysis (this handles older branches that don't have the feature) + - name: Check for memory impact script + id: check-script + run: | + if [ -f "script/ci_memory_impact_extract.py" ]; then + echo "skip=false" >> $GITHUB_OUTPUT + else + echo "skip=true" >> $GITHUB_OUTPUT + echo "::warning::ci_memory_impact_extract.py not found on target branch, skipping memory impact analysis" + fi + + # All remaining steps only run if script exists + - name: Generate cache key + id: cache-key + if: steps.check-script.outputs.skip != 'true' + run: | + # Get the commit SHA of the target branch + target_sha=$(git rev-parse HEAD) + + # Hash the build infrastructure files (all files that affect build/analysis) + infra_hash=$(cat \ + script/test_build_components.py \ + script/ci_memory_impact_extract.py \ + script/analyze_component_buses.py \ + script/merge_component_configs.py \ + script/ci_helpers.py \ + .github/workflows/ci.yml \ + | sha256sum | cut -d' ' -f1) + + # Get platform and components from job inputs + platform="${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}" + components='${{ toJSON(fromJSON(needs.determine-jobs.outputs.memory_impact).components) }}' + components_hash=$(echo "$components" | sha256sum | cut -d' ' -f1) + + # Combine into cache key + cache_key="memory-analysis-target-${target_sha}-${infra_hash}-${platform}-${components_hash}" + echo "cache-key=${cache_key}" >> $GITHUB_OUTPUT + echo "Cache key: ${cache_key}" + + - name: Restore cached memory analysis + id: cache-memory-analysis + if: steps.check-script.outputs.skip != 'true' + uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: memory-analysis-target.json + key: ${{ steps.cache-key.outputs.cache-key }} + + - name: Cache status + if: steps.check-script.outputs.skip != 'true' + run: | + if [ "${{ steps.cache-memory-analysis.outputs.cache-hit }}" == "true" ]; then + echo "✓ Cache hit! Using cached memory analysis results." + echo " Skipping build step to save time." + else + echo "✗ Cache miss. Will build and analyze memory usage." + fi + + - name: Restore Python + if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' + uses: ./.github/actions/restore-python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + cache-key: ${{ needs.common.outputs.cache-key }} + + - name: Cache platformio + if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' + uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: ~/.platformio + key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }} + + - name: Build, compile, and analyze memory + if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' + id: build + run: | + . venv/bin/activate + components='${{ toJSON(fromJSON(needs.determine-jobs.outputs.memory_impact).components) }}' + platform="${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}" + + echo "Building with test_build_components.py for $platform with components:" + echo "$components" | jq -r '.[]' | sed 's/^/ - /' + + # Use test_build_components.py which handles grouping automatically + # Pass components as comma-separated list + component_list=$(echo "$components" | jq -r 'join(",")') + + echo "Compiling with test_build_components.py..." + + # Run build and extract memory with auto-detection of build directory for detailed analysis + # Use tee to show output in CI while also piping to extraction script + python script/test_build_components.py \ + -e compile \ + -c "$component_list" \ + -t "$platform" 2>&1 | \ + tee /dev/stderr | \ + python script/ci_memory_impact_extract.py \ + --output-env \ + --output-json memory-analysis-target.json + + # Add metadata to JSON before caching + python script/ci_add_metadata_to_json.py \ + --json-file memory-analysis-target.json \ + --components "$components" \ + --platform "$platform" + + - 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@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: memory-analysis-target.json + key: ${{ steps.cache-key.outputs.cache-key }} + + - name: Extract memory usage for outputs + id: extract + if: steps.check-script.outputs.skip != 'true' + run: | + if [ -f memory-analysis-target.json ]; then + ram=$(jq -r '.ram_bytes' memory-analysis-target.json) + flash=$(jq -r '.flash_bytes' memory-analysis-target.json) + echo "ram_usage=${ram}" >> $GITHUB_OUTPUT + echo "flash_usage=${flash}" >> $GITHUB_OUTPUT + echo "RAM: ${ram} bytes, Flash: ${flash} bytes" + else + echo "Error: memory-analysis-target.json not found" + exit 1 + fi + + - name: Upload memory analysis JSON + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + with: + name: memory-analysis-target + path: memory-analysis-target.json + if-no-files-found: warn + retention-days: 1 + + memory-impact-pr-branch: + name: Build PR branch for memory impact + runs-on: ubuntu-24.04 + needs: + - common + - determine-jobs + if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.memory_impact).should_run == 'true' + outputs: + ram_usage: ${{ steps.extract.outputs.ram_usage }} + flash_usage: ${{ steps.extract.outputs.flash_usage }} + steps: + - name: Check out PR branch + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - name: Restore Python + uses: ./.github/actions/restore-python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + cache-key: ${{ needs.common.outputs.cache-key }} + - name: Cache platformio + uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: ~/.platformio + key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }} + - name: Build, compile, and analyze memory + id: extract + run: | + . venv/bin/activate + components='${{ toJSON(fromJSON(needs.determine-jobs.outputs.memory_impact).components) }}' + platform="${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}" + + echo "Building with test_build_components.py for $platform with components:" + echo "$components" | jq -r '.[]' | sed 's/^/ - /' + + # Use test_build_components.py which handles grouping automatically + # Pass components as comma-separated list + component_list=$(echo "$components" | jq -r 'join(",")') + + echo "Compiling with test_build_components.py..." + + # Run build and extract memory with auto-detection of build directory for detailed analysis + # Use tee to show output in CI while also piping to extraction script + python script/test_build_components.py \ + -e compile \ + -c "$component_list" \ + -t "$platform" 2>&1 | \ + tee /dev/stderr | \ + python script/ci_memory_impact_extract.py \ + --output-env \ + --output-json memory-analysis-pr.json + + # Add metadata to JSON (components and platform are in shell variables above) + python script/ci_add_metadata_to_json.py \ + --json-file memory-analysis-pr.json \ + --components "$components" \ + --platform "$platform" + + - name: Upload memory analysis JSON + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + with: + name: memory-analysis-pr + path: memory-analysis-pr.json + if-no-files-found: warn + retention-days: 1 + + memory-impact-comment: + name: Comment memory impact + runs-on: ubuntu-24.04 + needs: + - common + - determine-jobs + - memory-impact-target-branch + - memory-impact-pr-branch + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && fromJSON(needs.determine-jobs.outputs.memory_impact).should_run == 'true' && needs.memory-impact-target-branch.outputs.skip != 'true' + permissions: + contents: read + pull-requests: write + env: + GH_TOKEN: ${{ github.token }} + steps: + - name: Check out code + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - name: Restore Python + uses: ./.github/actions/restore-python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + cache-key: ${{ needs.common.outputs.cache-key }} + - name: Download target analysis JSON + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + with: + name: memory-analysis-target + path: ./memory-analysis + continue-on-error: true + - name: Download PR analysis JSON + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + with: + name: memory-analysis-pr + path: ./memory-analysis + continue-on-error: true + - name: Post or update PR comment + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + . venv/bin/activate + + # Pass JSON file paths directly to Python script + # All data is extracted from JSON files for security + python script/ci_memory_impact_comment.py \ + --pr-number "$PR_NUMBER" \ + --target-json ./memory-analysis/memory-analysis-target.json \ + --pr-json ./memory-analysis/memory-analysis-pr.json + ci-status: name: CI Status runs-on: ubuntu-24.04 @@ -480,12 +948,15 @@ jobs: - pylint - pytest - integration-tests - - clang-tidy + - clang-tidy-single + - clang-tidy-nosplit + - clang-tidy-split - determine-jobs - - test-build-components - - test-build-components-splitter - test-build-components-split - pre-commit-ci-lite + - memory-impact-target-branch + - memory-impact-pr-branch + - memory-impact-comment if: always() steps: - name: Success diff --git a/.github/workflows/codeowner-review-request.yml b/.github/workflows/codeowner-review-request.yml index ab3377365d..6f4351b298 100644 --- a/.github/workflows/codeowner-review-request.yml +++ b/.github/workflows/codeowner-review-request.yml @@ -21,11 +21,11 @@ permissions: jobs: request-codeowner-reviews: name: Run - if: ${{ !github.event.pull_request.draft }} + if: ${{ github.repository == 'esphome/esphome' && !github.event.pull_request.draft }} runs-on: ubuntu-latest steps: - name: Request reviews from component codeowners - uses: actions/github-script@v7.0.1 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: script: | const owner = context.repo.owner; diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index ddeb0a99d2..d10c8bf267 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -54,11 +54,11 @@ jobs: # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -86,6 +86,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/external-component-bot.yml b/.github/workflows/external-component-bot.yml index 29103e8eee..4fa020f63d 100644 --- a/.github/workflows/external-component-bot.yml +++ b/.github/workflows/external-component-bot.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Add external component comment - uses: actions/github-script@v7.0.1 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/issue-codeowner-notify.yml b/.github/workflows/issue-codeowner-notify.yml index 3639d346f5..6faf956c87 100644 --- a/.github/workflows/issue-codeowner-notify.yml +++ b/.github/workflows/issue-codeowner-notify.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Notify codeowners for component issues - uses: actions/github-script@v7.0.1 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: script: | const owner = context.repo.owner; diff --git a/.github/workflows/needs-docs.yml b/.github/workflows/needs-docs.yml deleted file mode 100644 index 628b5cc5e3..0000000000 --- a/.github/workflows/needs-docs.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Needs Docs - -on: - pull_request: - types: [labeled, unlabeled] - -jobs: - check: - name: Check - runs-on: ubuntu-latest - steps: - - name: Check for needs-docs label - uses: actions/github-script@v7.0.1 - with: - script: | - const { data: labels } = await github.rest.issues.listLabelsOnIssue({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number - }); - const needsDocs = labels.find(label => label.name === 'needs-docs'); - if (needsDocs) { - core.setFailed('Pull request needs docs'); - } diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 44919a6270..1ff810d869 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: branch_build: ${{ steps.tag.outputs.branch_build }} deploy_env: ${{ steps.tag.outputs.deploy_env }} steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Get tag id: tag # yamllint disable rule:line-length @@ -60,9 +60,9 @@ jobs: contents: read id-token: write steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Set up Python - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: "3.x" - name: Build @@ -70,7 +70,7 @@ jobs: pip3 install build python3 -m build - name: Publish - uses: pypa/gh-action-pypi-publish@v1.12.4 + uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 with: skip-existing: true @@ -92,22 +92,22 @@ jobs: os: "ubuntu-24.04-arm" steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Set up Python - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: "3.11" - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.11.1 + uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 - name: Log in to docker hub - uses: docker/login-action@v3.4.0 + uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 with: username: ${{ secrets.DOCKER_USER }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Log in to the GitHub container registry - uses: docker/login-action@v3.4.0 + uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 with: registry: ghcr.io username: ${{ github.actor }} @@ -138,7 +138,7 @@ jobs: # version: ${{ needs.init.outputs.tag }} - name: Upload digests - uses: actions/upload-artifact@v4.6.2 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: digests-${{ matrix.platform.arch }} path: /tmp/digests @@ -168,27 +168,27 @@ jobs: - ghcr - dockerhub steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Download digests - uses: actions/download-artifact@v4.3.0 + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 with: pattern: digests-* path: /tmp/digests merge-multiple: true - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.11.1 + uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 - name: Log in to docker hub if: matrix.registry == 'dockerhub' - uses: docker/login-action@v3.4.0 + uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 with: username: ${{ secrets.DOCKER_USER }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Log in to the GitHub container registry if: matrix.registry == 'ghcr' - uses: docker/login-action@v3.4.0 + uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 with: registry: ghcr.io username: ${{ github.actor }} @@ -220,7 +220,7 @@ jobs: - deploy-manifest steps: - name: Trigger Workflow - uses: actions/github-script@v7.0.1 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: github-token: ${{ secrets.DEPLOY_HA_ADDON_REPO_TOKEN }} script: | @@ -246,7 +246,7 @@ jobs: environment: ${{ needs.init.outputs.deploy_env }} steps: - name: Trigger Workflow - uses: actions/github-script@v7.0.1 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: github-token: ${{ secrets.DEPLOY_ESPHOME_SCHEMA_REPO_TOKEN }} script: | diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index b79939fc8e..5843b3a5e0 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -15,36 +15,52 @@ concurrency: jobs: stale: + if: github.repository_owner == 'esphome' runs-on: ubuntu-latest steps: - - uses: actions/stale@v9.1.0 + - name: Stale + uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0 with: + debug-only: ${{ github.ref != 'refs/heads/dev' }} # Dry-run when not run on dev branch + remove-stale-when-updated: true + operations-per-run: 400 + + # The 90 day stale policy for PRs + # - PRs + # - No PRs marked as "not-stale" + # - No Issues (see below) days-before-pr-stale: 90 days-before-pr-close: 7 - days-before-issue-stale: -1 - days-before-issue-close: -1 - remove-stale-when-updated: true stale-pr-label: "stale" exempt-pr-labels: "not-stale" stale-pr-message: > There hasn't been any activity on this pull request recently. This pull request has been automatically marked as stale because of that and will be closed if no further activity occurs within 7 days. - Thank you for your contributions. - # Use stale to automatically close issues with a - # reference to the issue tracker - close-issues: - runs-on: ubuntu-latest - steps: - - uses: actions/stale@v9.1.0 - with: - days-before-pr-stale: -1 - days-before-pr-close: -1 - days-before-issue-stale: 1 - days-before-issue-close: 1 - remove-stale-when-updated: true + If you are the author of this PR, please leave a comment if you want + to keep it open. Also, please rebase your PR onto the latest dev + branch to ensure that it's up to date with the latest changes. + + Thank you for your contribution! + + # The 90 day stale policy for Issues + # - Issues + # - No Issues marked as "not-stale" + # - No PRs (see above) + days-before-issue-stale: 90 + days-before-issue-close: 7 stale-issue-label: "stale" exempt-issue-labels: "not-stale" stale-issue-message: > - https://github.com/esphome/esphome/issues/430 + There hasn't been any activity on this issue recently. Due to the + high number of incoming GitHub notifications, we have to clean some + of the old issues, as many of them have already been resolved with + the latest updates. + + Please make sure to update to the latest ESPHome version and + check if that solves the issue. Let us know if that works for you by + adding a comment 👍 + + This issue has now been marked as stale and will be closed if no + further activity occurs. Thank you for your contributions. diff --git a/.github/workflows/status-check-labels.yml b/.github/workflows/status-check-labels.yml new file mode 100644 index 0000000000..cca70815b9 --- /dev/null +++ b/.github/workflows/status-check-labels.yml @@ -0,0 +1,31 @@ +name: Status check labels + +on: + pull_request: + types: [labeled, unlabeled] + +jobs: + check: + name: Check ${{ matrix.label }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + label: + - needs-docs + - merge-after-release + - chained-pr + steps: + - name: Check for ${{ matrix.label }} label + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const { data: labels } = await github.rest.issues.listLabelsOnIssue({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number + }); + const hasLabel = labels.find(label => label.name === '${{ matrix.label }}'); + if (hasLabel) { + core.setFailed('Pull request cannot be merged, it is labeled as ${{ matrix.label }}'); + } diff --git a/.github/workflows/sync-device-classes.yml b/.github/workflows/sync-device-classes.yml index a38825fc45..baaa29df2c 100644 --- a/.github/workflows/sync-device-classes.yml +++ b/.github/workflows/sync-device-classes.yml @@ -13,16 +13,16 @@ jobs: if: github.repository == 'esphome/esphome' steps: - name: Checkout - uses: actions/checkout@v4.2.2 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Checkout Home Assistant - uses: actions/checkout@v4.2.2 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: repository: home-assistant/core path: lib/home-assistant - name: Setup Python - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: 3.13 @@ -30,13 +30,18 @@ jobs: run: | python -m pip install --upgrade pip pip install -e lib/home-assistant + pip install -r requirements_test.txt pre-commit - name: Sync run: | python ./script/sync-device_class.py + - name: Run pre-commit hooks + run: | + python script/run-in-env.py pre-commit run --all-files + - name: Commit changes - uses: peter-evans/create-pull-request@v7.0.8 + uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7.0.9 with: commit-message: "Synchronise Device Classes from Home Assistant" committer: esphomebot diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3e2da47446..b86d00f2aa 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.12.7 + rev: v0.14.5 hooks: # Run the linter. - id: ruff diff --git a/CODEOWNERS b/CODEOWNERS index e40be9a737..c6332e3933 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -40,11 +40,11 @@ esphome/components/analog_threshold/* @ianchi esphome/components/animation/* @syndlex esphome/components/anova/* @buxtronix esphome/components/apds9306/* @aodrenah -esphome/components/api/* @OttoWinter +esphome/components/api/* @esphome/core esphome/components/as5600/* @ammmze esphome/components/as5600/sensor/* @ammmze esphome/components/as7341/* @mrgnr -esphome/components/async_tcp/* @OttoWinter +esphome/components/async_tcp/* @esphome/core esphome/components/at581x/* @X-Ryl669 esphome/components/atc_mithermometer/* @ahpohl esphome/components/atm90e26/* @danieltwagner @@ -62,14 +62,17 @@ esphome/components/bedjet/fan/* @jhansche esphome/components/bedjet/sensor/* @javawizard @jhansche esphome/components/beken_spi_led_strip/* @Mat931 esphome/components/bh1750/* @OttoWinter +esphome/components/bh1900nux/* @B48D81EFCC esphome/components/binary_sensor/* @esphome/core esphome/components/bk72xx/* @kuba2k2 esphome/components/bl0906/* @athom-tech @jesserockz @tarontop esphome/components/bl0939/* @ziceva -esphome/components/bl0940/* @tobias- +esphome/components/bl0940/* @dan-s-github @tobias- esphome/components/bl0942/* @dbuezas @dwmw2 esphome/components/ble_client/* @buxtronix @clydebarrow -esphome/components/bluetooth_proxy/* @jesserockz +esphome/components/ble_nus/* @tomaszduda23 +esphome/components/bluetooth_proxy/* @bdraco @jesserockz +esphome/components/bm8563/* @abmantis esphome/components/bme280_base/* @esphome/core esphome/components/bme280_spi/* @apbodrov esphome/components/bme680_bsec/* @trvrnrth @@ -88,10 +91,11 @@ esphome/components/bp1658cj/* @Cossid esphome/components/bp5758d/* @Cossid esphome/components/button/* @esphome/core esphome/components/bytebuffer/* @clydebarrow -esphome/components/camera/* @DT-art1 @bdraco +esphome/components/camera/* @bdraco @DT-art1 +esphome/components/camera_encoder/* @DT-art1 esphome/components/canbus/* @danielschramm @mvturnho esphome/components/cap1188/* @mreditor97 -esphome/components/captive_portal/* @OttoWinter +esphome/components/captive_portal/* @esphome/core esphome/components/ccs811/* @habbie esphome/components/cd74hc4067/* @asoehlke esphome/components/ch422g/* @clydebarrow @jesterret @@ -118,7 +122,7 @@ esphome/components/dallas_temp/* @ssieb esphome/components/daly_bms/* @s1lvi0 esphome/components/dashboard_import/* @esphome/core esphome/components/datetime/* @jesserockz @rfdarter -esphome/components/debug/* @OttoWinter +esphome/components/debug/* @esphome/core esphome/components/delonghi/* @grob6000 esphome/components/dfplayer/* @glmnet esphome/components/dfrobot_sen0395/* @niklasweber @@ -138,34 +142,37 @@ esphome/components/ens160_base/* @latonita @vincentscode esphome/components/ens160_i2c/* @latonita esphome/components/ens160_spi/* @latonita esphome/components/ens210/* @itn3rd77 +esphome/components/epaper_spi/* @esphome/core esphome/components/es7210/* @kahrendt esphome/components/es7243e/* @kbx81 esphome/components/es8156/* @kbx81 esphome/components/es8311/* @kahrendt @kroimon esphome/components/es8388/* @P4uLT esphome/components/esp32/* @esphome/core -esphome/components/esp32_ble/* @Rapsssito @jesserockz -esphome/components/esp32_ble_client/* @jesserockz -esphome/components/esp32_ble_server/* @Rapsssito @clydebarrow @jesserockz +esphome/components/esp32_ble/* @bdraco @jesserockz @Rapsssito +esphome/components/esp32_ble_client/* @bdraco @jesserockz +esphome/components/esp32_ble_server/* @clydebarrow @jesserockz @Rapsssito +esphome/components/esp32_ble_tracker/* @bdraco esphome/components/esp32_camera_web_server/* @ayufan esphome/components/esp32_can/* @Sympatron esphome/components/esp32_hosted/* @swoboda1337 +esphome/components/esp32_hosted/update/* @swoboda1337 esphome/components/esp32_improv/* @jesserockz esphome/components/esp32_rmt/* @jesserockz esphome/components/esp32_rmt_led_strip/* @jesserockz esphome/components/esp8266/* @esphome/core esphome/components/esp_ldo/* @clydebarrow esphome/components/espnow/* @jesserockz +esphome/components/espnow/packet_transport/* @EasilyBoredEngineer esphome/components/ethernet_info/* @gtjadsonsantos esphome/components/event/* @nohat -esphome/components/event_emitter/* @Rapsssito esphome/components/exposure_notifications/* @OttoWinter esphome/components/ezo/* @ssieb esphome/components/ezo_pmp/* @carlos-sarmiento esphome/components/factory_reset/* @anatoly-savchenkov esphome/components/fastled_base/* @OttoWinter esphome/components/feedback/* @ianchi -esphome/components/fingerprint_grow/* @OnFreund @alexborro @loongyh +esphome/components/fingerprint_grow/* @alexborro @loongyh @OnFreund esphome/components/font/* @clydebarrow @esphome/core esphome/components/fs3000/* @kahrendt esphome/components/ft5x06/* @clydebarrow @@ -175,7 +182,7 @@ esphome/components/gdk101/* @Szewcson esphome/components/gl_r01_i2c/* @pkejval esphome/components/globals/* @esphome/core esphome/components/gp2y1010au0f/* @zry98 -esphome/components/gp8403/* @jesserockz +esphome/components/gp8403/* @jesserockz @sebydocky esphome/components/gpio/* @esphome/core esphome/components/gpio/one_wire/* @ssieb esphome/components/gps/* @coogle @ximex @@ -196,12 +203,15 @@ esphome/components/havells_solar/* @sourabhjaiswal esphome/components/hbridge/fan/* @WeekendWarrior esphome/components/hbridge/light/* @DotNetDann esphome/components/hbridge/switch/* @dwmw2 +esphome/components/hc8/* @omartijn +esphome/components/hdc2010/* @optimusprimespace @ssieb esphome/components/he60r/* @clydebarrow esphome/components/heatpumpir/* @rob-deutsch esphome/components/hitachi_ac424/* @sourabhjaiswal +esphome/components/hlk_fm22x/* @OnFreund esphome/components/hm3301/* @freekode esphome/components/hmac_md5/* @dwmw2 -esphome/components/homeassistant/* @OttoWinter @esphome/core +esphome/components/homeassistant/* @esphome/core @OttoWinter esphome/components/homeassistant/number/* @landonr esphome/components/homeassistant/switch/* @Links2004 esphome/components/honeywell_hih_i2c/* @Benichou34 @@ -226,18 +236,18 @@ esphome/components/iaqcore/* @yozik04 esphome/components/ili9xxx/* @clydebarrow @nielsnl68 esphome/components/improv_base/* @esphome/core esphome/components/improv_serial/* @esphome/core -esphome/components/ina226/* @Sergio303 @latonita +esphome/components/ina226/* @latonita @Sergio303 esphome/components/ina260/* @mreditor97 esphome/components/ina2xx_base/* @latonita esphome/components/ina2xx_i2c/* @latonita esphome/components/ina2xx_spi/* @latonita esphome/components/inkbird_ibsth1_mini/* @fkirill -esphome/components/inkplate6/* @jesserockz +esphome/components/inkplate/* @jesserockz @JosipKuci esphome/components/integration/* @OttoWinter esphome/components/internal_temperature/* @Mat931 esphome/components/interval/* @esphome/core esphome/components/jsn_sr04t/* @Mafus1 -esphome/components/json/* @OttoWinter +esphome/components/json/* @esphome/core esphome/components/kamstrup_kmp/* @cfeenstra1024 esphome/components/key_collector/* @ssieb esphome/components/key_provider/* @ssieb @@ -245,6 +255,7 @@ esphome/components/kuntze/* @ssieb esphome/components/lc709203f/* @ilikecake esphome/components/lcd_menu/* @numo68 esphome/components/ld2410/* @regevbr @sebcaps +esphome/components/ld2412/* @Rihan9 esphome/components/ld2420/* @descipher esphome/components/ld2450/* @hareeshmu esphome/components/ld24xx/* @kbx81 @@ -254,6 +265,7 @@ esphome/components/libretiny_pwm/* @kuba2k2 esphome/components/light/* @esphome/core esphome/components/lightwaverf/* @max246 esphome/components/lilygo_t5_47/touchscreen/* @jesserockz +esphome/components/lm75b/* @beormund esphome/components/ln882x/* @lamauny esphome/components/lock/* @esphome/core esphome/components/logger/* @esphome/core @@ -274,13 +286,14 @@ esphome/components/max7219digit/* @rspaargaren esphome/components/max9611/* @mckaymatthew esphome/components/mcp23008/* @jesserockz esphome/components/mcp23017/* @jesserockz -esphome/components/mcp23s08/* @SenexCrenshaw @jesserockz -esphome/components/mcp23s17/* @SenexCrenshaw @jesserockz +esphome/components/mcp23s08/* @jesserockz @SenexCrenshaw +esphome/components/mcp23s17/* @jesserockz @SenexCrenshaw esphome/components/mcp23x08_base/* @jesserockz esphome/components/mcp23x17_base/* @jesserockz esphome/components/mcp23xxx_base/* @jesserockz esphome/components/mcp2515/* @danielschramm @mvturnho esphome/components/mcp3204/* @rsumner +esphome/components/mcp3221/* @philippderdiedas esphome/components/mcp4461/* @p1ngb4ck esphome/components/mcp4728/* @berfenger esphome/components/mcp47a1/* @jesserockz @@ -296,6 +309,7 @@ esphome/components/mics_4514/* @jesserockz esphome/components/midea/* @dudanov esphome/components/midea_ir/* @dudanov esphome/components/mipi_dsi/* @clydebarrow +esphome/components/mipi_rgb/* @clydebarrow esphome/components/mipi_spi/* @clydebarrow esphome/components/mitsubishi/* @RubyBailey esphome/components/mixer/speaker/* @kahrendt @@ -339,7 +353,7 @@ esphome/components/ota/* @esphome/core esphome/components/output/* @esphome/core esphome/components/packet_transport/* @clydebarrow esphome/components/pca6416a/* @Mat931 -esphome/components/pca9554/* @clydebarrow @hwstar +esphome/components/pca9554/* @bdraco @clydebarrow @hwstar esphome/components/pcf85063/* @brogon esphome/components/pcf8563/* @KoenBreeman esphome/components/pi4ioe5v6408/* @jesserockz @@ -350,9 +364,9 @@ esphome/components/pm2005/* @andrewjswan esphome/components/pmsa003i/* @sjtrny esphome/components/pmsx003/* @ximex esphome/components/pmwcs3/* @SeByDocKy -esphome/components/pn532/* @OttoWinter @jesserockz -esphome/components/pn532_i2c/* @OttoWinter @jesserockz -esphome/components/pn532_spi/* @OttoWinter @jesserockz +esphome/components/pn532/* @jesserockz @OttoWinter +esphome/components/pn532_i2c/* @jesserockz @OttoWinter +esphome/components/pn532_spi/* @jesserockz @OttoWinter esphome/components/pn7150/* @jesserockz @kbx81 esphome/components/pn7150_i2c/* @jesserockz @kbx81 esphome/components/pn7160/* @jesserockz @kbx81 @@ -361,7 +375,7 @@ esphome/components/pn7160_spi/* @jesserockz @kbx81 esphome/components/power_supply/* @esphome/core esphome/components/preferences/* @esphome/core esphome/components/psram/* @esphome/core -esphome/components/pulse_meter/* @TrentHouliston @cstaahl @stevebaxter +esphome/components/pulse_meter/* @cstaahl @stevebaxter @TrentHouliston esphome/components/pvvx_mithermometer/* @pasiz esphome/components/pylontech/* @functionpointer esphome/components/qmp6988/* @andrewpc @@ -384,6 +398,7 @@ esphome/components/rpi_dpi_rgb/* @clydebarrow esphome/components/rtl87xx/* @kuba2k2 esphome/components/rtttl/* @glmnet esphome/components/runtime_stats/* @bdraco +esphome/components/rx8130/* @beormund esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti esphome/components/scd4x/* @martgras @sjtrny esphome/components/script/* @esphome/core @@ -402,7 +417,8 @@ esphome/components/sensirion_common/* @martgras esphome/components/sensor/* @esphome/core esphome/components/sfa30/* @ghsensdev esphome/components/sgp40/* @SenexCrenshaw -esphome/components/sgp4x/* @SenexCrenshaw @martgras +esphome/components/sgp4x/* @martgras @SenexCrenshaw +esphome/components/sha256/* @esphome/core esphome/components/shelly_dimmer/* @edge90 @rnauber esphome/components/sht3xd/* @mrtoy-me esphome/components/sht4x/* @sjtrny @@ -424,6 +440,7 @@ esphome/components/speaker/media_player/* @kahrendt @synesthesiam esphome/components/spi/* @clydebarrow @esphome/core esphome/components/spi_device/* @clydebarrow esphome/components/spi_led_strip/* @clydebarrow +esphome/components/split_buffer/* @jesserockz esphome/components/sprinkler/* @kbx81 esphome/components/sps30/* @martgras esphome/components/ssd1322_base/* @kbx81 @@ -445,6 +462,7 @@ esphome/components/st7735/* @SenexCrenshaw esphome/components/st7789v/* @kbx81 esphome/components/st7920/* @marsjan155 esphome/components/statsd/* @Links2004 +esphome/components/stts22h/* @B48D81EFCC esphome/components/substitutions/* @esphome/core esphome/components/sun/* @OttoWinter esphome/components/sun_gtil2/* @Mat931 @@ -466,8 +484,10 @@ esphome/components/template/datetime/* @rfdarter esphome/components/template/event/* @nohat esphome/components/template/fan/* @ssieb esphome/components/text/* @mauritskorse +esphome/components/thermopro_ble/* @sittner esphome/components/thermostat/* @kbx81 -esphome/components/time/* @OttoWinter +esphome/components/time/* @esphome/core +esphome/components/tinyusb/* @kbx81 esphome/components/tlc5947/* @rnauber esphome/components/tlc5971/* @IJIJI esphome/components/tm1621/* @Philippe12 @@ -511,7 +531,7 @@ esphome/components/wake_on_lan/* @clydebarrow @willwill2will54 esphome/components/watchdog/* @oarcher esphome/components/waveshare_epaper/* @clydebarrow esphome/components/web_server/ota/* @esphome/core -esphome/components/web_server_base/* @OttoWinter +esphome/components/web_server_base/* @esphome/core esphome/components/web_server_idf/* @dentra esphome/components/weikai/* @DrCoolZic esphome/components/weikai_i2c/* @DrCoolZic @@ -529,6 +549,7 @@ esphome/components/wk2204_spi/* @DrCoolZic esphome/components/wk2212_i2c/* @DrCoolZic esphome/components/wk2212_spi/* @DrCoolZic esphome/components/wl_134/* @hobbypunk90 +esphome/components/wts01/* @alepee esphome/components/x9c/* @EtienneMD esphome/components/xgzp68xx/* @gcormier esphome/components/xiaomi_hhccjcy10/* @fariouche @@ -544,3 +565,4 @@ esphome/components/xxtea/* @clydebarrow esphome/components/zephyr/* @tomaszduda23 esphome/components/zhlt01/* @cfeenstra1024 esphome/components/zio_ultrasonic/* @kahrendt +esphome/components/zwave_proxy/* @kbx81 diff --git a/Doxyfile b/Doxyfile index 1f5ac5aa1b..a19120b9da 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2025.8.0-dev +PROJECT_NUMBER = 2025.12.0-dev # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/docker/build.py b/docker/build.py index 921adac7ab..4d093cf88d 100755 --- a/docker/build.py +++ b/docker/build.py @@ -90,7 +90,7 @@ def main(): def run_command(*cmd, ignore_error: bool = False): print(f"$ {shlex.join(list(cmd))}") if not args.dry_run: - rc = subprocess.call(list(cmd)) + rc = subprocess.call(list(cmd), close_fds=False) if rc != 0 and not ignore_error: print("Command failed") sys.exit(1) diff --git a/esphome/__main__.py b/esphome/__main__.py index 5e45b7f213..f8fb678cb2 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -6,17 +6,23 @@ import getpass import importlib import logging import os +from pathlib import Path import re import sys import time +from typing import Protocol import argcomplete +# Note: Do not import modules from esphome.components here, as this would +# cause them to be loaded before external components are processed, resulting +# in the built-in version being used instead of the external component one. from esphome import const, writer, yaml_util import esphome.codegen as cg from esphome.config import iter_component_configs, read_config, strip_default_ids from esphome.const import ( ALLOWED_NAME_CHARS, + CONF_API, CONF_BAUD_RATE, CONF_BROKER, CONF_DEASSERT_RTS_DTR, @@ -42,8 +48,10 @@ from esphome.const import ( SECRETS_FILES, ) from esphome.core import CORE, EsphomeError, coroutine +from esphome.enum import StrEnum from esphome.helpers import get_bool_env, indent, is_ip_address from esphome.log import AnsiFore, color, setup_log +from esphome.types import ConfigType from esphome.util import ( get_serial_ports, list_yaml_files, @@ -54,6 +62,57 @@ from esphome.util import ( _LOGGER = logging.getLogger(__name__) +# Special non-component keys that appear in configs +_NON_COMPONENT_KEYS = frozenset( + { + CONF_ESPHOME, + "substitutions", + "packages", + "globals", + "external_components", + "<<", + } +) + + +def detect_external_components(config: ConfigType) -> set[str]: + """Detect external/custom components in the configuration. + + External components are those that appear in the config but are not + part of ESPHome's built-in components and are not special config keys. + + Args: + config: The ESPHome configuration dictionary + + Returns: + A set of external component names + """ + from esphome.analyze_memory.helpers import get_esphome_components + + builtin_components = get_esphome_components() + return { + key + for key in config + if key not in builtin_components and key not in _NON_COMPONENT_KEYS + } + + +class ArgsProtocol(Protocol): + device: list[str] | None + reset: bool + username: str | None + password: str | None + client_id: str | None + topic: str | None + file: str | None + no_logs: bool + only_generate: bool + show_secrets: bool + dashboard: bool + configuration: str + name: str + upload_speed: str | None + def choose_prompt(options, purpose: str = None): if not options: @@ -87,51 +146,261 @@ def choose_prompt(options, purpose: str = None): return options[opt - 1][1] +class Purpose(StrEnum): + UPLOADING = "uploading" + LOGGING = "logging" + + +class PortType(StrEnum): + SERIAL = "SERIAL" + NETWORK = "NETWORK" + MQTT = "MQTT" + MQTTIP = "MQTTIP" + + +# Magic MQTT port types that require special handling +_MQTT_PORT_TYPES = frozenset({PortType.MQTT, PortType.MQTTIP}) + + +def _resolve_with_cache(address: str, purpose: Purpose) -> list[str]: + """Resolve an address using cache if available, otherwise return the address itself.""" + if CORE.address_cache and (cached := CORE.address_cache.get_addresses(address)): + _LOGGER.debug("Using cached addresses for %s: %s", purpose.value, cached) + return cached + return [address] + + def choose_upload_log_host( - default, check_default, show_ota, show_mqtt, show_api, purpose: str = None -): + default: list[str] | str | None, + check_default: str | None, + purpose: Purpose, +) -> list[str]: + # Convert to list for uniform handling + defaults = [default] if isinstance(default, str) else default or [] + + # If devices specified, resolve them + if defaults: + resolved: list[str] = [] + for device in defaults: + if device == "SERIAL": + serial_ports = get_serial_ports() + if not serial_ports: + _LOGGER.warning("No serial ports found, skipping SERIAL device") + continue + options = [ + (f"{port.path} ({port.description})", port.path) + for port in serial_ports + ] + resolved.append(choose_prompt(options, purpose=purpose)) + elif device == "OTA": + # ensure IP adresses are used first + if is_ip_address(CORE.address) and ( + (purpose == Purpose.LOGGING and has_api()) + or (purpose == Purpose.UPLOADING and has_ota()) + ): + resolved.extend(_resolve_with_cache(CORE.address, purpose)) + + if purpose == Purpose.LOGGING: + if has_api() and has_mqtt_ip_lookup(): + resolved.append("MQTTIP") + + if has_mqtt_logging(): + resolved.append("MQTT") + + if has_api() and has_non_ip_address() and has_resolvable_address(): + resolved.extend(_resolve_with_cache(CORE.address, purpose)) + + elif purpose == Purpose.UPLOADING: + if has_ota() and has_mqtt_ip_lookup(): + resolved.append("MQTTIP") + + if has_ota() and has_non_ip_address() and has_resolvable_address(): + resolved.extend(_resolve_with_cache(CORE.address, purpose)) + else: + resolved.append(device) + if not resolved: + raise EsphomeError( + f"All specified devices {defaults} could not be resolved. Is the device connected to the network?" + ) + return resolved + + # No devices specified, show interactive chooser options = [ (f"{port.path} ({port.description})", port.path) for port in get_serial_ports() ] - if default == "SERIAL": - return choose_prompt(options, purpose=purpose) - if (show_ota and "ota" in CORE.config) or (show_api and "api" in CORE.config): - options.append((f"Over The Air ({CORE.address})", CORE.address)) - if default == "OTA": - return CORE.address - if ( - show_mqtt - and (mqtt_config := CORE.config.get(CONF_MQTT)) - and mqtt_logging_enabled(mqtt_config) - ): - options.append((f"MQTT ({mqtt_config[CONF_BROKER]})", "MQTT")) - if default == "OTA": - return "MQTT" - if default is not None: - return default + + if purpose == Purpose.LOGGING: + if has_mqtt_logging(): + mqtt_config = CORE.config[CONF_MQTT] + options.append((f"MQTT ({mqtt_config[CONF_BROKER]})", "MQTT")) + + if has_api(): + if has_resolvable_address(): + options.append((f"Over The Air ({CORE.address})", CORE.address)) + if has_mqtt_ip_lookup(): + options.append(("Over The Air (MQTT IP lookup)", "MQTTIP")) + + elif purpose == Purpose.UPLOADING and has_ota(): + if has_resolvable_address(): + options.append((f"Over The Air ({CORE.address})", CORE.address)) + if has_mqtt_ip_lookup(): + options.append(("Over The Air (MQTT IP lookup)", "MQTTIP")) + if check_default is not None and check_default in [opt[1] for opt in options]: - return check_default - return choose_prompt(options, purpose=purpose) + return [check_default] + return [choose_prompt(options, purpose=purpose)] -def mqtt_logging_enabled(mqtt_config): +def has_mqtt_logging() -> bool: + """Check if MQTT logging is available.""" + if CONF_MQTT not in CORE.config: + return False + + mqtt_config = CORE.config[CONF_MQTT] + + # enabled by default + if CONF_LOG_TOPIC not in mqtt_config: + return True + log_topic = mqtt_config[CONF_LOG_TOPIC] if log_topic is None: return False + if CONF_TOPIC not in log_topic: return False + return log_topic.get(CONF_LEVEL, None) != "NONE" -def get_port_type(port): +def has_mqtt() -> bool: + """Check if MQTT is available.""" + return CONF_MQTT in CORE.config + + +def has_api() -> bool: + """Check if API is available.""" + return CONF_API in CORE.config + + +def has_ota() -> bool: + """Check if OTA is available.""" + return CONF_OTA in CORE.config + + +def has_mqtt_ip_lookup() -> bool: + """Check if MQTT is available and IP lookup is supported.""" + from esphome.components.mqtt import CONF_DISCOVER_IP + + if CONF_MQTT not in CORE.config: + return False + # Default Enabled + if CONF_DISCOVER_IP not in CORE.config[CONF_MQTT]: + return True + return CORE.config[CONF_MQTT][CONF_DISCOVER_IP] + + +def has_mdns() -> bool: + """Check if MDNS is available.""" + return CONF_MDNS not in CORE.config or not CORE.config[CONF_MDNS][CONF_DISABLED] + + +def has_non_ip_address() -> bool: + """Check if CORE.address is set and is not an IP address.""" + return CORE.address is not None and not is_ip_address(CORE.address) + + +def has_ip_address() -> bool: + """Check if CORE.address is a valid IP address.""" + return CORE.address is not None and is_ip_address(CORE.address) + + +def has_resolvable_address() -> bool: + """Check if CORE.address is resolvable (via mDNS, DNS, or is an IP address).""" + # Any address (IP, mDNS hostname, or regular DNS hostname) is resolvable + # The resolve_ip_address() function in helpers.py handles all types via AsyncResolver + if CORE.address is None: + return False + + if has_ip_address(): + return True + + if has_mdns(): + return True + + # .local mDNS hostnames are only resolvable if mDNS is enabled + return not CORE.address.endswith(".local") + + +def mqtt_get_ip(config: ConfigType, username: str, password: str, client_id: str): + from esphome import mqtt + + return mqtt.get_esphome_device_ip(config, username, password, client_id) + + +def _resolve_network_devices( + devices: list[str], config: ConfigType, args: ArgsProtocol +) -> list[str]: + """Resolve device list, converting MQTT magic strings to actual IP addresses. + + This function filters the devices list to: + - Replace MQTT/MQTTIP magic strings with actual IP addresses via MQTT lookup + - Deduplicate addresses while preserving order + - Only resolve MQTT once even if multiple MQTT strings are present + - If MQTT resolution fails, log a warning and continue with other devices + + Args: + devices: List of device identifiers (IPs, hostnames, or magic strings) + config: ESPHome configuration + args: Command-line arguments containing MQTT credentials + + Returns: + List of network addresses suitable for connection attempts + """ + network_devices: list[str] = [] + mqtt_resolved: bool = False + + for device in devices: + port_type = get_port_type(device) + if port_type in _MQTT_PORT_TYPES: + # Only resolve MQTT once, even if multiple MQTT entries + if not mqtt_resolved: + try: + mqtt_ips = mqtt_get_ip( + config, args.username, args.password, args.client_id + ) + network_devices.extend(mqtt_ips) + except EsphomeError as err: + _LOGGER.warning( + "MQTT IP discovery failed (%s), will try other devices if available", + err, + ) + mqtt_resolved = True + elif device not in network_devices: + # Regular network address or IP - add if not already present + network_devices.append(device) + + return network_devices + + +def get_port_type(port: str) -> PortType: + """Determine the type of port/device identifier. + + Returns: + PortType.SERIAL for serial ports (/dev/ttyUSB0, COM1, etc.) + PortType.MQTT for MQTT logging + PortType.MQTTIP for MQTT IP lookup + PortType.NETWORK for IP addresses, hostnames, or mDNS names + """ if port.startswith("/") or port.startswith("COM"): - return "SERIAL" + return PortType.SERIAL if port == "MQTT": - return "MQTT" - return "NETWORK" + return PortType.MQTT + if port == "MQTTIP": + return PortType.MQTTIP + return PortType.NETWORK -def run_miniterm(config, port, args): +def run_miniterm(config: ConfigType, port: str, args) -> int: from aioesphomeapi import LogParser import serial @@ -173,7 +442,9 @@ def run_miniterm(config, port, args): .replace(b"\n", b"") .decode("utf8", "backslashreplace") ) - time_str = datetime.now().time().strftime("[%H:%M:%S]") + time_ = datetime.now() + nanoseconds = time_.microsecond // 1000 + time_str = f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}.{nanoseconds:03}]" safe_print(parser.parse_line(line, time_str)) backtrace_state = platformio_api.process_stacktrace( @@ -208,7 +479,7 @@ def wrap_to_code(name, comp): return wrapped -def write_cpp(config): +def write_cpp(config: ConfigType) -> int: if not get_bool_env(ENV_NOGITIGNORE): writer.write_gitignore() @@ -216,7 +487,7 @@ def write_cpp(config): return write_cpp_file() -def generate_cpp_contents(config): +def generate_cpp_contents(config: ConfigType) -> None: _LOGGER.info("Generating C++ source...") for name, component, conf in iter_component_configs(CORE.config): @@ -227,7 +498,7 @@ def generate_cpp_contents(config): CORE.flush_tasks() -def write_cpp_file(): +def write_cpp_file() -> int: code_s = indent(CORE.cpp_main_section) writer.write_cpp(code_s) @@ -238,10 +509,12 @@ def write_cpp_file(): return 0 -def compile_program(args, config): +def compile_program(args: ArgsProtocol, config: ConfigType) -> int: from esphome import platformio_api - _LOGGER.info("Compiling app...") + # NOTE: "Build path:" format is parsed by script/ci_memory_impact_extract.py + # If you change this format, update the regex in that script as well + _LOGGER.info("Compiling app... Build path: %s", CORE.build_path) rc = platformio_api.run_compile(config, CORE.verbose) if rc != 0: return rc @@ -249,7 +522,9 @@ def compile_program(args, config): return 0 if idedata is not None else 1 -def upload_using_esptool(config, port, file, speed): +def upload_using_esptool( + config: ConfigType, port: str, file: str, speed: int +) -> str | int: from esphome import platformio_api first_baudrate = speed or config[CONF_ESPHOME][CONF_PLATFORMIO_OPTIONS].get( @@ -294,7 +569,7 @@ def upload_using_esptool(config, port, file, speed): "detect", ] for img in flash_images: - cmd += [img.offset, img.path] + cmd += [img.offset, str(img.path)] if os.environ.get("ESPHOME_USE_SUBPROCESS") is None: import esptool @@ -314,7 +589,7 @@ def upload_using_esptool(config, port, file, speed): return run_esptool(115200) -def upload_using_platformio(config, port): +def upload_using_platformio(config: ConfigType, port: str): from esphome import platformio_api upload_args = ["-t", "upload", "-t", "nobuild"] @@ -323,8 +598,8 @@ def upload_using_platformio(config, port): return platformio_api.run_platformio_cli_run(config, CORE.verbose, *upload_args) -def check_permissions(port): - if os.name == "posix" and get_port_type(port) == "SERIAL": +def check_permissions(port: str): + if os.name == "posix" and get_port_type(port) == PortType.SERIAL: # Check if we can open selected serial port if not os.access(port, os.F_OK): raise EsphomeError( @@ -341,27 +616,29 @@ def check_permissions(port): ) -def upload_program(config, args, host): +def upload_program( + config: ConfigType, args: ArgsProtocol, devices: list[str] +) -> tuple[int, str | None]: + host = devices[0] try: module = importlib.import_module("esphome.components." + CORE.target_platform) if getattr(module, "upload_program")(config, args, host): - return 0 + return 0, host except AttributeError: pass - if get_port_type(host) == "SERIAL": + if get_port_type(host) == PortType.SERIAL: check_permissions(host) + + exit_code = 1 if CORE.target_platform in (PLATFORM_ESP32, PLATFORM_ESP8266): file = getattr(args, "file", None) - return upload_using_esptool(config, host, file, args.upload_speed) + exit_code = upload_using_esptool(config, host, file, args.upload_speed) + elif CORE.target_platform == PLATFORM_RP2040 or CORE.is_libretiny: + exit_code = upload_using_platformio(config, host) + # else: Unknown target platform, exit_code remains 1 - if CORE.target_platform in (PLATFORM_RP2040): - return upload_using_platformio(config, args.device) - - if CORE.is_libretiny: - return upload_using_platformio(config, host) - - return 1 # Unknown target platform + return exit_code, host if exit_code == 0 else None ota_conf = {} for ota_item in config.get(CONF_OTA, []): @@ -377,46 +654,46 @@ def upload_program(config, args, host): from esphome import espota2 remote_port = int(ota_conf[CONF_PORT]) - password = ota_conf.get(CONF_PASSWORD, "") - - if ( - CONF_MQTT in config # pylint: disable=too-many-boolean-expressions - and (not args.device or args.device in ("MQTT", "OTA")) - and ( - ((config[CONF_MDNS][CONF_DISABLED]) and not is_ip_address(CORE.address)) - or get_port_type(host) == "MQTT" - ) - ): - from esphome import mqtt - - host = mqtt.get_esphome_device_ip( - config, args.username, args.password, args.client_id - ) - + password = ota_conf.get(CONF_PASSWORD) if getattr(args, "file", None) is not None: - return espota2.run_ota(host, remote_port, password, args.file) + binary = Path(args.file) + else: + binary = CORE.firmware_bin - return espota2.run_ota(host, remote_port, password, CORE.firmware_bin) + # Resolve MQTT magic strings to actual IP addresses + network_devices = _resolve_network_devices(devices, config, args) + + return espota2.run_ota(network_devices, remote_port, password, binary) -def show_logs(config, args, port): +def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int | None: + try: + module = importlib.import_module("esphome.components." + CORE.target_platform) + if getattr(module, "show_logs")(config, args, devices): + return 0 + except AttributeError: + pass + if "logger" not in config: raise EsphomeError("Logger is not configured!") - if get_port_type(port) == "SERIAL": + + port = devices[0] + port_type = get_port_type(port) + + if port_type == PortType.SERIAL: check_permissions(port) return run_miniterm(config, port, args) - if get_port_type(port) == "NETWORK" and "api" in config: - if config[CONF_MDNS][CONF_DISABLED] and CONF_MQTT in config: - from esphome import mqtt - - port = mqtt.get_esphome_device_ip( - config, args.username, args.password, args.client_id - )[0] + # Check if we should use API for logging + # Resolve MQTT magic strings to actual IP addresses + if has_api() and ( + network_devices := _resolve_network_devices(devices, config, args) + ): from esphome.components.api.client import run_logs - return run_logs(config, port) - if get_port_type(port) == "MQTT" and "mqtt" in config: + return run_logs(config, network_devices) + + if port_type in (PortType.NETWORK, PortType.MQTT) and has_mqtt_logging(): from esphome import mqtt return mqtt.show_logs( @@ -426,7 +703,7 @@ def show_logs(config, args, port): raise EsphomeError("No remote or local logging method configured (api/mqtt/logger)") -def clean_mqtt(config, args): +def clean_mqtt(config: ConfigType, args: ArgsProtocol) -> int | None: from esphome import mqtt return mqtt.clear_topic( @@ -434,13 +711,13 @@ def clean_mqtt(config, args): ) -def command_wizard(args): +def command_wizard(args: ArgsProtocol) -> int | None: from esphome import wizard - return wizard.wizard(args.configuration) + return wizard.wizard(Path(args.configuration)) -def command_config(args, config): +def command_config(args: ArgsProtocol, config: ConfigType) -> int | None: if not CORE.verbose: config = strip_default_ids(config) output = yaml_util.dump(config, args.show_secrets) @@ -455,7 +732,7 @@ def command_config(args, config): return 0 -def command_vscode(args): +def command_vscode(args: ArgsProtocol) -> int | None: from esphome import vscode logging.disable(logging.INFO) @@ -463,7 +740,7 @@ def command_vscode(args): vscode.read_config(args) -def command_compile(args, config): +def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None: exit_code = write_cpp(config) if exit_code != 0: return exit_code @@ -477,23 +754,23 @@ def command_compile(args, config): return 0 -def command_upload(args, config): - port = choose_upload_log_host( +def command_upload(args: ArgsProtocol, config: ConfigType) -> int | None: + # Get devices, resolving special identifiers like OTA + devices = choose_upload_log_host( default=args.device, check_default=None, - show_ota=True, - show_mqtt=False, - show_api=False, - purpose="uploading", + purpose=Purpose.UPLOADING, ) - exit_code = upload_program(config, args, port) - if exit_code != 0: - return exit_code - _LOGGER.info("Successfully uploaded program.") - return 0 + + exit_code, _ = upload_program(config, args, devices) + if exit_code == 0: + _LOGGER.info("Successfully uploaded program.") + else: + _LOGGER.warning("Failed to upload to %s", devices) + return exit_code -def command_discover(args, config): +def command_discover(args: ArgsProtocol, config: ConfigType) -> int | None: if "mqtt" in config: from esphome import mqtt @@ -502,19 +779,17 @@ def command_discover(args, config): raise EsphomeError("No discover method configured (mqtt)") -def command_logs(args, config): - port = choose_upload_log_host( +def command_logs(args: ArgsProtocol, config: ConfigType) -> int | None: + # Get devices, resolving special identifiers like OTA + devices = choose_upload_log_host( default=args.device, check_default=None, - show_ota=False, - show_mqtt=True, - show_api=True, - purpose="logging", + purpose=Purpose.LOGGING, ) - return show_logs(config, args, port) + return show_logs(config, args, devices) -def command_run(args, config): +def command_run(args: ArgsProtocol, config: ConfigType) -> int | None: exit_code = write_cpp(config) if exit_code != 0: return exit_code @@ -531,47 +806,58 @@ def command_run(args, config): program_path = idedata.raw["prog_path"] return run_external_process(program_path) - port = choose_upload_log_host( + # Get devices, resolving special identifiers like OTA + devices = choose_upload_log_host( default=args.device, check_default=None, - show_ota=True, - show_mqtt=False, - show_api=True, - purpose="uploading", + purpose=Purpose.UPLOADING, ) - exit_code = upload_program(config, args, port) - if exit_code != 0: + + exit_code, successful_device = upload_program(config, args, devices) + if exit_code == 0: + _LOGGER.info("Successfully uploaded program.") + else: + _LOGGER.warning("Failed to upload to %s", devices) return exit_code - _LOGGER.info("Successfully uploaded program.") + if args.no_logs: return 0 - port = choose_upload_log_host( - default=args.device, - check_default=port, - show_ota=False, - show_mqtt=True, - show_api=True, - purpose="logging", + + # For logs, prefer the device we successfully uploaded to + devices = choose_upload_log_host( + default=successful_device, + check_default=successful_device, + purpose=Purpose.LOGGING, ) - return show_logs(config, args, port) + return show_logs(config, args, devices) -def command_clean_mqtt(args, config): +def command_clean_mqtt(args: ArgsProtocol, config: ConfigType) -> int | None: return clean_mqtt(config, args) -def command_mqtt_fingerprint(args, config): +def command_clean_all(args: ArgsProtocol) -> int | None: + try: + writer.clean_all(args.configuration) + except OSError as err: + _LOGGER.error("Error cleaning all files: %s", err) + return 1 + _LOGGER.info("Done!") + return 0 + + +def command_mqtt_fingerprint(args: ArgsProtocol, config: ConfigType) -> int | None: from esphome import mqtt return mqtt.get_fingerprint(config) -def command_version(args): +def command_version(args: ArgsProtocol) -> int | None: safe_print(f"Version: {const.__version__}") return 0 -def command_clean(args, config): +def command_clean(args: ArgsProtocol, config: ConfigType) -> int | None: try: writer.clean_build() except OSError as err: @@ -581,13 +867,13 @@ def command_clean(args, config): return 0 -def command_dashboard(args): +def command_dashboard(args: ArgsProtocol) -> int | None: from esphome.dashboard import dashboard return dashboard.start_dashboard(args) -def command_update_all(args): +def command_update_all(args: ArgsProtocol) -> int | None: import click success = {} @@ -601,7 +887,7 @@ def command_update_all(args): safe_print(f"{half_line}{middle_text}{half_line}") for f in files: - safe_print(f"Updating {color(AnsiFore.CYAN, f)}") + safe_print(f"Updating {color(AnsiFore.CYAN, str(f))}") safe_print("-" * twidth) safe_print() if CORE.dashboard: @@ -613,10 +899,10 @@ def command_update_all(args): "esphome", "run", f, "--no-logs", "--device", "OTA" ) if rc == 0: - print_bar(f"[{color(AnsiFore.BOLD_GREEN, 'SUCCESS')}] {f}") + print_bar(f"[{color(AnsiFore.BOLD_GREEN, 'SUCCESS')}] {str(f)}") success[f] = True else: - print_bar(f"[{color(AnsiFore.BOLD_RED, 'ERROR')}] {f}") + print_bar(f"[{color(AnsiFore.BOLD_RED, 'ERROR')}] {str(f)}") success[f] = False safe_print() @@ -627,14 +913,14 @@ def command_update_all(args): failed = 0 for f in files: if success[f]: - safe_print(f" - {f}: {color(AnsiFore.GREEN, 'SUCCESS')}") + safe_print(f" - {str(f)}: {color(AnsiFore.GREEN, 'SUCCESS')}") else: - safe_print(f" - {f}: {color(AnsiFore.BOLD_RED, 'FAILED')}") + safe_print(f" - {str(f)}: {color(AnsiFore.BOLD_RED, 'FAILED')}") failed += 1 return failed -def command_idedata(args, config): +def command_idedata(args: ArgsProtocol, config: ConfigType) -> int: import json from esphome import platformio_api @@ -650,8 +936,57 @@ def command_idedata(args, config): return 0 -def command_rename(args, config): - for c in args.name: +def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int: + """Analyze memory usage by component. + + This command compiles the configuration and performs memory analysis. + Compilation is fast if sources haven't changed (just relinking). + """ + from esphome import platformio_api + from esphome.analyze_memory.cli import MemoryAnalyzerCLI + + # Always compile to ensure fresh data (fast if no changes - just relinks) + exit_code = write_cpp(config) + if exit_code != 0: + return exit_code + exit_code = compile_program(args, config) + if exit_code != 0: + return exit_code + _LOGGER.info("Successfully compiled program.") + + # Get idedata for analysis + idedata = platformio_api.get_idedata(config) + if idedata is None: + _LOGGER.error("Failed to get IDE data for memory analysis") + return 1 + + firmware_elf = Path(idedata.firmware_elf_path) + + # Extract external components from config + external_components = detect_external_components(config) + _LOGGER.debug("Detected external components: %s", external_components) + + # Perform memory analysis + _LOGGER.info("Analyzing memory usage...") + analyzer = MemoryAnalyzerCLI( + str(firmware_elf), + idedata.objdump_path, + idedata.readelf_path, + external_components, + ) + analyzer.analyze() + + # Generate and display report + report = analyzer.generate_report() + print() + print(report) + + return 0 + + +def command_rename(args: ArgsProtocol, config: ConfigType) -> int | None: + new_name = args.name + for c in new_name: if c not in ALLOWED_NAME_CHARS: print( color( @@ -662,8 +997,7 @@ def command_rename(args, config): ) return 1 # Load existing yaml file - with open(CORE.config_path, mode="r+", encoding="utf-8") as raw_file: - raw_contents = raw_file.read() + raw_contents = CORE.config_path.read_text(encoding="utf-8") yaml = yaml_util.load_yaml(CORE.config_path) if CONF_ESPHOME not in yaml or CONF_NAME not in yaml[CONF_ESPHOME]: @@ -678,7 +1012,7 @@ def command_rename(args, config): if match is None: new_raw = re.sub( rf"name:\s+[\"']?{old_name}[\"']?", - f'name: "{args.name}"', + f'name: "{new_name}"', raw_contents, ) else: @@ -698,29 +1032,28 @@ def command_rename(args, config): new_raw = re.sub( rf"^(\s+{match.group(1)}):\s+[\"']?{old_name}[\"']?", - f'\\1: "{args.name}"', + f'\\1: "{new_name}"', raw_contents, flags=re.MULTILINE, ) - new_path = os.path.join(CORE.config_dir, args.name + ".yaml") + new_path: Path = CORE.config_dir / (new_name + ".yaml") print( - f"Updating {color(AnsiFore.CYAN, CORE.config_path)} to {color(AnsiFore.CYAN, new_path)}" + f"Updating {color(AnsiFore.CYAN, str(CORE.config_path))} to {color(AnsiFore.CYAN, str(new_path))}" ) print() - with open(new_path, mode="w", encoding="utf-8") as new_file: - new_file.write(new_raw) + new_path.write_text(new_raw, encoding="utf-8") - rc = run_external_process("esphome", "config", new_path) + rc = run_external_process("esphome", "config", str(new_path)) if rc != 0: print(color(AnsiFore.BOLD_RED, "Rename failed. Reverting changes.")) - os.remove(new_path) + new_path.unlink() return 1 cli_args = [ "run", - new_path, + str(new_path), "--no-logs", "--device", CORE.address, @@ -734,11 +1067,11 @@ def command_rename(args, config): except KeyboardInterrupt: rc = 1 if rc != 0: - os.remove(new_path) + new_path.unlink() return 1 if CORE.config_path != new_path: - os.remove(CORE.config_path) + CORE.config_path.unlink() print(color(AnsiFore.BOLD_GREEN, "SUCCESS")) print() @@ -751,6 +1084,7 @@ PRE_CONFIG_ACTIONS = { "dashboard": command_dashboard, "vscode": command_vscode, "update-all": command_update_all, + "clean-all": command_clean_all, } POST_CONFIG_ACTIONS = { @@ -759,14 +1093,21 @@ POST_CONFIG_ACTIONS = { "upload": command_upload, "logs": command_logs, "run": command_run, + "clean": command_clean, "clean-mqtt": command_clean_mqtt, "mqtt-fingerprint": command_mqtt_fingerprint, - "clean": command_clean, "idedata": command_idedata, "rename": command_rename, "discover": command_discover, + "analyze-memory": command_analyze_memory, } +SIMPLE_CONFIG_ACTIONS = [ + "clean", + "clean-mqtt", + "config", +] + def parse_args(argv): options_parser = argparse.ArgumentParser(add_help=False) @@ -799,6 +1140,24 @@ def parse_args(argv): help="Add a substitution", metavar=("key", "value"), ) + options_parser.add_argument( + "--mdns-address-cache", + help="mDNS address cache mapping in format 'hostname=ip1,ip2'", + action="append", + default=[], + ) + options_parser.add_argument( + "--dns-address-cache", + help="DNS address cache mapping in format 'hostname=ip1,ip2'", + action="append", + default=[], + ) + options_parser.add_argument( + "--testing-mode", + help="Enable testing mode (disables validation checks for grouped component testing)", + action="store_true", + default=False, + ) parser = argparse.ArgumentParser( description=f"ESPHome {const.__version__}", parents=[options_parser] @@ -854,7 +1213,8 @@ def parse_args(argv): ) parser_upload.add_argument( "--device", - help="Manually specify the serial port/address to use, for example /dev/ttyUSB0.", + action="append", + help="Manually specify the serial port/address to use, for example /dev/ttyUSB0. Can be specified multiple times for fallback addresses.", ) parser_upload.add_argument( "--upload_speed", @@ -876,7 +1236,8 @@ def parse_args(argv): ) parser_logs.add_argument( "--device", - help="Manually specify the serial port/address to use, for example /dev/ttyUSB0.", + action="append", + help="Manually specify the serial port/address to use, for example /dev/ttyUSB0. Can be specified multiple times for fallback addresses.", ) parser_logs.add_argument( "--reset", @@ -905,7 +1266,8 @@ def parse_args(argv): ) parser_run.add_argument( "--device", - help="Manually specify the serial port/address to use, for example /dev/ttyUSB0.", + action="append", + help="Manually specify the serial port/address to use, for example /dev/ttyUSB0. Can be specified multiple times for fallback addresses.", ) parser_run.add_argument( "--upload_speed", @@ -953,6 +1315,13 @@ def parse_args(argv): "configuration", help="Your YAML configuration file(s).", nargs="+" ) + parser_clean_all = subparsers.add_parser( + "clean-all", help="Clean all build and platform files." + ) + parser_clean_all.add_argument( + "configuration", help="Your YAML file or configuration directory.", nargs="*" + ) + parser_dashboard = subparsers.add_parser( "dashboard", help="Create a simple web server for a dashboard." ) @@ -999,7 +1368,7 @@ def parse_args(argv): parser_update = subparsers.add_parser("update-all") parser_update.add_argument( - "configuration", help="Your YAML configuration file directories.", nargs="+" + "configuration", help="Your YAML configuration file or directory.", nargs="+" ) parser_idedata = subparsers.add_parser("idedata") @@ -1016,6 +1385,14 @@ def parse_args(argv): ) parser_rename.add_argument("name", help="The new name for the device.", type=str) + parser_analyze_memory = subparsers.add_parser( + "analyze-memory", + help="Analyze memory usage by component.", + ) + parser_analyze_memory.add_argument( + "configuration", help="Your YAML configuration file(s).", nargs="+" + ) + # Keep backward compatibility with the old command line format of # esphome . # @@ -1032,13 +1409,27 @@ def parse_args(argv): arguments = argv[1:] argcomplete.autocomplete(parser) + + if len(arguments) > 0 and arguments[0] in SIMPLE_CONFIG_ACTIONS: + args, unknown_args = parser.parse_known_args(arguments) + if unknown_args: + _LOGGER.warning("Ignored unrecognized arguments: %s", unknown_args) + return args + return parser.parse_args(arguments) def run_esphome(argv): + from esphome.address_cache import AddressCache + args = parse_args(argv) CORE.dashboard = args.dashboard + CORE.testing_mode = args.testing_mode + # Create address cache from command-line arguments + CORE.address_cache = AddressCache.from_cli_args( + args.mdns_address_cache, args.dns_address_cache + ) # Override log level if verbose is set if args.verbose: args.log_level = "DEBUG" @@ -1061,14 +1452,20 @@ def run_esphome(argv): _LOGGER.info("ESPHome %s", const.__version__) for conf_path in args.configuration: - if any(os.path.basename(conf_path) == x for x in SECRETS_FILES): + conf_path = Path(conf_path) + if any(conf_path.name == x for x in SECRETS_FILES): _LOGGER.warning("Skipping secrets file %s", conf_path) continue CORE.config_path = conf_path CORE.dashboard = args.dashboard - config = read_config(dict(args.substitution) if args.substitution else {}) + # For logs command, skip updating external components + skip_external = args.command == "logs" + config = read_config( + dict(args.substitution) if args.substitution else {}, + skip_external_update=skip_external, + ) if config is None: return 2 CORE.config = config diff --git a/esphome/address_cache.py b/esphome/address_cache.py new file mode 100644 index 0000000000..7c20be90f0 --- /dev/null +++ b/esphome/address_cache.py @@ -0,0 +1,142 @@ +"""Address cache for DNS and mDNS lookups.""" + +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Iterable + +_LOGGER = logging.getLogger(__name__) + + +def normalize_hostname(hostname: str) -> str: + """Normalize hostname for cache lookups. + + Removes trailing dots and converts to lowercase. + """ + return hostname.rstrip(".").lower() + + +class AddressCache: + """Cache for DNS and mDNS address lookups. + + This cache stores pre-resolved addresses from command-line arguments + to avoid slow DNS/mDNS lookups during builds. + """ + + def __init__( + self, + mdns_cache: dict[str, list[str]] | None = None, + dns_cache: dict[str, list[str]] | None = None, + ) -> None: + """Initialize the address cache. + + Args: + mdns_cache: Pre-populated mDNS addresses (hostname -> IPs) + dns_cache: Pre-populated DNS addresses (hostname -> IPs) + """ + self.mdns_cache = mdns_cache or {} + self.dns_cache = dns_cache or {} + + def _get_cached_addresses( + self, hostname: str, cache: dict[str, list[str]], cache_type: str + ) -> list[str] | None: + """Get cached addresses from a specific cache. + + Args: + hostname: The hostname to look up + cache: The cache dictionary to check + cache_type: Type of cache for logging ("mDNS" or "DNS") + + Returns: + List of IP addresses if found in cache, None otherwise + """ + normalized = normalize_hostname(hostname) + if addresses := cache.get(normalized): + _LOGGER.debug("Using %s cache for %s: %s", cache_type, hostname, addresses) + return addresses + return None + + def get_mdns_addresses(self, hostname: str) -> list[str] | None: + """Get cached mDNS addresses for a hostname. + + Args: + hostname: The hostname to look up (should end with .local) + + Returns: + List of IP addresses if found in cache, None otherwise + """ + return self._get_cached_addresses(hostname, self.mdns_cache, "mDNS") + + def get_dns_addresses(self, hostname: str) -> list[str] | None: + """Get cached DNS addresses for a hostname. + + Args: + hostname: The hostname to look up + + Returns: + List of IP addresses if found in cache, None otherwise + """ + return self._get_cached_addresses(hostname, self.dns_cache, "DNS") + + def get_addresses(self, hostname: str) -> list[str] | None: + """Get cached addresses for a hostname. + + Checks mDNS cache for .local domains, DNS cache otherwise. + + Args: + hostname: The hostname to look up + + Returns: + List of IP addresses if found in cache, None otherwise + """ + normalized = normalize_hostname(hostname) + if normalized.endswith(".local"): + return self.get_mdns_addresses(hostname) + return self.get_dns_addresses(hostname) + + def has_cache(self) -> bool: + """Check if any cache entries exist.""" + return bool(self.mdns_cache or self.dns_cache) + + @classmethod + def from_cli_args( + cls, mdns_args: Iterable[str], dns_args: Iterable[str] + ) -> AddressCache: + """Create cache from command-line arguments. + + Args: + mdns_args: List of mDNS cache entries like ['host=ip1,ip2'] + dns_args: List of DNS cache entries like ['host=ip1,ip2'] + + Returns: + Configured AddressCache instance + """ + mdns_cache = cls._parse_cache_args(mdns_args) + dns_cache = cls._parse_cache_args(dns_args) + return cls(mdns_cache=mdns_cache, dns_cache=dns_cache) + + @staticmethod + def _parse_cache_args(cache_args: Iterable[str]) -> dict[str, list[str]]: + """Parse cache arguments into a dictionary. + + Args: + cache_args: List of cache mappings like ['host1=ip1,ip2', 'host2=ip3'] + + Returns: + Dictionary mapping normalized hostnames to list of IP addresses + """ + cache: dict[str, list[str]] = {} + for arg in cache_args: + if "=" not in arg: + _LOGGER.warning( + "Invalid cache format: %s (expected 'hostname=ip1,ip2')", arg + ) + continue + hostname, ips = arg.split("=", 1) + # Normalize hostname for consistent lookups + normalized = normalize_hostname(hostname) + cache[normalized] = [ip.strip() for ip in ips.split(",")] + return cache diff --git a/esphome/analyze_memory/__init__.py b/esphome/analyze_memory/__init__.py new file mode 100644 index 0000000000..71e86e3788 --- /dev/null +++ b/esphome/analyze_memory/__init__.py @@ -0,0 +1,502 @@ +"""Memory usage analyzer for ESPHome compiled binaries.""" + +from collections import defaultdict +from dataclasses import dataclass, field +import logging +from pathlib import Path +import re +import subprocess +from typing import TYPE_CHECKING + +from .const import ( + CORE_SUBCATEGORY_PATTERNS, + DEMANGLED_PATTERNS, + ESPHOME_COMPONENT_PATTERN, + SECTION_TO_ATTR, + SYMBOL_PATTERNS, +) +from .helpers import ( + get_component_class_patterns, + get_esphome_components, + map_section_name, + parse_symbol_line, +) + +if TYPE_CHECKING: + from esphome.platformio_api import IDEData + +_LOGGER = logging.getLogger(__name__) + +# GCC global constructor/destructor prefix annotations +_GCC_PREFIX_ANNOTATIONS = { + "_GLOBAL__sub_I_": "global constructor for", + "_GLOBAL__sub_D_": "global destructor for", +} + +# GCC optimization suffix pattern (e.g., $isra$0, $part$1, $constprop$2) +_GCC_OPTIMIZATION_SUFFIX_PATTERN = re.compile(r"(\$(?:isra|part|constprop)\$\d+)") + +# C++ runtime patterns for categorization +_CPP_RUNTIME_PATTERNS = frozenset(["vtable", "typeinfo", "thunk"]) + +# libc printf/scanf family base names (used to detect variants like _printf_r, vfprintf, etc.) +_LIBC_PRINTF_SCANF_FAMILY = frozenset(["printf", "fprintf", "sprintf", "scanf"]) + +# Regex pattern for parsing readelf section headers +# Format: [ #] name type addr off size +_READELF_SECTION_PATTERN = re.compile( + r"\s*\[\s*\d+\]\s+([\.\w]+)\s+\w+\s+[\da-fA-F]+\s+[\da-fA-F]+\s+([\da-fA-F]+)" +) + +# Component category prefixes +_COMPONENT_PREFIX_ESPHOME = "[esphome]" +_COMPONENT_PREFIX_EXTERNAL = "[external]" +_COMPONENT_CORE = f"{_COMPONENT_PREFIX_ESPHOME}core" +_COMPONENT_API = f"{_COMPONENT_PREFIX_ESPHOME}api" + +# C++ namespace prefixes +_NAMESPACE_ESPHOME = "esphome::" +_NAMESPACE_STD = "std::" + +# Type alias for symbol information: (symbol_name, size, component) +SymbolInfoType = tuple[str, int, str] + + +@dataclass +class MemorySection: + """Represents a memory section with its symbols.""" + + name: str + symbols: list[SymbolInfoType] = field(default_factory=list) + total_size: int = 0 + + +@dataclass +class ComponentMemory: + """Tracks memory usage for a component.""" + + name: str + text_size: int = 0 # Code in flash + rodata_size: int = 0 # Read-only data in flash + data_size: int = 0 # Initialized data (flash + ram) + bss_size: int = 0 # Uninitialized data (ram only) + symbol_count: int = 0 + + @property + def flash_total(self) -> int: + """Total flash usage (text + rodata + data).""" + return self.text_size + self.rodata_size + self.data_size + + @property + def ram_total(self) -> int: + """Total RAM usage (data + bss).""" + return self.data_size + self.bss_size + + +class MemoryAnalyzer: + """Analyzes memory usage from ELF files.""" + + def __init__( + self, + elf_path: str, + objdump_path: str | None = None, + readelf_path: str | None = None, + external_components: set[str] | None = None, + idedata: "IDEData | None" = None, + ) -> None: + """Initialize memory analyzer. + + Args: + elf_path: Path to ELF file to analyze + objdump_path: Path to objdump binary (auto-detected from idedata if not provided) + readelf_path: Path to readelf binary (auto-detected from idedata if not provided) + external_components: Set of external component names + idedata: Optional PlatformIO IDEData object to auto-detect toolchain paths + """ + self.elf_path = Path(elf_path) + if not self.elf_path.exists(): + raise FileNotFoundError(f"ELF file not found: {elf_path}") + + # Auto-detect toolchain paths from idedata if not provided + if idedata is not None and (objdump_path is None or readelf_path is None): + objdump_path = objdump_path or idedata.objdump_path + readelf_path = readelf_path or idedata.readelf_path + _LOGGER.debug("Using toolchain paths from PlatformIO idedata") + + self.objdump_path = objdump_path or "objdump" + self.readelf_path = readelf_path or "readelf" + self.external_components = external_components or set() + + self.sections: dict[str, MemorySection] = {} + self.components: dict[str, ComponentMemory] = defaultdict( + lambda: ComponentMemory("") + ) + self._demangle_cache: dict[str, str] = {} + self._uncategorized_symbols: list[tuple[str, str, int]] = [] + self._esphome_core_symbols: list[ + tuple[str, str, int] + ] = [] # Track core symbols + self._component_symbols: dict[str, list[tuple[str, str, int]]] = defaultdict( + list + ) # Track symbols for all components + + def analyze(self) -> dict[str, ComponentMemory]: + """Analyze the ELF file and return component memory usage.""" + self._parse_sections() + self._parse_symbols() + self._categorize_symbols() + return dict(self.components) + + def _parse_sections(self) -> None: + """Parse section headers from ELF file.""" + result = subprocess.run( + [self.readelf_path, "-S", str(self.elf_path)], + capture_output=True, + text=True, + check=True, + ) + + # Parse section headers + for line in result.stdout.splitlines(): + # Look for section entries + if not (match := _READELF_SECTION_PATTERN.match(line)): + continue + + section_name = match.group(1) + size_hex = match.group(2) + size = int(size_hex, 16) + + # Map to standard section name + mapped_section = map_section_name(section_name) + if not mapped_section: + continue + + if mapped_section not in self.sections: + self.sections[mapped_section] = MemorySection(mapped_section) + self.sections[mapped_section].total_size += size + + def _parse_symbols(self) -> None: + """Parse symbols from ELF file.""" + result = subprocess.run( + [self.objdump_path, "-t", str(self.elf_path)], + capture_output=True, + text=True, + check=True, + ) + + # Track seen addresses to avoid duplicates + seen_addresses: set[str] = set() + + for line in result.stdout.splitlines(): + if not (symbol_info := parse_symbol_line(line)): + continue + + section, name, size, address = symbol_info + + # Skip duplicate symbols at the same address (e.g., C1/C2 constructors) + if address in seen_addresses or section not in self.sections: + continue + + self.sections[section].symbols.append((name, size, "")) + seen_addresses.add(address) + + def _categorize_symbols(self) -> None: + """Categorize symbols by component.""" + # First, collect all unique symbol names for batch demangling + all_symbols = { + symbol_name + for section in self.sections.values() + for symbol_name, _, _ in section.symbols + } + + # Batch demangle all symbols at once + self._batch_demangle_symbols(list(all_symbols)) + + # Now categorize with cached demangled names + for section_name, section in self.sections.items(): + for symbol_name, size, _ in section.symbols: + component = self._identify_component(symbol_name) + + if component not in self.components: + self.components[component] = ComponentMemory(component) + + comp_mem = self.components[component] + comp_mem.symbol_count += 1 + + # Update the appropriate size attribute based on section + if attr_name := SECTION_TO_ATTR.get(section_name): + setattr(comp_mem, attr_name, getattr(comp_mem, attr_name) + size) + + # Track uncategorized symbols + if component == "other" and size > 0: + demangled = self._demangle_symbol(symbol_name) + self._uncategorized_symbols.append((symbol_name, demangled, size)) + + # Track ESPHome core symbols for detailed analysis + if component == _COMPONENT_CORE and size > 0: + demangled = self._demangle_symbol(symbol_name) + self._esphome_core_symbols.append((symbol_name, demangled, size)) + + # Track all component symbols for detailed analysis + if size > 0: + demangled = self._demangle_symbol(symbol_name) + self._component_symbols[component].append( + (symbol_name, demangled, size) + ) + + def _identify_component(self, symbol_name: str) -> str: + """Identify which component a symbol belongs to.""" + # Demangle C++ names if needed + demangled = self._demangle_symbol(symbol_name) + + # Check for special component classes first (before namespace pattern) + # This handles cases like esphome::ESPHomeOTAComponent which should map to ota + if _NAMESPACE_ESPHOME in demangled: + # Check for special component classes that include component name in the class + # For example: esphome::ESPHomeOTAComponent -> ota component + for component_name in get_esphome_components(): + patterns = get_component_class_patterns(component_name) + if any(pattern in demangled for pattern in patterns): + return f"{_COMPONENT_PREFIX_ESPHOME}{component_name}" + + # Check for ESPHome component namespaces + match = ESPHOME_COMPONENT_PATTERN.search(demangled) + if match: + component_name = match.group(1) + # Strip trailing underscore if present (e.g., switch_ -> switch) + component_name = component_name.rstrip("_") + + # Check if this is an actual component in the components directory + if component_name in get_esphome_components(): + return f"{_COMPONENT_PREFIX_ESPHOME}{component_name}" + # Check if this is a known external component from the config + if component_name in self.external_components: + return f"{_COMPONENT_PREFIX_EXTERNAL}{component_name}" + # Everything else in esphome:: namespace is core + return _COMPONENT_CORE + + # Check for esphome core namespace (no component namespace) + if _NAMESPACE_ESPHOME in demangled: + # If no component match found, it's core + return _COMPONENT_CORE + + # Check against symbol patterns + for component, patterns in SYMBOL_PATTERNS.items(): + if any(pattern in symbol_name for pattern in patterns): + return component + + # Check against demangled patterns + for component, patterns in DEMANGLED_PATTERNS.items(): + if any(pattern in demangled for pattern in patterns): + return component + + # Special cases that need more complex logic + + # Check if spi_flash vs spi_driver + if "spi_" in symbol_name or "SPI" in symbol_name: + return "spi_flash" if "spi_flash" in symbol_name else "spi_driver" + + # libc special printf variants + if ( + symbol_name.startswith("_") + and symbol_name[1:].replace("_r", "").replace("v", "").replace("s", "") + in _LIBC_PRINTF_SCANF_FAMILY + ): + return "libc" + + # Track uncategorized symbols for analysis + return "other" + + def _batch_demangle_symbols(self, symbols: list[str]) -> None: + """Batch demangle C++ symbol names for efficiency.""" + if not symbols: + return + + # Try to find the appropriate c++filt for the platform + cppfilt_cmd = "c++filt" + + _LOGGER.info("Demangling %d symbols", len(symbols)) + _LOGGER.debug("objdump_path = %s", self.objdump_path) + + # Check if we have a toolchain-specific c++filt + if self.objdump_path and self.objdump_path != "objdump": + # Replace objdump with c++filt in the path + potential_cppfilt = self.objdump_path.replace("objdump", "c++filt") + _LOGGER.info("Checking for toolchain c++filt at: %s", potential_cppfilt) + if Path(potential_cppfilt).exists(): + cppfilt_cmd = potential_cppfilt + _LOGGER.info("✓ Using toolchain c++filt: %s", cppfilt_cmd) + else: + _LOGGER.info( + "✗ Toolchain c++filt not found at %s, using system c++filt", + potential_cppfilt, + ) + else: + _LOGGER.info("✗ Using system c++filt (objdump_path=%s)", self.objdump_path) + + # Strip GCC optimization suffixes and prefixes before demangling + # Suffixes like $isra$0, $part$0, $constprop$0 confuse c++filt + # Prefixes like _GLOBAL__sub_I_ need to be removed and tracked + symbols_stripped: list[str] = [] + symbols_prefixes: list[str] = [] # Track removed prefixes + for symbol in symbols: + # Remove GCC optimization markers + stripped = _GCC_OPTIMIZATION_SUFFIX_PATTERN.sub("", symbol) + + # Handle GCC global constructor/initializer prefixes + # _GLOBAL__sub_I_ -> extract for demangling + prefix = "" + for gcc_prefix in _GCC_PREFIX_ANNOTATIONS: + if stripped.startswith(gcc_prefix): + prefix = gcc_prefix + stripped = stripped[len(prefix) :] + break + + symbols_stripped.append(stripped) + symbols_prefixes.append(prefix) + + try: + # Send all symbols to c++filt at once + result = subprocess.run( + [cppfilt_cmd], + input="\n".join(symbols_stripped), + capture_output=True, + text=True, + check=False, + ) + except (subprocess.SubprocessError, OSError, UnicodeDecodeError) as e: + # On error, cache originals + _LOGGER.warning("Failed to batch demangle symbols: %s", e) + for symbol in symbols: + self._demangle_cache[symbol] = symbol + return + + if result.returncode != 0: + _LOGGER.warning( + "c++filt exited with code %d: %s", + result.returncode, + result.stderr[:200] if result.stderr else "(no error output)", + ) + # Cache originals on failure + for symbol in symbols: + self._demangle_cache[symbol] = symbol + return + + # Process demangled output + self._process_demangled_output( + symbols, symbols_stripped, symbols_prefixes, result.stdout, cppfilt_cmd + ) + + def _process_demangled_output( + self, + symbols: list[str], + symbols_stripped: list[str], + symbols_prefixes: list[str], + demangled_output: str, + cppfilt_cmd: str, + ) -> None: + """Process demangled symbol output and populate cache. + + Args: + symbols: Original symbol names + symbols_stripped: Stripped symbol names sent to c++filt + symbols_prefixes: Removed prefixes to restore + demangled_output: Output from c++filt + cppfilt_cmd: Path to c++filt command (for logging) + """ + demangled_lines = demangled_output.strip().split("\n") + failed_count = 0 + + for original, stripped, prefix, demangled in zip( + symbols, symbols_stripped, symbols_prefixes, demangled_lines + ): + # Add back any prefix that was removed + demangled = self._restore_symbol_prefix(prefix, stripped, demangled) + + # If we stripped a suffix, add it back to the demangled name for clarity + if original != stripped and not prefix: + demangled = self._restore_symbol_suffix(original, demangled) + + self._demangle_cache[original] = demangled + + # Log symbols that failed to demangle (stayed the same as stripped version) + if stripped == demangled and stripped.startswith("_Z"): + failed_count += 1 + if failed_count <= 5: # Only log first 5 failures + _LOGGER.warning("Failed to demangle: %s", original) + + if failed_count == 0: + _LOGGER.info("Successfully demangled all %d symbols", len(symbols)) + return + + _LOGGER.warning( + "Failed to demangle %d/%d symbols using %s", + failed_count, + len(symbols), + cppfilt_cmd, + ) + + @staticmethod + def _restore_symbol_prefix(prefix: str, stripped: str, demangled: str) -> str: + """Restore prefix that was removed before demangling. + + Args: + prefix: Prefix that was removed (e.g., "_GLOBAL__sub_I_") + stripped: Stripped symbol name + demangled: Demangled symbol name + + Returns: + Demangled name with prefix restored/annotated + """ + if not prefix: + return demangled + + # Successfully demangled - add descriptive prefix + if demangled != stripped and ( + annotation := _GCC_PREFIX_ANNOTATIONS.get(prefix) + ): + return f"[{annotation}: {demangled}]" + + # Failed to demangle - restore original prefix + return prefix + demangled + + @staticmethod + def _restore_symbol_suffix(original: str, demangled: str) -> str: + """Restore GCC optimization suffix that was removed before demangling. + + Args: + original: Original symbol name with suffix + demangled: Demangled symbol name without suffix + + Returns: + Demangled name with suffix annotation + """ + if suffix_match := _GCC_OPTIMIZATION_SUFFIX_PATTERN.search(original): + return f"{demangled} [{suffix_match.group(1)}]" + return demangled + + def _demangle_symbol(self, symbol: str) -> str: + """Get demangled C++ symbol name from cache.""" + return self._demangle_cache.get(symbol, symbol) + + def _categorize_esphome_core_symbol(self, demangled: str) -> str: + """Categorize ESPHome core symbols into subcategories.""" + # Special patterns that need to be checked separately + if any(pattern in demangled for pattern in _CPP_RUNTIME_PATTERNS): + return "C++ Runtime (vtables/RTTI)" + + if demangled.startswith(_NAMESPACE_STD): + return "C++ STL" + + # Check against patterns from const.py + for category, patterns in CORE_SUBCATEGORY_PATTERNS.items(): + if any(pattern in demangled for pattern in patterns): + return category + + return "Other Core" + + +if __name__ == "__main__": + from .cli import main + + main() diff --git a/esphome/analyze_memory/__main__.py b/esphome/analyze_memory/__main__.py new file mode 100644 index 0000000000..aa772c3ad4 --- /dev/null +++ b/esphome/analyze_memory/__main__.py @@ -0,0 +1,6 @@ +"""Main entry point for running the memory analyzer as a module.""" + +from .cli import main + +if __name__ == "__main__": + main() diff --git a/esphome/analyze_memory/cli.py b/esphome/analyze_memory/cli.py new file mode 100644 index 0000000000..44ade221f8 --- /dev/null +++ b/esphome/analyze_memory/cli.py @@ -0,0 +1,435 @@ +"""CLI interface for memory analysis with report generation.""" + +from collections import defaultdict +import sys + +from . import ( + _COMPONENT_API, + _COMPONENT_CORE, + _COMPONENT_PREFIX_ESPHOME, + _COMPONENT_PREFIX_EXTERNAL, + MemoryAnalyzer, +) + + +class MemoryAnalyzerCLI(MemoryAnalyzer): + """Memory analyzer with CLI-specific report generation.""" + + # Symbol size threshold for detailed analysis + SYMBOL_SIZE_THRESHOLD: int = ( + 100 # Show symbols larger than this in detailed analysis + ) + + # Column width constants + COL_COMPONENT: int = 29 + COL_FLASH_TEXT: int = 14 + COL_FLASH_DATA: int = 14 + COL_RAM_DATA: int = 12 + COL_RAM_BSS: int = 12 + COL_TOTAL_FLASH: int = 15 + COL_TOTAL_RAM: int = 12 + COL_SEPARATOR: int = 3 # " | " + + # Core analysis column widths + COL_CORE_SUBCATEGORY: int = 30 + COL_CORE_SIZE: int = 12 + COL_CORE_COUNT: int = 6 + COL_CORE_PERCENT: int = 10 + + # Calculate table width once at class level + TABLE_WIDTH: int = ( + COL_COMPONENT + + COL_SEPARATOR + + COL_FLASH_TEXT + + COL_SEPARATOR + + COL_FLASH_DATA + + COL_SEPARATOR + + COL_RAM_DATA + + COL_SEPARATOR + + COL_RAM_BSS + + COL_SEPARATOR + + COL_TOTAL_FLASH + + COL_SEPARATOR + + COL_TOTAL_RAM + ) + + @staticmethod + def _make_separator_line(*widths: int) -> str: + """Create a separator line with given column widths. + + Args: + widths: Column widths to create separators for + + Returns: + Separator line like "----+---------+-----" + """ + return "-+-".join("-" * width for width in widths) + + # Pre-computed separator lines + MAIN_TABLE_SEPARATOR: str = _make_separator_line( + COL_COMPONENT, + COL_FLASH_TEXT, + COL_FLASH_DATA, + COL_RAM_DATA, + COL_RAM_BSS, + COL_TOTAL_FLASH, + COL_TOTAL_RAM, + ) + + CORE_TABLE_SEPARATOR: str = _make_separator_line( + COL_CORE_SUBCATEGORY, + COL_CORE_SIZE, + COL_CORE_COUNT, + COL_CORE_PERCENT, + ) + + def generate_report(self, detailed: bool = False) -> str: + """Generate a formatted memory report.""" + components = sorted( + self.components.items(), key=lambda x: x[1].flash_total, reverse=True + ) + + # Calculate totals + total_flash = sum(c.flash_total for _, c in components) + total_ram = sum(c.ram_total for _, c in components) + + # Build report + lines: list[str] = [] + + lines.append("=" * self.TABLE_WIDTH) + lines.append("Component Memory Analysis".center(self.TABLE_WIDTH)) + lines.append("=" * self.TABLE_WIDTH) + lines.append("") + + # Main table - fixed column widths + lines.append( + f"{'Component':<{self.COL_COMPONENT}} | {'Flash (text)':>{self.COL_FLASH_TEXT}} | {'Flash (data)':>{self.COL_FLASH_DATA}} | {'RAM (data)':>{self.COL_RAM_DATA}} | {'RAM (bss)':>{self.COL_RAM_BSS}} | {'Total Flash':>{self.COL_TOTAL_FLASH}} | {'Total RAM':>{self.COL_TOTAL_RAM}}" + ) + lines.append(self.MAIN_TABLE_SEPARATOR) + + for name, mem in components: + if mem.flash_total > 0 or mem.ram_total > 0: + flash_rodata = mem.rodata_size + mem.data_size + lines.append( + f"{name:<{self.COL_COMPONENT}} | {mem.text_size:>{self.COL_FLASH_TEXT - 2},} B | {flash_rodata:>{self.COL_FLASH_DATA - 2},} B | " + f"{mem.data_size:>{self.COL_RAM_DATA - 2},} B | {mem.bss_size:>{self.COL_RAM_BSS - 2},} B | " + f"{mem.flash_total:>{self.COL_TOTAL_FLASH - 2},} B | {mem.ram_total:>{self.COL_TOTAL_RAM - 2},} B" + ) + + lines.append(self.MAIN_TABLE_SEPARATOR) + lines.append( + f"{'TOTAL':<{self.COL_COMPONENT}} | {' ':>{self.COL_FLASH_TEXT}} | {' ':>{self.COL_FLASH_DATA}} | " + f"{' ':>{self.COL_RAM_DATA}} | {' ':>{self.COL_RAM_BSS}} | " + f"{total_flash:>{self.COL_TOTAL_FLASH - 2},} B | {total_ram:>{self.COL_TOTAL_RAM - 2},} B" + ) + + # Top consumers + lines.append("") + lines.append("Top Flash Consumers:") + for i, (name, mem) in enumerate(components[:25]): + if mem.flash_total > 0: + percentage = ( + (mem.flash_total / total_flash * 100) if total_flash > 0 else 0 + ) + lines.append( + f"{i + 1}. {name} ({mem.flash_total:,} B) - {percentage:.1f}% of analyzed flash" + ) + + lines.append("") + lines.append("Top RAM Consumers:") + ram_components = sorted(components, key=lambda x: x[1].ram_total, reverse=True) + for i, (name, mem) in enumerate(ram_components[:25]): + if mem.ram_total > 0: + percentage = (mem.ram_total / total_ram * 100) if total_ram > 0 else 0 + lines.append( + f"{i + 1}. {name} ({mem.ram_total:,} B) - {percentage:.1f}% of analyzed RAM" + ) + + lines.append("") + lines.append( + "Note: This analysis covers symbols in the ELF file. Some runtime allocations may not be included." + ) + lines.append("=" * self.TABLE_WIDTH) + + # Add ESPHome core detailed analysis if there are core symbols + if self._esphome_core_symbols: + lines.append("") + lines.append("=" * self.TABLE_WIDTH) + lines.append( + f"{_COMPONENT_CORE} Detailed Analysis".center(self.TABLE_WIDTH) + ) + lines.append("=" * self.TABLE_WIDTH) + lines.append("") + + # Group core symbols by subcategory + core_subcategories: dict[str, list[tuple[str, str, int]]] = defaultdict( + list + ) + + for symbol, demangled, size in self._esphome_core_symbols: + # Categorize based on demangled name patterns + subcategory = self._categorize_esphome_core_symbol(demangled) + core_subcategories[subcategory].append((symbol, demangled, size)) + + # Sort subcategories by total size + sorted_subcategories = sorted( + [ + (name, symbols, sum(s[2] for s in symbols)) + for name, symbols in core_subcategories.items() + ], + key=lambda x: x[2], + reverse=True, + ) + + lines.append( + f"{'Subcategory':<{self.COL_CORE_SUBCATEGORY}} | {'Size':>{self.COL_CORE_SIZE}} | " + f"{'Count':>{self.COL_CORE_COUNT}} | {'% of Core':>{self.COL_CORE_PERCENT}}" + ) + lines.append(self.CORE_TABLE_SEPARATOR) + + core_total = sum(size for _, _, size in self._esphome_core_symbols) + + for subcategory, symbols, total_size in sorted_subcategories: + percentage = (total_size / core_total * 100) if core_total > 0 else 0 + lines.append( + f"{subcategory:<{self.COL_CORE_SUBCATEGORY}} | {total_size:>{self.COL_CORE_SIZE - 2},} B | " + f"{len(symbols):>{self.COL_CORE_COUNT}} | {percentage:>{self.COL_CORE_PERCENT - 1}.1f}%" + ) + + # All core symbols above threshold + lines.append("") + sorted_core_symbols = sorted( + self._esphome_core_symbols, key=lambda x: x[2], reverse=True + ) + large_core_symbols = [ + (symbol, demangled, size) + for symbol, demangled, size in sorted_core_symbols + if size > self.SYMBOL_SIZE_THRESHOLD + ] + + lines.append( + f"{_COMPONENT_CORE} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B ({len(large_core_symbols)} symbols):" + ) + for i, (symbol, demangled, size) in enumerate(large_core_symbols): + lines.append(f"{i + 1}. {demangled} ({size:,} B)") + + lines.append("=" * self.TABLE_WIDTH) + + # Add detailed analysis for top ESPHome and external components + esphome_components = [ + (name, mem) + for name, mem in components + if name.startswith(_COMPONENT_PREFIX_ESPHOME) and name != _COMPONENT_CORE + ] + external_components = [ + (name, mem) + for name, mem in components + if name.startswith(_COMPONENT_PREFIX_EXTERNAL) + ] + + top_esphome_components = sorted( + esphome_components, key=lambda x: x[1].flash_total, reverse=True + )[:30] + + # Include all external components (they're usually important) + top_external_components = sorted( + external_components, key=lambda x: x[1].flash_total, reverse=True + ) + + # Check if API component exists and ensure it's included + api_component = None + for name, mem in components: + if name == _COMPONENT_API: + api_component = (name, mem) + break + + # Also include wifi_stack and other important system components if they exist + system_components_to_include = [ + # Empty list - we've finished debugging symbol categorization + # Add component names here if you need to debug their symbols + ] + system_components = [ + (name, mem) + for name, mem in components + if name in system_components_to_include + ] + + # Combine all components to analyze: top ESPHome + all external + API if not already included + system components + components_to_analyze = ( + list(top_esphome_components) + + list(top_external_components) + + system_components + ) + if api_component and api_component not in components_to_analyze: + components_to_analyze.append(api_component) + + if components_to_analyze: + for comp_name, comp_mem in components_to_analyze: + if not (comp_symbols := self._component_symbols.get(comp_name, [])): + continue + lines.append("") + lines.append("=" * self.TABLE_WIDTH) + lines.append(f"{comp_name} Detailed Analysis".center(self.TABLE_WIDTH)) + lines.append("=" * self.TABLE_WIDTH) + lines.append("") + + # Sort symbols by size + sorted_symbols = sorted(comp_symbols, key=lambda x: x[2], reverse=True) + + lines.append(f"Total symbols: {len(sorted_symbols)}") + lines.append(f"Total size: {comp_mem.flash_total:,} B") + lines.append("") + + # Show all symbols above threshold for better visibility + large_symbols = [ + (sym, dem, size) + for sym, dem, size in sorted_symbols + if size > self.SYMBOL_SIZE_THRESHOLD + ] + + lines.append( + f"{comp_name} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B ({len(large_symbols)} symbols):" + ) + for i, (symbol, demangled, size) in enumerate(large_symbols): + lines.append(f"{i + 1}. {demangled} ({size:,} B)") + + lines.append("=" * self.TABLE_WIDTH) + + return "\n".join(lines) + + def dump_uncategorized_symbols(self, output_file: str | None = None) -> None: + """Dump uncategorized symbols for analysis.""" + # Sort by size descending + sorted_symbols = sorted( + self._uncategorized_symbols, key=lambda x: x[2], reverse=True + ) + + lines = ["Uncategorized Symbols Analysis", "=" * 80] + lines.append(f"Total uncategorized symbols: {len(sorted_symbols)}") + lines.append( + f"Total uncategorized size: {sum(s[2] for s in sorted_symbols):,} bytes" + ) + lines.append("") + lines.append(f"{'Size':>10} | {'Symbol':<60} | Demangled") + lines.append("-" * 10 + "-+-" + "-" * 60 + "-+-" + "-" * 40) + + for symbol, demangled, size in sorted_symbols[:100]: # Top 100 + demangled_display = ( + demangled[:100] if symbol != demangled else "[not demangled]" + ) + lines.append(f"{size:>10,} | {symbol[:60]:<60} | {demangled_display}") + + if len(sorted_symbols) > 100: + lines.append(f"\n... and {len(sorted_symbols) - 100} more symbols") + + content = "\n".join(lines) + + if output_file: + with open(output_file, "w", encoding="utf-8") as f: + f.write(content) + else: + print(content) + + +def analyze_elf( + elf_path: str, + objdump_path: str | None = None, + readelf_path: str | None = None, + detailed: bool = False, + external_components: set[str] | None = None, +) -> str: + """Analyze an ELF file and return a memory report.""" + analyzer = MemoryAnalyzerCLI( + elf_path, objdump_path, readelf_path, external_components + ) + analyzer.analyze() + return analyzer.generate_report(detailed) + + +def main(): + """CLI entrypoint for memory analysis.""" + if len(sys.argv) < 2: + print("Usage: python -m esphome.analyze_memory ") + print("\nAnalyze memory usage from an ESPHome build directory.") + print("The build directory should contain firmware.elf and idedata will be") + print("loaded from ~/.esphome/.internal/idedata/.json") + print("\nExamples:") + print(" python -m esphome.analyze_memory ~/.esphome/build/my-device") + print(" python -m esphome.analyze_memory .esphome/build/my-device") + print(" python -m esphome.analyze_memory my-device # Short form") + sys.exit(1) + + build_dir = sys.argv[1] + + # Load build directory + import json + from pathlib import Path + + from esphome.platformio_api import IDEData + + build_path = Path(build_dir) + + # If no path separator in name, assume it's a device name + if "/" not in build_dir and not build_path.is_dir(): + # Try current directory first + cwd_path = Path.cwd() / ".esphome" / "build" / build_dir + if cwd_path.is_dir(): + build_path = cwd_path + print(f"Using build directory: {build_path}", file=sys.stderr) + else: + # Fall back to home directory + build_path = Path.home() / ".esphome" / "build" / build_dir + print(f"Using build directory: {build_path}", file=sys.stderr) + + if not build_path.is_dir(): + print(f"Error: {build_path} is not a directory", file=sys.stderr) + sys.exit(1) + + # Find firmware.elf + elf_file = None + for elf_candidate in [ + build_path / "firmware.elf", + build_path / ".pioenvs" / build_path.name / "firmware.elf", + ]: + if elf_candidate.exists(): + elf_file = str(elf_candidate) + break + + if not elf_file: + print(f"Error: firmware.elf not found in {build_dir}", file=sys.stderr) + sys.exit(1) + + # Find idedata.json - check current directory first, then home + device_name = build_path.name + idedata_candidates = [ + Path.cwd() / ".esphome" / "idedata" / f"{device_name}.json", + Path.home() / ".esphome" / "idedata" / f"{device_name}.json", + ] + + idedata = None + for idedata_path in idedata_candidates: + if not idedata_path.exists(): + continue + try: + with open(idedata_path, encoding="utf-8") as f: + raw_data = json.load(f) + idedata = IDEData(raw_data) + print(f"Loaded idedata from: {idedata_path}", file=sys.stderr) + break + except (json.JSONDecodeError, OSError) as e: + print(f"Warning: Failed to load idedata: {e}", file=sys.stderr) + + if not idedata: + print( + f"Warning: idedata not found (searched {idedata_candidates[0]} and {idedata_candidates[1]})", + file=sys.stderr, + ) + + analyzer = MemoryAnalyzerCLI(elf_file, idedata=idedata) + analyzer.analyze() + report = analyzer.generate_report() + print(report) + + +if __name__ == "__main__": + main() diff --git a/esphome/analyze_memory/const.py b/esphome/analyze_memory/const.py new file mode 100644 index 0000000000..78af82059f --- /dev/null +++ b/esphome/analyze_memory/const.py @@ -0,0 +1,1052 @@ +"""Constants for memory analysis symbol pattern matching.""" + +import re + +# Pattern to extract ESPHome component namespaces dynamically +ESPHOME_COMPONENT_PATTERN = re.compile(r"esphome::([a-zA-Z0-9_]+)::") + +# Section mapping for ELF file sections +# Maps standard section names to their various platform-specific variants +SECTION_MAPPING = { + ".text": frozenset([".text", ".iram"]), + ".rodata": frozenset([".rodata"]), + ".data": frozenset([".data", ".dram"]), + ".bss": frozenset([".bss"]), +} + +# Section to ComponentMemory attribute mapping +# Maps section names to the attribute name in ComponentMemory dataclass +SECTION_TO_ATTR = { + ".text": "text_size", + ".rodata": "rodata_size", + ".data": "data_size", + ".bss": "bss_size", +} + +# Component identification rules +# Symbol patterns: patterns found in raw symbol names +SYMBOL_PATTERNS = { + "freertos": [ + "vTask", + "xTask", + "xQueue", + "pvPort", + "vPort", + "uxTask", + "pcTask", + "prvTimerTask", + "prvAddNewTaskToReadyList", + "pxReadyTasksLists", + "prvAddCurrentTaskToDelayedList", + "xEventGroupWaitBits", + "xRingbufferSendFromISR", + "prvSendItemDoneNoSplit", + "prvReceiveGeneric", + "prvSendAcquireGeneric", + "prvCopyItemAllowSplit", + "xEventGroup", + "xRingbuffer", + "prvSend", + "prvReceive", + "prvCopy", + "xPort", + "ulTaskGenericNotifyTake", + "prvIdleTask", + "prvInitialiseNewTask", + "prvIsYieldRequiredSMP", + "prvGetItemByteBuf", + "prvInitializeNewRingbuffer", + "prvAcquireItemNoSplit", + "prvNotifyQueueSetContainer", + "ucStaticTimerQueueStorage", + "eTaskGetState", + "main_task", + "do_system_init_fn", + "xSemaphoreCreateGenericWithCaps", + "vListInsert", + "uxListRemove", + "vRingbufferReturnItem", + "vRingbufferReturnItemFromISR", + "prvCheckItemFitsByteBuffer", + "prvGetCurMaxSizeAllowSplit", + "tick_hook", + "sys_sem_new", + "sys_arch_mbox_fetch", + "sys_arch_sem_wait", + "prvDeleteTCB", + "vQueueDeleteWithCaps", + "vRingbufferDeleteWithCaps", + "vSemaphoreDeleteWithCaps", + "prvCheckItemAvail", + "prvCheckTaskCanBeScheduledSMP", + "prvGetCurMaxSizeNoSplit", + "prvResetNextTaskUnblockTime", + "prvReturnItemByteBuf", + "vApplicationStackOverflowHook", + "vApplicationGetIdleTaskMemory", + "sys_init", + "sys_mbox_new", + "sys_arch_mbox_tryfetch", + ], + "xtensa": ["xt_", "_xt_", "xPortEnterCriticalTimeout"], + "heap": ["heap_", "multi_heap"], + "spi_flash": ["spi_flash"], + "rtc": ["rtc_", "rtcio_ll_"], + "gpio_driver": ["gpio_", "pins"], + "uart_driver": ["uart", "_uart", "UART"], + "timer": ["timer_", "esp_timer"], + "peripherals": ["periph_", "periman"], + "network_stack": [ + "vj_compress", + "raw_sendto", + "raw_input", + "etharp_", + "icmp_input", + "socket_ipv6", + "ip_napt", + "socket_ipv4_multicast", + "socket_ipv6_multicast", + "netconn_", + "recv_raw", + "accept_function", + "netconn_recv_data", + "netconn_accept", + "netconn_write_vectors_partly", + "netconn_drain", + "raw_connect", + "raw_bind", + "icmp_send_response", + "sockets", + "icmp_dest_unreach", + "inet_chksum_pseudo", + "alloc_socket", + "done_socket", + "set_global_fd_sets", + "inet_chksum_pbuf", + "tryget_socket_unconn_locked", + "tryget_socket_unconn", + "cs_create_ctrl_sock", + "netbuf_alloc", + "tcp_", # TCP protocol functions + "udp_", # UDP protocol functions + "lwip_", # LwIP stack functions + "eagle_lwip", # ESP-specific LwIP functions + "new_linkoutput", # Link output function + "acd_", # Address Conflict Detection (ACD) + "eth_", # Ethernet functions + "mac_enable_bb", # MAC baseband enable + "reassemble_and_dispatch", # Packet reassembly + ], + # dhcp must come before libc to avoid "dhcp_select" matching "select" pattern + "dhcp": ["dhcp", "handle_dhcp"], + "ipv6_stack": ["nd6_", "ip6_", "mld6_", "icmp6_", "icmp6_input"], + # Order matters! More specific categories must come before general ones. + # mdns must come before bluetooth to avoid "_mdns_disable_pcb" matching "ble_" pattern + "mdns_lib": ["mdns"], + # memory_mgmt must come before wifi_stack to catch mmu_hal_* symbols + "memory_mgmt": [ + "mem_", + "memory_", + "tlsf_", + "memp_", + "pbuf_", + "pbuf_alloc", + "pbuf_copy_partial_pbuf", + "esp_mmu_map", + "mmu_hal_", + "s_do_mapping", # Memory mapping function, not WiFi + "hash_map_", # Hash map data structure + "umm_assimilate", # UMM malloc assimilation + ], + # Bluetooth categories must come BEFORE wifi_stack to avoid misclassification + # Many BLE symbols contain patterns like "ble_" that would otherwise match wifi patterns + "bluetooth_rom": ["r_ble", "r_lld", "r_llc", "r_llm"], + "bluedroid_bt": [ + "bluedroid", + "btc_", + "bta_", + "btm_", + "btu_", + "BTM_", + "GATT", + "L2CA_", + "smp_", + "gatts_", + "attp_", + "l2cu_", + "l2cb", + "smp_cb", + "BTA_GATTC_", + "SMP_", + "BTU_", + "BTA_Dm", + "GAP_Ble", + "BT_tx_if", + "host_recv_pkt_cb", + "saved_local_oob_data", + "string_to_bdaddr", + "string_is_bdaddr", + "CalConnectParamTimeout", + "transmit_fragment", + "transmit_data", + "event_command_ready", + "read_command_complete_header", + "parse_read_local_extended_features_response", + "parse_read_local_version_info_response", + "should_request_high", + "btdm_wakeup_request", + "BTA_SetAttributeValue", + "BTA_EnableBluetooth", + "transmit_command_futured", + "transmit_command", + "get_waiting_command", + "make_command", + "transmit_downward", + "host_recv_adv_packet", + "copy_extra_byte_in_db", + "parse_read_local_supported_commands_response", + ], + "bluetooth": [ + "bt_", + "_ble_", # More specific than "ble_" to avoid matching "able_", "enable_", "disable_" + "l2c_", + "l2ble_", # L2CAP for BLE + "gatt_", + "gap_", + "hci_", + "btsnd_hcic_", # Bluetooth HCI command send functions + "BT_init", + "BT_tx_", # Bluetooth transmit functions + "esp_ble_", # Catch esp_ble_* functions + ], + "bluetooth_ll": [ + "llm_", # Link layer manager + "llc_", # Link layer control + "lld_", # Link layer driver + "ld_acl_", # Link layer ACL (Asynchronous Connection-Oriented) + "llcp_", # Link layer control protocol + "lmp_", # Link manager protocol + ], + "wifi_bt_coex": ["coex"], + "wifi_stack": [ + "ieee80211", + "hostap", + "sta_", + "wifi_ap_", # More specific than "ap_" to avoid matching "cap_", "map_" + "wifi_scan_", # More specific than "scan_" to avoid matching "_scan_" in other contexts + "wifi_", + "wpa_", + "wps_", + "esp_wifi", + "cnx_", + "wpa3_", + "sae_", + "wDev_", + "ic_mac_", # More specific than "mac_" to avoid matching emac_ + "esf_buf", + "gWpaSm", + "sm_WPA", + "eapol_", + "owe_", + "wifiLowLevelInit", + # Removed "s_do_mapping" - this is memory management, not WiFi + "gScanStruct", + "ppSearchTxframe", + "ppMapWaitTxq", + "ppFillAMPDUBar", + "ppCheckTxConnTrafficIdle", + "ppCalTkipMic", + "phy_force_wifi", + "phy_unforce_wifi", + "write_wifi_chan", + "wifi_track_pll", + ], + "crypto_math": [ + "ecp_", + "bignum_", + "mpi_", + "sswu", + "modp", + "dragonfly_", + "gcm_mult", + "__multiply", + "quorem", + "__mdiff", + "__lshift", + "__mprec_tens", + "ECC_", + "multiprecision_", + "mix_sub_columns", + "sbox", + "gfm2_sbox", + "gfm3_sbox", + "curve_p256", + "curve", + "p_256_init_curve", + "shift_sub_rows", + "rshift", + "rijndaelEncrypt", # AES Rijndael encryption + ], + # System and Arduino core functions must come before libc + "esp_system": [ + "system_", # ESP system functions + "postmortem_", # Postmortem reporting + ], + "arduino_core": [ + "pinMode", + "resetPins", + "millis", + "micros", + "delay(", # More specific - Arduino delay function with parenthesis + "delayMicroseconds", + "digitalWrite", + "digitalRead", + ], + "sntp": ["sntp_", "sntp_recv"], + "scheduler": [ + "run_scheduled_", + "compute_scheduled_", + "event_TaskQueue", + ], + "hw_crypto": ["esp_aes", "esp_sha", "esp_rsa", "esp_bignum", "esp_mpi"], + "libc": [ + "printf", + "scanf", + "malloc", + "_free", # More specific than "free" to match _free, __free_r, etc. but not arbitrary "free" substring + "umm_free", # UMM malloc free function + "memcpy", + "memset", + "strcpy", + "strlen", + "_dtoa", + "_fopen", + "__sfvwrite_r", + "qsort", + "__sf", + "__sflush_r", + "__srefill_r", + "_impure_data", + "_reclaim_reent", + "_open_r", + "strncpy", + "_strtod_l", + "__gethex", + "__hexnan", + "_setenv_r", + "_tzset_unlocked_r", + "__tzcalc_limits", + "_select", # More specific than "select" to avoid matching "dhcp_select", etc. + "scalbnf", + "strtof", + "strtof_l", + "__d2b", + "__b2d", + "__s2b", + "_Balloc", + "__multadd", + "__lo0bits", + "__atexit0", + "__smakebuf_r", + "__swhatbuf_r", + "_sungetc_r", + "_close_r", + "_link_r", + "_unsetenv_r", + "_rename_r", + "__month_lengths", + "tzinfo", + "__ratio", + "__hi0bits", + "__ulp", + "__any_on", + "__copybits", + "L_shift", + "_fcntl_r", + "_lseek_r", + "_read_r", + "_write_r", + "_unlink_r", + "_fstat_r", + "access", + "fsync", + "tcsetattr", + "tcgetattr", + "tcflush", + "tcdrain", + "__ssrefill_r", + "_stat_r", + "__hexdig_fun", + "__mcmp", + "_fwalk_sglue", + "__fpclassifyf", + "_setlocale_r", + "_mbrtowc_r", + "fcntl", + "__match", + "_lock_close", + "__c$", + "__func__$", + "__FUNCTION__$", + "DAYS_IN_MONTH", + "_DAYS_BEFORE_MONTH", + "CSWTCH$", + "dst$", + "sulp", + "_strtol_l", # String to long with locale + "__cvt", # Convert + "__utoa", # Unsigned to ASCII + "__global_locale", # Global locale + "_ctype_", # Character type + "impure_data", # Impure data + ], + "string_ops": [ + "strcmp", + "strncmp", + "strchr", + "strstr", + "strtok", + "strdup", + "strncasecmp_P", # String compare (case insensitive, from program memory) + "strnlen_P", # String length (from program memory) + "strncat_P", # String concatenate (from program memory) + ], + "memory_alloc": ["malloc", "calloc", "realloc", "free", "_sbrk"], + "file_io": [ + "fread", + "fwrite", + "fopen", + "fclose", + "fseek", + "ftell", + "fflush", + "s_fd_table", + ], + "string_formatting": [ + "snprintf", + "vsnprintf", + "sprintf", + "vsprintf", + "sscanf", + "vsscanf", + ], + "cpp_anonymous": ["_GLOBAL__N_", "n$"], + # Plain C patterns only - C++ symbols will be categorized via DEMANGLED_PATTERNS + "nvs": ["nvs_"], # Plain C NVS functions + "ota": ["ota_", "OTA", "esp_ota", "app_desc"], + # cpp_runtime: Removed _ZN, _ZL to let DEMANGLED_PATTERNS categorize C++ symbols properly + # Only keep patterns that are truly runtime-specific and not categorizable by namespace + "cpp_runtime": ["__cxx", "_ZSt", "__gxx_personality", "_Z16"], + "exception_handling": [ + "__cxa_", + "_Unwind_", + "__gcc_personality", + "uw_frame_state", + "search_object", # Search for exception handling object + "get_cie_encoding", # Get CIE encoding + "add_fdes", # Add frame description entries + "fde_unencoded_compare", # Compare FDEs + "fde_mixed_encoding_compare", # Compare mixed encoding FDEs + "frame_downheap", # Frame heap operations + "frame_heapsort", # Frame heap sorting + ], + "static_init": ["_GLOBAL__sub_I_"], + "phy_radio": [ + "phy_", + "rf_", + "chip_", + "register_chipv7", + "pbus_", + "bb_", + "fe_", + "rfcal_", + "ram_rfcal", + "tx_pwctrl", + "rx_chan", + "set_rx_gain", + "set_chan", + "agc_reg", + "ram_txiq", + "ram_txdc", + "ram_gen_rx_gain", + "rx_11b_opt", + "set_rx_sense", + "set_rx_gain_cal", + "set_chan_dig_gain", + "tx_pwctrl_init_cal", + "rfcal_txiq", + "set_tx_gain_table", + "correct_rfpll_offset", + "pll_correct_dcap", + "txiq_cal_init", + "pwdet_sar", + "pwdet_sar2_init", + "ram_iq_est_enable", + "ram_rfpll_set_freq", + "ant_wifirx_cfg", + "ant_btrx_cfg", + "force_txrxoff", + "force_txrx_off", + "tx_paon_set", + "opt_11b_resart", + "rfpll_1p2_opt", + "ram_dc_iq_est", + "ram_start_tx_tone", + "ram_en_pwdet", + "ram_cbw2040_cfg", + "rxdc_est_min", + "i2cmst_reg_init", + "temprature_sens_read", + "ram_restart_cal", + "ram_write_gain_mem", + "ram_wait_rfpll_cal_end", + "txcal_debuge_mode", + "ant_wifitx_cfg", + "reg_init_begin", + "tx_cap_init", # TX capacitance init + "ram_set_txcap", # RAM TX capacitance setting + "tx_atten_", # TX attenuation + "txiq_", # TX I/Q calibration + "ram_cal_", # RAM calibration + "ram_rxiq_", # RAM RX I/Q + "readvdd33", # Read VDD33 + "test_tout", # Test timeout + "tsen_meas", # Temperature sensor measurement + "bbpll_cal", # Baseband PLL calibration + "set_cal_", # Set calibration + "set_rfanagain_", # Set RF analog gain + "set_txdc_", # Set TX DC + "get_vdd33_", # Get VDD33 + "gen_rx_gain_table", # Generate RX gain table + "ram_ana_inf_gating_en", # RAM analog interface gating enable + "tx_cont_en", # TX continuous enable + "tx_delay_cfg", # TX delay configuration + "tx_gain_table_set", # TX gain table set + "check_and_reset_hw_deadlock", # Hardware deadlock check + "s_config", # System/hardware config + "chan14_mic_cfg", # Channel 14 MIC config + ], + "wifi_phy_pp": [ + "pp_", + "ppT", + "ppR", + "ppP", + "ppInstall", + "ppCalTxAMPDULength", + "ppCheckTx", # Packet processor TX check + "ppCal", # Packet processor calibration + "HdlAllBuffedEb", # Handle buffered EB + ], + "wifi_lmac": ["lmac"], + "wifi_device": [ + "wdev", + "wDev_", + "ic_set_sta", # Set station mode + "ic_set_vif", # Set virtual interface + ], + "power_mgmt": [ + "pm_", + "sleep", + "rtc_sleep", + "light_sleep", + "deep_sleep", + "power_down", + "g_pm", + "pmc", # Power Management Controller + ], + "hal_layer": ["hal_"], + "clock_mgmt": [ + "clk_", + "clock_", + "rtc_clk", + "apb_", + "cpu_freq", + "setCpuFrequencyMhz", + ], + "cache_mgmt": ["cache"], + "flash_ops": ["flash", "image_load"], + "interrupt_handlers": [ + "isr", + "interrupt", + "intr_", + "exc_", + "exception", + "port_IntStack", + ], + "wrapper_functions": ["_wrapper"], + "error_handling": ["panic", "abort", "assert", "error_", "fault"], + "authentication": ["auth"], + "ppp_protocol": ["ppp", "ipcp_", "lcp_", "chap_", "LcpEchoCheck"], + "ethernet_phy": [ + "emac_", + "eth_phy_", + "phy_tlk110", + "phy_lan87", + "phy_ip101", + "phy_rtl", + "phy_dp83", + "phy_ksz", + "lan87xx_", + "rtl8201_", + "ip101_", + "ksz80xx_", + "jl1101_", + "dp83848_", + "eth_on_state_changed", + ], + "threading": ["pthread_", "thread_", "_task_"], + "pthread": ["pthread"], + "synchronization": ["mutex", "semaphore", "spinlock", "portMUX"], + "math_lib": [ + "sin", + "cos", + "tan", + "sqrt", + "pow", + "exp", + "log", + "atan", + "asin", + "acos", + "floor", + "ceil", + "fabs", + "round", + ], + "random": ["rand", "random", "rng_", "prng"], + "time_lib": [ + "time", + "clock", + "gettimeofday", + "settimeofday", + "localtime", + "gmtime", + "mktime", + "strftime", + ], + "console_io": ["console_", "uart_tx", "uart_rx", "puts", "putchar", "getchar"], + "rom_functions": ["r_", "rom_"], + "compiler_runtime": [ + "__divdi3", + "__udivdi3", + "__moddi3", + "__muldi3", + "__ashldi3", + "__ashrdi3", + "__lshrdi3", + "__cmpdi2", + "__fixdfdi", + "__floatdidf", + ], + "libgcc": ["libgcc", "_divdi3", "_udivdi3"], + "boot_startup": ["boot", "start_cpu", "call_start", "startup", "bootloader"], + "bootloader": ["bootloader_", "esp_bootloader"], + "app_framework": ["app_", "initArduino", "setup", "loop", "Update"], + "weak_symbols": ["__weak_"], + "compiler_builtins": ["__builtin_"], + "vfs": ["vfs_", "VFS"], + "esp32_sdk": ["esp32_", "esp32c", "esp32s"], + "usb": ["usb_", "USB", "cdc_", "CDC"], + "i2c_driver": ["i2c_", "I2C"], + "i2s_driver": ["i2s_", "I2S"], + "spi_driver": ["spi_", "SPI"], + "adc_driver": ["adc_", "ADC"], + "dac_driver": ["dac_", "DAC"], + "touch_driver": ["touch_", "TOUCH"], + "pwm_driver": ["pwm_", "PWM", "ledc_", "LEDC"], + "rmt_driver": ["rmt_", "RMT"], + "pcnt_driver": ["pcnt_", "PCNT"], + "can_driver": ["can_", "CAN", "twai_", "TWAI"], + "sdmmc_driver": ["sdmmc_", "SDMMC", "sdcard", "sd_card"], + "temp_sensor": ["temp_sensor", "tsens_"], + "watchdog": ["wdt_", "WDT", "watchdog"], + "brownout": ["brownout", "bod_"], + "ulp": ["ulp_", "ULP"], + "psram": ["psram", "PSRAM", "spiram", "SPIRAM"], + "efuse": ["efuse", "EFUSE"], + "partition": ["partition", "esp_partition"], + "esp_event": ["esp_event", "event_loop", "event_callback"], + "esp_console": ["esp_console", "console_"], + "chip_specific": ["chip_", "esp_chip"], + "esp_system_utils": ["esp_system", "esp_hw", "esp_clk", "esp_sleep"], + "ipc": ["esp_ipc", "ipc_"], + "wifi_config": [ + "g_cnxMgr", + "gChmCxt", + "g_ic", + "TxRxCxt", + "s_dp", + "s_ni", + "s_reg_dump", + "packet$", + "d_mult_table", + "K", + "fcstab", + ], + "smartconfig": ["sc_ack_send"], + "rc_calibration": ["rc_cal", "rcUpdate"], + "noise_floor": ["noise_check"], + "rf_calibration": [ + "set_rx_sense", + "set_rx_gain_cal", + "set_chan_dig_gain", + "tx_pwctrl_init_cal", + "rfcal_txiq", + "set_tx_gain_table", + "correct_rfpll_offset", + "pll_correct_dcap", + "txiq_cal_init", + "pwdet_sar", + "rx_11b_opt", + ], + "wifi_crypto": [ + "pk_use_ecparams", + "process_segments", + "ccmp_", + "rc4_", + "aria_", + "mgf_mask", + "dh_group", + "ccmp_aad_nonce", + "ccmp_encrypt", + "rc4_skip", + "aria_sb1", + "aria_sb2", + "aria_is1", + "aria_is2", + "aria_sl", + "aria_a", + ], + "radio_control": ["fsm_input", "fsm_sconfreq"], + "pbuf": [ + "pbuf_", + ], + "event_group": ["xEventGroup"], + "ringbuffer": ["xRingbuffer", "prvSend", "prvReceive", "prvCopy"], + "provisioning": ["prov_", "prov_stop_and_notify"], + "scan": ["gScanStruct"], + "port": ["xPort"], + "elf_loader": [ + "elf_add", + "elf_add_note", + "elf_add_segment", + "process_image", + "read_encoded", + "read_encoded_value", + "read_encoded_value_with_base", + "process_image_header", + ], + "socket_api": [ + "sockets", + "netconn_", + "accept_function", + "recv_raw", + "socket_ipv4_multicast", + "socket_ipv6_multicast", + ], + "igmp": ["igmp_", "igmp_send", "igmp_input"], + "icmp6": ["icmp6_"], + "arp": ["arp_table"], + "ampdu": [ + "ampdu_", + "rcAmpdu", + "trc_onAmpduOp", + "rcAmpduLowerRate", + "ampdu_dispatch_upto", + ], + "ieee802_11": ["ieee802_11_", "ieee802_11_parse_elems"], + "rate_control": [ + "rssi_margin", + "rcGetSched", + "get_rate_fcc_index", + "rcGetRate", # Get rate + "rc_get_", # Rate control getters + "rc_set_", # Rate control setters + "rc_enable_", # Rate control enable functions + ], + "nan": ["nan_dp_", "nan_dp_post_tx", "nan_dp_delete_peer"], + "channel_mgmt": ["chm_init", "chm_set_current_channel"], + "trace": ["trc_init", "trc_onAmpduOp"], + "country_code": ["country_info", "country_info_24ghz"], + "multicore": ["do_multicore_settings"], + "Update_lib": ["Update"], + "stdio": [ + "__sf", + "__sflush_r", + "__srefill_r", + "_impure_data", + "_reclaim_reent", + "_open_r", + ], + "strncpy_ops": ["strncpy"], + "math_internal": ["__mdiff", "__lshift", "__mprec_tens", "quorem"], + "character_class": ["__chclass"], + "camellia": ["camellia_", "camellia_feistel"], + "crypto_tables": ["FSb", "FSb2", "FSb3", "FSb4"], + "event_buffer": ["g_eb_list_desc", "eb_space"], + "base_node": ["base_node_", "base_node_add_handler"], + "file_descriptor": ["s_fd_table"], + "tx_delay": ["tx_delay_cfg"], + "deinit": ["deinit_functions"], + "lcp_echo": ["LcpEchoCheck"], + "raw_api": ["raw_bind", "raw_connect"], + "checksum": ["process_checksum"], + "entry_management": ["add_entry"], + "esp_ota": ["esp_ota", "ota_", "read_otadata"], + "http_server": [ + "httpd_", + "parse_url_char", + "cb_headers_complete", + "delete_entry", + "validate_structure", + "config_save", + "config_new", + "verify_url", + "cb_url", + ], + "misc_system": [ + "alarm_cbs", + "start_up", + "tokens", + "unhex", + "osi_funcs_ro", + "enum_function", + "fragment_and_dispatch", + "alarm_set", + "osi_alarm_new", + "config_set_string", + "config_update_newest_section", + "config_remove_key", + "method_strings", + "interop_match", + "interop_database", + "__state_table", + "__action_table", + "s_stub_table", + "s_context", + "s_mmu_ctx", + "s_get_bus_mask", + "hli_queue_put", + "list_remove", + "list_delete", + "lock_acquire_generic", + "is_vect_desc_usable", + "io_mode_str", + "__c$20233", + "interface", + "read_id_core", + "subscribe_idle", + "unsubscribe_idle", + "s_clkout_handle", + "lock_release_generic", + "config_set_int", + "config_get_int", + "config_get_string", + "config_has_key", + "config_remove_section", + "osi_alarm_init", + "osi_alarm_deinit", + "fixed_queue_enqueue", + "fixed_queue_dequeue", + "fixed_queue_new", + "fixed_pkt_queue_enqueue", + "fixed_pkt_queue_new", + "list_append", + "list_prepend", + "list_insert_after", + "list_contains", + "list_get_node", + "hash_function_blob", + "cb_no_body", + "cb_on_body", + "profile_tab", + "get_arg", + "trim", + "buf$", + "process_appended_hash_and_sig$constprop$0", + "uuidType", + "allocate_svc_db_buf", + "_hostname_is_ours", + "s_hli_handlers", + "tick_cb", + "idle_cb", + "input", + "entry_find", + "section_find", + "find_bucket_entry_", + "config_has_section", + "hli_queue_create", + "hli_queue_get", + "hli_c_handler", + "future_ready", + "future_await", + "future_new", + "pkt_queue_enqueue", + "pkt_queue_dequeue", + "pkt_queue_cleanup", + "pkt_queue_create", + "pkt_queue_destroy", + "fixed_pkt_queue_dequeue", + "osi_alarm_cancel", + "osi_alarm_is_active", + "osi_sem_take", + "osi_event_create", + "osi_event_bind", + "alarm_cb_handler", + "list_foreach", + "list_back", + "list_front", + "list_clear", + "fixed_queue_try_peek_first", + "translate_path", + "get_idx", + "find_key", + "init", + "end", + "start", + "set_read_value", + "copy_address_list", + "copy_and_key", + "sdk_cfg_opts", + "leftshift_onebit", + "config_section_end", + "config_section_begin", + "find_entry_and_check_all_reset", + "image_validate", + "xPendingReadyList", + "vListInitialise", + "lock_init_generic", + "ant_bttx_cfg", + "ant_dft_cfg", + "cs_send_to_ctrl_sock", + "config_llc_util_funcs_reset", + "make_set_adv_report_flow_control", + "make_set_event_mask", + "raw_new", + "raw_remove", + "BTE_InitStack", + "parse_read_local_supported_features_response", + "__math_invalidf", + "tinytens", + "__mprec_tinytens", + "__mprec_bigtens", + "vRingbufferDelete", + "vRingbufferDeleteWithCaps", + "vRingbufferReturnItem", + "vRingbufferReturnItemFromISR", + "get_acl_data_size_ble", + "get_features_ble", + "get_features_classic", + "get_acl_packet_size_ble", + "get_acl_packet_size_classic", + "supports_extended_inquiry_response", + "supports_rssi_with_inquiry_results", + "supports_interlaced_inquiry_scan", + "supports_reading_remote_extended_features", + ], +} + +# Demangled patterns: patterns found in demangled C++ names +DEMANGLED_PATTERNS = { + "gpio_driver": ["GPIO"], + "uart_driver": ["UART"], + # mdns_lib must come before network_stack to avoid "udp" matching "_udpReadBuffer" in MDNSResponder + "mdns_lib": [ + "MDNSResponder", + "MDNSImplementation", + "MDNS", + ], + "network_stack": [ + "lwip", + "tcp", + "udp", + "ip4", + "ip6", + "dhcp", + "dns", + "netif", + "ethernet", + "ppp", + "slip", + "UdpContext", # UDP context class + "DhcpServer", # DHCP server class + ], + "arduino_core": [ + "String::", # Arduino String class + "Print::", # Arduino Print class + "HardwareSerial::", # Serial class + "IPAddress::", # IP address class + "EspClass::", # ESP class + "experimental::_SPI", # Experimental SPI + ], + "ota": [ + "UpdaterClass", + "Updater::", + ], + "wifi": [ + "ESP8266WiFi", + "WiFi::", + ], + "wifi_stack": ["NetworkInterface"], + "nimble_bt": [ + "nimble", + "NimBLE", + "ble_hs", + "ble_gap", + "ble_gatt", + "ble_att", + "ble_l2cap", + "ble_sm", + ], + "crypto": ["mbedtls", "crypto", "sha", "aes", "rsa", "ecc", "tls", "ssl"], + "cpp_stdlib": ["std::", "__gnu_cxx::", "__cxxabiv"], + "static_init": ["__static_initialization"], + "rtti": ["__type_info", "__class_type_info"], + "web_server_lib": ["AsyncWebServer", "AsyncWebHandler", "WebServer"], + "async_tcp": ["AsyncClient", "AsyncServer"], + "json_lib": [ + "ArduinoJson", + "JsonDocument", + "JsonArray", + "JsonObject", + "deserialize", + "serialize", + ], + "http_lib": ["HTTP", "http_", "Request", "Response", "Uri", "WebSocket"], + "logging": ["log", "Log", "print", "Print", "diag_"], + "authentication": ["checkDigestAuthentication"], + "libgcc": ["libgcc"], + "esp_system": ["esp_", "ESP"], + "arduino": ["arduino"], + "nvs": ["nvs_", "_ZTVN3nvs", "nvs::"], + "filesystem": ["spiffs", "vfs"], + "libc": ["newlib"], +} + +# Patterns for categorizing ESPHome core symbols into subcategories +CORE_SUBCATEGORY_PATTERNS = { + "Component Framework": ["Component"], + "Application Core": ["Application"], + "Scheduler": ["Scheduler"], + "Component Iterator": ["ComponentIterator"], + "Helper Functions": ["Helpers", "helpers"], + "Preferences/Storage": ["Preferences", "ESPPreferences"], + "I/O Utilities": ["HighFrequencyLoopRequester"], + "String Utilities": ["str_"], + "Bit Utilities": ["reverse_bits"], + "Data Conversion": ["convert_"], + "Network Utilities": ["network", "IPAddress"], + "API Protocol": ["api::"], + "WiFi Manager": ["wifi::"], + "MQTT Client": ["mqtt::"], + "Logger": ["logger::"], + "OTA Updates": ["ota::"], + "Web Server": ["web_server::"], + "Time Management": ["time::"], + "Sensor Framework": ["sensor::"], + "Binary Sensor": ["binary_sensor::"], + "Switch Framework": ["switch_::"], + "Light Framework": ["light::"], + "Climate Framework": ["climate::"], + "Cover Framework": ["cover::"], +} diff --git a/esphome/analyze_memory/helpers.py b/esphome/analyze_memory/helpers.py new file mode 100644 index 0000000000..cb503b37c5 --- /dev/null +++ b/esphome/analyze_memory/helpers.py @@ -0,0 +1,121 @@ +"""Helper functions for memory analysis.""" + +from functools import cache +from pathlib import Path + +from .const import SECTION_MAPPING + +# Import namespace constant from parent module +# Note: This would create a circular import if done at module level, +# so we'll define it locally here as well +_NAMESPACE_ESPHOME = "esphome::" + + +# Get the list of actual ESPHome components by scanning the components directory +@cache +def get_esphome_components(): + """Get set of actual ESPHome components from the components directory.""" + # Find the components directory relative to this file + # Go up two levels from analyze_memory/helpers.py to esphome/ + current_dir = Path(__file__).parent.parent + components_dir = current_dir / "components" + + if not components_dir.exists() or not components_dir.is_dir(): + return frozenset() + + return frozenset( + item.name + for item in components_dir.iterdir() + if item.is_dir() + and not item.name.startswith(".") + and not item.name.startswith("__") + ) + + +@cache +def get_component_class_patterns(component_name: str) -> list[str]: + """Generate component class name patterns for symbol matching. + + Args: + component_name: The component name (e.g., "ota", "wifi", "api") + + Returns: + List of pattern strings to match against demangled symbols + """ + component_upper = component_name.upper() + component_camel = component_name.replace("_", "").title() + return [ + f"{_NAMESPACE_ESPHOME}{component_upper}Component", # e.g., esphome::OTAComponent + f"{_NAMESPACE_ESPHOME}ESPHome{component_upper}Component", # e.g., esphome::ESPHomeOTAComponent + f"{_NAMESPACE_ESPHOME}{component_camel}Component", # e.g., esphome::OtaComponent + f"{_NAMESPACE_ESPHOME}ESPHome{component_camel}Component", # e.g., esphome::ESPHomeOtaComponent + ] + + +def map_section_name(raw_section: str) -> str | None: + """Map raw section name to standard section. + + Args: + raw_section: Raw section name from ELF file (e.g., ".iram0.text", ".rodata.str1.1") + + Returns: + Standard section name (".text", ".rodata", ".data", ".bss") or None + """ + for standard_section, patterns in SECTION_MAPPING.items(): + if any(pattern in raw_section for pattern in patterns): + return standard_section + return None + + +def parse_symbol_line(line: str) -> tuple[str, str, int, str] | None: + """Parse a single symbol line from objdump output. + + Args: + line: Line from objdump -t output + + Returns: + Tuple of (section, name, size, address) or None if not a valid symbol. + Format: address l/g w/d F/O section size name + Example: 40084870 l F .iram0.text 00000000 _xt_user_exc + """ + parts = line.split() + if len(parts) < 5: + return None + + try: + # Validate and extract address + address = parts[0] + int(address, 16) + except ValueError: + return None + + # Look for F (function) or O (object) flag + if "F" not in parts and "O" not in parts: + return None + + # Find section, size, and name + for i, part in enumerate(parts): + if not part.startswith("."): + continue + + section = map_section_name(part) + if not section: + break + + # Need at least size field after section + if i + 1 >= len(parts): + break + + try: + size = int(parts[i + 1], 16) + except ValueError: + break + + # Need symbol name and non-zero size + if i + 2 >= len(parts) or size == 0: + break + + name = " ".join(parts[i + 2 :]) + return (section, name, size, address) + + return None diff --git a/esphome/automation.py b/esphome/automation.py index 99d4362845..2439b1ddc4 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -15,7 +15,15 @@ from esphome.const import ( CONF_TYPE_ID, CONF_UPDATE_INTERVAL, ) +from esphome.core import ID, Lambda +from esphome.cpp_generator import ( + LambdaExpression, + MockObj, + MockObjClass, + TemplateArgsType, +) from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor +from esphome.types import ConfigType from esphome.util import Registry @@ -49,11 +57,11 @@ def maybe_conf(conf, *validators): return validate -def register_action(name, action_type, schema): +def register_action(name: str, action_type: MockObjClass, schema: cv.Schema): return ACTION_REGISTRY.register(name, action_type, schema) -def register_condition(name, condition_type, schema): +def register_condition(name: str, condition_type: MockObjClass, schema: cv.Schema): return CONDITION_REGISTRY.register(name, condition_type, schema) @@ -84,6 +92,7 @@ def validate_potentially_or_condition(value): DelayAction = cg.esphome_ns.class_("DelayAction", Action, cg.Component) LambdaAction = cg.esphome_ns.class_("LambdaAction", Action) +StatelessLambdaAction = cg.esphome_ns.class_("StatelessLambdaAction", Action) IfAction = cg.esphome_ns.class_("IfAction", Action) WhileAction = cg.esphome_ns.class_("WhileAction", Action) RepeatAction = cg.esphome_ns.class_("RepeatAction", Action) @@ -94,9 +103,40 @@ ResumeComponentAction = cg.esphome_ns.class_("ResumeComponentAction", Action) Automation = cg.esphome_ns.class_("Automation") LambdaCondition = cg.esphome_ns.class_("LambdaCondition", Condition) +StatelessLambdaCondition = cg.esphome_ns.class_("StatelessLambdaCondition", Condition) ForCondition = cg.esphome_ns.class_("ForCondition", Condition, cg.Component) +def new_lambda_pvariable( + id_obj: ID, + lambda_expr: LambdaExpression, + stateless_class: MockObjClass, + template_arg: cg.TemplateArguments | None = None, +) -> MockObj: + """Create Pvariable for lambda, using stateless class if applicable. + + Combines ID selection and Pvariable creation in one call. For stateless + lambdas (empty capture), uses function pointer instead of std::function. + + Args: + id_obj: The ID object (action_id, condition_id, or filter_id) + lambda_expr: The lambda expression object + stateless_class: The stateless class to use for stateless lambdas + template_arg: Optional template arguments (for actions/conditions) + + Returns: + The created Pvariable + """ + # For stateless lambdas, use function pointer instead of std::function + if lambda_expr.capture == "": + id_obj = id_obj.copy() + id_obj.type = stateless_class + + if template_arg is not None: + return cg.new_Pvariable(id_obj, template_arg, lambda_expr) + return cg.new_Pvariable(id_obj, lambda_expr) + + def validate_automation(extra_schema=None, extra_validators=None, single=False): if extra_schema is None: extra_schema = {} @@ -142,7 +182,7 @@ def validate_automation(extra_schema=None, extra_validators=None, single=False): value = cv.Schema([extra_validators])(value) if single: if len(value) != 1: - raise cv.Invalid("Cannot have more than 1 automation for templates") + raise cv.Invalid("This trigger allows only a single automation") return value[0] return value @@ -164,45 +204,82 @@ XorCondition = cg.esphome_ns.class_("XorCondition", Condition) @register_condition("and", AndCondition, validate_condition_list) -async def and_condition_to_code(config, condition_id, template_arg, args): +async def and_condition_to_code( + config: ConfigType, + condition_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: conditions = await build_condition_list(config, template_arg, args) return cg.new_Pvariable(condition_id, template_arg, conditions) @register_condition("or", OrCondition, validate_condition_list) -async def or_condition_to_code(config, condition_id, template_arg, args): +async def or_condition_to_code( + config: ConfigType, + condition_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: conditions = await build_condition_list(config, template_arg, args) return cg.new_Pvariable(condition_id, template_arg, conditions) @register_condition("all", AndCondition, validate_condition_list) -async def all_condition_to_code(config, condition_id, template_arg, args): +async def all_condition_to_code( + config: ConfigType, + condition_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: conditions = await build_condition_list(config, template_arg, args) return cg.new_Pvariable(condition_id, template_arg, conditions) @register_condition("any", OrCondition, validate_condition_list) -async def any_condition_to_code(config, condition_id, template_arg, args): +async def any_condition_to_code( + config: ConfigType, + condition_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: conditions = await build_condition_list(config, template_arg, args) return cg.new_Pvariable(condition_id, template_arg, conditions) @register_condition("not", NotCondition, validate_potentially_and_condition) -async def not_condition_to_code(config, condition_id, template_arg, args): +async def not_condition_to_code( + config: ConfigType, + condition_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: condition = await build_condition(config, template_arg, args) return cg.new_Pvariable(condition_id, template_arg, condition) @register_condition("xor", XorCondition, validate_condition_list) -async def xor_condition_to_code(config, condition_id, template_arg, args): +async def xor_condition_to_code( + config: ConfigType, + condition_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: conditions = await build_condition_list(config, template_arg, args) return cg.new_Pvariable(condition_id, template_arg, conditions) @register_condition("lambda", LambdaCondition, cv.returning_lambda) -async def lambda_condition_to_code(config, condition_id, template_arg, args): +async def lambda_condition_to_code( + config: ConfigType, + condition_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: lambda_ = await cg.process_lambda(config, args, return_type=bool) - return cg.new_Pvariable(condition_id, template_arg, lambda_) + return new_lambda_pvariable( + condition_id, lambda_, StatelessLambdaCondition, template_arg + ) @register_condition( @@ -217,7 +294,12 @@ async def lambda_condition_to_code(config, condition_id, template_arg, args): } ).extend(cv.COMPONENT_SCHEMA), ) -async def for_condition_to_code(config, condition_id, template_arg, args): +async def for_condition_to_code( + config: ConfigType, + condition_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: condition = await build_condition( config[CONF_CONDITION], cg.TemplateArguments(), [] ) @@ -228,10 +310,39 @@ async def for_condition_to_code(config, condition_id, template_arg, args): return var +@register_condition( + "component.is_idle", + LambdaCondition, + maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(cg.Component), + } + ), +) +async def component_is_idle_condition_to_code( + config: ConfigType, + condition_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: + comp = await cg.get_variable(config[CONF_ID]) + lambda_ = await cg.process_lambda( + Lambda(f"return {comp}->is_idle();"), args, return_type=bool + ) + return new_lambda_pvariable( + condition_id, lambda_, StatelessLambdaCondition, template_arg + ) + + @register_action( "delay", DelayAction, cv.templatable(cv.positive_time_period_milliseconds) ) -async def delay_action_to_code(config, action_id, template_arg, args): +async def delay_action_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_component(var, {}) template_ = await cg.templatable(config, args, cg.uint32) @@ -256,10 +367,15 @@ async def delay_action_to_code(config, action_id, template_arg, args): cv.has_at_least_one_key(CONF_CONDITION, CONF_ANY, CONF_ALL), ), ) -async def if_action_to_code(config, action_id, template_arg, args): +async def if_action_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: cond_conf = next(el for el in config if el in (CONF_ANY, CONF_ALL, CONF_CONDITION)) - conditions = await build_condition(config[cond_conf], template_arg, args) - var = cg.new_Pvariable(action_id, template_arg, conditions) + condition = await build_condition(config[cond_conf], template_arg, args) + var = cg.new_Pvariable(action_id, template_arg, condition) if CONF_THEN in config: actions = await build_action_list(config[CONF_THEN], template_arg, args) cg.add(var.add_then(actions)) @@ -279,9 +395,14 @@ async def if_action_to_code(config, action_id, template_arg, args): } ), ) -async def while_action_to_code(config, action_id, template_arg, args): - conditions = await build_condition(config[CONF_CONDITION], template_arg, args) - var = cg.new_Pvariable(action_id, template_arg, conditions) +async def while_action_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: + condition = await build_condition(config[CONF_CONDITION], template_arg, args) + var = cg.new_Pvariable(action_id, template_arg, condition) actions = await build_action_list(config[CONF_THEN], template_arg, args) cg.add(var.add_then(actions)) return var @@ -297,7 +418,12 @@ async def while_action_to_code(config, action_id, template_arg, args): } ), ) -async def repeat_action_to_code(config, action_id, template_arg, args): +async def repeat_action_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) count_template = await cg.templatable(config[CONF_COUNT], args, cg.uint32) cg.add(var.set_count(count_template)) @@ -320,9 +446,14 @@ _validate_wait_until = cv.maybe_simple_value( @register_action("wait_until", WaitUntilAction, _validate_wait_until) -async def wait_until_action_to_code(config, action_id, template_arg, args): - conditions = await build_condition(config[CONF_CONDITION], template_arg, args) - var = cg.new_Pvariable(action_id, template_arg, conditions) +async def wait_until_action_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: + condition = await build_condition(config[CONF_CONDITION], template_arg, args) + var = cg.new_Pvariable(action_id, template_arg, condition) if CONF_TIMEOUT in config: template_ = await cg.templatable(config[CONF_TIMEOUT], args, cg.uint32) cg.add(var.set_timeout_value(template_)) @@ -331,9 +462,14 @@ async def wait_until_action_to_code(config, action_id, template_arg, args): @register_action("lambda", LambdaAction, cv.lambda_) -async def lambda_action_to_code(config, action_id, template_arg, args): +async def lambda_action_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: lambda_ = await cg.process_lambda(config, args, return_type=cg.void) - return cg.new_Pvariable(action_id, template_arg, lambda_) + return new_lambda_pvariable(action_id, lambda_, StatelessLambdaAction, template_arg) @register_action( @@ -345,7 +481,12 @@ async def lambda_action_to_code(config, action_id, template_arg, args): } ), ) -async def component_update_action_to_code(config, action_id, template_arg, args): +async def component_update_action_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: comp = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, comp) @@ -359,7 +500,12 @@ async def component_update_action_to_code(config, action_id, template_arg, args) } ), ) -async def component_suspend_action_to_code(config, action_id, template_arg, args): +async def component_suspend_action_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: comp = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, comp) @@ -376,7 +522,12 @@ async def component_suspend_action_to_code(config, action_id, template_arg, args } ), ) -async def component_resume_action_to_code(config, action_id, template_arg, args): +async def component_resume_action_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: comp = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, comp) if CONF_UPDATE_INTERVAL in config: @@ -385,7 +536,9 @@ async def component_resume_action_to_code(config, action_id, template_arg, args) return var -async def build_action(full_config, template_arg, args): +async def build_action( + full_config: ConfigType, template_arg: cg.TemplateArguments, args: TemplateArgsType +) -> MockObj: registry_entry, config = cg.extract_registry_entry_config( ACTION_REGISTRY, full_config ) @@ -394,15 +547,19 @@ async def build_action(full_config, template_arg, args): return await builder(config, action_id, template_arg, args) -async def build_action_list(config, templ, arg_type): - actions = [] +async def build_action_list( + config: list[ConfigType], templ: cg.TemplateArguments, arg_type: TemplateArgsType +) -> list[MockObj]: + actions: list[MockObj] = [] for conf in config: action = await build_action(conf, templ, arg_type) actions.append(action) return actions -async def build_condition(full_config, template_arg, args): +async def build_condition( + full_config: ConfigType, template_arg: cg.TemplateArguments, args: TemplateArgsType +) -> MockObj: registry_entry, config = cg.extract_registry_entry_config( CONDITION_REGISTRY, full_config ) @@ -411,15 +568,19 @@ async def build_condition(full_config, template_arg, args): return await builder(config, action_id, template_arg, args) -async def build_condition_list(config, templ, args): - conditions = [] +async def build_condition_list( + config: ConfigType, templ: cg.TemplateArguments, args: TemplateArgsType +) -> list[MockObj]: + conditions: list[MockObj] = [] for conf in config: condition = await build_condition(conf, templ, args) conditions.append(condition) return conditions -async def build_automation(trigger, args, config): +async def build_automation( + trigger: MockObj, args: TemplateArgsType, config: ConfigType +) -> MockObj: arg_types = [arg[0] for arg in args] templ = cg.TemplateArguments(*arg_types) obj = cg.new_Pvariable(config[CONF_AUTOMATION_ID], templ, trigger) diff --git a/esphome/build_gen/platformio.py b/esphome/build_gen/platformio.py index 9bbe86694b..30dbb69d86 100644 --- a/esphome/build_gen/platformio.py +++ b/esphome/build_gen/platformio.py @@ -1,5 +1,3 @@ -import os - from esphome.const import __version__ from esphome.core import CORE from esphome.helpers import mkdir_p, read_file, write_file_if_changed @@ -63,7 +61,7 @@ def write_ini(content): update_storage_json() path = CORE.relative_build_path("platformio.ini") - if os.path.isfile(path): + if path.is_file(): text = read_file(path) content_format = find_begin_end( text, INI_AUTO_GENERATE_BEGIN, INI_AUTO_GENERATE_END diff --git a/esphome/codegen.py b/esphome/codegen.py index 8e02ec1164..6d55c6023d 100644 --- a/esphome/codegen.py +++ b/esphome/codegen.py @@ -12,6 +12,7 @@ from esphome.cpp_generator import ( # noqa: F401 ArrayInitializer, Expression, LineComment, + LogStringLiteral, MockObj, MockObjClass, Pvariable, @@ -61,6 +62,7 @@ from esphome.cpp_types import ( # noqa: F401 EntityBase, EntityCategory, ESPTime, + FixedVector, GPIOPin, InternalGPIOPin, JsonObject, diff --git a/esphome/components/absolute_humidity/absolute_humidity.cpp b/esphome/components/absolute_humidity/absolute_humidity.cpp index b8717ac5f1..d16a024d86 100644 --- a/esphome/components/absolute_humidity/absolute_humidity.cpp +++ b/esphome/components/absolute_humidity/absolute_humidity.cpp @@ -61,11 +61,10 @@ void AbsoluteHumidityComponent::loop() { ESP_LOGW(TAG, "No valid state from temperature sensor!"); } if (no_humidity) { - ESP_LOGW(TAG, "No valid state from temperature sensor!"); + ESP_LOGW(TAG, "No valid state from humidity sensor!"); } - ESP_LOGW(TAG, "Unable to calculate absolute humidity."); this->publish_state(NAN); - this->status_set_warning(); + this->status_set_warning(LOG_STR("Unable to calculate absolute humidity.")); return; } @@ -87,9 +86,8 @@ void AbsoluteHumidityComponent::loop() { es = es_wobus(temperature_c); break; default: - ESP_LOGE(TAG, "Invalid saturation vapor pressure equation selection!"); this->publish_state(NAN); - this->status_set_error(); + this->status_set_error(LOG_STR("Invalid saturation vapor pressure equation selection!")); return; } ESP_LOGD(TAG, "Saturation vapor pressure %f kPa", es); diff --git a/esphome/components/absolute_humidity/sensor.py b/esphome/components/absolute_humidity/sensor.py index 62a2c8ab7c..caaa546e25 100644 --- a/esphome/components/absolute_humidity/sensor.py +++ b/esphome/components/absolute_humidity/sensor.py @@ -5,7 +5,7 @@ from esphome.const import ( CONF_EQUATION, CONF_HUMIDITY, CONF_TEMPERATURE, - ICON_WATER, + DEVICE_CLASS_ABSOLUTE_HUMIDITY, STATE_CLASS_MEASUREMENT, UNIT_GRAMS_PER_CUBIC_METER, ) @@ -27,8 +27,8 @@ EQUATION = { CONFIG_SCHEMA = ( sensor.sensor_schema( unit_of_measurement=UNIT_GRAMS_PER_CUBIC_METER, - icon=ICON_WATER, accuracy_decimals=2, + device_class=DEVICE_CLASS_ABSOLUTE_HUMIDITY, state_class=STATE_CLASS_MEASUREMENT, ) .extend( diff --git a/esphome/components/adalight/adalight_light_effect.cpp b/esphome/components/adalight/adalight_light_effect.cpp index 35e98d7360..4cf639a01f 100644 --- a/esphome/components/adalight/adalight_light_effect.cpp +++ b/esphome/components/adalight/adalight_light_effect.cpp @@ -9,7 +9,7 @@ static const char *const TAG = "adalight_light_effect"; static const uint32_t ADALIGHT_ACK_INTERVAL = 1000; static const uint32_t ADALIGHT_RECEIVE_TIMEOUT = 1000; -AdalightLightEffect::AdalightLightEffect(const std::string &name) : AddressableLightEffect(name) {} +AdalightLightEffect::AdalightLightEffect(const char *name) : AddressableLightEffect(name) {} void AdalightLightEffect::start() { AddressableLightEffect::start(); diff --git a/esphome/components/adalight/adalight_light_effect.h b/esphome/components/adalight/adalight_light_effect.h index 72faf44269..bb7319c99c 100644 --- a/esphome/components/adalight/adalight_light_effect.h +++ b/esphome/components/adalight/adalight_light_effect.h @@ -11,7 +11,7 @@ namespace adalight { class AdalightLightEffect : public light::AddressableLightEffect, public uart::UARTDevice { public: - AdalightLightEffect(const std::string &name); + AdalightLightEffect(const char *name); void start() override; void stop() override; diff --git a/esphome/components/adc/__init__.py b/esphome/components/adc/__init__.py index f260e13242..15dc447b6c 100644 --- a/esphome/components/adc/__init__.py +++ b/esphome/components/adc/__init__.py @@ -11,15 +11,8 @@ from esphome.components.esp32.const import ( VARIANT_ESP32S2, VARIANT_ESP32S3, ) -from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv -from esphome.const import ( - CONF_ANALOG, - CONF_INPUT, - CONF_NUMBER, - PLATFORM_ESP8266, - PlatformFramework, -) +from esphome.const import CONF_ANALOG, CONF_INPUT, CONF_NUMBER, PLATFORM_ESP8266 from esphome.core import CORE CODEOWNERS = ["@esphome/core"] @@ -273,21 +266,3 @@ def validate_adc_pin(value): )(value) raise NotImplementedError - - -FILTER_SOURCE_FILES = filter_source_files_from_platform( - { - "adc_sensor_esp32.cpp": { - PlatformFramework.ESP32_ARDUINO, - PlatformFramework.ESP32_IDF, - }, - "adc_sensor_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, - "adc_sensor_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO}, - "adc_sensor_libretiny.cpp": { - PlatformFramework.BK72XX_ARDUINO, - PlatformFramework.RTL87XX_ARDUINO, - PlatformFramework.LN882X_ARDUINO, - }, - "adc_sensor_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR}, - } -) diff --git a/esphome/components/adc/adc_sensor_esp32.cpp b/esphome/components/adc/adc_sensor_esp32.cpp index 87d4ddd35f..ab6a89fce0 100644 --- a/esphome/components/adc/adc_sensor_esp32.cpp +++ b/esphome/components/adc/adc_sensor_esp32.cpp @@ -241,6 +241,8 @@ float ADCSensor::sample_autorange_() { cali_config.bitwidth = ADC_BITWIDTH_DEFAULT; err = adc_cali_create_scheme_curve_fitting(&cali_config, &handle); + ESP_LOGVV(TAG, "Autorange atten=%d: Calibration handle creation %s (err=%d)", atten, + (err == ESP_OK) ? "SUCCESS" : "FAILED", err); #else adc_cali_line_fitting_config_t cali_config = { .unit_id = this->adc_unit_, @@ -251,10 +253,14 @@ float ADCSensor::sample_autorange_() { #endif }; err = adc_cali_create_scheme_line_fitting(&cali_config, &handle); + ESP_LOGVV(TAG, "Autorange atten=%d: Calibration handle creation %s (err=%d)", atten, + (err == ESP_OK) ? "SUCCESS" : "FAILED", err); #endif int raw; err = adc_oneshot_read(this->adc_handle_, this->channel_, &raw); + ESP_LOGVV(TAG, "Autorange atten=%d: Raw ADC read %s, value=%d (err=%d)", atten, + (err == ESP_OK) ? "SUCCESS" : "FAILED", raw, err); if (err != ESP_OK) { ESP_LOGW(TAG, "ADC read failed in autorange with error %d", err); @@ -275,8 +281,10 @@ float ADCSensor::sample_autorange_() { err = adc_cali_raw_to_voltage(handle, raw, &voltage_mv); if (err == ESP_OK) { voltage = voltage_mv / 1000.0f; + ESP_LOGVV(TAG, "Autorange atten=%d: CALIBRATED - raw=%d -> %dmV -> %.6fV", atten, raw, voltage_mv, voltage); } else { voltage = raw * 3.3f / 4095.0f; + ESP_LOGVV(TAG, "Autorange atten=%d: UNCALIBRATED FALLBACK - raw=%d -> %.6fV (3.3V ref)", atten, raw, voltage); } // Clean up calibration handle #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ @@ -287,6 +295,7 @@ float ADCSensor::sample_autorange_() { #endif } else { voltage = raw * 3.3f / 4095.0f; + ESP_LOGVV(TAG, "Autorange atten=%d: NO CALIBRATION - raw=%d -> %.6fV (3.3V ref)", atten, raw, voltage); } return {raw, voltage}; @@ -324,18 +333,32 @@ float ADCSensor::sample_autorange_() { } const int adc_half = 2048; - uint32_t c12 = std::min(raw12, adc_half); - uint32_t c6 = adc_half - std::abs(raw6 - adc_half); - uint32_t c2 = adc_half - std::abs(raw2 - adc_half); - uint32_t c0 = std::min(4095 - raw0, adc_half); - uint32_t csum = c12 + c6 + c2 + c0; + const uint32_t c12 = std::min(raw12, adc_half); + + const int32_t c6_signed = adc_half - std::abs(raw6 - adc_half); + const uint32_t c6 = (c6_signed > 0) ? c6_signed : 0; // Clamp to prevent underflow + + const int32_t c2_signed = adc_half - std::abs(raw2 - adc_half); + const uint32_t c2 = (c2_signed > 0) ? c2_signed : 0; // Clamp to prevent underflow + + const uint32_t c0 = std::min(4095 - raw0, adc_half); + const uint32_t csum = c12 + c6 + c2 + c0; + + ESP_LOGVV(TAG, "Autorange summary:"); + ESP_LOGVV(TAG, " Raw readings: 12db=%d, 6db=%d, 2.5db=%d, 0db=%d", raw12, raw6, raw2, raw0); + ESP_LOGVV(TAG, " Voltages: 12db=%.6f, 6db=%.6f, 2.5db=%.6f, 0db=%.6f", mv12, mv6, mv2, mv0); + ESP_LOGVV(TAG, " Coefficients: c12=%u, c6=%u, c2=%u, c0=%u, sum=%u", c12, c6, c2, c0, csum); if (csum == 0) { ESP_LOGE(TAG, "Invalid weight sum in autorange calculation"); return NAN; } - return (mv12 * c12 + mv6 * c6 + mv2 * c2 + mv0 * c0) / csum; + const float final_result = (mv12 * c12 + mv6 * c6 + mv2 * c2 + mv0 * c0) / csum; + ESP_LOGV(TAG, "Autorange final: (%.6f*%u + %.6f*%u + %.6f*%u + %.6f*%u)/%u = %.6fV", mv12, c12, mv6, c6, mv2, c2, mv0, + c0, csum, final_result); + + return final_result; } } // namespace adc diff --git a/esphome/components/adc/sensor.py b/esphome/components/adc/sensor.py index 49970c5e3d..607609bbc7 100644 --- a/esphome/components/adc/sensor.py +++ b/esphome/components/adc/sensor.py @@ -9,6 +9,7 @@ from esphome.components.zephyr import ( zephyr_add_prj_conf, zephyr_add_user, ) +from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( CONF_ATTENUATION, @@ -20,6 +21,7 @@ from esphome.const import ( PLATFORM_NRF52, STATE_CLASS_MEASUREMENT, UNIT_VOLT, + PlatformFramework, ) from esphome.core import CORE @@ -174,3 +176,21 @@ async def to_code(config): }}; """ ) + + +FILTER_SOURCE_FILES = filter_source_files_from_platform( + { + "adc_sensor_esp32.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + }, + "adc_sensor_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, + "adc_sensor_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO}, + "adc_sensor_libretiny.cpp": { + PlatformFramework.BK72XX_ARDUINO, + PlatformFramework.RTL87XX_ARDUINO, + PlatformFramework.LN882X_ARDUINO, + }, + "adc_sensor_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR}, + } +) diff --git a/esphome/components/ade7880/ade7880.cpp b/esphome/components/ade7880/ade7880.cpp index 55f834bf86..fd560e0676 100644 --- a/esphome/components/ade7880/ade7880.cpp +++ b/esphome/components/ade7880/ade7880.cpp @@ -113,7 +113,7 @@ void ADE7880::update() { if (this->channel_a_ != nullptr) { auto *chan = this->channel_a_; this->update_sensor_from_s24zp_register16_(chan->current, AIRMS, [](float val) { return val / 100000.0f; }); - this->update_sensor_from_s24zp_register16_(chan->voltage, BVRMS, [](float val) { return val / 10000.0f; }); + this->update_sensor_from_s24zp_register16_(chan->voltage, AVRMS, [](float val) { return val / 10000.0f; }); this->update_sensor_from_s24zp_register16_(chan->active_power, AWATT, [](float val) { return val / 100.0f; }); this->update_sensor_from_s24zp_register16_(chan->apparent_power, AVA, [](float val) { return val / 100.0f; }); this->update_sensor_from_s16_register16_(chan->power_factor, APF, diff --git a/esphome/components/ade7880/sensor.py b/esphome/components/ade7880/sensor.py index 3ef5e6bfff..39dbeb225f 100644 --- a/esphome/components/ade7880/sensor.py +++ b/esphome/components/ade7880/sensor.py @@ -36,6 +36,7 @@ from esphome.const import ( UNIT_WATT, UNIT_WATT_HOURS, ) +from esphome.types import ConfigType DEPENDENCIES = ["i2c"] @@ -51,6 +52,20 @@ CONF_POWER_GAIN = "power_gain" CONF_NEUTRAL = "neutral" +# Tuple of power channel phases +POWER_PHASES = (CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C) + +# Tuple of sensor types that can be configured for power channels +POWER_SENSOR_TYPES = ( + CONF_CURRENT, + CONF_VOLTAGE, + CONF_ACTIVE_POWER, + CONF_APPARENT_POWER, + CONF_POWER_FACTOR, + CONF_FORWARD_ACTIVE_ENERGY, + CONF_REVERSE_ACTIVE_ENERGY, +) + NEUTRAL_CHANNEL_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(NeutralChannel), @@ -150,7 +165,64 @@ POWER_CHANNEL_SCHEMA = cv.Schema( } ) -CONFIG_SCHEMA = ( + +def prefix_sensor_name( + sensor_conf: ConfigType, + channel_name: str, + channel_config: ConfigType, + sensor_type: str, +) -> None: + """Helper to prefix sensor name with channel name. + + Args: + sensor_conf: The sensor configuration (dict or string) + channel_name: The channel name to prefix with + channel_config: The channel configuration to update + sensor_type: The sensor type key in the channel config + """ + if isinstance(sensor_conf, dict) and CONF_NAME in sensor_conf: + sensor_name = sensor_conf[CONF_NAME] + if sensor_name and not sensor_name.startswith(channel_name): + sensor_conf[CONF_NAME] = f"{channel_name} {sensor_name}" + elif isinstance(sensor_conf, str): + # Simple value case - convert to dict with prefixed name + channel_config[sensor_type] = {CONF_NAME: f"{channel_name} {sensor_conf}"} + + +def process_channel_sensors( + config: ConfigType, channel_key: str, sensor_types: tuple +) -> None: + """Process sensors for a channel and prefix their names. + + Args: + config: The main configuration + channel_key: The channel key (e.g., CONF_PHASE_A, CONF_NEUTRAL) + sensor_types: Tuple of sensor types to process for this channel + """ + if not (channel_config := config.get(channel_key)) or not ( + channel_name := channel_config.get(CONF_NAME) + ): + return + + for sensor_type in sensor_types: + if sensor_conf := channel_config.get(sensor_type): + prefix_sensor_name(sensor_conf, channel_name, channel_config, sensor_type) + + +def preprocess_channels(config: ConfigType) -> ConfigType: + """Preprocess channel configurations to add channel name prefix to sensor names.""" + # Process power channels + for channel in POWER_PHASES: + process_channel_sensors(config, channel, POWER_SENSOR_TYPES) + + # Process neutral channel + process_channel_sensors(config, CONF_NEUTRAL, (CONF_CURRENT,)) + + return config + + +CONFIG_SCHEMA = cv.All( + preprocess_channels, cv.Schema( { cv.GenerateID(): cv.declare_id(ADE7880), @@ -167,7 +239,7 @@ CONFIG_SCHEMA = ( } ) .extend(cv.polling_component_schema("60s")) - .extend(i2c.i2c_device_schema(0x38)) + .extend(i2c.i2c_device_schema(0x38)), ) @@ -188,15 +260,7 @@ async def neutral_channel(config): async def power_channel(config): var = cg.new_Pvariable(config[CONF_ID]) - for sensor_type in [ - CONF_CURRENT, - CONF_VOLTAGE, - CONF_ACTIVE_POWER, - CONF_APPARENT_POWER, - CONF_POWER_FACTOR, - CONF_FORWARD_ACTIVE_ENERGY, - CONF_REVERSE_ACTIVE_ENERGY, - ]: + for sensor_type in POWER_SENSOR_TYPES: if conf := config.get(sensor_type): sens = await sensor.new_sensor(conf) cg.add(getattr(var, f"set_{sensor_type}")(sens)) @@ -216,44 +280,6 @@ async def power_channel(config): return var -def final_validate(config): - for channel in [CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C]: - if channel := config.get(channel): - channel_name = channel.get(CONF_NAME) - - for sensor_type in [ - CONF_CURRENT, - CONF_VOLTAGE, - CONF_ACTIVE_POWER, - CONF_APPARENT_POWER, - CONF_POWER_FACTOR, - CONF_FORWARD_ACTIVE_ENERGY, - CONF_REVERSE_ACTIVE_ENERGY, - ]: - if conf := channel.get(sensor_type): - sensor_name = conf.get(CONF_NAME) - if ( - sensor_name - and channel_name - and not sensor_name.startswith(channel_name) - ): - conf[CONF_NAME] = f"{channel_name} {sensor_name}" - - if channel := config.get(CONF_NEUTRAL): - channel_name = channel.get(CONF_NAME) - if conf := channel.get(CONF_CURRENT): - sensor_name = conf.get(CONF_NAME) - if ( - sensor_name - and channel_name - and not sensor_name.startswith(channel_name) - ): - conf[CONF_NAME] = f"{channel_name} {sensor_name}" - - -FINAL_VALIDATE_SCHEMA = final_validate - - async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/ags10/ags10.cpp b/esphome/components/ags10/ags10.cpp index 9a29a979f3..fa7170114c 100644 --- a/esphome/components/ags10/ags10.cpp +++ b/esphome/components/ags10/ags10.cpp @@ -89,7 +89,7 @@ void AGS10Component::dump_config() { bool AGS10Component::new_i2c_address(uint8_t newaddress) { uint8_t rev_newaddress = ~newaddress; std::array data{newaddress, rev_newaddress, newaddress, rev_newaddress, 0}; - data[4] = calc_crc8_(data, 4); + data[4] = crc8(data.data(), 4, 0xFF, 0x31, true); if (!this->write_bytes(REG_ADDRESS, data)) { this->error_code_ = COMMUNICATION_FAILED; this->status_set_warning(); @@ -109,7 +109,7 @@ bool AGS10Component::set_zero_point_with_current_resistance() { return this->set bool AGS10Component::set_zero_point_with(uint16_t value) { std::array data{0x00, 0x0C, (uint8_t) ((value >> 8) & 0xFF), (uint8_t) (value & 0xFF), 0}; - data[4] = calc_crc8_(data, 4); + data[4] = crc8(data.data(), 4, 0xFF, 0x31, true); if (!this->write_bytes(REG_CALIBRATION, data)) { this->error_code_ = COMMUNICATION_FAILED; this->status_set_warning(); @@ -184,7 +184,7 @@ template optional> AGS10Component::read_and_che auto res = *data; auto crc_byte = res[len]; - if (crc_byte != calc_crc8_(res, len)) { + if (crc_byte != crc8(res.data(), len, 0xFF, 0x31, true)) { this->error_code_ = CRC_CHECK_FAILED; ESP_LOGE(TAG, "Reading AGS10 version failed: crc error!"); return optional>(); @@ -192,20 +192,5 @@ template optional> AGS10Component::read_and_che return data; } - -template uint8_t AGS10Component::calc_crc8_(std::array dat, uint8_t num) { - uint8_t i, byte1, crc = 0xFF; - for (byte1 = 0; byte1 < num; byte1++) { - crc ^= (dat[byte1]); - for (i = 0; i < 8; i++) { - if (crc & 0x80) { - crc = (crc << 1) ^ 0x31; - } else { - crc = (crc << 1); - } - } - } - return crc; -} } // namespace ags10 } // namespace esphome diff --git a/esphome/components/ags10/ags10.h b/esphome/components/ags10/ags10.h index 3e184ae176..9e034b20cb 100644 --- a/esphome/components/ags10/ags10.h +++ b/esphome/components/ags10/ags10.h @@ -1,9 +1,9 @@ #pragma once +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensor/sensor.h" #include "esphome/core/automation.h" #include "esphome/core/component.h" -#include "esphome/components/sensor/sensor.h" -#include "esphome/components/i2c/i2c.h" namespace esphome { namespace ags10 { @@ -99,23 +99,13 @@ class AGS10Component : public PollingComponent, public i2c::I2CDevice { * Read, checks and returns data from the sensor. */ template optional> read_and_check_(uint8_t a_register); - - /** - * Calculates CRC8 value. - * - * CRC8 calculation, initial value: 0xFF, polynomial: 0x31 (x8+ x5+ x4+1) - * - * @param[in] dat the data buffer - * @param num number of bytes in the buffer - */ - template uint8_t calc_crc8_(std::array dat, uint8_t num); }; template class AGS10NewI2cAddressAction : public Action, public Parented { public: TEMPLATABLE_VALUE(uint8_t, new_address) - void play(Ts... x) override { this->parent_->new_i2c_address(this->new_address_.value(x...)); } + void play(const Ts &...x) override { this->parent_->new_i2c_address(this->new_address_.value(x...)); } }; enum AGS10SetZeroPointActionMode { @@ -132,7 +122,7 @@ template class AGS10SetZeroPointAction : public Action, p TEMPLATABLE_VALUE(uint16_t, value) TEMPLATABLE_VALUE(AGS10SetZeroPointActionMode, mode) - void play(Ts... x) override { + void play(const Ts &...x) override { switch (this->mode_.value(x...)) { case FACTORY_DEFAULT: this->parent_->set_zero_point_with_factory_defaults(); diff --git a/esphome/components/aht10/aht10.cpp b/esphome/components/aht10/aht10.cpp index 6202a27c42..03d9d9cd9e 100644 --- a/esphome/components/aht10/aht10.cpp +++ b/esphome/components/aht10/aht10.cpp @@ -83,7 +83,7 @@ void AHT10Component::setup() { void AHT10Component::restart_read_() { if (this->read_count_ == AHT10_ATTEMPTS) { this->read_count_ = 0; - this->status_set_error("Reading timed out"); + this->status_set_error(LOG_STR("Reading timed out")); return; } this->read_count_++; @@ -96,7 +96,7 @@ void AHT10Component::read_data_() { ESP_LOGD(TAG, "Read attempt %d at %ums", this->read_count_, (unsigned) (millis() - this->start_time_)); } if (this->read(data, 6) != i2c::ERROR_OK) { - this->status_set_warning("Read failed, will retry"); + this->status_set_warning(LOG_STR("Read failed, will retry")); this->restart_read_(); return; } @@ -113,7 +113,7 @@ void AHT10Component::read_data_() { } else { ESP_LOGD(TAG, "Invalid humidity, retrying"); if (this->write(AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD)) != i2c::ERROR_OK) { - this->status_set_warning(ESP_LOG_MSG_COMM_FAIL); + this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); } this->restart_read_(); return; @@ -144,7 +144,7 @@ void AHT10Component::update() { return; this->start_time_ = millis(); if (this->write(AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD)) != i2c::ERROR_OK) { - this->status_set_warning(ESP_LOG_MSG_COMM_FAIL); + this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); return; } this->restart_read_(); diff --git a/esphome/components/aic3204/automation.h b/esphome/components/aic3204/automation.h index 416a88fa12..851ff930f8 100644 --- a/esphome/components/aic3204/automation.h +++ b/esphome/components/aic3204/automation.h @@ -13,7 +13,7 @@ template class SetAutoMuteAction : public Action { TEMPLATABLE_VALUE(uint8_t, auto_mute_mode) - void play(Ts... x) override { this->aic3204_->set_auto_mute_mode(this->auto_mute_mode_.value(x...)); } + void play(const Ts &...x) override { this->aic3204_->set_auto_mute_mode(this->auto_mute_mode_.value(x...)); } protected: AIC3204 *aic3204_; diff --git a/esphome/components/airthings_ble/__init__.py b/esphome/components/airthings_ble/__init__.py index eae400ab39..1545110798 100644 --- a/esphome/components/airthings_ble/__init__.py +++ b/esphome/components/airthings_ble/__init__.py @@ -18,6 +18,6 @@ CONFIG_SCHEMA = cv.Schema( ).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield esp32_ble_tracker.register_ble_device(var, config) + await esp32_ble_tracker.register_ble_device(var, config) diff --git a/esphome/components/alarm_control_panel/__init__.py b/esphome/components/alarm_control_panel/__init__.py index 058e061d1e..b1e2252ce7 100644 --- a/esphome/components/alarm_control_panel/__init__.py +++ b/esphome/components/alarm_control_panel/__init__.py @@ -13,7 +13,7 @@ from esphome.const import ( CONF_TRIGGER_ID, CONF_WEB_SERVER, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass @@ -172,12 +172,6 @@ def alarm_control_panel_schema( return _ALARM_CONTROL_PANEL_SCHEMA.extend(schema) -# Remove before 2025.11.0 -ALARM_CONTROL_PANEL_SCHEMA = alarm_control_panel_schema(AlarmControlPanel) -ALARM_CONTROL_PANEL_SCHEMA.add_extra( - cv.deprecated_schema_constant("alarm_control_panel") -) - ALARM_CONTROL_PANEL_ACTION_SCHEMA = maybe_simple_id( { cv.GenerateID(): cv.use_id(AlarmControlPanel), @@ -345,6 +339,6 @@ async def alarm_control_panel_is_armed_to_code( return cg.new_Pvariable(condition_id, template_arg, paren) -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(alarm_control_panel_ns.using) diff --git a/esphome/components/alarm_control_panel/alarm_control_panel.cpp b/esphome/components/alarm_control_panel/alarm_control_panel.cpp index 9f1485ee90..c29e02c8ef 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel.cpp +++ b/esphome/components/alarm_control_panel/alarm_control_panel.cpp @@ -1,6 +1,8 @@ -#include - #include "alarm_control_panel.h" +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" + +#include #include "esphome/core/application.h" #include "esphome/core/helpers.h" @@ -34,6 +36,9 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) { LOG_STR_ARG(alarm_control_panel_state_to_string(prev_state))); this->current_state_ = state; this->state_callback_.call(); +#if defined(USE_ALARM_CONTROL_PANEL) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_alarm_control_panel_update(this); +#endif if (state == ACP_STATE_TRIGGERED) { this->triggered_callback_.call(); } else if (state == ACP_STATE_ARMING) { diff --git a/esphome/components/alarm_control_panel/automation.h b/esphome/components/alarm_control_panel/automation.h index 2177fb710f..db2ef78158 100644 --- a/esphome/components/alarm_control_panel/automation.h +++ b/esphome/components/alarm_control_panel/automation.h @@ -89,7 +89,7 @@ template class ArmAwayAction : public Action { TEMPLATABLE_VALUE(std::string, code) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->alarm_control_panel_->make_call(); auto code = this->code_.optional_value(x...); if (code.has_value()) { @@ -109,7 +109,7 @@ template class ArmHomeAction : public Action { TEMPLATABLE_VALUE(std::string, code) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->alarm_control_panel_->make_call(); auto code = this->code_.optional_value(x...); if (code.has_value()) { @@ -129,7 +129,7 @@ template class ArmNightAction : public Action { TEMPLATABLE_VALUE(std::string, code) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->alarm_control_panel_->make_call(); auto code = this->code_.optional_value(x...); if (code.has_value()) { @@ -149,7 +149,7 @@ template class DisarmAction : public Action { TEMPLATABLE_VALUE(std::string, code) - void play(Ts... x) override { this->alarm_control_panel_->disarm(this->code_.optional_value(x...)); } + void play(const Ts &...x) override { this->alarm_control_panel_->disarm(this->code_.optional_value(x...)); } protected: AlarmControlPanel *alarm_control_panel_; @@ -159,7 +159,7 @@ template class PendingAction : public Action { public: explicit PendingAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {} - void play(Ts... x) override { this->alarm_control_panel_->make_call().pending().perform(); } + void play(const Ts &...x) override { this->alarm_control_panel_->make_call().pending().perform(); } protected: AlarmControlPanel *alarm_control_panel_; @@ -169,7 +169,7 @@ template class TriggeredAction : public Action { public: explicit TriggeredAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {} - void play(Ts... x) override { this->alarm_control_panel_->make_call().triggered().perform(); } + void play(const Ts &...x) override { this->alarm_control_panel_->make_call().triggered().perform(); } protected: AlarmControlPanel *alarm_control_panel_; @@ -178,7 +178,7 @@ template class TriggeredAction : public Action { template class AlarmControlPanelCondition : public Condition { public: AlarmControlPanelCondition(AlarmControlPanel *parent) : parent_(parent) {} - bool check(Ts... x) override { + bool check(const Ts &...x) override { return this->parent_->is_state_armed(this->parent_->get_state()) || this->parent_->get_state() == ACP_STATE_PENDING || this->parent_->get_state() == ACP_STATE_TRIGGERED; } diff --git a/esphome/components/alpha3/alpha3.cpp b/esphome/components/alpha3/alpha3.cpp index 344f2d5a03..f22a8e2444 100644 --- a/esphome/components/alpha3/alpha3.cpp +++ b/esphome/components/alpha3/alpha3.cpp @@ -56,13 +56,13 @@ bool Alpha3::is_current_response_type_(const uint8_t *response_type) { void Alpha3::handle_geni_response_(const uint8_t *response, uint16_t length) { if (this->response_offset_ >= this->response_length_) { - ESP_LOGD(TAG, "[%s] GENI response begin", this->parent_->address_str().c_str()); + ESP_LOGD(TAG, "[%s] GENI response begin", this->parent_->address_str()); if (length < GENI_RESPONSE_HEADER_LENGTH) { - ESP_LOGW(TAG, "[%s] response to short", this->parent_->address_str().c_str()); + ESP_LOGW(TAG, "[%s] response too short", this->parent_->address_str()); return; } if (response[0] != 36 || response[2] != 248 || response[3] != 231 || response[4] != 10) { - ESP_LOGW(TAG, "[%s] response bytes %d %d %d %d %d don't match GENI HEADER", this->parent_->address_str().c_str(), + ESP_LOGW(TAG, "[%s] response bytes %d %d %d %d %d don't match GENI HEADER", this->parent_->address_str(), response[0], response[1], response[2], response[3], response[4]); return; } @@ -77,11 +77,11 @@ void Alpha3::handle_geni_response_(const uint8_t *response, uint16_t length) { }; if (this->is_current_response_type_(GENI_RESPONSE_TYPE_FLOW_HEAD)) { - ESP_LOGD(TAG, "[%s] FLOW HEAD Response", this->parent_->address_str().c_str()); + ESP_LOGD(TAG, "[%s] FLOW HEAD Response", this->parent_->address_str()); extract_publish_sensor_value(GENI_RESPONSE_FLOW_OFFSET, this->flow_sensor_, 3600.0F); extract_publish_sensor_value(GENI_RESPONSE_HEAD_OFFSET, this->head_sensor_, .0001F); } else if (this->is_current_response_type_(GENI_RESPONSE_TYPE_POWER)) { - ESP_LOGD(TAG, "[%s] POWER Response", this->parent_->address_str().c_str()); + ESP_LOGD(TAG, "[%s] POWER Response", this->parent_->address_str()); extract_publish_sensor_value(GENI_RESPONSE_POWER_OFFSET, this->power_sensor_, 1.0F); extract_publish_sensor_value(GENI_RESPONSE_CURRENT_OFFSET, this->current_sensor_, 1.0F); extract_publish_sensor_value(GENI_RESPONSE_MOTOR_SPEED_OFFSET, this->speed_sensor_, 1.0F); @@ -100,7 +100,7 @@ void Alpha3::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc if (param->open.status == ESP_GATT_OK) { this->response_offset_ = 0; this->response_length_ = 0; - ESP_LOGI(TAG, "[%s] connection open", this->parent_->address_str().c_str()); + ESP_LOGI(TAG, "[%s] connection open", this->parent_->address_str()); } break; } @@ -132,7 +132,7 @@ void Alpha3::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc case ESP_GATTC_SEARCH_CMPL_EVT: { auto *chr = this->parent_->get_characteristic(ALPHA3_GENI_SERVICE_UUID, ALPHA3_GENI_CHARACTERISTIC_UUID); if (chr == nullptr) { - ESP_LOGE(TAG, "[%s] No GENI service found at device, not an Alpha3..?", this->parent_->address_str().c_str()); + ESP_LOGE(TAG, "[%s] No GENI service found at device, not an Alpha3..?", this->parent_->address_str()); break; } auto status = esp_ble_gattc_register_for_notify(this->parent_->get_gattc_if(), this->parent_->get_remote_bda(), @@ -164,12 +164,12 @@ void Alpha3::send_request_(uint8_t *request, size_t len) { esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->geni_handle_, len, request, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); if (status) - ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); } void Alpha3::update() { if (this->node_state != espbt::ClientState::ESTABLISHED) { - ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str().c_str()); + ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str()); return; } diff --git a/esphome/components/am2315c/am2315c.cpp b/esphome/components/am2315c/am2315c.cpp index 048c34d749..b20a8c6cbb 100644 --- a/esphome/components/am2315c/am2315c.cpp +++ b/esphome/components/am2315c/am2315c.cpp @@ -29,22 +29,6 @@ namespace am2315c { static const char *const TAG = "am2315c"; -uint8_t AM2315C::crc8_(uint8_t *data, uint8_t len) { - uint8_t crc = 0xFF; - while (len--) { - crc ^= *data++; - for (uint8_t i = 0; i < 8; i++) { - if (crc & 0x80) { - crc <<= 1; - crc ^= 0x31; - } else { - crc <<= 1; - } - } - } - return crc; -} - bool AM2315C::reset_register_(uint8_t reg) { // code based on demo code sent by www.aosong.com // no further documentation. @@ -86,7 +70,7 @@ bool AM2315C::convert_(uint8_t *data, float &humidity, float &temperature) { humidity = raw * 9.5367431640625e-5; raw = ((data[3] & 0x0F) << 16) | (data[4] << 8) | data[5]; temperature = raw * 1.9073486328125e-4 - 50; - return this->crc8_(data, 6) == data[6]; + return crc8(data, 6, 0xFF, 0x31, true) == data[6]; } void AM2315C::setup() { diff --git a/esphome/components/am2315c/am2315c.h b/esphome/components/am2315c/am2315c.h index 9cec40e4c2..c8d01beeaa 100644 --- a/esphome/components/am2315c/am2315c.h +++ b/esphome/components/am2315c/am2315c.h @@ -21,9 +21,9 @@ // SOFTWARE. #pragma once -#include "esphome/core/component.h" -#include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/core/component.h" namespace esphome { namespace am2315c { @@ -39,7 +39,6 @@ class AM2315C : public PollingComponent, public i2c::I2CDevice { void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; } protected: - uint8_t crc8_(uint8_t *data, uint8_t len); bool convert_(uint8_t *data, float &humidity, float &temperature); bool reset_register_(uint8_t reg); diff --git a/esphome/components/am43/sensor/am43_sensor.cpp b/esphome/components/am43/sensor/am43_sensor.cpp index 4cc99001ae..b2bc3254e2 100644 --- a/esphome/components/am43/sensor/am43_sensor.cpp +++ b/esphome/components/am43/sensor/am43_sensor.cpp @@ -44,11 +44,9 @@ void Am43::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_i auto *chr = this->parent_->get_characteristic(AM43_SERVICE_UUID, AM43_CHARACTERISTIC_UUID); if (chr == nullptr) { if (this->parent_->get_characteristic(AM43_TUYA_SERVICE_UUID, AM43_TUYA_CHARACTERISTIC_UUID) != nullptr) { - ESP_LOGE(TAG, "[%s] Detected a Tuya AM43 which is not supported, sorry.", - this->parent_->address_str().c_str()); + ESP_LOGE(TAG, "[%s] Detected a Tuya AM43 which is not supported, sorry.", this->parent_->address_str()); } else { - ESP_LOGE(TAG, "[%s] No control service found at device, not an AM43..?", - this->parent_->address_str().c_str()); + ESP_LOGE(TAG, "[%s] No control service found at device, not an AM43..?", this->parent_->address_str()); } break; } @@ -82,8 +80,7 @@ void Am43::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_i this->char_handle_, packet->length, packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); if (status) { - ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), - status); + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); } } this->current_sensor_ = 0; @@ -97,7 +94,7 @@ void Am43::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_i void Am43::update() { if (this->node_state != espbt::ClientState::ESTABLISHED) { - ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str().c_str()); + ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str()); return; } if (this->current_sensor_ == 0) { @@ -107,7 +104,7 @@ void Am43::update() { esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_, packet->length, packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); if (status) { - ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); } } this->current_sensor_++; diff --git a/esphome/components/animation/animation.cpp b/esphome/components/animation/animation.cpp index 6db6f1a7bd..c2ae3b2f76 100644 --- a/esphome/components/animation/animation.cpp +++ b/esphome/components/animation/animation.cpp @@ -26,12 +26,12 @@ uint32_t Animation::get_animation_frame_count() const { return this->animation_f int Animation::get_current_frame() const { return this->current_frame_; } void Animation::next_frame() { this->current_frame_++; - if (loop_count_ && this->current_frame_ == loop_end_frame_ && + if (loop_count_ && static_cast(this->current_frame_) == loop_end_frame_ && (this->loop_current_iteration_ < loop_count_ || loop_count_ < 0)) { this->current_frame_ = loop_start_frame_; this->loop_current_iteration_++; } - if (this->current_frame_ >= animation_frame_count_) { + if (static_cast(this->current_frame_) >= animation_frame_count_) { this->loop_current_iteration_ = 1; this->current_frame_ = 0; } diff --git a/esphome/components/animation/animation.h b/esphome/components/animation/animation.h index c44e0060af..b33254df30 100644 --- a/esphome/components/animation/animation.h +++ b/esphome/components/animation/animation.h @@ -39,7 +39,7 @@ class Animation : public image::Image { template class AnimationNextFrameAction : public Action { public: AnimationNextFrameAction(Animation *parent) : parent_(parent) {} - void play(Ts... x) override { this->parent_->next_frame(); } + void play(const Ts &...x) override { this->parent_->next_frame(); } protected: Animation *parent_; @@ -48,7 +48,7 @@ template class AnimationNextFrameAction : public Action { template class AnimationPrevFrameAction : public Action { public: AnimationPrevFrameAction(Animation *parent) : parent_(parent) {} - void play(Ts... x) override { this->parent_->prev_frame(); } + void play(const Ts &...x) override { this->parent_->prev_frame(); } protected: Animation *parent_; @@ -58,7 +58,7 @@ template class AnimationSetFrameAction : public Action { public: AnimationSetFrameAction(Animation *parent) : parent_(parent) {} TEMPLATABLE_VALUE(uint16_t, frame) - void play(Ts... x) override { this->parent_->set_frame(this->frame_.value(x...)); } + void play(const Ts &...x) override { this->parent_->set_frame(this->frame_.value(x...)); } protected: Animation *parent_; diff --git a/esphome/components/anova/anova.cpp b/esphome/components/anova/anova.cpp index d0e8f6827f..2693224a97 100644 --- a/esphome/components/anova/anova.cpp +++ b/esphome/components/anova/anova.cpp @@ -42,7 +42,7 @@ void Anova::control(const ClimateCall &call) { esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_, pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); if (status) { - ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); } } if (call.get_target_temperature().has_value()) { @@ -51,7 +51,7 @@ void Anova::control(const ClimateCall &call) { esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_, pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); if (status) { - ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); } } } @@ -124,8 +124,7 @@ void Anova::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_ esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_, pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); if (status) { - ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), - status); + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); } } } @@ -150,7 +149,7 @@ void Anova::update() { esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_, pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); if (status) { - ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); } this->current_request_++; } diff --git a/esphome/components/anova/anova.h b/esphome/components/anova/anova.h index 560d96baa7..2e43ebfb98 100644 --- a/esphome/components/anova/anova.h +++ b/esphome/components/anova/anova.h @@ -28,7 +28,7 @@ class Anova : public climate::Climate, public esphome::ble_client::BLEClientNode void dump_config() override; climate::ClimateTraits traits() override { auto traits = climate::ClimateTraits(); - traits.set_supports_current_temperature(true); + traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE); traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::ClimateMode::CLIMATE_MODE_HEAT}); traits.set_visual_min_temperature(25.0); traits.set_visual_max_temperature(100.0); diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 5d398a4e23..2910643dfb 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -1,4 +1,5 @@ import base64 +import logging from esphome import automation from esphome.automation import Condition @@ -8,34 +9,59 @@ import esphome.config_validation as cv from esphome.const import ( CONF_ACTION, CONF_ACTIONS, + CONF_CAPTURE_RESPONSE, CONF_DATA, CONF_DATA_TEMPLATE, CONF_EVENT, CONF_ID, CONF_KEY, + CONF_MAX_CONNECTIONS, CONF_ON_CLIENT_CONNECTED, CONF_ON_CLIENT_DISCONNECTED, + CONF_ON_ERROR, + CONF_ON_SUCCESS, CONF_PASSWORD, CONF_PORT, CONF_REBOOT_TIMEOUT, + CONF_RESPONSE_TEMPLATE, CONF_SERVICE, CONF_SERVICES, CONF_TAG, CONF_TRIGGER_ID, CONF_VARIABLES, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority +from esphome.cpp_generator import TemplateArgsType +from esphome.types import ConfigType + +_LOGGER = logging.getLogger(__name__) DOMAIN = "api" DEPENDENCIES = ["network"] -AUTO_LOAD = ["socket"] -CODEOWNERS = ["@OttoWinter"] +CODEOWNERS = ["@esphome/core"] + + +def AUTO_LOAD(config: ConfigType) -> list[str]: + """Conditionally auto-load json only when capture_response is used.""" + base = ["socket"] + + # Check if any homeassistant.action/homeassistant.service has capture_response: true + # This flag is set during config validation in _validate_response_config + if not config or CORE.data.get(DOMAIN, {}).get(CONF_CAPTURE_RESPONSE, False): + return base + ["json"] + + return base + api_ns = cg.esphome_ns.namespace("api") APIServer = api_ns.class_("APIServer", cg.Component, cg.Controller) HomeAssistantServiceCallAction = api_ns.class_( "HomeAssistantServiceCallAction", automation.Action ) +ActionResponse = api_ns.class_("ActionResponse") +HomeAssistantActionResponseTrigger = api_ns.class_( + "HomeAssistantActionResponseTrigger", automation.Trigger +) APIConnectedCondition = api_ns.class_("APIConnectedCondition", Condition) UserServiceTrigger = api_ns.class_("UserServiceTrigger", automation.Trigger) @@ -45,16 +71,21 @@ SERVICE_ARG_NATIVE_TYPES = { "int": cg.int32, "float": float, "string": cg.std_string, - "bool[]": cg.std_vector.template(bool), - "int[]": cg.std_vector.template(cg.int32), - "float[]": cg.std_vector.template(float), - "string[]": cg.std_vector.template(cg.std_string), + "bool[]": cg.FixedVector.template(bool).operator("const").operator("ref"), + "int[]": cg.FixedVector.template(cg.int32).operator("const").operator("ref"), + "float[]": cg.FixedVector.template(float).operator("const").operator("ref"), + "string[]": cg.FixedVector.template(cg.std_string) + .operator("const") + .operator("ref"), } CONF_ENCRYPTION = "encryption" CONF_BATCH_DELAY = "batch_delay" CONF_CUSTOM_SERVICES = "custom_services" CONF_HOMEASSISTANT_SERVICES = "homeassistant_services" CONF_HOMEASSISTANT_STATES = "homeassistant_states" +CONF_LISTEN_BACKLOG = "listen_backlog" +CONF_MAX_SEND_QUEUE = "max_send_queue" +CONF_STATE_SUBSCRIPTION_ONLY = "state_subscription_only" def validate_encryption_key(value): @@ -101,6 +132,43 @@ def _encryption_schema(config): return ENCRYPTION_SCHEMA(config) +def _validate_api_config(config: ConfigType) -> ConfigType: + """Validate API configuration with mutual exclusivity check and deprecation warning.""" + # Check if both password and encryption are configured + has_password = CONF_PASSWORD in config and config[CONF_PASSWORD] + has_encryption = CONF_ENCRYPTION in config + + if has_password and has_encryption: + raise cv.Invalid( + "The 'password' and 'encryption' options are mutually exclusive. " + "The API client only supports one authentication method at a time. " + "Please remove one of them. " + "Note: 'password' authentication is deprecated and will be removed in version 2026.1.0. " + "We strongly recommend using 'encryption' instead for better security." + ) + + # Warn about password deprecation + if has_password: + _LOGGER.warning( + "API 'password' authentication has been deprecated since May 2022 and will be removed in version 2026.1.0. " + "Please migrate to the 'encryption' configuration. " + "See https://esphome.io/components/api.html#configuration-variables" + ) + + return config + + +def _consume_api_sockets(config: ConfigType) -> ConfigType: + """Register socket needs for API component.""" + from esphome.components import socket + + # API needs 1 listening socket + typically 3 concurrent client connections + # (not max_connections, which is the upper limit rarely reached) + sockets_needed = 1 + 3 + socket.consume_sockets(sockets_needed, "api")(config) + return config + + CONFIG_SCHEMA = cv.All( cv.Schema( { @@ -128,27 +196,78 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_ON_CLIENT_DISCONNECTED): automation.validate_automation( single=True ), + # Connection limits to prevent memory exhaustion on resource-constrained devices + # Each connection uses ~500-1000 bytes of RAM plus system resources + # Platform defaults based on available RAM and network stack implementation: + cv.SplitDefault( + CONF_LISTEN_BACKLOG, + esp8266=1, # Limited RAM (~40KB free), LWIP raw sockets + esp32=4, # More RAM (520KB), BSD sockets + rp2040=1, # Limited RAM (264KB), LWIP raw sockets like ESP8266 + bk72xx=4, # Moderate RAM, BSD-style sockets + rtl87xx=4, # Moderate RAM, BSD-style sockets + host=4, # Abundant resources + ln882x=4, # Moderate RAM + ): cv.int_range(min=1, max=10), + cv.SplitDefault( + CONF_MAX_CONNECTIONS, + esp8266=4, # ~40KB free RAM, each connection uses ~500-1000 bytes + esp32=8, # 520KB RAM available + rp2040=4, # 264KB RAM but LWIP constraints + bk72xx=8, # Moderate RAM + rtl87xx=8, # Moderate RAM + host=8, # Abundant resources + ln882x=8, # Moderate RAM + ): cv.int_range(min=1, max=20), + # Maximum queued send buffers per connection before dropping connection + # Each buffer uses ~8-12 bytes overhead plus actual message size + # Platform defaults based on available RAM and typical message rates: + cv.SplitDefault( + CONF_MAX_SEND_QUEUE, + esp8266=5, # Limited RAM, need to fail fast + esp32=8, # More RAM, can buffer more + rp2040=5, # Limited RAM + bk72xx=8, # Moderate RAM + nrf52=8, # Moderate RAM + rtl87xx=8, # Moderate RAM + host=16, # Abundant resources + ln882x=8, # Moderate RAM + ): cv.int_range(min=1, max=64), } ).extend(cv.COMPONENT_SCHEMA), cv.rename_key(CONF_SERVICES, CONF_ACTIONS), + _validate_api_config, + _consume_api_sockets, ) -@coroutine_with_priority(40.0) +@coroutine_with_priority(CoroPriority.WEB) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) + # Track controller registration for StaticVector sizing + CORE.register_controller() + cg.add(var.set_port(config[CONF_PORT])) if config[CONF_PASSWORD]: cg.add_define("USE_API_PASSWORD") cg.add(var.set_password(config[CONF_PASSWORD])) cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) cg.add(var.set_batch_delay(config[CONF_BATCH_DELAY])) + if CONF_LISTEN_BACKLOG in config: + cg.add(var.set_listen_backlog(config[CONF_LISTEN_BACKLOG])) + if CONF_MAX_CONNECTIONS in config: + cg.add(var.set_max_connections(config[CONF_MAX_CONNECTIONS])) + cg.add_define("API_MAX_SEND_QUEUE", config[CONF_MAX_SEND_QUEUE]) - # Set USE_API_SERVICES if any services are enabled + # Set USE_API_USER_DEFINED_ACTIONS if any services are enabled if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]: - cg.add_define("USE_API_SERVICES") + cg.add_define("USE_API_USER_DEFINED_ACTIONS") + + # Set USE_API_CUSTOM_SERVICES if external components need dynamic service registration + if config[CONF_CUSTOM_SERVICES]: + cg.add_define("USE_API_CUSTOM_SERVICES") if config[CONF_HOMEASSISTANT_SERVICES]: cg.add_define("USE_API_HOMEASSISTANT_SERVICES") @@ -157,6 +276,8 @@ async def to_code(config): cg.add_define("USE_API_HOMEASSISTANT_STATES") if actions := config.get(CONF_ACTIONS, []): + # Collect all triggers first, then register all at once with initializer_list + triggers: list[cg.Pvariable] = [] for conf in actions: template_args = [] func_args = [] @@ -170,8 +291,10 @@ async def to_code(config): trigger = cg.new_Pvariable( conf[CONF_TRIGGER_ID], templ, conf[CONF_ACTION], service_arg_names ) - cg.add(var.register_user_service(trigger)) + triggers.append(trigger) await automation.build_automation(trigger, func_args, conf) + # Register all services at once - single allocation, no reallocations + cg.add(var.initialize_user_services(triggers)) if CONF_ON_CLIENT_CONNECTED in config: cg.add_define("USE_API_CLIENT_CONNECTED_TRIGGER") @@ -193,6 +316,7 @@ async def to_code(config): if key := encryption_config.get(CONF_KEY): decoded = base64.b64decode(key) cg.add(var.set_noise_psk(list(decoded))) + cg.add_define("USE_API_NOISE_PSK_FROM_YAML") else: # No key provided, but encryption desired # This will allow a plaintext client to provide a noise key, @@ -212,6 +336,29 @@ async def to_code(config): KEY_VALUE_SCHEMA = cv.Schema({cv.string: cv.templatable(cv.string_strict)}) +def _validate_response_config(config: ConfigType) -> ConfigType: + # Validate dependencies: + # - response_template requires capture_response: true + # - capture_response: true requires on_success + if CONF_RESPONSE_TEMPLATE in config and not config[CONF_CAPTURE_RESPONSE]: + raise cv.Invalid( + f"`{CONF_RESPONSE_TEMPLATE}` requires `{CONF_CAPTURE_RESPONSE}: true` to be set.", + path=[CONF_RESPONSE_TEMPLATE], + ) + + if config[CONF_CAPTURE_RESPONSE] and CONF_ON_SUCCESS not in config: + raise cv.Invalid( + f"`{CONF_CAPTURE_RESPONSE}: true` requires `{CONF_ON_SUCCESS}` to be set.", + path=[CONF_CAPTURE_RESPONSE], + ) + + # Track if any action uses capture_response for AUTO_LOAD + if config[CONF_CAPTURE_RESPONSE]: + CORE.data.setdefault(DOMAIN, {})[CONF_CAPTURE_RESPONSE] = True + + return config + + HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All( cv.Schema( { @@ -227,10 +374,15 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All( cv.Optional(CONF_VARIABLES, default={}): cv.Schema( {cv.string: cv.returning_lambda} ), + cv.Optional(CONF_RESPONSE_TEMPLATE): cv.templatable(cv.string), + cv.Optional(CONF_CAPTURE_RESPONSE, default=False): cv.boolean, + cv.Optional(CONF_ON_SUCCESS): automation.validate_automation(single=True), + cv.Optional(CONF_ON_ERROR): automation.validate_automation(single=True), } ), cv.has_exactly_one_key(CONF_SERVICE, CONF_ACTION), cv.rename_key(CONF_SERVICE, CONF_ACTION), + _validate_response_config, ) @@ -244,21 +396,67 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All( HomeAssistantServiceCallAction, HOMEASSISTANT_ACTION_ACTION_SCHEMA, ) -async def homeassistant_service_to_code(config, action_id, template_arg, args): +async def homeassistant_service_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +): cg.add_define("USE_API_HOMEASSISTANT_SERVICES") serv = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, serv, False) templ = await cg.templatable(config[CONF_ACTION], args, None) cg.add(var.set_service(templ)) + + # Initialize FixedVectors with exact sizes from config + cg.add(var.init_data(len(config[CONF_DATA]))) for key, value in config[CONF_DATA].items(): templ = await cg.templatable(value, args, None) cg.add(var.add_data(key, templ)) + + cg.add(var.init_data_template(len(config[CONF_DATA_TEMPLATE]))) for key, value in config[CONF_DATA_TEMPLATE].items(): templ = await cg.templatable(value, args, None) cg.add(var.add_data_template(key, templ)) + + cg.add(var.init_variables(len(config[CONF_VARIABLES]))) for key, value in config[CONF_VARIABLES].items(): templ = await cg.templatable(value, args, None) cg.add(var.add_variable(key, templ)) + + if on_error := config.get(CONF_ON_ERROR): + cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES") + cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES_ERRORS") + cg.add(var.set_wants_status()) + await automation.build_automation( + var.get_error_trigger(), + [(cg.std_string, "error"), *args], + on_error, + ) + + if on_success := config.get(CONF_ON_SUCCESS): + cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES") + cg.add(var.set_wants_status()) + if config[CONF_CAPTURE_RESPONSE]: + cg.add(var.set_wants_response()) + cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON") + await automation.build_automation( + var.get_success_trigger_with_response(), + [(cg.JsonObjectConst, "response"), *args], + on_success, + ) + + if response_template := config.get(CONF_RESPONSE_TEMPLATE): + templ = await cg.templatable(response_template, args, cg.std_string) + cg.add(var.set_response_template(templ)) + + else: + await automation.build_automation( + var.get_success_trigger(), + args, + on_success, + ) + return var @@ -294,15 +492,23 @@ async def homeassistant_event_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg, serv, True) templ = await cg.templatable(config[CONF_EVENT], args, None) cg.add(var.set_service(templ)) + + # Initialize FixedVectors with exact sizes from config + cg.add(var.init_data(len(config[CONF_DATA]))) for key, value in config[CONF_DATA].items(): templ = await cg.templatable(value, args, None) cg.add(var.add_data(key, templ)) + + cg.add(var.init_data_template(len(config[CONF_DATA_TEMPLATE]))) for key, value in config[CONF_DATA_TEMPLATE].items(): templ = await cg.templatable(value, args, None) cg.add(var.add_data_template(key, templ)) + + cg.add(var.init_variables(len(config[CONF_VARIABLES]))) for key, value in config[CONF_VARIABLES].items(): templ = await cg.templatable(value, args, None) cg.add(var.add_variable(key, templ)) + return var @@ -321,17 +527,35 @@ HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA = cv.maybe_simple_value( HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA, ) async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, args): + cg.add_define("USE_API_HOMEASSISTANT_SERVICES") serv = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, serv, True) cg.add(var.set_service("esphome.tag_scanned")) + # Initialize FixedVector with exact size (1 data field) + cg.add(var.init_data(1)) templ = await cg.templatable(config[CONF_TAG], args, cg.std_string) cg.add(var.add_data("tag_id", templ)) return var -@automation.register_condition("api.connected", APIConnectedCondition, {}) +API_CONNECTED_CONDITION_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(APIServer), + cv.Optional(CONF_STATE_SUBSCRIPTION_ONLY, default=False): cv.templatable( + cv.boolean + ), + } +) + + +@automation.register_condition( + "api.connected", APIConnectedCondition, API_CONNECTED_CONDITION_SCHEMA +) async def api_connected_to_code(config, condition_id, template_arg, args): - return cg.new_Pvariable(condition_id, template_arg) + var = cg.new_Pvariable(condition_id, template_arg) + templ = await cg.templatable(config[CONF_STATE_SUBSCRIPTION_ONLY], args, cg.bool_) + cg.add(var.set_state_subscription_only(templ)) + return var def FILTER_SOURCE_FILES() -> list[str]: diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 67e91cc8e3..74a8e8ff7f 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -7,7 +7,7 @@ service APIConnection { option (needs_setup_connection) = false; option (needs_authentication) = false; } - rpc connect (ConnectRequest) returns (ConnectResponse) { + rpc authenticate (AuthenticationRequest) returns (AuthenticationResponse) { option (needs_setup_connection) = false; option (needs_authentication) = false; } @@ -27,9 +27,6 @@ service APIConnection { rpc subscribe_logs (SubscribeLogsRequest) returns (void) {} rpc subscribe_homeassistant_services (SubscribeHomeassistantServicesRequest) returns (void) {} rpc subscribe_home_assistant_states (SubscribeHomeAssistantStatesRequest) returns (void) {} - rpc get_time (GetTimeRequest) returns (GetTimeResponse) { - option (needs_authentication) = false; - } rpc execute_service (ExecuteServiceRequest) returns (void) {} rpc noise_encryption_set_key (NoiseEncryptionSetKeyRequest) returns (NoiseEncryptionSetKeyResponse) {} @@ -69,6 +66,9 @@ service APIConnection { rpc voice_assistant_set_configuration(VoiceAssistantSetConfiguration) returns (void) {} rpc alarm_control_panel_command (AlarmControlPanelCommandRequest) returns (void) {} + + rpc zwave_proxy_frame(ZWaveProxyFrame) returns (void) {} + rpc zwave_proxy_request(ZWaveProxyRequest) returns (void) {} } @@ -102,7 +102,7 @@ message HelloRequest { // For example "Home Assistant" // Not strictly necessary to send but nice for debugging // purposes. - string client_info = 1; + string client_info = 1 [(pointer_to_buffer) = true]; uint32 api_version_major = 2; uint32 api_version_minor = 3; } @@ -132,21 +132,23 @@ message HelloResponse { // Message sent at the beginning of each connection to authenticate the client // Can only be sent by the client and only at the beginning of the connection -message ConnectRequest { +message AuthenticationRequest { option (id) = 3; option (source) = SOURCE_CLIENT; option (no_delay) = true; + option (ifdef) = "USE_API_PASSWORD"; // The password to log in with - string password = 1; + string password = 1 [(pointer_to_buffer) = true]; } // Confirmation of successful connection. After this the connection is available for all traffic. // Can only be sent by the server and only at the beginning of the connection -message ConnectResponse { +message AuthenticationResponse { option (id) = 4; option (source) = SOURCE_SERVER; option (no_delay) = true; + option (ifdef) = "USE_API_PASSWORD"; bool invalid_password = 1; } @@ -255,6 +257,10 @@ message DeviceInfoResponse { // Top-level area info to phase out suggested_area AreaInfo area = 22 [(field_ifdef) = "USE_AREAS"]; + + // Indicates if Z-Wave proxy support is available and features supported + uint32 zwave_proxy_feature_flags = 23 [(field_ifdef) = "USE_ZWAVE_PROXY"]; + uint32 zwave_home_id = 24 [(field_ifdef) = "USE_ZWAVE_PROXY"]; } message ListEntitiesRequest { @@ -419,7 +425,7 @@ message ListEntitiesFanResponse { bool disabled_by_default = 9; string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON"]; EntityCategory entity_category = 11; - repeated string supported_preset_modes = 12 [(container_pointer) = "std::set"]; + repeated string supported_preset_modes = 12 [(container_pointer_no_template) = "std::vector"]; uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"]; } // Deprecated in API version 1.6 - only used in deprecated fields @@ -500,7 +506,7 @@ message ListEntitiesLightResponse { string name = 3; reserved 4; // Deprecated: was string unique_id - repeated ColorMode supported_color_modes = 12 [(container_pointer) = "std::set"]; + repeated ColorMode supported_color_modes = 12 [(container_pointer_no_template) = "light::ColorModeMask"]; // next four supports_* are for legacy clients, newer clients should use color modes // Deprecated in API version 1.6 bool legacy_supports_brightness = 5 [deprecated=true]; @@ -512,7 +518,7 @@ message ListEntitiesLightResponse { bool legacy_supports_color_temperature = 8 [deprecated=true]; float min_mireds = 9; float max_mireds = 10; - repeated string effects = 11; + repeated string effects = 11 [(container_pointer_no_template) = "FixedVector"]; bool disabled_by_default = 13; string icon = 14 [(field_ifdef) = "USE_ENTITY_ICON"]; EntityCategory entity_category = 15; @@ -763,17 +769,33 @@ message HomeassistantServiceMap { string value = 2 [(no_zero_copy) = true]; } -message HomeassistantServiceResponse { +message HomeassistantActionRequest { option (id) = 35; option (source) = SOURCE_SERVER; option (no_delay) = true; option (ifdef) = "USE_API_HOMEASSISTANT_SERVICES"; string service = 1; - repeated HomeassistantServiceMap data = 2; - repeated HomeassistantServiceMap data_template = 3; - repeated HomeassistantServiceMap variables = 4; + repeated HomeassistantServiceMap data = 2 [(fixed_vector) = true]; + repeated HomeassistantServiceMap data_template = 3 [(fixed_vector) = true]; + repeated HomeassistantServiceMap variables = 4 [(fixed_vector) = true]; bool is_event = 5; + uint32 call_id = 6 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES"]; + bool wants_response = 7 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"]; + string response_template = 8 [(no_zero_copy) = true, (field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"]; +} + +// Message sent by Home Assistant to ESPHome with service call response data +message HomeassistantActionResponse { + option (id) = 130; + option (source) = SOURCE_CLIENT; + option (no_delay) = true; + option (ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES"; + + uint32 call_id = 1; // Matches the call_id from HomeassistantActionRequest + bool success = 2; // Whether the service call succeeded + string error_message = 3; // Error message if success = false + bytes response_data = 4 [(pointer_to_buffer) = true, (field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"]; } // ==================== IMPORT HOME ASSISTANT STATES ==================== @@ -809,15 +831,16 @@ message HomeAssistantStateResponse { // ==================== IMPORT TIME ==================== message GetTimeRequest { option (id) = 36; - option (source) = SOURCE_BOTH; + option (source) = SOURCE_SERVER; } message GetTimeResponse { option (id) = 37; - option (source) = SOURCE_BOTH; + option (source) = SOURCE_CLIENT; option (no_delay) = true; fixed32 epoch_seconds = 1; + string timezone = 2 [(pointer_to_buffer) = true]; } // ==================== USER-DEFINES SERVICES ==================== @@ -832,40 +855,40 @@ enum ServiceArgType { SERVICE_ARG_TYPE_STRING_ARRAY = 7; } message ListEntitiesServicesArgument { - option (ifdef) = "USE_API_SERVICES"; + option (ifdef) = "USE_API_USER_DEFINED_ACTIONS"; string name = 1; ServiceArgType type = 2; } message ListEntitiesServicesResponse { option (id) = 41; option (source) = SOURCE_SERVER; - option (ifdef) = "USE_API_SERVICES"; + option (ifdef) = "USE_API_USER_DEFINED_ACTIONS"; string name = 1; fixed32 key = 2; - repeated ListEntitiesServicesArgument args = 3; + repeated ListEntitiesServicesArgument args = 3 [(fixed_vector) = true]; } message ExecuteServiceArgument { - option (ifdef) = "USE_API_SERVICES"; + option (ifdef) = "USE_API_USER_DEFINED_ACTIONS"; bool bool_ = 1; int32 legacy_int = 2; float float_ = 3; string string_ = 4; // ESPHome 1.14 (api v1.3) make int a signed value sint32 int_ = 5; - repeated bool bool_array = 6 [packed=false]; - repeated sint32 int_array = 7 [packed=false]; - repeated float float_array = 8 [packed=false]; - repeated string string_array = 9; + repeated bool bool_array = 6 [packed=false, (fixed_vector) = true]; + repeated sint32 int_array = 7 [packed=false, (fixed_vector) = true]; + repeated float float_array = 8 [packed=false, (fixed_vector) = true]; + repeated string string_array = 9 [(fixed_vector) = true]; } message ExecuteServiceRequest { option (id) = 42; option (source) = SOURCE_CLIENT; option (no_delay) = true; - option (ifdef) = "USE_API_SERVICES"; + option (ifdef) = "USE_API_USER_DEFINED_ACTIONS"; fixed32 key = 1; - repeated ExecuteServiceArgument args = 2; + repeated ExecuteServiceArgument args = 2 [(fixed_vector) = true]; } // ==================== CAMERA ==================== @@ -964,9 +987,9 @@ message ListEntitiesClimateResponse { string name = 3; reserved 4; // Deprecated: was string unique_id - bool supports_current_temperature = 5; - bool supports_two_point_target_temperature = 6; - repeated ClimateMode supported_modes = 7 [(container_pointer) = "std::set"]; + bool supports_current_temperature = 5; // Deprecated: use feature_flags + bool supports_two_point_target_temperature = 6; // Deprecated: use feature_flags + repeated ClimateMode supported_modes = 7 [(container_pointer_no_template) = "climate::ClimateModeMask"]; float visual_min_temperature = 8; float visual_max_temperature = 9; float visual_target_temperature_step = 10; @@ -974,21 +997,22 @@ message ListEntitiesClimateResponse { // is if CLIMATE_PRESET_AWAY exists is supported_presets // Deprecated in API version 1.5 bool legacy_supports_away = 11 [deprecated=true]; - bool supports_action = 12; - repeated ClimateFanMode supported_fan_modes = 13 [(container_pointer) = "std::set"]; - repeated ClimateSwingMode supported_swing_modes = 14 [(container_pointer) = "std::set"]; - repeated string supported_custom_fan_modes = 15 [(container_pointer) = "std::set"]; - repeated ClimatePreset supported_presets = 16 [(container_pointer) = "std::set"]; - repeated string supported_custom_presets = 17 [(container_pointer) = "std::set"]; + bool supports_action = 12; // Deprecated: use feature_flags + repeated ClimateFanMode supported_fan_modes = 13 [(container_pointer_no_template) = "climate::ClimateFanModeMask"]; + repeated ClimateSwingMode supported_swing_modes = 14 [(container_pointer_no_template) = "climate::ClimateSwingModeMask"]; + repeated string supported_custom_fan_modes = 15 [(container_pointer_no_template) = "std::vector"]; + repeated ClimatePreset supported_presets = 16 [(container_pointer_no_template) = "climate::ClimatePresetMask"]; + repeated string supported_custom_presets = 17 [(container_pointer_no_template) = "std::vector"]; bool disabled_by_default = 18; string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON"]; EntityCategory entity_category = 20; float visual_current_temperature_step = 21; - bool supports_current_humidity = 22; - bool supports_target_humidity = 23; + bool supports_current_humidity = 22; // Deprecated: use feature_flags + bool supports_target_humidity = 23; // Deprecated: use feature_flags float visual_min_humidity = 24; float visual_max_humidity = 25; uint32 device_id = 26 [(field_ifdef) = "USE_DEVICES"]; + uint32 feature_flags = 27; } message ClimateStateResponse { option (id) = 47; @@ -1119,7 +1143,7 @@ message ListEntitiesSelectResponse { reserved 4; // Deprecated: was string unique_id string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; - repeated string options = 6 [(container_pointer) = "std::vector"]; + repeated string options = 6 [(container_pointer_no_template) = "FixedVector"]; bool disabled_by_default = 7; EntityCategory entity_category = 8; uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"]; @@ -1438,11 +1462,11 @@ message BluetoothLERawAdvertisementsResponse { option (ifdef) = "USE_BLUETOOTH_PROXY"; option (no_delay) = true; - repeated BluetoothLERawAdvertisement advertisements = 1; + repeated BluetoothLERawAdvertisement advertisements = 1 [(fixed_array_with_length_define) = "BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE"]; } enum BluetoothDeviceRequestType { - BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT = 0; + BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT = 0 [deprecated = true]; // V1 removed, use V3 variants BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT = 1; BLUETOOTH_DEVICE_REQUEST_TYPE_PAIR = 2; BLUETOOTH_DEVICE_REQUEST_TYPE_UNPAIR = 3; @@ -1458,7 +1482,7 @@ message BluetoothDeviceRequest { uint64 address = 1; BluetoothDeviceRequestType request_type = 2; - bool has_address_type = 3; + bool has_address_type = 3; // Deprecated, should be removed in 2027.8 - https://github.com/esphome/esphome/pull/10318 uint32 address_type = 4; } @@ -1496,7 +1520,7 @@ message BluetoothGATTCharacteristic { repeated uint64 uuid = 1 [(fixed_array_size) = 2, (fixed_array_skip_zero) = true]; uint32 handle = 2; uint32 properties = 3; - repeated BluetoothGATTDescriptor descriptors = 4; + repeated BluetoothGATTDescriptor descriptors = 4 [(fixed_vector) = true]; // New field for efficient UUID (v1.12+) // Only one of uuid or short_uuid will be set. @@ -1508,7 +1532,7 @@ message BluetoothGATTCharacteristic { message BluetoothGATTService { repeated uint64 uuid = 1 [(fixed_array_size) = 2, (fixed_array_skip_zero) = true]; uint32 handle = 2; - repeated BluetoothGATTCharacteristic characteristics = 3; + repeated BluetoothGATTCharacteristic characteristics = 3 [(fixed_vector) = true]; // New field for efficient UUID (v1.12+) // Only one of uuid or short_uuid will be set. @@ -1564,7 +1588,7 @@ message BluetoothGATTWriteRequest { uint32 handle = 2; bool response = 3; - bytes data = 4; + bytes data = 4 [(pointer_to_buffer) = true]; } message BluetoothGATTReadDescriptorRequest { @@ -1584,7 +1608,7 @@ message BluetoothGATTWriteDescriptorRequest { uint64 address = 1; uint32 handle = 2; - bytes data = 3; + bytes data = 3 [(pointer_to_buffer) = true]; } message BluetoothGATTNotifyRequest { @@ -1712,6 +1736,7 @@ message BluetoothScannerStateResponse { BluetoothScannerState state = 1; BluetoothScannerMode mode = 2; + BluetoothScannerMode configured_mode = 3; } message BluetoothScannerSetModeRequest { @@ -1857,10 +1882,22 @@ message VoiceAssistantWakeWord { repeated string trained_languages = 3; } +message VoiceAssistantExternalWakeWord { + string id = 1; + string wake_word = 2; + repeated string trained_languages = 3; + string model_type = 4; + uint32 model_size = 5; + string model_hash = 6; + string url = 7; +} + message VoiceAssistantConfigurationRequest { option (id) = 121; option (source) = SOURCE_CLIENT; option (ifdef) = "USE_VOICE_ASSISTANT"; + + repeated VoiceAssistantExternalWakeWord external_wake_words = 1; } message VoiceAssistantConfigurationResponse { @@ -2110,7 +2147,7 @@ message ListEntitiesEventResponse { EntityCategory entity_category = 7; string device_class = 8; - repeated string event_types = 9; + repeated string event_types = 9 [(container_pointer_no_template) = "FixedVector"]; uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"]; } message EventResponse { @@ -2275,3 +2312,28 @@ message UpdateCommandRequest { UpdateCommand command = 2; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } + +// ==================== Z-WAVE ==================== + +message ZWaveProxyFrame { + option (id) = 128; + option (source) = SOURCE_BOTH; + option (ifdef) = "USE_ZWAVE_PROXY"; + option (no_delay) = true; + + bytes data = 1 [(pointer_to_buffer) = true]; +} + +enum ZWaveProxyRequestType { + ZWAVE_PROXY_REQUEST_TYPE_SUBSCRIBE = 0; + ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE = 1; + ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE = 2; +} +message ZWaveProxyRequest { + option (id) = 129; + option (source) = SOURCE_BOTH; + option (ifdef) = "USE_ZWAVE_PROXY"; + + ZWaveProxyRequestType type = 1; + bytes data = 2 [(pointer_to_buffer) = true]; +} diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index cdeabb5cac..12cbbb991d 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -8,9 +8,9 @@ #endif #include #include -#include #include #include +#include #include "esphome/components/network/util.h" #include "esphome/core/application.h" #include "esphome/core/entity_base.h" @@ -27,9 +27,15 @@ #ifdef USE_BLUETOOTH_PROXY #include "esphome/components/bluetooth_proxy/bluetooth_proxy.h" #endif +#ifdef USE_CLIMATE +#include "esphome/components/climate/climate_mode.h" +#endif #ifdef USE_VOICE_ASSISTANT #include "esphome/components/voice_assistant/voice_assistant.h" #endif +#ifdef USE_ZWAVE_PROXY +#include "esphome/components/zwave_proxy/zwave_proxy.h" +#endif namespace esphome::api { @@ -42,6 +48,8 @@ static constexpr uint8_t MAX_PING_RETRIES = 60; static constexpr uint16_t PING_RETRY_INTERVAL = 1000; static constexpr uint32_t KEEPALIVE_DISCONNECT_TIMEOUT = (KEEPALIVE_TIMEOUT_MS * 5) / 2; +static constexpr auto ESPHOME_VERSION_REF = StringRef::from_lit(ESPHOME_VERSION); + static const char *const TAG = "api.connection"; #ifdef USE_CAMERA static const int CAMERA_STOP_STREAM = 5000; @@ -82,8 +90,8 @@ static const int CAMERA_STOP_STREAM = 5000; APIConnection::APIConnection(std::unique_ptr sock, APIServer *parent) : parent_(parent), initial_state_iterator_(this), list_entities_iterator_(this) { #if defined(USE_API_PLAINTEXT) && defined(USE_API_NOISE) - auto noise_ctx = parent->get_noise_ctx(); - if (noise_ctx->has_psk()) { + auto &noise_ctx = parent->get_noise_ctx(); + if (noise_ctx.has_psk()) { this->helper_ = std::unique_ptr{new APINoiseFrameHelper(std::move(sock), noise_ctx, &this->client_info_)}; } else { @@ -111,8 +119,7 @@ void APIConnection::start() { APIError err = this->helper_->init(); if (err != APIError::OK) { - on_fatal_error(); - this->log_warning_("Helper init failed", err); + this->fatal_error_with_log_(LOG_STR("Helper init failed"), err); return; } this->client_info_.peername = helper_->getpeername(); @@ -142,8 +149,7 @@ void APIConnection::loop() { APIError err = this->helper_->loop(); if (err != APIError::OK) { - on_fatal_error(); - this->log_socket_operation_failed_(err); + this->fatal_error_with_log_(LOG_STR("Socket operation failed"), err); return; } @@ -158,17 +164,13 @@ void APIConnection::loop() { // No more data available break; } else if (err != APIError::OK) { - on_fatal_error(); - this->log_warning_("Reading failed", err); + this->fatal_error_with_log_(LOG_STR("Reading failed"), err); return; } else { this->last_traffic_ = now; // read a packet - if (buffer.data_len > 0) { - this->read_message(buffer.data_len, buffer.type, &buffer.container[buffer.data_offset]); - } else { - this->read_message(0, buffer.type, nullptr); - } + this->read_message(buffer.data_len, buffer.type, + buffer.data_len > 0 ? &buffer.container[buffer.data_offset] : nullptr); if (this->flags_.remove) return; } @@ -200,7 +202,8 @@ void APIConnection::loop() { // Disconnect if not responded within 2.5*keepalive if (now - this->last_traffic_ > KEEPALIVE_DISCONNECT_TIMEOUT) { on_fatal_error(); - ESP_LOGW(TAG, "%s is unresponsive; disconnecting", this->get_client_combined_info().c_str()); + ESP_LOGW(TAG, "%s (%s) is unresponsive; disconnecting", this->client_info_.name.c_str(), + this->client_info_.peername.c_str()); } } else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS && !this->flags_.remove) { // Only send ping if we're not disconnecting @@ -250,7 +253,7 @@ bool APIConnection::send_disconnect_response(const DisconnectRequest &msg) { // remote initiated disconnect_client // don't close yet, we still need to send the disconnect response // close will happen on next loop - ESP_LOGD(TAG, "%s disconnected", this->get_client_combined_info().c_str()); + ESP_LOGD(TAG, "%s (%s) disconnected", this->client_info_.name.c_str(), this->client_info_.peername.c_str()); this->flags_.next_close = true; DisconnectResponse resp; return this->send_message(resp, DisconnectResponse::MESSAGE_TYPE); @@ -289,16 +292,26 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint8_t mess return 0; // Doesn't fit } - // Allocate buffer space - pass payload size, allocation functions add header/footer space - ProtoWriteBuffer buffer = is_single ? conn->allocate_single_message_buffer(calculated_size) - : conn->allocate_batch_message_buffer(calculated_size); - // Get buffer size after allocation (which includes header padding) std::vector &shared_buf = conn->parent_->get_shared_buffer_ref(); - size_t size_before_encode = shared_buf.size(); + + if (is_single || conn->flags_.batch_first_message) { + // Single message or first batch message + conn->prepare_first_message_buffer(shared_buf, header_padding, total_calculated_size); + if (conn->flags_.batch_first_message) { + conn->flags_.batch_first_message = false; + } + } else { + // Batch message second or later + // Add padding for previous message footer + this message header + size_t current_size = shared_buf.size(); + shared_buf.reserve(current_size + total_calculated_size); + shared_buf.resize(current_size + footer_size + header_padding); + } // Encode directly into buffer - msg.encode(buffer); + size_t size_before_encode = shared_buf.size(); + msg.encode({&shared_buf}); // Calculate actual encoded size (not including header that was already added) size_t actual_payload_size = shared_buf.size() - size_before_encode; @@ -397,8 +410,8 @@ uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *co } if (traits.supports_direction()) msg.direction = static_cast(fan->direction); - if (traits.supports_preset_modes()) - msg.set_preset_mode(StringRef(fan->preset_mode)); + if (traits.supports_preset_modes() && fan->has_preset_mode()) + msg.set_preset_mode(StringRef(fan->get_preset_mode())); return fill_and_encode_entity_state(fan, msg, FanStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, @@ -410,7 +423,7 @@ uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *con msg.supports_speed = traits.supports_speed(); msg.supports_direction = traits.supports_direction(); msg.supported_speed_count = traits.supported_speed_count(); - msg.supported_preset_modes = &traits.supported_preset_modes_for_api_(); + msg.supported_preset_modes = &traits.supported_preset_modes(); return fill_and_encode_entity_info(fan, msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } void APIConnection::fan_command(const FanCommandRequest &msg) { @@ -440,7 +453,6 @@ uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection * bool is_single) { auto *light = static_cast(entity); LightStateResponse resp; - auto traits = light->get_traits(); auto values = light->remote_values; auto color_mode = values.get_color_mode(); resp.state = values.is_on(); @@ -455,9 +467,7 @@ uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection * resp.cold_white = values.get_cold_white(); resp.warm_white = values.get_warm_white(); if (light->supports_effects()) { - // get_effect_name() returns temporary std::string - must store it - std::string effect_name = light->get_effect_name(); - resp.set_effect(StringRef(effect_name)); + resp.set_effect(light->get_effect_name_ref()); } return fill_and_encode_entity_state(light, resp, LightStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } @@ -466,18 +476,24 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c auto *light = static_cast(entity); ListEntitiesLightResponse msg; auto traits = light->get_traits(); - msg.supported_color_modes = &traits.get_supported_color_modes_for_api_(); + auto supported_modes = traits.get_supported_color_modes(); + // Pass pointer to ColorModeMask so the iterator can encode actual ColorMode enum values + msg.supported_color_modes = &supported_modes; if (traits.supports_color_capability(light::ColorCapability::COLOR_TEMPERATURE) || traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE)) { msg.min_mireds = traits.get_min_mireds(); msg.max_mireds = traits.get_max_mireds(); } + FixedVector effects_list; if (light->supports_effects()) { - msg.effects.emplace_back("None"); - for (auto *effect : light->get_effects()) { - msg.effects.push_back(effect->get_name()); + auto &light_effects = light->get_effects(); + effects_list.init(light_effects.size() + 1); + effects_list.push_back("None"); + for (auto *effect : light_effects) { + effects_list.push_back(effect->get_name()); } } + msg.effects = &effects_list; return fill_and_encode_entity_info(light, msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } @@ -615,9 +631,10 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection auto traits = climate->get_traits(); resp.mode = static_cast(climate->mode); resp.action = static_cast(climate->action); - if (traits.get_supports_current_temperature()) + if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) resp.current_temperature = climate->current_temperature; - if (traits.get_supports_two_point_target_temperature()) { + if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | + climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) { resp.target_temperature_low = climate->target_temperature_low; resp.target_temperature_high = climate->target_temperature_high; } else { @@ -625,20 +642,20 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection } if (traits.get_supports_fan_modes() && climate->fan_mode.has_value()) resp.fan_mode = static_cast(climate->fan_mode.value()); - if (!traits.get_supported_custom_fan_modes().empty() && climate->custom_fan_mode.has_value()) { - resp.set_custom_fan_mode(StringRef(climate->custom_fan_mode.value())); + if (!traits.get_supported_custom_fan_modes().empty() && climate->has_custom_fan_mode()) { + resp.set_custom_fan_mode(StringRef(climate->get_custom_fan_mode())); } if (traits.get_supports_presets() && climate->preset.has_value()) { resp.preset = static_cast(climate->preset.value()); } - if (!traits.get_supported_custom_presets().empty() && climate->custom_preset.has_value()) { - resp.set_custom_preset(StringRef(climate->custom_preset.value())); + if (!traits.get_supported_custom_presets().empty() && climate->has_custom_preset()) { + resp.set_custom_preset(StringRef(climate->get_custom_preset())); } if (traits.get_supports_swing_modes()) resp.swing_mode = static_cast(climate->swing_mode); - if (traits.get_supports_current_humidity()) + if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) resp.current_humidity = climate->current_humidity; - if (traits.get_supports_target_humidity()) + if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) resp.target_humidity = climate->target_humidity; return fill_and_encode_entity_state(climate, resp, ClimateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); @@ -648,23 +665,27 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection auto *climate = static_cast(entity); ListEntitiesClimateResponse msg; auto traits = climate->get_traits(); - msg.supports_current_temperature = traits.get_supports_current_temperature(); - msg.supports_current_humidity = traits.get_supports_current_humidity(); - msg.supports_two_point_target_temperature = traits.get_supports_two_point_target_temperature(); - msg.supports_target_humidity = traits.get_supports_target_humidity(); - msg.supported_modes = &traits.get_supported_modes_for_api_(); + // Flags set for backward compatibility, deprecated in 2025.11.0 + msg.supports_current_temperature = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE); + msg.supports_current_humidity = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY); + msg.supports_two_point_target_temperature = traits.has_feature_flags( + climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE); + msg.supports_target_humidity = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY); + msg.supports_action = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION); + // Current feature flags and other supported parameters + msg.feature_flags = traits.get_feature_flags(); + msg.supported_modes = &traits.get_supported_modes(); msg.visual_min_temperature = traits.get_visual_min_temperature(); msg.visual_max_temperature = traits.get_visual_max_temperature(); msg.visual_target_temperature_step = traits.get_visual_target_temperature_step(); msg.visual_current_temperature_step = traits.get_visual_current_temperature_step(); msg.visual_min_humidity = traits.get_visual_min_humidity(); msg.visual_max_humidity = traits.get_visual_max_humidity(); - msg.supports_action = traits.get_supports_action(); - msg.supported_fan_modes = &traits.get_supported_fan_modes_for_api_(); - msg.supported_custom_fan_modes = &traits.get_supported_custom_fan_modes_for_api_(); - msg.supported_presets = &traits.get_supported_presets_for_api_(); - msg.supported_custom_presets = &traits.get_supported_custom_presets_for_api_(); - msg.supported_swing_modes = &traits.get_supported_swing_modes_for_api_(); + msg.supported_fan_modes = &traits.get_supported_fan_modes(); + msg.supported_custom_fan_modes = &traits.get_supported_custom_fan_modes(); + msg.supported_presets = &traits.get_supported_presets(); + msg.supported_custom_presets = &traits.get_supported_custom_presets(); + msg.supported_swing_modes = &traits.get_supported_swing_modes(); return fill_and_encode_entity_info(climate, msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } @@ -861,7 +882,7 @@ uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection bool is_single) { auto *select = static_cast(entity); SelectStateResponse resp; - resp.set_state(StringRef(select->state)); + resp.set_state(StringRef(select->current_option())); resp.missing_state = !select->has_state(); return fill_and_encode_entity_state(select, resp, SelectStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } @@ -1062,17 +1083,18 @@ void APIConnection::camera_image(const CameraImageRequest &msg) { #ifdef USE_HOMEASSISTANT_TIME void APIConnection::on_get_time_response(const GetTimeResponse &value) { - if (homeassistant::global_homeassistant_time != nullptr) + if (homeassistant::global_homeassistant_time != nullptr) { homeassistant::global_homeassistant_time->set_epoch_time(value.epoch_seconds); +#ifdef USE_TIME_TIMEZONE + if (value.timezone_len > 0) { + homeassistant::global_homeassistant_time->set_timezone(reinterpret_cast(value.timezone), + value.timezone_len); + } +#endif + } } #endif -bool APIConnection::send_get_time_response(const GetTimeRequest &msg) { - GetTimeResponse resp; - resp.epoch_seconds = ::time(nullptr); - return this->send_message(resp, GetTimeResponse::MESSAGE_TYPE); -} - #ifdef USE_BLUETOOTH_PROXY void APIConnection::subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) { bluetooth_proxy::global_bluetooth_proxy->subscribe_api_connection(this, msg.flags); @@ -1183,6 +1205,23 @@ bool APIConnection::send_voice_assistant_get_configuration_response(const VoiceA resp_wake_word.trained_languages.push_back(lang); } } + + // Filter external wake words + for (auto &wake_word : msg.external_wake_words) { + if (wake_word.model_type != "micro") { + // microWakeWord only + continue; + } + + resp.available_wake_words.emplace_back(); + auto &resp_wake_word = resp.available_wake_words.back(); + resp_wake_word.set_id(StringRef(wake_word.id)); + resp_wake_word.set_wake_word(StringRef(wake_word.wake_word)); + for (const auto &lang : wake_word.trained_languages) { + resp_wake_word.trained_languages.push_back(lang); + } + } + resp.active_wake_words = &config.active_wake_words; resp.max_active_wake_words = config.max_active_wake_words; return this->send_message(resp, VoiceAssistantConfigurationResponse::MESSAGE_TYPE); @@ -1193,7 +1232,16 @@ void APIConnection::voice_assistant_set_configuration(const VoiceAssistantSetCon voice_assistant::global_voice_assistant->on_set_configuration(msg.active_wake_words); } } +#endif +#ifdef USE_ZWAVE_PROXY +void APIConnection::zwave_proxy_frame(const ZWaveProxyFrame &msg) { + zwave_proxy::global_zwave_proxy->send_frame(msg.data, msg.data_len); +} + +void APIConnection::zwave_proxy_request(const ZWaveProxyRequest &msg) { + zwave_proxy::global_zwave_proxy->zwave_proxy_request(this, msg.type); +} #endif #ifdef USE_ALARM_CONTROL_PANEL @@ -1251,11 +1299,11 @@ void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRe #endif #ifdef USE_EVENT -void APIConnection::send_event(event::Event *event, const std::string &event_type) { - this->schedule_message_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE, - EventResponse::ESTIMATED_SIZE); +void APIConnection::send_event(event::Event *event, const char *event_type) { + this->send_message_smart_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE, + EventResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_event_response(event::Event *event, const std::string &event_type, APIConnection *conn, +uint16_t APIConnection::try_send_event_response(event::Event *event, const char *event_type, APIConnection *conn, uint32_t remaining_size, bool is_single) { EventResponse resp; resp.set_event_type(StringRef(event_type)); @@ -1267,8 +1315,7 @@ uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *c auto *event = static_cast(entity); ListEntitiesEventResponse msg; msg.set_device_class(event->get_device_class_ref()); - for (const auto &event_type : event->get_event_types()) - msg.event_types.push_back(event_type); + msg.event_types = &event->get_event_types(); return fill_and_encode_entity_info(event, msg, ListEntitiesEventResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } @@ -1340,7 +1387,7 @@ void APIConnection::complete_authentication_() { } this->flags_.connection_state = static_cast(ConnectionState::AUTHENTICATED); - ESP_LOGD(TAG, "%s connected", this->get_client_combined_info().c_str()); + ESP_LOGD(TAG, "%s (%s) connected", this->client_info_.name.c_str(), this->client_info_.peername.c_str()); #ifdef USE_API_CLIENT_CONNECTED_TRIGGER this->parent_->get_client_connected_trigger()->trigger(this->client_info_.name, this->client_info_.peername); #endif @@ -1349,10 +1396,15 @@ void APIConnection::complete_authentication_() { this->send_time_request(); } #endif +#ifdef USE_ZWAVE_PROXY + if (zwave_proxy::global_zwave_proxy != nullptr) { + zwave_proxy::global_zwave_proxy->api_connection_authenticated(this); + } +#endif } bool APIConnection::send_hello_response(const HelloRequest &msg) { - this->client_info_.name = msg.client_info; + this->client_info_.name.assign(reinterpret_cast(msg.client_info), msg.client_info_len); this->client_info_.peername = this->helper_->getpeername(); this->client_api_version_major_ = msg.api_version_major; this->client_api_version_minor_ = msg.api_version_minor; @@ -1361,10 +1413,9 @@ bool APIConnection::send_hello_response(const HelloRequest &msg) { HelloResponse resp; resp.api_version_major = 1; - resp.api_version_minor = 12; - // Temporary string for concatenation - will be valid during send_message call - std::string server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")"; - resp.set_server_info(StringRef(server_info)); + resp.api_version_minor = 13; + // Send only the version string - the client only logs this for debugging and doesn't use it otherwise + resp.set_server_info(ESPHOME_VERSION_REF); resp.set_name(StringRef(App.get_name())); #ifdef USE_API_PASSWORD @@ -1377,20 +1428,17 @@ bool APIConnection::send_hello_response(const HelloRequest &msg) { return this->send_message(resp, HelloResponse::MESSAGE_TYPE); } -bool APIConnection::send_connect_response(const ConnectRequest &msg) { - bool correct = true; #ifdef USE_API_PASSWORD - correct = this->parent_->check_password(msg.password); -#endif - - ConnectResponse resp; +bool APIConnection::send_authenticate_response(const AuthenticationRequest &msg) { + AuthenticationResponse resp; // bool invalid_password = 1; - resp.invalid_password = !correct; - if (correct) { + resp.invalid_password = !this->parent_->check_password(msg.password, msg.password_len); + if (!resp.invalid_password) { this->complete_authentication_(); } - return this->send_message(resp, ConnectResponse::MESSAGE_TYPE); + return this->send_message(resp, AuthenticationResponse::MESSAGE_TYPE); } +#endif // USE_API_PASSWORD bool APIConnection::send_ping_response(const PingRequest &msg) { PingResponse resp; @@ -1407,17 +1455,16 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) { #ifdef USE_AREAS resp.set_suggested_area(StringRef(App.get_area())); #endif - // mac_address must store temporary string - will be valid during send_message call - std::string mac_address = get_mac_address_pretty(); + // Stack buffer for MAC address (XX:XX:XX:XX:XX:XX\0 = 18 bytes) + char mac_address[18]; + uint8_t mac[6]; + get_mac_address_raw(mac); + format_mac_addr_upper(mac, mac_address); resp.set_mac_address(StringRef(mac_address)); - // Compile-time StringRef constants - static constexpr auto ESPHOME_VERSION_REF = StringRef::from_lit(ESPHOME_VERSION); resp.set_esphome_version(ESPHOME_VERSION_REF); - // get_compilation_time() returns temporary std::string - must store it - std::string compilation_time = App.get_compilation_time(); - resp.set_compilation_time(StringRef(compilation_time)); + resp.set_compilation_time(App.get_compilation_time_ref()); // Compile-time StringRef constants for manufacturers #if defined(USE_ESP8266) || defined(USE_ESP32) @@ -1428,6 +1475,8 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) { static constexpr auto MANUFACTURER = StringRef::from_lit("Beken"); #elif defined(USE_LN882X) static constexpr auto MANUFACTURER = StringRef::from_lit("Lightning"); +#elif defined(USE_NRF52) + static constexpr auto MANUFACTURER = StringRef::from_lit("Nordic Semiconductor"); #elif defined(USE_RTL87XX) static constexpr auto MANUFACTURER = StringRef::from_lit("Realtek"); #elif defined(USE_HOST) @@ -1451,13 +1500,18 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) { #endif #ifdef USE_BLUETOOTH_PROXY resp.bluetooth_proxy_feature_flags = bluetooth_proxy::global_bluetooth_proxy->get_feature_flags(); - // bt_mac must store temporary string - will be valid during send_message call - std::string bluetooth_mac = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_mac_address_pretty(); + // Stack buffer for Bluetooth MAC address (XX:XX:XX:XX:XX:XX\0 = 18 bytes) + char bluetooth_mac[18]; + bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_mac_address_pretty(bluetooth_mac); resp.set_bluetooth_mac_address(StringRef(bluetooth_mac)); #endif #ifdef USE_VOICE_ASSISTANT resp.voice_assistant_feature_flags = voice_assistant::global_voice_assistant->get_feature_flags(); #endif +#ifdef USE_ZWAVE_PROXY + resp.zwave_proxy_feature_flags = zwave_proxy::global_zwave_proxy->get_feature_flags(); + resp.zwave_home_id = zwave_proxy::global_zwave_proxy->get_home_id(); +#endif #ifdef USE_API_NOISE resp.api_encryption_supported = true; #endif @@ -1495,7 +1549,7 @@ void APIConnection::on_home_assistant_state_response(const HomeAssistantStateRes } } #endif -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS void APIConnection::execute_service(const ExecuteServiceRequest &msg) { bool found = false; for (auto *service : this->parent_->get_user_services()) { @@ -1508,13 +1562,33 @@ void APIConnection::execute_service(const ExecuteServiceRequest &msg) { } } #endif + +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES +void APIConnection::on_homeassistant_action_response(const HomeassistantActionResponse &msg) { +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON + if (msg.response_data_len > 0) { + this->parent_->handle_action_response(msg.call_id, msg.success, msg.error_message, msg.response_data, + msg.response_data_len); + } else +#endif + { + this->parent_->handle_action_response(msg.call_id, msg.success, msg.error_message); + } +}; +#endif #ifdef USE_API_NOISE bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) { NoiseEncryptionSetKeyResponse resp; resp.success = false; psk_t psk{}; - if (base64_decode(msg.key, psk.data(), msg.key.size()) != psk.size()) { + if (msg.key.empty()) { + if (this->parent_->clear_noise_psk(true)) { + resp.success = true; + } else { + ESP_LOGW(TAG, "Failed to clear encryption key"); + } + } else if (base64_decode(msg.key, psk.data(), msg.key.size()) != psk.size()) { ESP_LOGW(TAG, "Invalid encryption key length"); } else if (!this->parent_->save_noise_psk(psk, true)) { ESP_LOGW(TAG, "Failed to save encryption key"); @@ -1538,8 +1612,7 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) { delay(0); APIError err = this->helper_->loop(); if (err != APIError::OK) { - on_fatal_error(); - this->log_socket_operation_failed_(err); + this->fatal_error_with_log_(LOG_STR("Socket operation failed"), err); return false; } if (this->helper_->can_write_without_blocking()) @@ -1558,8 +1631,7 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) { if (err == APIError::WOULD_BLOCK) return false; if (err != APIError::OK) { - on_fatal_error(); - this->log_warning_("Packet write failed", err); + this->fatal_error_with_log_(LOG_STR("Packet write failed"), err); return false; } // Do not set last_traffic_ on send @@ -1568,12 +1640,12 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) { #ifdef USE_API_PASSWORD void APIConnection::on_unauthenticated_access() { this->on_fatal_error(); - ESP_LOGD(TAG, "%s access without authentication", this->get_client_combined_info().c_str()); + ESP_LOGD(TAG, "%s (%s) no authentication", this->client_info_.name.c_str(), this->client_info_.peername.c_str()); } #endif void APIConnection::on_no_setup_connection() { this->on_fatal_error(); - ESP_LOGD(TAG, "%s access without full connection", this->get_client_combined_info().c_str()); + ESP_LOGD(TAG, "%s (%s) no connection setup", this->client_info_.name.c_str(), this->client_info_.peername.c_str()); } void APIConnection::on_fatal_error() { this->helper_->close(); @@ -1587,9 +1659,7 @@ void APIConnection::DeferredBatch::add_item(EntityBase *entity, MessageCreator c // O(n) but optimized for RAM and not performance. for (auto &item : items) { if (item.entity == entity && item.message_type == message_type) { - // Clean up old creator before replacing - item.creator.cleanup(message_type); - // Move assign the new creator + // Replace with new creator item.creator = std::move(creator); return; } @@ -1620,14 +1690,6 @@ bool APIConnection::schedule_batch_() { return true; } -ProtoWriteBuffer APIConnection::allocate_single_message_buffer(uint16_t size) { return this->create_buffer(size); } - -ProtoWriteBuffer APIConnection::allocate_batch_message_buffer(uint16_t size) { - ProtoWriteBuffer result = this->prepare_message_buffer(size, this->flags_.batch_first_message); - this->flags_.batch_first_message = false; - return result; -} - void APIConnection::process_batch_() { // Ensure PacketInfo remains trivially destructible for our placement new approach static_assert(std::is_trivially_destructible::value, @@ -1735,7 +1797,7 @@ void APIConnection::process_batch_() { } remaining_size -= payload_size; // Calculate where the next message's header padding will start - // Current buffer size + footer space (that prepare_message_buffer will add for this message) + // Current buffer size + footer space for this message current_offset = shared_buf.size() + footer_size; } @@ -1753,8 +1815,7 @@ void APIConnection::process_batch_() { APIError err = this->helper_->write_protobuf_packets(ProtoWriteBuffer{&shared_buf}, std::span(packet_info, packet_count)); if (err != APIError::OK && err != APIError::WOULD_BLOCK) { - on_fatal_error(); - this->log_warning_("Batch write failed", err); + this->fatal_error_with_log_(LOG_STR("Batch write failed"), err); } #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1768,7 +1829,7 @@ void APIConnection::process_batch_() { // Handle remaining items more efficiently if (items_processed < this->deferred_batch_.size()) { - // Remove processed items from the beginning with proper cleanup + // Remove processed items from the beginning this->deferred_batch_.remove_front(items_processed); // Reschedule for remaining items this->schedule_batch_(); @@ -1781,10 +1842,10 @@ void APIConnection::process_batch_() { uint16_t APIConnection::MessageCreator::operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single, uint8_t message_type) const { #ifdef USE_EVENT - // Special case: EventResponse uses string pointer + // Special case: EventResponse uses const char * pointer if (message_type == EventResponse::MESSAGE_TYPE) { auto *e = static_cast(entity); - return APIConnection::try_send_event_response(e, *data_.string_ptr, conn, remaining_size, is_single); + return APIConnection::try_send_event_response(e, data_.const_char_ptr, conn, remaining_size, is_single); } #endif @@ -1832,11 +1893,10 @@ void APIConnection::process_state_subscriptions_() { } #endif // USE_API_HOMEASSISTANT_STATES -void APIConnection::log_warning_(const char *message, APIError err) { - ESP_LOGW(TAG, "%s: %s %s errno=%d", this->get_client_combined_info().c_str(), message, api_error_to_str(err), errno); +void APIConnection::log_warning_(const LogString *message, APIError err) { + ESP_LOGW(TAG, "%s (%s): %s %s errno=%d", this->client_info_.name.c_str(), this->client_info_.peername.c_str(), + LOG_STR_ARG(message), LOG_STR_ARG(api_error_to_logstr(err)), errno); } -void APIConnection::log_socket_operation_failed_(APIError err) { this->log_warning_("Socket operation failed", err); } - } // namespace esphome::api #endif diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index f0f308c248..af3a19909f 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -10,8 +10,8 @@ #include "esphome/core/component.h" #include "esphome/core/entity_base.h" -#include #include +#include namespace esphome::api { @@ -19,14 +19,6 @@ namespace esphome::api { struct ClientInfo { std::string name; // Client name from Hello message std::string peername; // IP:port from socket - - std::string get_combined_info() const { - if (name == peername) { - // Before Hello message, both are the same - return name; - } - return name + " (" + peername + ")"; - } }; // Keepalive timeout in milliseconds @@ -44,7 +36,7 @@ static constexpr size_t MAX_PACKETS_PER_BATCH = 64; // ESP32 has 8KB+ stack, HO static constexpr size_t MAX_PACKETS_PER_BATCH = 32; // ESP8266/RP2040/etc have smaller stacks #endif -class APIConnection : public APIServerConnection { +class APIConnection final : public APIServerConnection { public: friend class APIServer; friend class ListEntitiesIterator; @@ -132,12 +124,15 @@ class APIConnection : public APIServerConnection { #endif bool try_send_log_message(int level, const char *tag, const char *line, size_t message_len); #ifdef USE_API_HOMEASSISTANT_SERVICES - void send_homeassistant_service_call(const HomeassistantServiceResponse &call) { + void send_homeassistant_action(const HomeassistantActionRequest &call) { if (!this->flags_.service_call_subscription) return; - this->send_message(call, HomeassistantServiceResponse::MESSAGE_TYPE); + this->send_message(call, HomeassistantActionRequest::MESSAGE_TYPE); } -#endif +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES + void on_homeassistant_action_response(const HomeassistantActionResponse &msg) override; +#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES +#endif // USE_API_HOMEASSISTANT_SERVICES #ifdef USE_BLUETOOTH_PROXY void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override; void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override; @@ -171,13 +166,18 @@ class APIConnection : public APIServerConnection { void voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) override; #endif +#ifdef USE_ZWAVE_PROXY + void zwave_proxy_frame(const ZWaveProxyFrame &msg) override; + void zwave_proxy_request(const ZWaveProxyRequest &msg) override; +#endif + #ifdef USE_ALARM_CONTROL_PANEL bool send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel); void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) override; #endif #ifdef USE_EVENT - void send_event(event::Event *event, const std::string &event_type); + void send_event(event::Event *event, const char *event_type); #endif #ifdef USE_UPDATE @@ -197,7 +197,9 @@ class APIConnection : public APIServerConnection { void on_get_time_response(const GetTimeResponse &value) override; #endif bool send_hello_response(const HelloRequest &msg) override; - bool send_connect_response(const ConnectRequest &msg) override; +#ifdef USE_API_PASSWORD + bool send_authenticate_response(const AuthenticationRequest &msg) override; +#endif bool send_disconnect_response(const DisconnectRequest &msg) override; bool send_ping_response(const PingRequest &msg) override; bool send_device_info_response(const DeviceInfoRequest &msg) override; @@ -219,8 +221,7 @@ class APIConnection : public APIServerConnection { #ifdef USE_API_HOMEASSISTANT_STATES void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) override; #endif - bool send_get_time_response(const GetTimeRequest &msg) override; -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS void execute_service(const ExecuteServiceRequest &msg) override; #endif #ifdef USE_API_NOISE @@ -252,54 +253,28 @@ class APIConnection : public APIServerConnection { // Get header padding size - used for both reserve and insert uint8_t header_padding = this->helper_->frame_header_padding(); - // Get shared buffer from parent server std::vector &shared_buf = this->parent_->get_shared_buffer_ref(); + this->prepare_first_message_buffer(shared_buf, header_padding, + reserve_size + header_padding + this->helper_->frame_footer_size()); + return {&shared_buf}; + } + + void prepare_first_message_buffer(std::vector &shared_buf, size_t header_padding, size_t total_size) { shared_buf.clear(); // Reserve space for header padding + message + footer // - Header padding: space for protocol headers (7 bytes for Noise, 6 for Plaintext) // - Footer: space for MAC (16 bytes for Noise, 0 for Plaintext) - shared_buf.reserve(reserve_size + header_padding + this->helper_->frame_footer_size()); + shared_buf.reserve(total_size); // Resize to add header padding so message encoding starts at the correct position shared_buf.resize(header_padding); - return {&shared_buf}; - } - - // Prepare buffer for next message in batch - ProtoWriteBuffer prepare_message_buffer(uint16_t message_size, bool is_first_message) { - // Get reference to shared buffer (it maintains state between batch messages) - std::vector &shared_buf = this->parent_->get_shared_buffer_ref(); - - if (is_first_message) { - shared_buf.clear(); - } - - size_t current_size = shared_buf.size(); - - // Calculate padding to add: - // - First message: just header padding - // - Subsequent messages: footer for previous message + header padding for this message - size_t padding_to_add = is_first_message - ? this->helper_->frame_header_padding() - : this->helper_->frame_header_padding() + this->helper_->frame_footer_size(); - - // Reserve space for padding + message - shared_buf.reserve(current_size + padding_to_add + message_size); - - // Resize to add the padding bytes - shared_buf.resize(current_size + padding_to_add); - - return {&shared_buf}; } bool try_to_clear_buffer(bool log_out_of_space); bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override; - std::string get_client_combined_info() const { return this->client_info_.get_combined_info(); } - - // Buffer allocator methods for batch processing - ProtoWriteBuffer allocate_single_message_buffer(uint16_t size); - ProtoWriteBuffer allocate_batch_message_buffer(uint16_t size); + const std::string &get_name() const { return this->client_info_.name; } + const std::string &get_peername() const { return this->client_info_.peername; } protected: // Helper function to handle authentication completion @@ -328,9 +303,17 @@ class APIConnection : public APIServerConnection { APIConnection *conn, uint32_t remaining_size, bool is_single) { // Set common fields that are shared by all entity types msg.key = entity->get_object_id_hash(); - // IMPORTANT: get_object_id() may return a temporary std::string - std::string object_id = entity->get_object_id(); - msg.set_object_id(StringRef(object_id)); + // Try to use static reference first to avoid allocation + StringRef static_ref = entity->get_object_id_ref_for_api_(); + // Store dynamic string outside the if-else to maintain lifetime + std::string object_id; + if (!static_ref.empty()) { + msg.set_object_id(static_ref); + } else { + // Dynamic case - need to allocate + object_id = entity->get_object_id(); + msg.set_object_id(StringRef(object_id)); + } if (entity->has_own_name()) { msg.set_name(entity->get_name()); @@ -467,7 +450,7 @@ class APIConnection : public APIServerConnection { bool is_single); #endif #ifdef USE_EVENT - static uint16_t try_send_event_response(event::Event *event, const std::string &event_type, APIConnection *conn, + static uint16_t try_send_event_response(event::Event *event, const char *event_type, APIConnection *conn, uint32_t remaining_size, bool is_single); static uint16_t try_send_event_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); #endif @@ -525,10 +508,8 @@ class APIConnection : public APIServerConnection { // Constructor for function pointer MessageCreator(MessageCreatorPtr ptr) { data_.function_ptr = ptr; } - // Constructor for string state capture - explicit MessageCreator(const std::string &str_value) { data_.string_ptr = new std::string(str_value); } - - // No destructor - cleanup must be called explicitly with message_type + // Constructor for const char * (Event types - no allocation needed) + explicit MessageCreator(const char *str_value) { data_.const_char_ptr = str_value; } // Delete copy operations - MessageCreator should only be moved MessageCreator(const MessageCreator &other) = delete; @@ -540,8 +521,6 @@ class APIConnection : public APIServerConnection { // Move assignment MessageCreator &operator=(MessageCreator &&other) noexcept { if (this != &other) { - // IMPORTANT: Caller must ensure cleanup() was called if this contains a string! - // In our usage, this happens in add_item() deduplication and vector::erase() data_ = other.data_; other.data_.function_ptr = nullptr; } @@ -552,20 +531,10 @@ class APIConnection : public APIServerConnection { uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single, uint8_t message_type) const; - // Manual cleanup method - must be called before destruction for string types - void cleanup(uint8_t message_type) { -#ifdef USE_EVENT - if (message_type == EventResponse::MESSAGE_TYPE && data_.string_ptr != nullptr) { - delete data_.string_ptr; - data_.string_ptr = nullptr; - } -#endif - } - private: union Data { MessageCreatorPtr function_ptr; - std::string *string_ptr; + const char *const_char_ptr; } data_; // 4 bytes on 32-bit, 8 bytes on 64-bit - same as before }; @@ -585,42 +554,24 @@ class APIConnection : public APIServerConnection { std::vector items; uint32_t batch_start_time{0}; - private: - // Helper to cleanup items from the beginning - void cleanup_items_(size_t count) { - for (size_t i = 0; i < count; i++) { - items[i].creator.cleanup(items[i].message_type); - } - } - - public: DeferredBatch() { // Pre-allocate capacity for typical batch sizes to avoid reallocation items.reserve(8); } - ~DeferredBatch() { - // Ensure cleanup of any remaining items - clear(); - } - // Add item to the batch void add_item(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size); // Add item to the front of the batch (for high priority messages like ping) void add_item_front(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size); - // Clear all items with proper cleanup + // Clear all items void clear() { - cleanup_items_(items.size()); items.clear(); batch_start_time = 0; } - // Remove processed items from the front with proper cleanup - void remove_front(size_t count) { - cleanup_items_(count); - items.erase(items.begin(), items.begin() + count); - } + // Remove processed items from the front + void remove_front(size_t count) { items.erase(items.begin(), items.begin() + count); } bool empty() const { return items.empty(); } size_t size() const { return items.size(); } @@ -699,21 +650,30 @@ class APIConnection : public APIServerConnection { } #endif + // Helper to check if a message type should bypass batching + // Returns true if: + // 1. It's an UpdateStateResponse (always send immediately to handle cases where + // the main loop is blocked, e.g., during OTA updates) + // 2. It's an EventResponse (events are edge-triggered - every occurrence matters) + // 3. OR: User has opted into immediate sending (should_try_send_immediately = true + // AND batch_delay = 0) + inline bool should_send_immediately_(uint8_t message_type) const { + return ( +#ifdef USE_UPDATE + message_type == UpdateStateResponse::MESSAGE_TYPE || +#endif +#ifdef USE_EVENT + message_type == EventResponse::MESSAGE_TYPE || +#endif + (this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0)); + } + // Helper method to send a message either immediately or via batching + // Tries immediate send if should_send_immediately_() returns true and buffer has space + // Falls back to batching if immediate send fails or isn't applicable bool send_message_smart_(EntityBase *entity, MessageCreatorPtr creator, uint8_t message_type, uint8_t estimated_size) { - // Try to send immediately if: - // 1. It's an UpdateStateResponse (always send immediately to handle cases where - // the main loop is blocked, e.g., during OTA updates) - // 2. OR: We should try to send immediately (should_try_send_immediately = true) - // AND Batch delay is 0 (user has opted in to immediate sending) - // 3. AND: Buffer has space available - if (( -#ifdef USE_UPDATE - message_type == UpdateStateResponse::MESSAGE_TYPE || -#endif - (this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0)) && - this->helper_->can_write_without_blocking()) { + if (this->should_send_immediately_(message_type) && this->helper_->can_write_without_blocking()) { // Now actually encode and send if (creator(entity, this, MAX_BATCH_PACKET_SIZE, true) && this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) { @@ -731,6 +691,27 @@ class APIConnection : public APIServerConnection { return this->schedule_message_(entity, creator, message_type, estimated_size); } + // Overload for MessageCreator (used by events which need to capture event_type) + bool send_message_smart_(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size) { + // Try to send immediately if message type should bypass batching and buffer has space + if (this->should_send_immediately_(message_type) && this->helper_->can_write_without_blocking()) { + // Now actually encode and send + if (creator(entity, this, MAX_BATCH_PACKET_SIZE, true, message_type) && + this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) { +#ifdef HAS_PROTO_MESSAGE_DUMP + // Log the message in verbose mode + this->log_proto_message_(entity, creator, message_type); +#endif + return true; + } + + // If immediate send failed, fall through to batching + } + + // Fall back to scheduled batching + return this->schedule_message_(entity, std::move(creator), message_type, estimated_size); + } + // Helper function to schedule a deferred message with known message type bool schedule_message_(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size) { this->deferred_batch_.add_item(entity, std::move(creator), message_type, estimated_size); @@ -751,9 +732,12 @@ class APIConnection : public APIServerConnection { } // Helper function to log API errors with errno - void log_warning_(const char *message, APIError err); - // Specific helper for duplicated error message - void log_socket_operation_failed_(APIError err); + void log_warning_(const LogString *message, APIError err); + // Helper to handle fatal errors with logging + inline void fatal_error_with_log_(const LogString *message, APIError err) { + this->on_fatal_error(); + this->log_warning_(message, err); + } }; } // namespace esphome::api diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index 6ca38e80ed..20f8fcaf61 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -13,7 +13,8 @@ namespace esphome::api { static const char *const TAG = "api.frame_helper"; -#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->client_info_->get_combined_info().c_str(), ##__VA_ARGS__) +#define HELPER_LOG(msg, ...) \ + ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.c_str(), ##__VA_ARGS__) #ifdef HELPER_LOG_PACKETS #define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str()) @@ -23,64 +24,64 @@ static const char *const TAG = "api.frame_helper"; #define LOG_PACKET_SENDING(data, len) ((void) 0) #endif -const char *api_error_to_str(APIError err) { +const LogString *api_error_to_logstr(APIError err) { // not using switch to ensure compiler doesn't try to build a big table out of it if (err == APIError::OK) { - return "OK"; + return LOG_STR("OK"); } else if (err == APIError::WOULD_BLOCK) { - return "WOULD_BLOCK"; + return LOG_STR("WOULD_BLOCK"); } else if (err == APIError::BAD_INDICATOR) { - return "BAD_INDICATOR"; + return LOG_STR("BAD_INDICATOR"); } else if (err == APIError::BAD_DATA_PACKET) { - return "BAD_DATA_PACKET"; + return LOG_STR("BAD_DATA_PACKET"); } else if (err == APIError::TCP_NODELAY_FAILED) { - return "TCP_NODELAY_FAILED"; + return LOG_STR("TCP_NODELAY_FAILED"); } else if (err == APIError::TCP_NONBLOCKING_FAILED) { - return "TCP_NONBLOCKING_FAILED"; + return LOG_STR("TCP_NONBLOCKING_FAILED"); } else if (err == APIError::CLOSE_FAILED) { - return "CLOSE_FAILED"; + return LOG_STR("CLOSE_FAILED"); } else if (err == APIError::SHUTDOWN_FAILED) { - return "SHUTDOWN_FAILED"; + return LOG_STR("SHUTDOWN_FAILED"); } else if (err == APIError::BAD_STATE) { - return "BAD_STATE"; + return LOG_STR("BAD_STATE"); } else if (err == APIError::BAD_ARG) { - return "BAD_ARG"; + return LOG_STR("BAD_ARG"); } else if (err == APIError::SOCKET_READ_FAILED) { - return "SOCKET_READ_FAILED"; + return LOG_STR("SOCKET_READ_FAILED"); } else if (err == APIError::SOCKET_WRITE_FAILED) { - return "SOCKET_WRITE_FAILED"; + return LOG_STR("SOCKET_WRITE_FAILED"); } else if (err == APIError::OUT_OF_MEMORY) { - return "OUT_OF_MEMORY"; + return LOG_STR("OUT_OF_MEMORY"); } else if (err == APIError::CONNECTION_CLOSED) { - return "CONNECTION_CLOSED"; + return LOG_STR("CONNECTION_CLOSED"); } #ifdef USE_API_NOISE else if (err == APIError::BAD_HANDSHAKE_PACKET_LEN) { - return "BAD_HANDSHAKE_PACKET_LEN"; + return LOG_STR("BAD_HANDSHAKE_PACKET_LEN"); } else if (err == APIError::HANDSHAKESTATE_READ_FAILED) { - return "HANDSHAKESTATE_READ_FAILED"; + return LOG_STR("HANDSHAKESTATE_READ_FAILED"); } else if (err == APIError::HANDSHAKESTATE_WRITE_FAILED) { - return "HANDSHAKESTATE_WRITE_FAILED"; + return LOG_STR("HANDSHAKESTATE_WRITE_FAILED"); } else if (err == APIError::HANDSHAKESTATE_BAD_STATE) { - return "HANDSHAKESTATE_BAD_STATE"; + return LOG_STR("HANDSHAKESTATE_BAD_STATE"); } else if (err == APIError::CIPHERSTATE_DECRYPT_FAILED) { - return "CIPHERSTATE_DECRYPT_FAILED"; + return LOG_STR("CIPHERSTATE_DECRYPT_FAILED"); } else if (err == APIError::CIPHERSTATE_ENCRYPT_FAILED) { - return "CIPHERSTATE_ENCRYPT_FAILED"; + return LOG_STR("CIPHERSTATE_ENCRYPT_FAILED"); } else if (err == APIError::HANDSHAKESTATE_SETUP_FAILED) { - return "HANDSHAKESTATE_SETUP_FAILED"; + return LOG_STR("HANDSHAKESTATE_SETUP_FAILED"); } else if (err == APIError::HANDSHAKESTATE_SPLIT_FAILED) { - return "HANDSHAKESTATE_SPLIT_FAILED"; + return LOG_STR("HANDSHAKESTATE_SPLIT_FAILED"); } else if (err == APIError::BAD_HANDSHAKE_ERROR_BYTE) { - return "BAD_HANDSHAKE_ERROR_BYTE"; + return LOG_STR("BAD_HANDSHAKE_ERROR_BYTE"); } #endif - return "UNKNOWN"; + return LOG_STR("UNKNOWN"); } // Default implementation for loop - handles sending buffered data APIError APIFrameHelper::loop() { - if (!this->tx_buf_.empty()) { + if (this->tx_buf_count_ > 0) { APIError err = try_send_tx_buf_(); if (err != APIError::OK && err != APIError::WOULD_BLOCK) { return err; @@ -102,9 +103,20 @@ APIError APIFrameHelper::handle_socket_write_error_() { // Helper method to buffer data from IOVs void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len, uint16_t offset) { - SendBuffer buffer; - buffer.size = total_write_len - offset; - buffer.data = std::make_unique(buffer.size); + // Check if queue is full + if (this->tx_buf_count_ >= API_MAX_SEND_QUEUE) { + HELPER_LOG("Send queue full (%u buffers), dropping connection", this->tx_buf_count_); + this->state_ = State::FAILED; + return; + } + + uint16_t buffer_size = total_write_len - offset; + auto &buffer = this->tx_buf_[this->tx_buf_tail_]; + buffer = std::make_unique(SendBuffer{ + .data = std::make_unique(buffer_size), + .size = buffer_size, + .offset = 0, + }); uint16_t to_skip = offset; uint16_t write_pos = 0; @@ -117,12 +129,15 @@ void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt, // Include this segment (partially or fully) const uint8_t *src = reinterpret_cast(iov[i].iov_base) + to_skip; uint16_t len = static_cast(iov[i].iov_len) - to_skip; - std::memcpy(buffer.data.get() + write_pos, src, len); + std::memcpy(buffer->data.get() + write_pos, src, len); write_pos += len; to_skip = 0; } } - this->tx_buf_.push_back(std::move(buffer)); + + // Update circular buffer tracking + this->tx_buf_tail_ = (this->tx_buf_tail_ + 1) % API_MAX_SEND_QUEUE; + this->tx_buf_count_++; } // This method writes data to socket or buffers it @@ -140,7 +155,7 @@ APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, uint16_ #endif // Try to send any existing buffered data first if there is any - if (!this->tx_buf_.empty()) { + if (this->tx_buf_count_ > 0) { APIError send_result = try_send_tx_buf_(); // If real error occurred (not just WOULD_BLOCK), return it if (send_result != APIError::OK && send_result != APIError::WOULD_BLOCK) { @@ -149,14 +164,16 @@ APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, uint16_ // If there is still data in the buffer, we can't send, buffer // the new data and return - if (!this->tx_buf_.empty()) { + if (this->tx_buf_count_ > 0) { this->buffer_data_from_iov_(iov, iovcnt, total_write_len, 0); return APIError::OK; // Success, data buffered } } // Try to send directly if no buffered data - ssize_t sent = this->socket_->writev(iov, iovcnt); + // Optimize for single iovec case (common for plaintext API) + ssize_t sent = + (iovcnt == 1) ? this->socket_->write(iov[0].iov_base, iov[0].iov_len) : this->socket_->writev(iov, iovcnt); if (sent == -1) { APIError err = this->handle_socket_write_error_(); @@ -175,32 +192,31 @@ APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, uint16_ } // Common implementation for trying to send buffered data -// IMPORTANT: Caller MUST ensure tx_buf_ is not empty before calling this method +// IMPORTANT: Caller MUST ensure tx_buf_count_ > 0 before calling this method APIError APIFrameHelper::try_send_tx_buf_() { // Try to send from tx_buf - we assume it's not empty as it's the caller's responsibility to check - bool tx_buf_empty = false; - while (!tx_buf_empty) { + while (this->tx_buf_count_ > 0) { // Get the first buffer in the queue - SendBuffer &front_buffer = this->tx_buf_.front(); + SendBuffer *front_buffer = this->tx_buf_[this->tx_buf_head_].get(); // Try to send the remaining data in this buffer - ssize_t sent = this->socket_->write(front_buffer.current_data(), front_buffer.remaining()); + ssize_t sent = this->socket_->write(front_buffer->current_data(), front_buffer->remaining()); if (sent == -1) { return this->handle_socket_write_error_(); } else if (sent == 0) { // Nothing sent but not an error return APIError::WOULD_BLOCK; - } else if (static_cast(sent) < front_buffer.remaining()) { + } else if (static_cast(sent) < front_buffer->remaining()) { // Partially sent, update offset // Cast to ensure no overflow issues with uint16_t - front_buffer.offset += static_cast(sent); + front_buffer->offset += static_cast(sent); return APIError::WOULD_BLOCK; // Stop processing more buffers if we couldn't send a complete buffer } else { // Buffer completely sent, remove it from the queue - this->tx_buf_.pop_front(); - // Update empty status for the loop condition - tx_buf_empty = this->tx_buf_.empty(); + this->tx_buf_[this->tx_buf_head_].reset(); + this->tx_buf_head_ = (this->tx_buf_head_ + 1) % API_MAX_SEND_QUEUE; + this->tx_buf_count_--; // Continue loop to try sending the next buffer } } diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 76dfe1366c..d931a6e3a9 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -1,7 +1,8 @@ #pragma once +#include #include -#include #include +#include #include #include #include @@ -17,6 +18,17 @@ namespace esphome::api { // uncomment to log raw packets //#define HELPER_LOG_PACKETS +// Maximum message size limits to prevent OOM on constrained devices +// Handshake messages are limited to a small size for security +static constexpr uint16_t MAX_HANDSHAKE_SIZE = 128; + +// Data message limits vary by platform based on available memory +#ifdef USE_ESP8266 +static constexpr uint16_t MAX_MESSAGE_SIZE = 8192; // 8 KiB for ESP8266 +#else +static constexpr uint16_t MAX_MESSAGE_SIZE = 32768; // 32 KiB for ESP32 and other platforms +#endif + // Forward declaration struct ClientInfo; @@ -66,20 +78,18 @@ enum class APIError : uint16_t { #endif }; -const char *api_error_to_str(APIError err); +const LogString *api_error_to_logstr(APIError err); class APIFrameHelper { public: APIFrameHelper() = default; explicit APIFrameHelper(std::unique_ptr socket, const ClientInfo *client_info) - : socket_owned_(std::move(socket)), client_info_(client_info) { - socket_ = socket_owned_.get(); - } + : socket_(std::move(socket)), client_info_(client_info) {} virtual ~APIFrameHelper() = default; virtual APIError init() = 0; virtual APIError loop(); virtual APIError read_packet(ReadPacketBuffer *buffer) = 0; - bool can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); } + bool can_write_without_blocking() { return this->state_ == State::DATA && this->tx_buf_count_ == 0; } std::string getpeername() { return socket_->getpeername(); } int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); } APIError close() { @@ -104,9 +114,9 @@ class APIFrameHelper { // The buffer contains all messages with appropriate padding before each virtual APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span packets) = 0; // Get the frame header padding required by this protocol - virtual uint8_t frame_header_padding() = 0; + uint8_t frame_header_padding() const { return frame_header_padding_; } // Get the frame footer size required by this protocol - virtual uint8_t frame_footer_size() = 0; + uint8_t frame_footer_size() const { return frame_footer_size_; } // Check if socket has data ready to read bool is_socket_ready() const { return socket_ != nullptr && socket_->ready(); } @@ -137,9 +147,8 @@ class APIFrameHelper { APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector &tx_buf, const std::string &info, StateEnum &state, StateEnum failed_state); - // Pointers first (4 bytes each) - socket::Socket *socket_{nullptr}; - std::unique_ptr socket_owned_; + // Socket ownership (4 bytes on 32-bit, 8 bytes on 64-bit) + std::unique_ptr socket_; // Common state enum for all frame helpers // Note: Not all states are used by all implementations @@ -161,7 +170,7 @@ class APIFrameHelper { }; // Containers (size varies, but typically 12+ bytes on 32-bit) - std::deque tx_buf_; + std::array, API_MAX_SEND_QUEUE> tx_buf_; std::vector reusable_iovs_; std::vector rx_buf_; @@ -174,7 +183,10 @@ class APIFrameHelper { State state_{State::INITIALIZE}; uint8_t frame_header_padding_{0}; uint8_t frame_footer_size_{0}; - // 5 bytes total, 3 bytes padding + uint8_t tx_buf_head_{0}; + uint8_t tx_buf_tail_{0}; + uint8_t tx_buf_count_{0}; + // 8 bytes total, 0 bytes padding // Common initialization for both plaintext and noise protocols APIError init_common_(); diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 35d1715931..f1028fa299 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -10,13 +10,22 @@ #include #include +#ifdef USE_ESP8266 +#include +#endif + namespace esphome::api { static const char *const TAG = "api.noise"; +#ifdef USE_ESP8266 +static const char PROLOGUE_INIT[] PROGMEM = "NoiseAPIInit"; +#else static const char *const PROLOGUE_INIT = "NoiseAPIInit"; +#endif static constexpr size_t PROLOGUE_INIT_LEN = 12; // strlen("NoiseAPIInit") -#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->client_info_->get_combined_info().c_str(), ##__VA_ARGS__) +#define HELPER_LOG(msg, ...) \ + ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.c_str(), ##__VA_ARGS__) #ifdef HELPER_LOG_PACKETS #define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str()) @@ -27,42 +36,42 @@ static constexpr size_t PROLOGUE_INIT_LEN = 12; // strlen("NoiseAPIInit") #endif /// Convert a noise error code to a readable error -std::string noise_err_to_str(int err) { +const LogString *noise_err_to_logstr(int err) { if (err == NOISE_ERROR_NO_MEMORY) - return "NO_MEMORY"; + return LOG_STR("NO_MEMORY"); if (err == NOISE_ERROR_UNKNOWN_ID) - return "UNKNOWN_ID"; + return LOG_STR("UNKNOWN_ID"); if (err == NOISE_ERROR_UNKNOWN_NAME) - return "UNKNOWN_NAME"; + return LOG_STR("UNKNOWN_NAME"); if (err == NOISE_ERROR_MAC_FAILURE) - return "MAC_FAILURE"; + return LOG_STR("MAC_FAILURE"); if (err == NOISE_ERROR_NOT_APPLICABLE) - return "NOT_APPLICABLE"; + return LOG_STR("NOT_APPLICABLE"); if (err == NOISE_ERROR_SYSTEM) - return "SYSTEM"; + return LOG_STR("SYSTEM"); if (err == NOISE_ERROR_REMOTE_KEY_REQUIRED) - return "REMOTE_KEY_REQUIRED"; + return LOG_STR("REMOTE_KEY_REQUIRED"); if (err == NOISE_ERROR_LOCAL_KEY_REQUIRED) - return "LOCAL_KEY_REQUIRED"; + return LOG_STR("LOCAL_KEY_REQUIRED"); if (err == NOISE_ERROR_PSK_REQUIRED) - return "PSK_REQUIRED"; + return LOG_STR("PSK_REQUIRED"); if (err == NOISE_ERROR_INVALID_LENGTH) - return "INVALID_LENGTH"; + return LOG_STR("INVALID_LENGTH"); if (err == NOISE_ERROR_INVALID_PARAM) - return "INVALID_PARAM"; + return LOG_STR("INVALID_PARAM"); if (err == NOISE_ERROR_INVALID_STATE) - return "INVALID_STATE"; + return LOG_STR("INVALID_STATE"); if (err == NOISE_ERROR_INVALID_NONCE) - return "INVALID_NONCE"; + return LOG_STR("INVALID_NONCE"); if (err == NOISE_ERROR_INVALID_PRIVATE_KEY) - return "INVALID_PRIVATE_KEY"; + return LOG_STR("INVALID_PRIVATE_KEY"); if (err == NOISE_ERROR_INVALID_PUBLIC_KEY) - return "INVALID_PUBLIC_KEY"; + return LOG_STR("INVALID_PUBLIC_KEY"); if (err == NOISE_ERROR_INVALID_FORMAT) - return "INVALID_FORMAT"; + return LOG_STR("INVALID_FORMAT"); if (err == NOISE_ERROR_INVALID_SIGNATURE) - return "INVALID_SIGNATURE"; - return to_string(err); + return LOG_STR("INVALID_SIGNATURE"); + return LOG_STR("UNKNOWN"); } /// Initialize the frame helper, returns OK if successful. @@ -75,7 +84,11 @@ APIError APINoiseFrameHelper::init() { // init prologue size_t old_size = prologue_.size(); prologue_.resize(old_size + PROLOGUE_INIT_LEN); +#ifdef USE_ESP8266 + memcpy_P(prologue_.data() + old_size, PROLOGUE_INIT, PROLOGUE_INIT_LEN); +#else std::memcpy(prologue_.data() + old_size, PROLOGUE_INIT, PROLOGUE_INIT_LEN); +#endif state_ = State::CLIENT_HELLO; return APIError::OK; @@ -83,18 +96,18 @@ APIError APINoiseFrameHelper::init() { // Helper for handling handshake frame errors APIError APINoiseFrameHelper::handle_handshake_frame_error_(APIError aerr) { if (aerr == APIError::BAD_INDICATOR) { - send_explicit_handshake_reject_("Bad indicator byte"); + send_explicit_handshake_reject_(LOG_STR("Bad indicator byte")); } else if (aerr == APIError::BAD_HANDSHAKE_PACKET_LEN) { - send_explicit_handshake_reject_("Bad handshake packet len"); + send_explicit_handshake_reject_(LOG_STR("Bad handshake packet len")); } return aerr; } // Helper for handling noise library errors -APIError APINoiseFrameHelper::handle_noise_error_(int err, const char *func_name, APIError api_err) { +APIError APINoiseFrameHelper::handle_noise_error_(int err, const LogString *func_name, APIError api_err) { if (err != 0) { state_ = State::FAILED; - HELPER_LOG("%s failed: %s", func_name, noise_err_to_str(err).c_str()); + HELPER_LOG("%s failed: %s", LOG_STR_ARG(func_name), LOG_STR_ARG(noise_err_to_logstr(err))); return api_err; } return APIError::OK; @@ -119,26 +132,16 @@ APIError APINoiseFrameHelper::loop() { return APIFrameHelper::loop(); } -/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter +/** Read a packet into the rx_buf_. * - * @param frame: The struct to hold the frame information in. - * msg_start: points to the start of the payload - this pointer is only valid until the next - * try_receive_raw_ call - * - * @return 0 if a full packet is in rx_buf_ - * @return -1 if error, check errno. + * @return APIError::OK if a full packet is in rx_buf_ * * errno EWOULDBLOCK: Packet could not be read without blocking. Try again later. * errno ENOMEM: Not enough memory for reading packet. * errno API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame. * errno API_ERROR_HANDSHAKE_PACKET_LEN: Packet too big for this phase. */ -APIError APINoiseFrameHelper::try_read_frame_(std::vector *frame) { - if (frame == nullptr) { - HELPER_LOG("Bad argument for try_read_frame_"); - return APIError::BAD_ARG; - } - +APIError APINoiseFrameHelper::try_read_frame_() { // read header if (rx_header_buf_len_ < 3) { // no header information yet @@ -165,16 +168,17 @@ APIError APINoiseFrameHelper::try_read_frame_(std::vector *frame) { // read body uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2]; - if (state_ != State::DATA && msg_size > 128) { - // for handshake message only permit up to 128 bytes + // Check against size limits to prevent OOM: MAX_HANDSHAKE_SIZE for handshake, MAX_MESSAGE_SIZE for data + uint16_t limit = (state_ == State::DATA) ? MAX_MESSAGE_SIZE : MAX_HANDSHAKE_SIZE; + if (msg_size > limit) { state_ = State::FAILED; - HELPER_LOG("Bad packet len for handshake: %d", msg_size); - return APIError::BAD_HANDSHAKE_PACKET_LEN; + HELPER_LOG("Bad packet: message size %u exceeds maximum %u", msg_size, limit); + return (state_ == State::DATA) ? APIError::BAD_DATA_PACKET : APIError::BAD_HANDSHAKE_PACKET_LEN; } - // reserve space for body - if (rx_buf_.size() != msg_size) { - rx_buf_.resize(msg_size); + // Reserve space for body + if (this->rx_buf_.size() != msg_size) { + this->rx_buf_.resize(msg_size); } if (rx_buf_len_ < msg_size) { @@ -192,12 +196,12 @@ APIError APINoiseFrameHelper::try_read_frame_(std::vector *frame) { } } - LOG_PACKET_RECEIVED(rx_buf_); - *frame = std::move(rx_buf_); - // consume msg - rx_buf_ = {}; - rx_buf_len_ = 0; - rx_header_buf_len_ = 0; + LOG_PACKET_RECEIVED(this->rx_buf_); + + // Clear state for next frame (rx_buf_ still contains data for caller) + this->rx_buf_len_ = 0; + this->rx_header_buf_len_ = 0; + return APIError::OK; } @@ -219,45 +223,44 @@ APIError APINoiseFrameHelper::state_action_() { } if (state_ == State::CLIENT_HELLO) { // waiting for client hello - std::vector frame; - aerr = try_read_frame_(&frame); + aerr = this->try_read_frame_(); if (aerr != APIError::OK) { return handle_handshake_frame_error_(aerr); } // ignore contents, may be used in future for flags // Resize for: existing prologue + 2 size bytes + frame data - size_t old_size = prologue_.size(); - prologue_.resize(old_size + 2 + frame.size()); - prologue_[old_size] = (uint8_t) (frame.size() >> 8); - prologue_[old_size + 1] = (uint8_t) frame.size(); - std::memcpy(prologue_.data() + old_size + 2, frame.data(), frame.size()); + size_t old_size = this->prologue_.size(); + this->prologue_.resize(old_size + 2 + this->rx_buf_.size()); + this->prologue_[old_size] = (uint8_t) (this->rx_buf_.size() >> 8); + this->prologue_[old_size + 1] = (uint8_t) this->rx_buf_.size(); + std::memcpy(this->prologue_.data() + old_size + 2, this->rx_buf_.data(), this->rx_buf_.size()); state_ = State::SERVER_HELLO; } if (state_ == State::SERVER_HELLO) { // send server hello + constexpr size_t mac_len = 13; // 12 hex chars + null terminator const std::string &name = App.get_name(); - const std::string &mac = get_mac_address(); + char mac[mac_len]; + get_mac_address_into_buffer(mac); - std::vector msg; // Calculate positions and sizes size_t name_len = name.size() + 1; // including null terminator - size_t mac_len = mac.size() + 1; // including null terminator size_t name_offset = 1; size_t mac_offset = name_offset + name_len; size_t total_size = 1 + name_len + mac_len; - msg.resize(total_size); + auto msg = std::make_unique(total_size); // chosen proto msg[0] = 0x01; // node name, terminated by null byte - std::memcpy(msg.data() + name_offset, name.c_str(), name_len); + std::memcpy(msg.get() + name_offset, name.c_str(), name_len); // node mac, terminated by null byte - std::memcpy(msg.data() + mac_offset, mac.c_str(), mac_len); + std::memcpy(msg.get() + mac_offset, mac, mac_len); - aerr = write_frame_(msg.data(), msg.size()); + aerr = write_frame_(msg.get(), total_size); if (aerr != APIError::OK) return aerr; @@ -272,29 +275,30 @@ APIError APINoiseFrameHelper::state_action_() { int action = noise_handshakestate_get_action(handshake_); if (action == NOISE_ACTION_READ_MESSAGE) { // waiting for handshake msg - std::vector frame; - aerr = try_read_frame_(&frame); + aerr = this->try_read_frame_(); if (aerr != APIError::OK) { return handle_handshake_frame_error_(aerr); } - if (frame.empty()) { - send_explicit_handshake_reject_("Empty handshake message"); + if (this->rx_buf_.empty()) { + send_explicit_handshake_reject_(LOG_STR("Empty handshake message")); return APIError::BAD_HANDSHAKE_ERROR_BYTE; - } else if (frame[0] != 0x00) { - HELPER_LOG("Bad handshake error byte: %u", frame[0]); - send_explicit_handshake_reject_("Bad handshake error byte"); + } else if (this->rx_buf_[0] != 0x00) { + HELPER_LOG("Bad handshake error byte: %u", this->rx_buf_[0]); + send_explicit_handshake_reject_(LOG_STR("Bad handshake error byte")); return APIError::BAD_HANDSHAKE_ERROR_BYTE; } NoiseBuffer mbuf; noise_buffer_init(mbuf); - noise_buffer_set_input(mbuf, frame.data() + 1, frame.size() - 1); + noise_buffer_set_input(mbuf, this->rx_buf_.data() + 1, this->rx_buf_.size() - 1); err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr); if (err != 0) { // Special handling for MAC failure - send_explicit_handshake_reject_(err == NOISE_ERROR_MAC_FAILURE ? "Handshake MAC failure" : "Handshake error"); - return handle_noise_error_(err, "noise_handshakestate_read_message", APIError::HANDSHAKESTATE_READ_FAILED); + send_explicit_handshake_reject_(err == NOISE_ERROR_MAC_FAILURE ? LOG_STR("Handshake MAC failure") + : LOG_STR("Handshake error")); + return handle_noise_error_(err, LOG_STR("noise_handshakestate_read_message"), + APIError::HANDSHAKESTATE_READ_FAILED); } aerr = check_handshake_finished_(); @@ -307,8 +311,8 @@ APIError APINoiseFrameHelper::state_action_() { noise_buffer_set_output(mbuf, buffer + 1, sizeof(buffer) - 1); err = noise_handshakestate_write_message(handshake_, &mbuf, nullptr); - APIError aerr_write = - handle_noise_error_(err, "noise_handshakestate_write_message", APIError::HANDSHAKESTATE_WRITE_FAILED); + APIError aerr_write = handle_noise_error_(err, LOG_STR("noise_handshakestate_write_message"), + APIError::HANDSHAKESTATE_WRITE_FAILED); if (aerr_write != APIError::OK) return aerr_write; buffer[0] = 0x00; // success @@ -331,51 +335,66 @@ APIError APINoiseFrameHelper::state_action_() { } return APIError::OK; } -void APINoiseFrameHelper::send_explicit_handshake_reject_(const std::string &reason) { - std::vector data; - data.resize(reason.length() + 1); +void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reason) { +#ifdef USE_STORE_LOG_STR_IN_FLASH + // On ESP8266 with flash strings, we need to use PROGMEM-aware functions + size_t reason_len = strlen_P(reinterpret_cast(reason)); + size_t data_size = reason_len + 1; + auto data = std::make_unique(data_size); + data[0] = 0x01; // failure + + // Copy error message from PROGMEM + if (reason_len > 0) { + memcpy_P(data.get() + 1, reinterpret_cast(reason), reason_len); + } +#else + // Normal memory access + const char *reason_str = LOG_STR_ARG(reason); + size_t reason_len = strlen(reason_str); + size_t data_size = reason_len + 1; + auto data = std::make_unique(data_size); data[0] = 0x01; // failure // Copy error message in bulk - if (!reason.empty()) { - std::memcpy(data.data() + 1, reason.c_str(), reason.length()); + if (reason_len > 0) { + std::memcpy(data.get() + 1, reason_str, reason_len); } +#endif // temporarily remove failed state auto orig_state = state_; state_ = State::EXPLICIT_REJECT; - write_frame_(data.data(), data.size()); + write_frame_(data.get(), data_size); state_ = orig_state; } APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { - int err; - APIError aerr; - aerr = state_action_(); + APIError aerr = this->state_action_(); if (aerr != APIError::OK) { return aerr; } - if (state_ != State::DATA) { + if (this->state_ != State::DATA) { return APIError::WOULD_BLOCK; } - std::vector frame; - aerr = try_read_frame_(&frame); + aerr = this->try_read_frame_(); if (aerr != APIError::OK) return aerr; NoiseBuffer mbuf; noise_buffer_init(mbuf); - noise_buffer_set_inout(mbuf, frame.data(), frame.size(), frame.size()); - err = noise_cipherstate_decrypt(recv_cipher_, &mbuf); - APIError decrypt_err = handle_noise_error_(err, "noise_cipherstate_decrypt", APIError::CIPHERSTATE_DECRYPT_FAILED); - if (decrypt_err != APIError::OK) + noise_buffer_set_inout(mbuf, this->rx_buf_.data(), this->rx_buf_.size(), this->rx_buf_.size()); + int err = noise_cipherstate_decrypt(this->recv_cipher_, &mbuf); + APIError decrypt_err = + handle_noise_error_(err, LOG_STR("noise_cipherstate_decrypt"), APIError::CIPHERSTATE_DECRYPT_FAILED); + if (decrypt_err != APIError::OK) { return decrypt_err; + } uint16_t msg_size = mbuf.size; - uint8_t *msg_data = frame.data(); + uint8_t *msg_data = this->rx_buf_.data(); if (msg_size < 4) { - state_ = State::FAILED; + this->state_ = State::FAILED; HELPER_LOG("Bad data packet: size %d too short", msg_size); return APIError::BAD_DATA_PACKET; } @@ -383,12 +402,12 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { uint16_t type = (((uint16_t) msg_data[0]) << 8) | msg_data[1]; uint16_t data_len = (((uint16_t) msg_data[2]) << 8) | msg_data[3]; if (data_len > msg_size - 4) { - state_ = State::FAILED; + this->state_ = State::FAILED; HELPER_LOG("Bad data packet: data_len %u greater than msg_size %u", data_len, msg_size); return APIError::BAD_DATA_PACKET; } - buffer->container = std::move(frame); + buffer->container = std::move(this->rx_buf_); buffer->data_offset = 4; buffer->data_len = data_len; buffer->type = type; @@ -416,8 +435,7 @@ APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, st return APIError::OK; } - std::vector *raw_buffer = buffer.get_buffer(); - uint8_t *buffer_data = raw_buffer->data(); // Cache buffer pointer + uint8_t *buffer_data = buffer.get_buffer()->data(); this->reusable_iovs_.clear(); this->reusable_iovs_.reserve(packets.size()); @@ -450,7 +468,8 @@ APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, st 4 + packet.payload_size + frame_footer_size_); int err = noise_cipherstate_encrypt(send_cipher_, &mbuf); - APIError aerr = handle_noise_error_(err, "noise_cipherstate_encrypt", APIError::CIPHERSTATE_ENCRYPT_FAILED); + APIError aerr = + handle_noise_error_(err, LOG_STR("noise_cipherstate_encrypt"), APIError::CIPHERSTATE_ENCRYPT_FAILED); if (aerr != APIError::OK) return aerr; @@ -504,25 +523,27 @@ APIError APINoiseFrameHelper::init_handshake_() { nid_.modifier_ids[0] = NOISE_MODIFIER_PSK0; err = noise_handshakestate_new_by_id(&handshake_, &nid_, NOISE_ROLE_RESPONDER); - APIError aerr = handle_noise_error_(err, "noise_handshakestate_new_by_id", APIError::HANDSHAKESTATE_SETUP_FAILED); + APIError aerr = + handle_noise_error_(err, LOG_STR("noise_handshakestate_new_by_id"), APIError::HANDSHAKESTATE_SETUP_FAILED); if (aerr != APIError::OK) return aerr; - const auto &psk = ctx_->get_psk(); + const auto &psk = this->ctx_.get_psk(); err = noise_handshakestate_set_pre_shared_key(handshake_, psk.data(), psk.size()); - aerr = handle_noise_error_(err, "noise_handshakestate_set_pre_shared_key", APIError::HANDSHAKESTATE_SETUP_FAILED); + aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_set_pre_shared_key"), + APIError::HANDSHAKESTATE_SETUP_FAILED); if (aerr != APIError::OK) return aerr; err = noise_handshakestate_set_prologue(handshake_, prologue_.data(), prologue_.size()); - aerr = handle_noise_error_(err, "noise_handshakestate_set_prologue", APIError::HANDSHAKESTATE_SETUP_FAILED); + aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_set_prologue"), APIError::HANDSHAKESTATE_SETUP_FAILED); if (aerr != APIError::OK) return aerr; // set_prologue copies it into handshakestate, so we can get rid of it now prologue_ = {}; err = noise_handshakestate_start(handshake_); - aerr = handle_noise_error_(err, "noise_handshakestate_start", APIError::HANDSHAKESTATE_SETUP_FAILED); + aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_start"), APIError::HANDSHAKESTATE_SETUP_FAILED); if (aerr != APIError::OK) return aerr; return APIError::OK; @@ -540,7 +561,8 @@ APIError APINoiseFrameHelper::check_handshake_finished_() { return APIError::HANDSHAKESTATE_BAD_STATE; } int err = noise_handshakestate_split(handshake_, &send_cipher_, &recv_cipher_); - APIError aerr = handle_noise_error_(err, "noise_handshakestate_split", APIError::HANDSHAKESTATE_SPLIT_FAILED); + APIError aerr = + handle_noise_error_(err, LOG_STR("noise_handshakestate_split"), APIError::HANDSHAKESTATE_SPLIT_FAILED); if (aerr != APIError::OK) return aerr; diff --git a/esphome/components/api/api_frame_helper_noise.h b/esphome/components/api/api_frame_helper_noise.h index e82e5daadb..7eb01058db 100644 --- a/esphome/components/api/api_frame_helper_noise.h +++ b/esphome/components/api/api_frame_helper_noise.h @@ -7,11 +7,10 @@ namespace esphome::api { -class APINoiseFrameHelper : public APIFrameHelper { +class APINoiseFrameHelper final : public APIFrameHelper { public: - APINoiseFrameHelper(std::unique_ptr socket, std::shared_ptr ctx, - const ClientInfo *client_info) - : APIFrameHelper(std::move(socket), client_info), ctx_(std::move(ctx)) { + APINoiseFrameHelper(std::unique_ptr socket, APINoiseContext &ctx, const ClientInfo *client_info) + : APIFrameHelper(std::move(socket), client_info), ctx_(ctx) { // Noise header structure: // Pos 0: indicator (0x01) // Pos 1-2: encrypted payload size (16-bit big-endian) @@ -25,28 +24,24 @@ class APINoiseFrameHelper : public APIFrameHelper { APIError read_packet(ReadPacketBuffer *buffer) override; APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override; APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span packets) override; - // Get the frame header padding required by this protocol - uint8_t frame_header_padding() override { return frame_header_padding_; } - // Get the frame footer size required by this protocol - uint8_t frame_footer_size() override { return frame_footer_size_; } protected: APIError state_action_(); - APIError try_read_frame_(std::vector *frame); + APIError try_read_frame_(); APIError write_frame_(const uint8_t *data, uint16_t len); APIError init_handshake_(); APIError check_handshake_finished_(); - void send_explicit_handshake_reject_(const std::string &reason); + void send_explicit_handshake_reject_(const LogString *reason); APIError handle_handshake_frame_error_(APIError aerr); - APIError handle_noise_error_(int err, const char *func_name, APIError api_err); + APIError handle_noise_error_(int err, const LogString *func_name, APIError api_err); // Pointers first (4 bytes each) NoiseHandshakeState *handshake_{nullptr}; NoiseCipherState *send_cipher_{nullptr}; NoiseCipherState *recv_cipher_{nullptr}; - // Shared pointer (8 bytes on 32-bit = 4 bytes control block pointer + 4 bytes object pointer) - std::shared_ptr ctx_; + // Reference to noise context (4 bytes on 32-bit) + APINoiseContext &ctx_; // Vector (12 bytes on 32-bit) std::vector prologue_; diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index fdaacbd94e..dcbd35aa32 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -10,11 +10,16 @@ #include #include +#ifdef USE_ESP8266 +#include +#endif + namespace esphome::api { static const char *const TAG = "api.plaintext"; -#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->client_info_->get_combined_info().c_str(), ##__VA_ARGS__) +#define HELPER_LOG(msg, ...) \ + ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.c_str(), ##__VA_ARGS__) #ifdef HELPER_LOG_PACKETS #define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str()) @@ -42,21 +47,13 @@ APIError APIPlaintextFrameHelper::loop() { return APIFrameHelper::loop(); } -/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter - * - * @param frame: The struct to hold the frame information in. - * msg: store the parsed frame in that struct +/** Read a packet into the rx_buf_. * * @return See APIError * * error API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame. */ -APIError APIPlaintextFrameHelper::try_read_frame_(std::vector *frame) { - if (frame == nullptr) { - HELPER_LOG("Bad argument for try_read_frame_"); - return APIError::BAD_ARG; - } - +APIError APIPlaintextFrameHelper::try_read_frame_() { // read header while (!rx_header_parsed_) { // Now that we know when the socket is ready, we can read up to 3 bytes @@ -118,10 +115,10 @@ APIError APIPlaintextFrameHelper::try_read_frame_(std::vector *frame) { continue; } - if (msg_size_varint->as_uint32() > std::numeric_limits::max()) { + if (msg_size_varint->as_uint32() > MAX_MESSAGE_SIZE) { state_ = State::FAILED; HELPER_LOG("Bad packet: message size %" PRIu32 " exceeds maximum %u", msg_size_varint->as_uint32(), - std::numeric_limits::max()); + MAX_MESSAGE_SIZE); return APIError::BAD_DATA_PACKET; } rx_header_parsed_len_ = msg_size_varint->as_uint16(); @@ -145,9 +142,9 @@ APIError APIPlaintextFrameHelper::try_read_frame_(std::vector *frame) { } // header reading done - // reserve space for body - if (rx_buf_.size() != rx_header_parsed_len_) { - rx_buf_.resize(rx_header_parsed_len_); + // Reserve space for body + if (this->rx_buf_.size() != this->rx_header_parsed_len_) { + this->rx_buf_.resize(this->rx_header_parsed_len_); } if (rx_buf_len_ < rx_header_parsed_len_) { @@ -165,24 +162,22 @@ APIError APIPlaintextFrameHelper::try_read_frame_(std::vector *frame) { } } - LOG_PACKET_RECEIVED(rx_buf_); - *frame = std::move(rx_buf_); - // consume msg - rx_buf_ = {}; - rx_buf_len_ = 0; - rx_header_buf_pos_ = 0; - rx_header_parsed_ = false; + LOG_PACKET_RECEIVED(this->rx_buf_); + + // Clear state for next frame (rx_buf_ still contains data for caller) + this->rx_buf_len_ = 0; + this->rx_header_buf_pos_ = 0; + this->rx_header_parsed_ = false; + return APIError::OK; } -APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { - APIError aerr; - if (state_ != State::DATA) { +APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { + if (this->state_ != State::DATA) { return APIError::WOULD_BLOCK; } - std::vector frame; - aerr = try_read_frame_(&frame); + APIError aerr = this->try_read_frame_(); if (aerr != APIError::OK) { if (aerr == APIError::BAD_INDICATOR) { // Make sure to tell the remote that we don't @@ -197,19 +192,28 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { // We must send at least 3 bytes to be read, so we add // a message after the indicator byte to ensures its long // enough and can aid in debugging. - const char msg[] = "\x00" - "Bad indicator byte"; + static constexpr uint8_t INDICATOR_MSG_SIZE = 19; +#ifdef USE_ESP8266 + static const char MSG_PROGMEM[] PROGMEM = "\x00" + "Bad indicator byte"; + char msg[INDICATOR_MSG_SIZE]; + memcpy_P(msg, MSG_PROGMEM, INDICATOR_MSG_SIZE); iov[0].iov_base = (void *) msg; - iov[0].iov_len = 19; - this->write_raw_(iov, 1, 19); +#else + static const char MSG[] = "\x00" + "Bad indicator byte"; + iov[0].iov_base = (void *) MSG; +#endif + iov[0].iov_len = INDICATOR_MSG_SIZE; + this->write_raw_(iov, 1, INDICATOR_MSG_SIZE); } return aerr; } - buffer->container = std::move(frame); + buffer->container = std::move(this->rx_buf_); buffer->data_offset = 0; - buffer->data_len = rx_header_parsed_len_; - buffer->type = rx_header_parsed_type_; + buffer->data_len = this->rx_header_parsed_len_; + buffer->type = this->rx_header_parsed_type_; return APIError::OK; } APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) { @@ -226,8 +230,7 @@ APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer return APIError::OK; } - std::vector *raw_buffer = buffer.get_buffer(); - uint8_t *buffer_data = raw_buffer->data(); // Cache buffer pointer + uint8_t *buffer_data = buffer.get_buffer()->data(); this->reusable_iovs_.clear(); this->reusable_iovs_.reserve(packets.size()); diff --git a/esphome/components/api/api_frame_helper_plaintext.h b/esphome/components/api/api_frame_helper_plaintext.h index b50902dd75..bba981d26b 100644 --- a/esphome/components/api/api_frame_helper_plaintext.h +++ b/esphome/components/api/api_frame_helper_plaintext.h @@ -5,7 +5,7 @@ namespace esphome::api { -class APIPlaintextFrameHelper : public APIFrameHelper { +class APIPlaintextFrameHelper final : public APIFrameHelper { public: APIPlaintextFrameHelper(std::unique_ptr socket, const ClientInfo *client_info) : APIFrameHelper(std::move(socket), client_info) { @@ -22,12 +22,9 @@ class APIPlaintextFrameHelper : public APIFrameHelper { APIError read_packet(ReadPacketBuffer *buffer) override; APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override; APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span packets) override; - uint8_t frame_header_padding() override { return frame_header_padding_; } - // Get the frame footer size required by this protocol - uint8_t frame_footer_size() override { return frame_footer_size_; } protected: - APIError try_read_frame_(std::vector *frame); + APIError try_read_frame_(); // Group 2-byte aligned types uint16_t rx_header_parsed_type_ = 0; diff --git a/esphome/components/api/api_options.proto b/esphome/components/api/api_options.proto index ed0e0d7455..6b33408e2f 100644 --- a/esphome/components/api/api_options.proto +++ b/esphome/components/api/api_options.proto @@ -30,6 +30,14 @@ extend google.protobuf.FieldOptions { optional bool no_zero_copy = 50008 [default=false]; optional bool fixed_array_skip_zero = 50009 [default=false]; optional string fixed_array_size_define = 50010; + optional string fixed_array_with_length_define = 50011; + + // pointer_to_buffer: Use pointer instead of array for fixed-size byte fields + // When set, the field will be declared as a pointer (const uint8_t *data) + // instead of an array (uint8_t data[N]). This allows zero-copy on decode + // by pointing directly to the protobuf buffer. The buffer must remain valid + // until the message is processed (which is guaranteed for stack-allocated messages). + optional bool pointer_to_buffer = 50012 [default=false]; // container_pointer: Zero-copy optimization for repeated fields. // @@ -56,4 +64,20 @@ extend google.protobuf.FieldOptions { // This is typically done through methods returning const T& or special accessor // methods like get_options() or supported_modes_for_api_(). optional string container_pointer = 50001; + + // fixed_vector: Use FixedVector instead of std::vector for repeated fields + // When set, the repeated field will use FixedVector which requires calling + // init(size) before adding elements. This eliminates std::vector template overhead + // and is ideal when the exact size is known before populating the array. + optional bool fixed_vector = 50013 [default=false]; + + // container_pointer_no_template: Use a non-template container type for repeated fields + // Similar to container_pointer, but for containers that don't take template parameters. + // The container type is used as-is without appending element type. + // The container must have: + // - begin() and end() methods returning iterators + // - empty() method + // Example: [(container_pointer_no_template) = "light::ColorModeMask"] + // generates: const light::ColorModeMask *supported_color_modes{}; + optional string container_pointer_no_template = 50014; } diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 5dddc79b49..c131815456 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -22,9 +22,12 @@ bool HelloRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { } bool HelloRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 1: - this->client_info = value.as_string(); + case 1: { + // Use raw data directly to avoid allocation + this->client_info = value.data(); + this->client_info_len = value.size(); break; + } default: return false; } @@ -42,18 +45,23 @@ void HelloResponse::calculate_size(ProtoSize &size) const { size.add_length(1, this->server_info_ref_.size()); size.add_length(1, this->name_ref_.size()); } -bool ConnectRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { +#ifdef USE_API_PASSWORD +bool AuthenticationRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 1: - this->password = value.as_string(); + case 1: { + // Use raw data directly to avoid allocation + this->password = value.data(); + this->password_len = value.size(); break; + } default: return false; } return true; } -void ConnectResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->invalid_password); } -void ConnectResponse::calculate_size(ProtoSize &size) const { size.add_bool(1, this->invalid_password); } +void AuthenticationResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->invalid_password); } +void AuthenticationResponse::calculate_size(ProtoSize &size) const { size.add_bool(1, this->invalid_password); } +#endif #ifdef USE_AREAS void AreaInfo::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(1, this->area_id); @@ -127,6 +135,12 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { #ifdef USE_AREAS buffer.encode_message(22, this->area); #endif +#ifdef USE_ZWAVE_PROXY + buffer.encode_uint32(23, this->zwave_proxy_feature_flags); +#endif +#ifdef USE_ZWAVE_PROXY + buffer.encode_uint32(24, this->zwave_home_id); +#endif } void DeviceInfoResponse::calculate_size(ProtoSize &size) const { #ifdef USE_API_PASSWORD @@ -179,6 +193,12 @@ void DeviceInfoResponse::calculate_size(ProtoSize &size) const { #ifdef USE_AREAS size.add_message_object(2, this->area); #endif +#ifdef USE_ZWAVE_PROXY + size.add_uint32(2, this->zwave_proxy_feature_flags); +#endif +#ifdef USE_ZWAVE_PROXY + size.add_uint32(2, this->zwave_home_id); +#endif } #ifdef USE_BINARY_SENSOR void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer buffer) const { @@ -335,8 +355,8 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(10, this->icon_ref_); #endif buffer.encode_uint32(11, static_cast(this->entity_category)); - for (const auto &it : *this->supported_preset_modes) { - buffer.encode_string(12, it, true); + for (const char *it : *this->supported_preset_modes) { + buffer.encode_string(12, it, strlen(it), true); } #ifdef USE_DEVICES buffer.encode_uint32(13, this->device_id); @@ -356,8 +376,8 @@ void ListEntitiesFanResponse::calculate_size(ProtoSize &size) const { #endif size.add_uint32(1, static_cast(this->entity_category)); if (!this->supported_preset_modes->empty()) { - for (const auto &it : *this->supported_preset_modes) { - size.add_length_force(1, it.size()); + for (const char *it : *this->supported_preset_modes) { + size.add_length_force(1, strlen(it)); } } #ifdef USE_DEVICES @@ -456,8 +476,8 @@ void ListEntitiesLightResponse::encode(ProtoWriteBuffer buffer) const { } buffer.encode_float(9, this->min_mireds); buffer.encode_float(10, this->max_mireds); - for (auto &it : this->effects) { - buffer.encode_string(11, it, true); + for (const char *it : *this->effects) { + buffer.encode_string(11, it, strlen(it), true); } buffer.encode_bool(13, this->disabled_by_default); #ifdef USE_ENTITY_ICON @@ -479,9 +499,9 @@ void ListEntitiesLightResponse::calculate_size(ProtoSize &size) const { } size.add_float(1, this->min_mireds); size.add_float(1, this->max_mireds); - if (!this->effects.empty()) { - for (const auto &it : this->effects) { - size.add_length_force(1, it.size()); + if (!this->effects->empty()) { + for (const char *it : *this->effects) { + size.add_length_force(1, strlen(it)); } } size.add_bool(1, this->disabled_by_default); @@ -852,7 +872,7 @@ void HomeassistantServiceMap::calculate_size(ProtoSize &size) const { size.add_length(1, this->key_ref_.size()); size.add_length(1, this->value.size()); } -void HomeassistantServiceResponse::encode(ProtoWriteBuffer buffer) const { +void HomeassistantActionRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->service_ref_); for (auto &it : this->data) { buffer.encode_message(2, it, true); @@ -864,13 +884,64 @@ void HomeassistantServiceResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_message(4, it, true); } buffer.encode_bool(5, this->is_event); +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES + buffer.encode_uint32(6, this->call_id); +#endif +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON + buffer.encode_bool(7, this->wants_response); +#endif +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON + buffer.encode_string(8, this->response_template); +#endif } -void HomeassistantServiceResponse::calculate_size(ProtoSize &size) const { +void HomeassistantActionRequest::calculate_size(ProtoSize &size) const { size.add_length(1, this->service_ref_.size()); size.add_repeated_message(1, this->data); size.add_repeated_message(1, this->data_template); size.add_repeated_message(1, this->variables); size.add_bool(1, this->is_event); +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES + size.add_uint32(1, this->call_id); +#endif +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON + size.add_bool(1, this->wants_response); +#endif +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON + size.add_length(1, this->response_template.size()); +#endif +} +#endif +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES +bool HomeassistantActionResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: + this->call_id = value.as_uint32(); + break; + case 2: + this->success = value.as_bool(); + break; + default: + return false; + } + return true; +} +bool HomeassistantActionResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 3: + this->error_message = value.as_string(); + break; +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON + case 4: { + // Use raw data directly to avoid allocation + this->response_data = value.data(); + this->response_data_len = value.size(); + break; + } +#endif + default: + return false; + } + return true; } #endif #ifdef USE_API_HOMEASSISTANT_STATES @@ -901,6 +972,19 @@ bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDel return true; } #endif +bool GetTimeResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 2: { + // Use raw data directly to avoid allocation + this->timezone = value.data(); + this->timezone_len = value.size(); + break; + } + default: + return false; + } + return true; +} bool GetTimeResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { case 1: @@ -911,9 +995,7 @@ bool GetTimeResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { } return true; } -void GetTimeResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->epoch_seconds); } -void GetTimeResponse::calculate_size(ProtoSize &size) const { size.add_fixed32(1, this->epoch_seconds); } -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS void ListEntitiesServicesArgument::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->name_ref_); buffer.encode_uint32(2, static_cast(this->type)); @@ -982,6 +1064,17 @@ bool ExecuteServiceArgument::decode_32bit(uint32_t field_id, Proto32Bit value) { } return true; } +void ExecuteServiceArgument::decode(const uint8_t *buffer, size_t length) { + uint32_t count_bool_array = ProtoDecodableMessage::count_repeated_field(buffer, length, 6); + this->bool_array.init(count_bool_array); + uint32_t count_int_array = ProtoDecodableMessage::count_repeated_field(buffer, length, 7); + this->int_array.init(count_int_array); + uint32_t count_float_array = ProtoDecodableMessage::count_repeated_field(buffer, length, 8); + this->float_array.init(count_float_array); + uint32_t count_string_array = ProtoDecodableMessage::count_repeated_field(buffer, length, 9); + this->string_array.init(count_string_array); + ProtoDecodableMessage::decode(buffer, length); +} bool ExecuteServiceRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 2: @@ -1003,6 +1096,11 @@ bool ExecuteServiceRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } return true; } +void ExecuteServiceRequest::decode(const uint8_t *buffer, size_t length) { + uint32_t count_args = ProtoDecodableMessage::count_repeated_field(buffer, length, 2); + this->args.init(count_args); + ProtoDecodableMessage::decode(buffer, length); +} #endif #ifdef USE_CAMERA void ListEntitiesCameraResponse::encode(ProtoWriteBuffer buffer) const { @@ -1081,14 +1179,14 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { for (const auto &it : *this->supported_swing_modes) { buffer.encode_uint32(14, static_cast(it), true); } - for (const auto &it : *this->supported_custom_fan_modes) { - buffer.encode_string(15, it, true); + for (const char *it : *this->supported_custom_fan_modes) { + buffer.encode_string(15, it, strlen(it), true); } for (const auto &it : *this->supported_presets) { buffer.encode_uint32(16, static_cast(it), true); } - for (const auto &it : *this->supported_custom_presets) { - buffer.encode_string(17, it, true); + for (const char *it : *this->supported_custom_presets) { + buffer.encode_string(17, it, strlen(it), true); } buffer.encode_bool(18, this->disabled_by_default); #ifdef USE_ENTITY_ICON @@ -1103,6 +1201,7 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { #ifdef USE_DEVICES buffer.encode_uint32(26, this->device_id); #endif + buffer.encode_uint32(27, this->feature_flags); } void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const { size.add_length(1, this->object_id_ref_.size()); @@ -1130,8 +1229,8 @@ void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const { } } if (!this->supported_custom_fan_modes->empty()) { - for (const auto &it : *this->supported_custom_fan_modes) { - size.add_length_force(1, it.size()); + for (const char *it : *this->supported_custom_fan_modes) { + size.add_length_force(1, strlen(it)); } } if (!this->supported_presets->empty()) { @@ -1140,8 +1239,8 @@ void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const { } } if (!this->supported_custom_presets->empty()) { - for (const auto &it : *this->supported_custom_presets) { - size.add_length_force(2, it.size()); + for (const char *it : *this->supported_custom_presets) { + size.add_length_force(2, strlen(it)); } } size.add_bool(2, this->disabled_by_default); @@ -1157,6 +1256,7 @@ void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const { #ifdef USE_DEVICES size.add_uint32(2, this->device_id); #endif + size.add_uint32(2, this->feature_flags); } void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); @@ -1375,8 +1475,8 @@ void ListEntitiesSelectResponse::encode(ProtoWriteBuffer buffer) const { #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon_ref_); #endif - for (const auto &it : *this->options) { - buffer.encode_string(6, it, true); + for (const char *it : *this->options) { + buffer.encode_string(6, it, strlen(it), true); } buffer.encode_bool(7, this->disabled_by_default); buffer.encode_uint32(8, static_cast(this->entity_category)); @@ -1392,8 +1492,8 @@ void ListEntitiesSelectResponse::calculate_size(ProtoSize &size) const { size.add_length(1, this->icon_ref_.size()); #endif if (!this->options->empty()) { - for (const auto &it : *this->options) { - size.add_length_force(1, it.size()); + for (const char *it : *this->options) { + size.add_length_force(1, strlen(it)); } } size.add_bool(1, this->disabled_by_default); @@ -1843,12 +1943,14 @@ void BluetoothLERawAdvertisement::calculate_size(ProtoSize &size) const { size.add_length(1, this->data_len); } void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer buffer) const { - for (auto &it : this->advertisements) { - buffer.encode_message(1, it, true); + for (uint16_t i = 0; i < this->advertisements_len; i++) { + buffer.encode_message(1, this->advertisements[i], true); } } void BluetoothLERawAdvertisementsResponse::calculate_size(ProtoSize &size) const { - size.add_repeated_message(1, this->advertisements); + for (uint16_t i = 0; i < this->advertisements_len; i++) { + size.add_message_object_force(1, this->advertisements[i]); + } } bool BluetoothDeviceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -2004,9 +2106,12 @@ bool BluetoothGATTWriteRequest::decode_varint(uint32_t field_id, ProtoVarInt val } bool BluetoothGATTWriteRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 4: - this->data = value.as_string(); + case 4: { + // Use raw data directly to avoid allocation + this->data = value.data(); + this->data_len = value.size(); break; + } default: return false; } @@ -2040,9 +2145,12 @@ bool BluetoothGATTWriteDescriptorRequest::decode_varint(uint32_t field_id, Proto } bool BluetoothGATTWriteDescriptorRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 3: - this->data = value.as_string(); + case 3: { + // Use raw data directly to avoid allocation + this->data = value.data(); + this->data_len = value.size(); break; + } default: return false; } @@ -2151,10 +2259,12 @@ void BluetoothDeviceClearCacheResponse::calculate_size(ProtoSize &size) const { void BluetoothScannerStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(1, static_cast(this->state)); buffer.encode_uint32(2, static_cast(this->mode)); + buffer.encode_uint32(3, static_cast(this->configured_mode)); } void BluetoothScannerStateResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, static_cast(this->state)); size.add_uint32(1, static_cast(this->mode)); + size.add_uint32(1, static_cast(this->configured_mode)); } bool BluetoothScannerSetModeRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -2356,6 +2466,52 @@ void VoiceAssistantWakeWord::calculate_size(ProtoSize &size) const { } } } +bool VoiceAssistantExternalWakeWord::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 5: + this->model_size = value.as_uint32(); + break; + default: + return false; + } + return true; +} +bool VoiceAssistantExternalWakeWord::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: + this->id = value.as_string(); + break; + case 2: + this->wake_word = value.as_string(); + break; + case 3: + this->trained_languages.push_back(value.as_string()); + break; + case 4: + this->model_type = value.as_string(); + break; + case 6: + this->model_hash = value.as_string(); + break; + case 7: + this->url = value.as_string(); + break; + default: + return false; + } + return true; +} +bool VoiceAssistantConfigurationRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: + this->external_wake_words.emplace_back(); + value.decode_to_message(this->external_wake_words.back()); + break; + default: + return false; + } + return true; +} void VoiceAssistantConfigurationResponse::encode(ProtoWriteBuffer buffer) const { for (auto &it : this->available_wake_words) { buffer.encode_message(1, it, true); @@ -2721,8 +2877,8 @@ void ListEntitiesEventResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); buffer.encode_string(8, this->device_class_ref_); - for (auto &it : this->event_types) { - buffer.encode_string(9, it, true); + for (const char *it : *this->event_types) { + buffer.encode_string(9, it, strlen(it), true); } #ifdef USE_DEVICES buffer.encode_uint32(10, this->device_id); @@ -2738,9 +2894,9 @@ void ListEntitiesEventResponse::calculate_size(ProtoSize &size) const { size.add_bool(1, this->disabled_by_default); size.add_uint32(1, static_cast(this->entity_category)); size.add_length(1, this->device_class_ref_.size()); - if (!this->event_types.empty()) { - for (const auto &it : this->event_types) { - size.add_length_force(1, it.size()); + if (!this->event_types->empty()) { + for (const char *it : *this->event_types) { + size.add_length_force(1, strlen(it)); } } #ifdef USE_DEVICES @@ -2999,5 +3155,53 @@ bool UpdateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { return true; } #endif +#ifdef USE_ZWAVE_PROXY +bool ZWaveProxyFrame::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: { + // Use raw data directly to avoid allocation + this->data = value.data(); + this->data_len = value.size(); + break; + } + default: + return false; + } + return true; +} +void ZWaveProxyFrame::encode(ProtoWriteBuffer buffer) const { buffer.encode_bytes(1, this->data, this->data_len); } +void ZWaveProxyFrame::calculate_size(ProtoSize &size) const { size.add_length(1, this->data_len); } +bool ZWaveProxyRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: + this->type = static_cast(value.as_uint32()); + break; + default: + return false; + } + return true; +} +bool ZWaveProxyRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 2: { + // Use raw data directly to avoid allocation + this->data = value.data(); + this->data_len = value.size(); + break; + } + default: + return false; + } + return true; +} +void ZWaveProxyRequest::encode(ProtoWriteBuffer buffer) const { + buffer.encode_uint32(1, static_cast(this->type)); + buffer.encode_bytes(2, this->data, this->data_len); +} +void ZWaveProxyRequest::calculate_size(ProtoSize &size) const { + size.add_uint32(1, static_cast(this->type)); + size.add_length(2, this->data_len); +} +#endif } // namespace esphome::api diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index d43d3c61b7..93ece74d85 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -63,7 +63,7 @@ enum LogLevel : uint32_t { LOG_LEVEL_VERBOSE = 6, LOG_LEVEL_VERY_VERBOSE = 7, }; -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS enum ServiceArgType : uint32_t { SERVICE_ARG_TYPE_BOOL = 0, SERVICE_ARG_TYPE_INT = 1, @@ -276,6 +276,13 @@ enum UpdateCommand : uint32_t { UPDATE_COMMAND_CHECK = 2, }; #endif +#ifdef USE_ZWAVE_PROXY +enum ZWaveProxyRequestType : uint32_t { + ZWAVE_PROXY_REQUEST_TYPE_SUBSCRIBE = 0, + ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE = 1, + ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE = 2, +}; +#endif } // namespace enums @@ -321,14 +328,15 @@ class CommandProtoMessage : public ProtoDecodableMessage { protected: }; -class HelloRequest : public ProtoDecodableMessage { +class HelloRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 1; - static constexpr uint8_t ESTIMATED_SIZE = 17; + static constexpr uint8_t ESTIMATED_SIZE = 27; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "hello_request"; } #endif - std::string client_info{}; + const uint8_t *client_info{nullptr}; + uint16_t client_info_len{0}; uint32_t api_version_major{0}; uint32_t api_version_minor{0}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -339,7 +347,7 @@ class HelloRequest : public ProtoDecodableMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class HelloResponse : public ProtoMessage { +class HelloResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 2; static constexpr uint8_t ESTIMATED_SIZE = 26; @@ -360,14 +368,16 @@ class HelloResponse : public ProtoMessage { protected: }; -class ConnectRequest : public ProtoDecodableMessage { +#ifdef USE_API_PASSWORD +class AuthenticationRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 3; - static constexpr uint8_t ESTIMATED_SIZE = 9; + static constexpr uint8_t ESTIMATED_SIZE = 19; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "connect_request"; } + const char *message_name() const override { return "authentication_request"; } #endif - std::string password{}; + const uint8_t *password{nullptr}; + uint16_t password_len{0}; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -375,12 +385,12 @@ class ConnectRequest : public ProtoDecodableMessage { protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; }; -class ConnectResponse : public ProtoMessage { +class AuthenticationResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 4; static constexpr uint8_t ESTIMATED_SIZE = 2; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "connect_response"; } + const char *message_name() const override { return "authentication_response"; } #endif bool invalid_password{false}; void encode(ProtoWriteBuffer buffer) const override; @@ -391,7 +401,8 @@ class ConnectResponse : public ProtoMessage { protected: }; -class DisconnectRequest : public ProtoMessage { +#endif +class DisconnectRequest final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 5; static constexpr uint8_t ESTIMATED_SIZE = 0; @@ -404,7 +415,7 @@ class DisconnectRequest : public ProtoMessage { protected: }; -class DisconnectResponse : public ProtoMessage { +class DisconnectResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 6; static constexpr uint8_t ESTIMATED_SIZE = 0; @@ -417,7 +428,7 @@ class DisconnectResponse : public ProtoMessage { protected: }; -class PingRequest : public ProtoMessage { +class PingRequest final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 7; static constexpr uint8_t ESTIMATED_SIZE = 0; @@ -430,7 +441,7 @@ class PingRequest : public ProtoMessage { protected: }; -class PingResponse : public ProtoMessage { +class PingResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 8; static constexpr uint8_t ESTIMATED_SIZE = 0; @@ -443,7 +454,7 @@ class PingResponse : public ProtoMessage { protected: }; -class DeviceInfoRequest : public ProtoMessage { +class DeviceInfoRequest final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 9; static constexpr uint8_t ESTIMATED_SIZE = 0; @@ -457,7 +468,7 @@ class DeviceInfoRequest : public ProtoMessage { protected: }; #ifdef USE_AREAS -class AreaInfo : public ProtoMessage { +class AreaInfo final : public ProtoMessage { public: uint32_t area_id{0}; StringRef name_ref_{}; @@ -472,7 +483,7 @@ class AreaInfo : public ProtoMessage { }; #endif #ifdef USE_DEVICES -class DeviceInfo : public ProtoMessage { +class DeviceInfo final : public ProtoMessage { public: uint32_t device_id{0}; StringRef name_ref_{}; @@ -487,10 +498,10 @@ class DeviceInfo : public ProtoMessage { protected: }; #endif -class DeviceInfoResponse : public ProtoMessage { +class DeviceInfoResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 10; - static constexpr uint8_t ESTIMATED_SIZE = 247; + static constexpr uint16_t ESTIMATED_SIZE = 257; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "device_info_response"; } #endif @@ -550,6 +561,12 @@ class DeviceInfoResponse : public ProtoMessage { #endif #ifdef USE_AREAS AreaInfo area{}; +#endif +#ifdef USE_ZWAVE_PROXY + uint32_t zwave_proxy_feature_flags{0}; +#endif +#ifdef USE_ZWAVE_PROXY + uint32_t zwave_home_id{0}; #endif void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; @@ -559,7 +576,7 @@ class DeviceInfoResponse : public ProtoMessage { protected: }; -class ListEntitiesRequest : public ProtoMessage { +class ListEntitiesRequest final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 11; static constexpr uint8_t ESTIMATED_SIZE = 0; @@ -572,7 +589,7 @@ class ListEntitiesRequest : public ProtoMessage { protected: }; -class ListEntitiesDoneResponse : public ProtoMessage { +class ListEntitiesDoneResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 19; static constexpr uint8_t ESTIMATED_SIZE = 0; @@ -585,7 +602,7 @@ class ListEntitiesDoneResponse : public ProtoMessage { protected: }; -class SubscribeStatesRequest : public ProtoMessage { +class SubscribeStatesRequest final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 20; static constexpr uint8_t ESTIMATED_SIZE = 0; @@ -599,7 +616,7 @@ class SubscribeStatesRequest : public ProtoMessage { protected: }; #ifdef USE_BINARY_SENSOR -class ListEntitiesBinarySensorResponse : public InfoResponseProtoMessage { +class ListEntitiesBinarySensorResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 12; static constexpr uint8_t ESTIMATED_SIZE = 51; @@ -617,7 +634,7 @@ class ListEntitiesBinarySensorResponse : public InfoResponseProtoMessage { protected: }; -class BinarySensorStateResponse : public StateResponseProtoMessage { +class BinarySensorStateResponse final : public StateResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 21; static constexpr uint8_t ESTIMATED_SIZE = 13; @@ -636,7 +653,7 @@ class BinarySensorStateResponse : public StateResponseProtoMessage { }; #endif #ifdef USE_COVER -class ListEntitiesCoverResponse : public InfoResponseProtoMessage { +class ListEntitiesCoverResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 13; static constexpr uint8_t ESTIMATED_SIZE = 57; @@ -657,7 +674,7 @@ class ListEntitiesCoverResponse : public InfoResponseProtoMessage { protected: }; -class CoverStateResponse : public StateResponseProtoMessage { +class CoverStateResponse final : public StateResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 22; static constexpr uint8_t ESTIMATED_SIZE = 21; @@ -675,7 +692,7 @@ class CoverStateResponse : public StateResponseProtoMessage { protected: }; -class CoverCommandRequest : public CommandProtoMessage { +class CoverCommandRequest final : public CommandProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 30; static constexpr uint8_t ESTIMATED_SIZE = 25; @@ -697,7 +714,7 @@ class CoverCommandRequest : public CommandProtoMessage { }; #endif #ifdef USE_FAN -class ListEntitiesFanResponse : public InfoResponseProtoMessage { +class ListEntitiesFanResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 14; static constexpr uint8_t ESTIMATED_SIZE = 68; @@ -708,7 +725,7 @@ class ListEntitiesFanResponse : public InfoResponseProtoMessage { bool supports_speed{false}; bool supports_direction{false}; int32_t supported_speed_count{0}; - const std::set *supported_preset_modes{}; + const std::vector *supported_preset_modes{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -717,7 +734,7 @@ class ListEntitiesFanResponse : public InfoResponseProtoMessage { protected: }; -class FanStateResponse : public StateResponseProtoMessage { +class FanStateResponse final : public StateResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 23; static constexpr uint8_t ESTIMATED_SIZE = 28; @@ -738,7 +755,7 @@ class FanStateResponse : public StateResponseProtoMessage { protected: }; -class FanCommandRequest : public CommandProtoMessage { +class FanCommandRequest final : public CommandProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 31; static constexpr uint8_t ESTIMATED_SIZE = 38; @@ -766,17 +783,17 @@ class FanCommandRequest : public CommandProtoMessage { }; #endif #ifdef USE_LIGHT -class ListEntitiesLightResponse : public InfoResponseProtoMessage { +class ListEntitiesLightResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 15; static constexpr uint8_t ESTIMATED_SIZE = 73; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_light_response"; } #endif - const std::set *supported_color_modes{}; + const light::ColorModeMask *supported_color_modes{}; float min_mireds{0.0f}; float max_mireds{0.0f}; - std::vector effects{}; + const FixedVector *effects{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -785,7 +802,7 @@ class ListEntitiesLightResponse : public InfoResponseProtoMessage { protected: }; -class LightStateResponse : public StateResponseProtoMessage { +class LightStateResponse final : public StateResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 24; static constexpr uint8_t ESTIMATED_SIZE = 67; @@ -813,7 +830,7 @@ class LightStateResponse : public StateResponseProtoMessage { protected: }; -class LightCommandRequest : public CommandProtoMessage { +class LightCommandRequest final : public CommandProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 32; static constexpr uint8_t ESTIMATED_SIZE = 112; @@ -857,7 +874,7 @@ class LightCommandRequest : public CommandProtoMessage { }; #endif #ifdef USE_SENSOR -class ListEntitiesSensorResponse : public InfoResponseProtoMessage { +class ListEntitiesSensorResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 16; static constexpr uint8_t ESTIMATED_SIZE = 66; @@ -879,7 +896,7 @@ class ListEntitiesSensorResponse : public InfoResponseProtoMessage { protected: }; -class SensorStateResponse : public StateResponseProtoMessage { +class SensorStateResponse final : public StateResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 25; static constexpr uint8_t ESTIMATED_SIZE = 16; @@ -898,7 +915,7 @@ class SensorStateResponse : public StateResponseProtoMessage { }; #endif #ifdef USE_SWITCH -class ListEntitiesSwitchResponse : public InfoResponseProtoMessage { +class ListEntitiesSwitchResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 17; static constexpr uint8_t ESTIMATED_SIZE = 51; @@ -916,7 +933,7 @@ class ListEntitiesSwitchResponse : public InfoResponseProtoMessage { protected: }; -class SwitchStateResponse : public StateResponseProtoMessage { +class SwitchStateResponse final : public StateResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 26; static constexpr uint8_t ESTIMATED_SIZE = 11; @@ -932,7 +949,7 @@ class SwitchStateResponse : public StateResponseProtoMessage { protected: }; -class SwitchCommandRequest : public CommandProtoMessage { +class SwitchCommandRequest final : public CommandProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 33; static constexpr uint8_t ESTIMATED_SIZE = 11; @@ -950,7 +967,7 @@ class SwitchCommandRequest : public CommandProtoMessage { }; #endif #ifdef USE_TEXT_SENSOR -class ListEntitiesTextSensorResponse : public InfoResponseProtoMessage { +class ListEntitiesTextSensorResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 18; static constexpr uint8_t ESTIMATED_SIZE = 49; @@ -967,7 +984,7 @@ class ListEntitiesTextSensorResponse : public InfoResponseProtoMessage { protected: }; -class TextSensorStateResponse : public StateResponseProtoMessage { +class TextSensorStateResponse final : public StateResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 27; static constexpr uint8_t ESTIMATED_SIZE = 20; @@ -986,7 +1003,7 @@ class TextSensorStateResponse : public StateResponseProtoMessage { protected: }; #endif -class SubscribeLogsRequest : public ProtoDecodableMessage { +class SubscribeLogsRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 28; static constexpr uint8_t ESTIMATED_SIZE = 4; @@ -1002,7 +1019,7 @@ class SubscribeLogsRequest : public ProtoDecodableMessage { protected: bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class SubscribeLogsResponse : public ProtoMessage { +class SubscribeLogsResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 29; static constexpr uint8_t ESTIMATED_SIZE = 11; @@ -1025,7 +1042,7 @@ class SubscribeLogsResponse : public ProtoMessage { protected: }; #ifdef USE_API_NOISE -class NoiseEncryptionSetKeyRequest : public ProtoDecodableMessage { +class NoiseEncryptionSetKeyRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 124; static constexpr uint8_t ESTIMATED_SIZE = 9; @@ -1040,7 +1057,7 @@ class NoiseEncryptionSetKeyRequest : public ProtoDecodableMessage { protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; }; -class NoiseEncryptionSetKeyResponse : public ProtoMessage { +class NoiseEncryptionSetKeyResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 125; static constexpr uint8_t ESTIMATED_SIZE = 2; @@ -1058,7 +1075,7 @@ class NoiseEncryptionSetKeyResponse : public ProtoMessage { }; #endif #ifdef USE_API_HOMEASSISTANT_SERVICES -class SubscribeHomeassistantServicesRequest : public ProtoMessage { +class SubscribeHomeassistantServicesRequest final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 34; static constexpr uint8_t ESTIMATED_SIZE = 0; @@ -1071,7 +1088,7 @@ class SubscribeHomeassistantServicesRequest : public ProtoMessage { protected: }; -class HomeassistantServiceMap : public ProtoMessage { +class HomeassistantServiceMap final : public ProtoMessage { public: StringRef key_ref_{}; void set_key(const StringRef &ref) { this->key_ref_ = ref; } @@ -1084,19 +1101,28 @@ class HomeassistantServiceMap : public ProtoMessage { protected: }; -class HomeassistantServiceResponse : public ProtoMessage { +class HomeassistantActionRequest final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 35; - static constexpr uint8_t ESTIMATED_SIZE = 113; + static constexpr uint8_t ESTIMATED_SIZE = 128; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "homeassistant_service_response"; } + const char *message_name() const override { return "homeassistant_action_request"; } #endif StringRef service_ref_{}; void set_service(const StringRef &ref) { this->service_ref_ = ref; } - std::vector data{}; - std::vector data_template{}; - std::vector variables{}; + FixedVector data{}; + FixedVector data_template{}; + FixedVector variables{}; bool is_event{false}; +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES + uint32_t call_id{0}; +#endif +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON + bool wants_response{false}; +#endif +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON + std::string response_template{}; +#endif void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1106,8 +1132,32 @@ class HomeassistantServiceResponse : public ProtoMessage { protected: }; #endif +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES +class HomeassistantActionResponse final : public ProtoDecodableMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 130; + static constexpr uint8_t ESTIMATED_SIZE = 34; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *message_name() const override { return "homeassistant_action_response"; } +#endif + uint32_t call_id{0}; + bool success{false}; + std::string error_message{}; +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON + const uint8_t *response_data{nullptr}; + uint16_t response_data_len{0}; +#endif +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +#endif #ifdef USE_API_HOMEASSISTANT_STATES -class SubscribeHomeAssistantStatesRequest : public ProtoMessage { +class SubscribeHomeAssistantStatesRequest final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 38; static constexpr uint8_t ESTIMATED_SIZE = 0; @@ -1120,7 +1170,7 @@ class SubscribeHomeAssistantStatesRequest : public ProtoMessage { protected: }; -class SubscribeHomeAssistantStateResponse : public ProtoMessage { +class SubscribeHomeAssistantStateResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 39; static constexpr uint8_t ESTIMATED_SIZE = 20; @@ -1140,7 +1190,7 @@ class SubscribeHomeAssistantStateResponse : public ProtoMessage { protected: }; -class HomeAssistantStateResponse : public ProtoDecodableMessage { +class HomeAssistantStateResponse final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 40; static constexpr uint8_t ESTIMATED_SIZE = 27; @@ -1158,7 +1208,7 @@ class HomeAssistantStateResponse : public ProtoDecodableMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; }; #endif -class GetTimeRequest : public ProtoMessage { +class GetTimeRequest final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 36; static constexpr uint8_t ESTIMATED_SIZE = 0; @@ -1171,25 +1221,26 @@ class GetTimeRequest : public ProtoMessage { protected: }; -class GetTimeResponse : public ProtoDecodableMessage { +class GetTimeResponse final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 37; - static constexpr uint8_t ESTIMATED_SIZE = 5; + static constexpr uint8_t ESTIMATED_SIZE = 24; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "get_time_response"; } #endif uint32_t epoch_seconds{0}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + const uint8_t *timezone{nullptr}; + uint16_t timezone_len{0}; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; }; -#ifdef USE_API_SERVICES -class ListEntitiesServicesArgument : public ProtoMessage { +#ifdef USE_API_USER_DEFINED_ACTIONS +class ListEntitiesServicesArgument final : public ProtoMessage { public: StringRef name_ref_{}; void set_name(const StringRef &ref) { this->name_ref_ = ref; } @@ -1202,7 +1253,7 @@ class ListEntitiesServicesArgument : public ProtoMessage { protected: }; -class ListEntitiesServicesResponse : public ProtoMessage { +class ListEntitiesServicesResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 41; static constexpr uint8_t ESTIMATED_SIZE = 48; @@ -1212,7 +1263,7 @@ class ListEntitiesServicesResponse : public ProtoMessage { StringRef name_ref_{}; void set_name(const StringRef &ref) { this->name_ref_ = ref; } uint32_t key{0}; - std::vector args{}; + FixedVector args{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1221,17 +1272,18 @@ class ListEntitiesServicesResponse : public ProtoMessage { protected: }; -class ExecuteServiceArgument : public ProtoDecodableMessage { +class ExecuteServiceArgument final : public ProtoDecodableMessage { public: bool bool_{false}; int32_t legacy_int{0}; float float_{0.0f}; std::string string_{}; int32_t int_{0}; - std::vector bool_array{}; - std::vector int_array{}; - std::vector float_array{}; - std::vector string_array{}; + FixedVector bool_array{}; + FixedVector int_array{}; + FixedVector float_array{}; + FixedVector string_array{}; + void decode(const uint8_t *buffer, size_t length) override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1241,7 +1293,7 @@ class ExecuteServiceArgument : public ProtoDecodableMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class ExecuteServiceRequest : public ProtoDecodableMessage { +class ExecuteServiceRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 42; static constexpr uint8_t ESTIMATED_SIZE = 39; @@ -1249,7 +1301,8 @@ class ExecuteServiceRequest : public ProtoDecodableMessage { const char *message_name() const override { return "execute_service_request"; } #endif uint32_t key{0}; - std::vector args{}; + FixedVector args{}; + void decode(const uint8_t *buffer, size_t length) override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1260,7 +1313,7 @@ class ExecuteServiceRequest : public ProtoDecodableMessage { }; #endif #ifdef USE_CAMERA -class ListEntitiesCameraResponse : public InfoResponseProtoMessage { +class ListEntitiesCameraResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 43; static constexpr uint8_t ESTIMATED_SIZE = 40; @@ -1275,7 +1328,7 @@ class ListEntitiesCameraResponse : public InfoResponseProtoMessage { protected: }; -class CameraImageResponse : public StateResponseProtoMessage { +class CameraImageResponse final : public StateResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 44; static constexpr uint8_t ESTIMATED_SIZE = 20; @@ -1297,7 +1350,7 @@ class CameraImageResponse : public StateResponseProtoMessage { protected: }; -class CameraImageRequest : public ProtoDecodableMessage { +class CameraImageRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 45; static constexpr uint8_t ESTIMATED_SIZE = 4; @@ -1315,30 +1368,31 @@ class CameraImageRequest : public ProtoDecodableMessage { }; #endif #ifdef USE_CLIMATE -class ListEntitiesClimateResponse : public InfoResponseProtoMessage { +class ListEntitiesClimateResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 46; - static constexpr uint8_t ESTIMATED_SIZE = 145; + static constexpr uint8_t ESTIMATED_SIZE = 150; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_climate_response"; } #endif bool supports_current_temperature{false}; bool supports_two_point_target_temperature{false}; - const std::set *supported_modes{}; + const climate::ClimateModeMask *supported_modes{}; float visual_min_temperature{0.0f}; float visual_max_temperature{0.0f}; float visual_target_temperature_step{0.0f}; bool supports_action{false}; - const std::set *supported_fan_modes{}; - const std::set *supported_swing_modes{}; - const std::set *supported_custom_fan_modes{}; - const std::set *supported_presets{}; - const std::set *supported_custom_presets{}; + const climate::ClimateFanModeMask *supported_fan_modes{}; + const climate::ClimateSwingModeMask *supported_swing_modes{}; + const std::vector *supported_custom_fan_modes{}; + const climate::ClimatePresetMask *supported_presets{}; + const std::vector *supported_custom_presets{}; float visual_current_temperature_step{0.0f}; bool supports_current_humidity{false}; bool supports_target_humidity{false}; float visual_min_humidity{0.0f}; float visual_max_humidity{0.0f}; + uint32_t feature_flags{0}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1347,7 +1401,7 @@ class ListEntitiesClimateResponse : public InfoResponseProtoMessage { protected: }; -class ClimateStateResponse : public StateResponseProtoMessage { +class ClimateStateResponse final : public StateResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 47; static constexpr uint8_t ESTIMATED_SIZE = 68; @@ -1377,7 +1431,7 @@ class ClimateStateResponse : public StateResponseProtoMessage { protected: }; -class ClimateCommandRequest : public CommandProtoMessage { +class ClimateCommandRequest final : public CommandProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 48; static constexpr uint8_t ESTIMATED_SIZE = 84; @@ -1415,7 +1469,7 @@ class ClimateCommandRequest : public CommandProtoMessage { }; #endif #ifdef USE_NUMBER -class ListEntitiesNumberResponse : public InfoResponseProtoMessage { +class ListEntitiesNumberResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 49; static constexpr uint8_t ESTIMATED_SIZE = 75; @@ -1438,7 +1492,7 @@ class ListEntitiesNumberResponse : public InfoResponseProtoMessage { protected: }; -class NumberStateResponse : public StateResponseProtoMessage { +class NumberStateResponse final : public StateResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 50; static constexpr uint8_t ESTIMATED_SIZE = 16; @@ -1455,7 +1509,7 @@ class NumberStateResponse : public StateResponseProtoMessage { protected: }; -class NumberCommandRequest : public CommandProtoMessage { +class NumberCommandRequest final : public CommandProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 51; static constexpr uint8_t ESTIMATED_SIZE = 14; @@ -1473,14 +1527,14 @@ class NumberCommandRequest : public CommandProtoMessage { }; #endif #ifdef USE_SELECT -class ListEntitiesSelectResponse : public InfoResponseProtoMessage { +class ListEntitiesSelectResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 52; static constexpr uint8_t ESTIMATED_SIZE = 58; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_select_response"; } #endif - const std::vector *options{}; + const FixedVector *options{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1489,7 +1543,7 @@ class ListEntitiesSelectResponse : public InfoResponseProtoMessage { protected: }; -class SelectStateResponse : public StateResponseProtoMessage { +class SelectStateResponse final : public StateResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 53; static constexpr uint8_t ESTIMATED_SIZE = 20; @@ -1507,7 +1561,7 @@ class SelectStateResponse : public StateResponseProtoMessage { protected: }; -class SelectCommandRequest : public CommandProtoMessage { +class SelectCommandRequest final : public CommandProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 54; static constexpr uint8_t ESTIMATED_SIZE = 18; @@ -1526,7 +1580,7 @@ class SelectCommandRequest : public CommandProtoMessage { }; #endif #ifdef USE_SIREN -class ListEntitiesSirenResponse : public InfoResponseProtoMessage { +class ListEntitiesSirenResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 55; static constexpr uint8_t ESTIMATED_SIZE = 62; @@ -1544,7 +1598,7 @@ class ListEntitiesSirenResponse : public InfoResponseProtoMessage { protected: }; -class SirenStateResponse : public StateResponseProtoMessage { +class SirenStateResponse final : public StateResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 56; static constexpr uint8_t ESTIMATED_SIZE = 11; @@ -1560,7 +1614,7 @@ class SirenStateResponse : public StateResponseProtoMessage { protected: }; -class SirenCommandRequest : public CommandProtoMessage { +class SirenCommandRequest final : public CommandProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 57; static constexpr uint8_t ESTIMATED_SIZE = 37; @@ -1586,7 +1640,7 @@ class SirenCommandRequest : public CommandProtoMessage { }; #endif #ifdef USE_LOCK -class ListEntitiesLockResponse : public InfoResponseProtoMessage { +class ListEntitiesLockResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 58; static constexpr uint8_t ESTIMATED_SIZE = 55; @@ -1606,7 +1660,7 @@ class ListEntitiesLockResponse : public InfoResponseProtoMessage { protected: }; -class LockStateResponse : public StateResponseProtoMessage { +class LockStateResponse final : public StateResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 59; static constexpr uint8_t ESTIMATED_SIZE = 11; @@ -1622,7 +1676,7 @@ class LockStateResponse : public StateResponseProtoMessage { protected: }; -class LockCommandRequest : public CommandProtoMessage { +class LockCommandRequest final : public CommandProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 60; static constexpr uint8_t ESTIMATED_SIZE = 22; @@ -1643,7 +1697,7 @@ class LockCommandRequest : public CommandProtoMessage { }; #endif #ifdef USE_BUTTON -class ListEntitiesButtonResponse : public InfoResponseProtoMessage { +class ListEntitiesButtonResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 61; static constexpr uint8_t ESTIMATED_SIZE = 49; @@ -1660,7 +1714,7 @@ class ListEntitiesButtonResponse : public InfoResponseProtoMessage { protected: }; -class ButtonCommandRequest : public CommandProtoMessage { +class ButtonCommandRequest final : public CommandProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 62; static constexpr uint8_t ESTIMATED_SIZE = 9; @@ -1677,7 +1731,7 @@ class ButtonCommandRequest : public CommandProtoMessage { }; #endif #ifdef USE_MEDIA_PLAYER -class MediaPlayerSupportedFormat : public ProtoMessage { +class MediaPlayerSupportedFormat final : public ProtoMessage { public: StringRef format_ref_{}; void set_format(const StringRef &ref) { this->format_ref_ = ref; } @@ -1693,7 +1747,7 @@ class MediaPlayerSupportedFormat : public ProtoMessage { protected: }; -class ListEntitiesMediaPlayerResponse : public InfoResponseProtoMessage { +class ListEntitiesMediaPlayerResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 63; static constexpr uint8_t ESTIMATED_SIZE = 80; @@ -1711,7 +1765,7 @@ class ListEntitiesMediaPlayerResponse : public InfoResponseProtoMessage { protected: }; -class MediaPlayerStateResponse : public StateResponseProtoMessage { +class MediaPlayerStateResponse final : public StateResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 64; static constexpr uint8_t ESTIMATED_SIZE = 18; @@ -1729,7 +1783,7 @@ class MediaPlayerStateResponse : public StateResponseProtoMessage { protected: }; -class MediaPlayerCommandRequest : public CommandProtoMessage { +class MediaPlayerCommandRequest final : public CommandProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 65; static constexpr uint8_t ESTIMATED_SIZE = 35; @@ -1755,7 +1809,7 @@ class MediaPlayerCommandRequest : public CommandProtoMessage { }; #endif #ifdef USE_BLUETOOTH_PROXY -class SubscribeBluetoothLEAdvertisementsRequest : public ProtoDecodableMessage { +class SubscribeBluetoothLEAdvertisementsRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 66; static constexpr uint8_t ESTIMATED_SIZE = 4; @@ -1770,7 +1824,7 @@ class SubscribeBluetoothLEAdvertisementsRequest : public ProtoDecodableMessage { protected: bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class BluetoothLERawAdvertisement : public ProtoMessage { +class BluetoothLERawAdvertisement final : public ProtoMessage { public: uint64_t address{0}; int32_t rssi{0}; @@ -1785,14 +1839,15 @@ class BluetoothLERawAdvertisement : public ProtoMessage { protected: }; -class BluetoothLERawAdvertisementsResponse : public ProtoMessage { +class BluetoothLERawAdvertisementsResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 93; - static constexpr uint8_t ESTIMATED_SIZE = 34; + static constexpr uint8_t ESTIMATED_SIZE = 136; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "bluetooth_le_raw_advertisements_response"; } #endif - std::vector advertisements{}; + std::array advertisements{}; + uint16_t advertisements_len{0}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1801,7 +1856,7 @@ class BluetoothLERawAdvertisementsResponse : public ProtoMessage { protected: }; -class BluetoothDeviceRequest : public ProtoDecodableMessage { +class BluetoothDeviceRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 68; static constexpr uint8_t ESTIMATED_SIZE = 12; @@ -1819,7 +1874,7 @@ class BluetoothDeviceRequest : public ProtoDecodableMessage { protected: bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class BluetoothDeviceConnectionResponse : public ProtoMessage { +class BluetoothDeviceConnectionResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 69; static constexpr uint8_t ESTIMATED_SIZE = 14; @@ -1838,7 +1893,7 @@ class BluetoothDeviceConnectionResponse : public ProtoMessage { protected: }; -class BluetoothGATTGetServicesRequest : public ProtoDecodableMessage { +class BluetoothGATTGetServicesRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 70; static constexpr uint8_t ESTIMATED_SIZE = 4; @@ -1853,7 +1908,7 @@ class BluetoothGATTGetServicesRequest : public ProtoDecodableMessage { protected: bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class BluetoothGATTDescriptor : public ProtoMessage { +class BluetoothGATTDescriptor final : public ProtoMessage { public: std::array uuid{}; uint32_t handle{0}; @@ -1866,12 +1921,12 @@ class BluetoothGATTDescriptor : public ProtoMessage { protected: }; -class BluetoothGATTCharacteristic : public ProtoMessage { +class BluetoothGATTCharacteristic final : public ProtoMessage { public: std::array uuid{}; uint32_t handle{0}; uint32_t properties{0}; - std::vector descriptors{}; + FixedVector descriptors{}; uint32_t short_uuid{0}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; @@ -1881,11 +1936,11 @@ class BluetoothGATTCharacteristic : public ProtoMessage { protected: }; -class BluetoothGATTService : public ProtoMessage { +class BluetoothGATTService final : public ProtoMessage { public: std::array uuid{}; uint32_t handle{0}; - std::vector characteristics{}; + FixedVector characteristics{}; uint32_t short_uuid{0}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; @@ -1895,7 +1950,7 @@ class BluetoothGATTService : public ProtoMessage { protected: }; -class BluetoothGATTGetServicesResponse : public ProtoMessage { +class BluetoothGATTGetServicesResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 71; static constexpr uint8_t ESTIMATED_SIZE = 38; @@ -1912,7 +1967,7 @@ class BluetoothGATTGetServicesResponse : public ProtoMessage { protected: }; -class BluetoothGATTGetServicesDoneResponse : public ProtoMessage { +class BluetoothGATTGetServicesDoneResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 72; static constexpr uint8_t ESTIMATED_SIZE = 4; @@ -1928,7 +1983,7 @@ class BluetoothGATTGetServicesDoneResponse : public ProtoMessage { protected: }; -class BluetoothGATTReadRequest : public ProtoDecodableMessage { +class BluetoothGATTReadRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 73; static constexpr uint8_t ESTIMATED_SIZE = 8; @@ -1944,7 +1999,7 @@ class BluetoothGATTReadRequest : public ProtoDecodableMessage { protected: bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class BluetoothGATTReadResponse : public ProtoMessage { +class BluetoothGATTReadResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 74; static constexpr uint8_t ESTIMATED_SIZE = 17; @@ -1967,17 +2022,18 @@ class BluetoothGATTReadResponse : public ProtoMessage { protected: }; -class BluetoothGATTWriteRequest : public ProtoDecodableMessage { +class BluetoothGATTWriteRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 75; - static constexpr uint8_t ESTIMATED_SIZE = 19; + static constexpr uint8_t ESTIMATED_SIZE = 29; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "bluetooth_gatt_write_request"; } #endif uint64_t address{0}; uint32_t handle{0}; bool response{false}; - std::string data{}; + const uint8_t *data{nullptr}; + uint16_t data_len{0}; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1986,7 +2042,7 @@ class BluetoothGATTWriteRequest : public ProtoDecodableMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class BluetoothGATTReadDescriptorRequest : public ProtoDecodableMessage { +class BluetoothGATTReadDescriptorRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 76; static constexpr uint8_t ESTIMATED_SIZE = 8; @@ -2002,16 +2058,17 @@ class BluetoothGATTReadDescriptorRequest : public ProtoDecodableMessage { protected: bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class BluetoothGATTWriteDescriptorRequest : public ProtoDecodableMessage { +class BluetoothGATTWriteDescriptorRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 77; - static constexpr uint8_t ESTIMATED_SIZE = 17; + static constexpr uint8_t ESTIMATED_SIZE = 27; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "bluetooth_gatt_write_descriptor_request"; } #endif uint64_t address{0}; uint32_t handle{0}; - std::string data{}; + const uint8_t *data{nullptr}; + uint16_t data_len{0}; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2020,7 +2077,7 @@ class BluetoothGATTWriteDescriptorRequest : public ProtoDecodableMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class BluetoothGATTNotifyRequest : public ProtoDecodableMessage { +class BluetoothGATTNotifyRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 78; static constexpr uint8_t ESTIMATED_SIZE = 10; @@ -2037,7 +2094,7 @@ class BluetoothGATTNotifyRequest : public ProtoDecodableMessage { protected: bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class BluetoothGATTNotifyDataResponse : public ProtoMessage { +class BluetoothGATTNotifyDataResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 79; static constexpr uint8_t ESTIMATED_SIZE = 17; @@ -2060,7 +2117,7 @@ class BluetoothGATTNotifyDataResponse : public ProtoMessage { protected: }; -class SubscribeBluetoothConnectionsFreeRequest : public ProtoMessage { +class SubscribeBluetoothConnectionsFreeRequest final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 80; static constexpr uint8_t ESTIMATED_SIZE = 0; @@ -2073,7 +2130,7 @@ class SubscribeBluetoothConnectionsFreeRequest : public ProtoMessage { protected: }; -class BluetoothConnectionsFreeResponse : public ProtoMessage { +class BluetoothConnectionsFreeResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 81; static constexpr uint8_t ESTIMATED_SIZE = 20; @@ -2091,7 +2148,7 @@ class BluetoothConnectionsFreeResponse : public ProtoMessage { protected: }; -class BluetoothGATTErrorResponse : public ProtoMessage { +class BluetoothGATTErrorResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 82; static constexpr uint8_t ESTIMATED_SIZE = 12; @@ -2109,7 +2166,7 @@ class BluetoothGATTErrorResponse : public ProtoMessage { protected: }; -class BluetoothGATTWriteResponse : public ProtoMessage { +class BluetoothGATTWriteResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 83; static constexpr uint8_t ESTIMATED_SIZE = 8; @@ -2126,7 +2183,7 @@ class BluetoothGATTWriteResponse : public ProtoMessage { protected: }; -class BluetoothGATTNotifyResponse : public ProtoMessage { +class BluetoothGATTNotifyResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 84; static constexpr uint8_t ESTIMATED_SIZE = 8; @@ -2143,7 +2200,7 @@ class BluetoothGATTNotifyResponse : public ProtoMessage { protected: }; -class BluetoothDevicePairingResponse : public ProtoMessage { +class BluetoothDevicePairingResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 85; static constexpr uint8_t ESTIMATED_SIZE = 10; @@ -2161,7 +2218,7 @@ class BluetoothDevicePairingResponse : public ProtoMessage { protected: }; -class BluetoothDeviceUnpairingResponse : public ProtoMessage { +class BluetoothDeviceUnpairingResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 86; static constexpr uint8_t ESTIMATED_SIZE = 10; @@ -2179,7 +2236,7 @@ class BluetoothDeviceUnpairingResponse : public ProtoMessage { protected: }; -class UnsubscribeBluetoothLEAdvertisementsRequest : public ProtoMessage { +class UnsubscribeBluetoothLEAdvertisementsRequest final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 87; static constexpr uint8_t ESTIMATED_SIZE = 0; @@ -2192,7 +2249,7 @@ class UnsubscribeBluetoothLEAdvertisementsRequest : public ProtoMessage { protected: }; -class BluetoothDeviceClearCacheResponse : public ProtoMessage { +class BluetoothDeviceClearCacheResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 88; static constexpr uint8_t ESTIMATED_SIZE = 10; @@ -2210,15 +2267,16 @@ class BluetoothDeviceClearCacheResponse : public ProtoMessage { protected: }; -class BluetoothScannerStateResponse : public ProtoMessage { +class BluetoothScannerStateResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 126; - static constexpr uint8_t ESTIMATED_SIZE = 4; + static constexpr uint8_t ESTIMATED_SIZE = 6; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "bluetooth_scanner_state_response"; } #endif enums::BluetoothScannerState state{}; enums::BluetoothScannerMode mode{}; + enums::BluetoothScannerMode configured_mode{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -2227,7 +2285,7 @@ class BluetoothScannerStateResponse : public ProtoMessage { protected: }; -class BluetoothScannerSetModeRequest : public ProtoDecodableMessage { +class BluetoothScannerSetModeRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 127; static constexpr uint8_t ESTIMATED_SIZE = 2; @@ -2244,7 +2302,7 @@ class BluetoothScannerSetModeRequest : public ProtoDecodableMessage { }; #endif #ifdef USE_VOICE_ASSISTANT -class SubscribeVoiceAssistantRequest : public ProtoDecodableMessage { +class SubscribeVoiceAssistantRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 89; static constexpr uint8_t ESTIMATED_SIZE = 6; @@ -2260,7 +2318,7 @@ class SubscribeVoiceAssistantRequest : public ProtoDecodableMessage { protected: bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class VoiceAssistantAudioSettings : public ProtoMessage { +class VoiceAssistantAudioSettings final : public ProtoMessage { public: uint32_t noise_suppression_level{0}; uint32_t auto_gain{0}; @@ -2273,7 +2331,7 @@ class VoiceAssistantAudioSettings : public ProtoMessage { protected: }; -class VoiceAssistantRequest : public ProtoMessage { +class VoiceAssistantRequest final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 90; static constexpr uint8_t ESTIMATED_SIZE = 41; @@ -2295,7 +2353,7 @@ class VoiceAssistantRequest : public ProtoMessage { protected: }; -class VoiceAssistantResponse : public ProtoDecodableMessage { +class VoiceAssistantResponse final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 91; static constexpr uint8_t ESTIMATED_SIZE = 6; @@ -2311,7 +2369,7 @@ class VoiceAssistantResponse : public ProtoDecodableMessage { protected: bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class VoiceAssistantEventData : public ProtoDecodableMessage { +class VoiceAssistantEventData final : public ProtoDecodableMessage { public: std::string name{}; std::string value{}; @@ -2322,7 +2380,7 @@ class VoiceAssistantEventData : public ProtoDecodableMessage { protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; }; -class VoiceAssistantEventResponse : public ProtoDecodableMessage { +class VoiceAssistantEventResponse final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 92; static constexpr uint8_t ESTIMATED_SIZE = 36; @@ -2339,7 +2397,7 @@ class VoiceAssistantEventResponse : public ProtoDecodableMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class VoiceAssistantAudio : public ProtoDecodableMessage { +class VoiceAssistantAudio final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 106; static constexpr uint8_t ESTIMATED_SIZE = 11; @@ -2364,7 +2422,7 @@ class VoiceAssistantAudio : public ProtoDecodableMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class VoiceAssistantTimerEventResponse : public ProtoDecodableMessage { +class VoiceAssistantTimerEventResponse final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 115; static constexpr uint8_t ESTIMATED_SIZE = 30; @@ -2385,7 +2443,7 @@ class VoiceAssistantTimerEventResponse : public ProtoDecodableMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class VoiceAssistantAnnounceRequest : public ProtoDecodableMessage { +class VoiceAssistantAnnounceRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 119; static constexpr uint8_t ESTIMATED_SIZE = 29; @@ -2404,7 +2462,7 @@ class VoiceAssistantAnnounceRequest : public ProtoDecodableMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class VoiceAssistantAnnounceFinished : public ProtoMessage { +class VoiceAssistantAnnounceFinished final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 120; static constexpr uint8_t ESTIMATED_SIZE = 2; @@ -2420,7 +2478,7 @@ class VoiceAssistantAnnounceFinished : public ProtoMessage { protected: }; -class VoiceAssistantWakeWord : public ProtoMessage { +class VoiceAssistantWakeWord final : public ProtoMessage { public: StringRef id_ref_{}; void set_id(const StringRef &ref) { this->id_ref_ = ref; } @@ -2435,20 +2493,39 @@ class VoiceAssistantWakeWord : public ProtoMessage { protected: }; -class VoiceAssistantConfigurationRequest : public ProtoMessage { +class VoiceAssistantExternalWakeWord final : public ProtoDecodableMessage { public: - static constexpr uint8_t MESSAGE_TYPE = 121; - static constexpr uint8_t ESTIMATED_SIZE = 0; -#ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "voice_assistant_configuration_request"; } -#endif + std::string id{}; + std::string wake_word{}; + std::vector trained_languages{}; + std::string model_type{}; + uint32_t model_size{0}; + std::string model_hash{}; + std::string url{}; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif protected: + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class VoiceAssistantConfigurationResponse : public ProtoMessage { +class VoiceAssistantConfigurationRequest final : public ProtoDecodableMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 121; + static constexpr uint8_t ESTIMATED_SIZE = 34; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *message_name() const override { return "voice_assistant_configuration_request"; } +#endif + std::vector external_wake_words{}; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; +}; +class VoiceAssistantConfigurationResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 122; static constexpr uint8_t ESTIMATED_SIZE = 56; @@ -2466,7 +2543,7 @@ class VoiceAssistantConfigurationResponse : public ProtoMessage { protected: }; -class VoiceAssistantSetConfiguration : public ProtoDecodableMessage { +class VoiceAssistantSetConfiguration final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 123; static constexpr uint8_t ESTIMATED_SIZE = 18; @@ -2483,7 +2560,7 @@ class VoiceAssistantSetConfiguration : public ProtoDecodableMessage { }; #endif #ifdef USE_ALARM_CONTROL_PANEL -class ListEntitiesAlarmControlPanelResponse : public InfoResponseProtoMessage { +class ListEntitiesAlarmControlPanelResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 94; static constexpr uint8_t ESTIMATED_SIZE = 48; @@ -2501,7 +2578,7 @@ class ListEntitiesAlarmControlPanelResponse : public InfoResponseProtoMessage { protected: }; -class AlarmControlPanelStateResponse : public StateResponseProtoMessage { +class AlarmControlPanelStateResponse final : public StateResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 95; static constexpr uint8_t ESTIMATED_SIZE = 11; @@ -2517,7 +2594,7 @@ class AlarmControlPanelStateResponse : public StateResponseProtoMessage { protected: }; -class AlarmControlPanelCommandRequest : public CommandProtoMessage { +class AlarmControlPanelCommandRequest final : public CommandProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 96; static constexpr uint8_t ESTIMATED_SIZE = 20; @@ -2537,7 +2614,7 @@ class AlarmControlPanelCommandRequest : public CommandProtoMessage { }; #endif #ifdef USE_TEXT -class ListEntitiesTextResponse : public InfoResponseProtoMessage { +class ListEntitiesTextResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 97; static constexpr uint8_t ESTIMATED_SIZE = 59; @@ -2557,7 +2634,7 @@ class ListEntitiesTextResponse : public InfoResponseProtoMessage { protected: }; -class TextStateResponse : public StateResponseProtoMessage { +class TextStateResponse final : public StateResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 98; static constexpr uint8_t ESTIMATED_SIZE = 20; @@ -2575,7 +2652,7 @@ class TextStateResponse : public StateResponseProtoMessage { protected: }; -class TextCommandRequest : public CommandProtoMessage { +class TextCommandRequest final : public CommandProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 99; static constexpr uint8_t ESTIMATED_SIZE = 18; @@ -2594,7 +2671,7 @@ class TextCommandRequest : public CommandProtoMessage { }; #endif #ifdef USE_DATETIME_DATE -class ListEntitiesDateResponse : public InfoResponseProtoMessage { +class ListEntitiesDateResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 100; static constexpr uint8_t ESTIMATED_SIZE = 40; @@ -2609,7 +2686,7 @@ class ListEntitiesDateResponse : public InfoResponseProtoMessage { protected: }; -class DateStateResponse : public StateResponseProtoMessage { +class DateStateResponse final : public StateResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 101; static constexpr uint8_t ESTIMATED_SIZE = 23; @@ -2628,7 +2705,7 @@ class DateStateResponse : public StateResponseProtoMessage { protected: }; -class DateCommandRequest : public CommandProtoMessage { +class DateCommandRequest final : public CommandProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 102; static constexpr uint8_t ESTIMATED_SIZE = 21; @@ -2648,7 +2725,7 @@ class DateCommandRequest : public CommandProtoMessage { }; #endif #ifdef USE_DATETIME_TIME -class ListEntitiesTimeResponse : public InfoResponseProtoMessage { +class ListEntitiesTimeResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 103; static constexpr uint8_t ESTIMATED_SIZE = 40; @@ -2663,7 +2740,7 @@ class ListEntitiesTimeResponse : public InfoResponseProtoMessage { protected: }; -class TimeStateResponse : public StateResponseProtoMessage { +class TimeStateResponse final : public StateResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 104; static constexpr uint8_t ESTIMATED_SIZE = 23; @@ -2682,7 +2759,7 @@ class TimeStateResponse : public StateResponseProtoMessage { protected: }; -class TimeCommandRequest : public CommandProtoMessage { +class TimeCommandRequest final : public CommandProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 105; static constexpr uint8_t ESTIMATED_SIZE = 21; @@ -2702,7 +2779,7 @@ class TimeCommandRequest : public CommandProtoMessage { }; #endif #ifdef USE_EVENT -class ListEntitiesEventResponse : public InfoResponseProtoMessage { +class ListEntitiesEventResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 107; static constexpr uint8_t ESTIMATED_SIZE = 67; @@ -2711,7 +2788,7 @@ class ListEntitiesEventResponse : public InfoResponseProtoMessage { #endif StringRef device_class_ref_{}; void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; } - std::vector event_types{}; + const FixedVector *event_types{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -2720,7 +2797,7 @@ class ListEntitiesEventResponse : public InfoResponseProtoMessage { protected: }; -class EventResponse : public StateResponseProtoMessage { +class EventResponse final : public StateResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 108; static constexpr uint8_t ESTIMATED_SIZE = 18; @@ -2739,7 +2816,7 @@ class EventResponse : public StateResponseProtoMessage { }; #endif #ifdef USE_VALVE -class ListEntitiesValveResponse : public InfoResponseProtoMessage { +class ListEntitiesValveResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 109; static constexpr uint8_t ESTIMATED_SIZE = 55; @@ -2759,7 +2836,7 @@ class ListEntitiesValveResponse : public InfoResponseProtoMessage { protected: }; -class ValveStateResponse : public StateResponseProtoMessage { +class ValveStateResponse final : public StateResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 110; static constexpr uint8_t ESTIMATED_SIZE = 16; @@ -2776,7 +2853,7 @@ class ValveStateResponse : public StateResponseProtoMessage { protected: }; -class ValveCommandRequest : public CommandProtoMessage { +class ValveCommandRequest final : public CommandProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 111; static constexpr uint8_t ESTIMATED_SIZE = 18; @@ -2796,7 +2873,7 @@ class ValveCommandRequest : public CommandProtoMessage { }; #endif #ifdef USE_DATETIME_DATETIME -class ListEntitiesDateTimeResponse : public InfoResponseProtoMessage { +class ListEntitiesDateTimeResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 112; static constexpr uint8_t ESTIMATED_SIZE = 40; @@ -2811,7 +2888,7 @@ class ListEntitiesDateTimeResponse : public InfoResponseProtoMessage { protected: }; -class DateTimeStateResponse : public StateResponseProtoMessage { +class DateTimeStateResponse final : public StateResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 113; static constexpr uint8_t ESTIMATED_SIZE = 16; @@ -2828,7 +2905,7 @@ class DateTimeStateResponse : public StateResponseProtoMessage { protected: }; -class DateTimeCommandRequest : public CommandProtoMessage { +class DateTimeCommandRequest final : public CommandProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 114; static constexpr uint8_t ESTIMATED_SIZE = 14; @@ -2846,7 +2923,7 @@ class DateTimeCommandRequest : public CommandProtoMessage { }; #endif #ifdef USE_UPDATE -class ListEntitiesUpdateResponse : public InfoResponseProtoMessage { +class ListEntitiesUpdateResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 116; static constexpr uint8_t ESTIMATED_SIZE = 49; @@ -2863,7 +2940,7 @@ class ListEntitiesUpdateResponse : public InfoResponseProtoMessage { protected: }; -class UpdateStateResponse : public StateResponseProtoMessage { +class UpdateStateResponse final : public StateResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 117; static constexpr uint8_t ESTIMATED_SIZE = 65; @@ -2892,7 +2969,7 @@ class UpdateStateResponse : public StateResponseProtoMessage { protected: }; -class UpdateCommandRequest : public CommandProtoMessage { +class UpdateCommandRequest final : public CommandProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 118; static constexpr uint8_t ESTIMATED_SIZE = 11; @@ -2909,5 +2986,45 @@ class UpdateCommandRequest : public CommandProtoMessage { bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; #endif +#ifdef USE_ZWAVE_PROXY +class ZWaveProxyFrame final : public ProtoDecodableMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 128; + static constexpr uint8_t ESTIMATED_SIZE = 19; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *message_name() const override { return "z_wave_proxy_frame"; } +#endif + const uint8_t *data{nullptr}; + uint16_t data_len{0}; + void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(ProtoSize &size) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; +}; +class ZWaveProxyRequest final : public ProtoDecodableMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 129; + static constexpr uint8_t ESTIMATED_SIZE = 21; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *message_name() const override { return "z_wave_proxy_request"; } +#endif + enums::ZWaveProxyRequestType type{}; + const uint8_t *data{nullptr}; + uint16_t data_len{0}; + void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(ProtoSize &size) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +#endif } // namespace esphome::api diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index b212353ad8..a985e052ac 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -66,7 +66,7 @@ static void dump_field(std::string &out, const char *field_name, float value, in static void dump_field(std::string &out, const char *field_name, uint64_t value, int indent = 2) { char buffer[64]; append_field_prefix(out, field_name, indent); - snprintf(buffer, 64, "%llu", value); + snprintf(buffer, 64, "%" PRIu64, value); append_with_newline(out, buffer); } @@ -88,6 +88,12 @@ static void dump_field(std::string &out, const char *field_name, StringRef value out.append("\n"); } +static void dump_field(std::string &out, const char *field_name, const char *value, int indent = 2) { + append_field_prefix(out, field_name, indent); + out.append("'").append(value).append("'"); + out.append("\n"); +} + template static void dump_field(std::string &out, const char *field_name, T value, int indent = 2) { append_field_prefix(out, field_name, indent); out.append(proto_enum_to_string(value)); @@ -200,7 +206,7 @@ template<> const char *proto_enum_to_string(enums::LogLevel val return "UNKNOWN"; } } -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS template<> const char *proto_enum_to_string(enums::ServiceArgType value) { switch (value) { case enums::SERVICE_ARG_TYPE_BOOL: @@ -655,10 +661,26 @@ template<> const char *proto_enum_to_string(enums::UpdateC } } #endif +#ifdef USE_ZWAVE_PROXY +template<> const char *proto_enum_to_string(enums::ZWaveProxyRequestType value) { + switch (value) { + case enums::ZWAVE_PROXY_REQUEST_TYPE_SUBSCRIBE: + return "ZWAVE_PROXY_REQUEST_TYPE_SUBSCRIBE"; + case enums::ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE: + return "ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE"; + case enums::ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE: + return "ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE"; + default: + return "UNKNOWN"; + } +} +#endif void HelloRequest::dump_to(std::string &out) const { MessageDumpHelper helper(out, "HelloRequest"); - dump_field(out, "client_info", this->client_info); + out.append(" client_info: "); + out.append(format_hex_pretty(this->client_info, this->client_info_len)); + out.append("\n"); dump_field(out, "api_version_major", this->api_version_major); dump_field(out, "api_version_minor", this->api_version_minor); } @@ -669,8 +691,18 @@ void HelloResponse::dump_to(std::string &out) const { dump_field(out, "server_info", this->server_info_ref_); dump_field(out, "name", this->name_ref_); } -void ConnectRequest::dump_to(std::string &out) const { dump_field(out, "password", this->password); } -void ConnectResponse::dump_to(std::string &out) const { dump_field(out, "invalid_password", this->invalid_password); } +#ifdef USE_API_PASSWORD +void AuthenticationRequest::dump_to(std::string &out) const { + MessageDumpHelper helper(out, "AuthenticationRequest"); + out.append(" password: "); + out.append(format_hex_pretty(this->password, this->password_len)); + out.append("\n"); +} +void AuthenticationResponse::dump_to(std::string &out) const { + MessageDumpHelper helper(out, "AuthenticationResponse"); + dump_field(out, "invalid_password", this->invalid_password); +} +#endif void DisconnectRequest::dump_to(std::string &out) const { out.append("DisconnectRequest {}"); } void DisconnectResponse::dump_to(std::string &out) const { out.append("DisconnectResponse {}"); } void PingRequest::dump_to(std::string &out) const { out.append("PingRequest {}"); } @@ -749,6 +781,12 @@ void DeviceInfoResponse::dump_to(std::string &out) const { this->area.dump_to(out); out.append("\n"); #endif +#ifdef USE_ZWAVE_PROXY + dump_field(out, "zwave_proxy_feature_flags", this->zwave_proxy_feature_flags); +#endif +#ifdef USE_ZWAVE_PROXY + dump_field(out, "zwave_home_id", this->zwave_home_id); +#endif } void ListEntitiesRequest::dump_to(std::string &out) const { out.append("ListEntitiesRequest {}"); } void ListEntitiesDoneResponse::dump_to(std::string &out) const { out.append("ListEntitiesDoneResponse {}"); } @@ -886,7 +924,7 @@ void ListEntitiesLightResponse::dump_to(std::string &out) const { } dump_field(out, "min_mireds", this->min_mireds); dump_field(out, "max_mireds", this->max_mireds); - for (const auto &it : this->effects) { + for (const auto &it : *this->effects) { dump_field(out, "effects", it, 4); } dump_field(out, "disabled_by_default", this->disabled_by_default); @@ -1071,8 +1109,8 @@ void HomeassistantServiceMap::dump_to(std::string &out) const { dump_field(out, "key", this->key_ref_); dump_field(out, "value", this->value); } -void HomeassistantServiceResponse::dump_to(std::string &out) const { - MessageDumpHelper helper(out, "HomeassistantServiceResponse"); +void HomeassistantActionRequest::dump_to(std::string &out) const { + MessageDumpHelper helper(out, "HomeassistantActionRequest"); dump_field(out, "service", this->service_ref_); for (const auto &it : this->data) { out.append(" data: "); @@ -1090,6 +1128,28 @@ void HomeassistantServiceResponse::dump_to(std::string &out) const { out.append("\n"); } dump_field(out, "is_event", this->is_event); +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES + dump_field(out, "call_id", this->call_id); +#endif +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON + dump_field(out, "wants_response", this->wants_response); +#endif +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON + dump_field(out, "response_template", this->response_template); +#endif +} +#endif +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES +void HomeassistantActionResponse::dump_to(std::string &out) const { + MessageDumpHelper helper(out, "HomeassistantActionResponse"); + dump_field(out, "call_id", this->call_id); + dump_field(out, "success", this->success); + dump_field(out, "error_message", this->error_message); +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON + out.append(" response_data: "); + out.append(format_hex_pretty(this->response_data, this->response_data_len)); + out.append("\n"); +#endif } #endif #ifdef USE_API_HOMEASSISTANT_STATES @@ -1110,8 +1170,14 @@ void HomeAssistantStateResponse::dump_to(std::string &out) const { } #endif void GetTimeRequest::dump_to(std::string &out) const { out.append("GetTimeRequest {}"); } -void GetTimeResponse::dump_to(std::string &out) const { dump_field(out, "epoch_seconds", this->epoch_seconds); } -#ifdef USE_API_SERVICES +void GetTimeResponse::dump_to(std::string &out) const { + MessageDumpHelper helper(out, "GetTimeResponse"); + dump_field(out, "epoch_seconds", this->epoch_seconds); + out.append(" timezone: "); + out.append(format_hex_pretty(this->timezone, this->timezone_len)); + out.append("\n"); +} +#ifdef USE_API_USER_DEFINED_ACTIONS void ListEntitiesServicesArgument::dump_to(std::string &out) const { MessageDumpHelper helper(out, "ListEntitiesServicesArgument"); dump_field(out, "name", this->name_ref_); @@ -1135,7 +1201,7 @@ void ExecuteServiceArgument::dump_to(std::string &out) const { dump_field(out, "string_", this->string_); dump_field(out, "int_", this->int_); for (const auto it : this->bool_array) { - dump_field(out, "bool_array", it, 4); + dump_field(out, "bool_array", static_cast(it), 4); } for (const auto &it : this->int_array) { dump_field(out, "int_array", it, 4); @@ -1232,6 +1298,7 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { #ifdef USE_DEVICES dump_field(out, "device_id", this->device_id); #endif + dump_field(out, "feature_flags", this->feature_flags); } void ClimateStateResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "ClimateStateResponse"); @@ -1534,9 +1601,9 @@ void BluetoothLERawAdvertisement::dump_to(std::string &out) const { } void BluetoothLERawAdvertisementsResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "BluetoothLERawAdvertisementsResponse"); - for (const auto &it : this->advertisements) { + for (uint16_t i = 0; i < this->advertisements_len; i++) { out.append(" advertisements: "); - it.dump_to(out); + this->advertisements[i].dump_to(out); out.append("\n"); } } @@ -1622,7 +1689,7 @@ void BluetoothGATTWriteRequest::dump_to(std::string &out) const { dump_field(out, "handle", this->handle); dump_field(out, "response", this->response); out.append(" data: "); - out.append(format_hex_pretty(reinterpret_cast(this->data.data()), this->data.size())); + out.append(format_hex_pretty(this->data, this->data_len)); out.append("\n"); } void BluetoothGATTReadDescriptorRequest::dump_to(std::string &out) const { @@ -1635,7 +1702,7 @@ void BluetoothGATTWriteDescriptorRequest::dump_to(std::string &out) const { dump_field(out, "address", this->address); dump_field(out, "handle", this->handle); out.append(" data: "); - out.append(format_hex_pretty(reinterpret_cast(this->data.data()), this->data.size())); + out.append(format_hex_pretty(this->data, this->data_len)); out.append("\n"); } void BluetoothGATTNotifyRequest::dump_to(std::string &out) const { @@ -1704,6 +1771,7 @@ void BluetoothScannerStateResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "BluetoothScannerStateResponse"); dump_field(out, "state", static_cast(this->state)); dump_field(out, "mode", static_cast(this->mode)); + dump_field(out, "configured_mode", static_cast(this->configured_mode)); } void BluetoothScannerSetModeRequest::dump_to(std::string &out) const { MessageDumpHelper helper(out, "BluetoothScannerSetModeRequest"); @@ -1787,8 +1855,25 @@ void VoiceAssistantWakeWord::dump_to(std::string &out) const { dump_field(out, "trained_languages", it, 4); } } +void VoiceAssistantExternalWakeWord::dump_to(std::string &out) const { + MessageDumpHelper helper(out, "VoiceAssistantExternalWakeWord"); + dump_field(out, "id", this->id); + dump_field(out, "wake_word", this->wake_word); + for (const auto &it : this->trained_languages) { + dump_field(out, "trained_languages", it, 4); + } + dump_field(out, "model_type", this->model_type); + dump_field(out, "model_size", this->model_size); + dump_field(out, "model_hash", this->model_hash); + dump_field(out, "url", this->url); +} void VoiceAssistantConfigurationRequest::dump_to(std::string &out) const { - out.append("VoiceAssistantConfigurationRequest {}"); + MessageDumpHelper helper(out, "VoiceAssistantConfigurationRequest"); + for (const auto &it : this->external_wake_words) { + out.append(" external_wake_words: "); + it.dump_to(out); + out.append("\n"); + } } void VoiceAssistantConfigurationResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "VoiceAssistantConfigurationResponse"); @@ -1968,7 +2053,7 @@ void ListEntitiesEventResponse::dump_to(std::string &out) const { dump_field(out, "disabled_by_default", this->disabled_by_default); dump_field(out, "entity_category", static_cast(this->entity_category)); dump_field(out, "device_class", this->device_class_ref_); - for (const auto &it : this->event_types) { + for (const auto &it : *this->event_types) { dump_field(out, "event_types", it, 4); } #ifdef USE_DEVICES @@ -2097,6 +2182,21 @@ void UpdateCommandRequest::dump_to(std::string &out) const { #endif } #endif +#ifdef USE_ZWAVE_PROXY +void ZWaveProxyFrame::dump_to(std::string &out) const { + MessageDumpHelper helper(out, "ZWaveProxyFrame"); + out.append(" data: "); + out.append(format_hex_pretty(this->data, this->data_len)); + out.append("\n"); +} +void ZWaveProxyRequest::dump_to(std::string &out) const { + MessageDumpHelper helper(out, "ZWaveProxyRequest"); + dump_field(out, "type", static_cast(this->type)); + out.append(" data: "); + out.append(format_hex_pretty(this->data, this->data_len)); + out.append("\n"); +} +#endif } // namespace esphome::api diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index 6b7b8b9ebd..3d28a137c8 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -24,15 +24,17 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, this->on_hello_request(msg); break; } - case ConnectRequest::MESSAGE_TYPE: { - ConnectRequest msg; +#ifdef USE_API_PASSWORD + case AuthenticationRequest::MESSAGE_TYPE: { + AuthenticationRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP - ESP_LOGVV(TAG, "on_connect_request: %s", msg.dump().c_str()); + ESP_LOGVV(TAG, "on_authentication_request: %s", msg.dump().c_str()); #endif - this->on_connect_request(msg); + this->on_authentication_request(msg); break; } +#endif case DisconnectRequest::MESSAGE_TYPE: { DisconnectRequest msg; // Empty message: no decode needed @@ -160,15 +162,6 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, break; } #endif - case GetTimeRequest::MESSAGE_TYPE: { - GetTimeRequest msg; - // Empty message: no decode needed -#ifdef HAS_PROTO_MESSAGE_DUMP - ESP_LOGVV(TAG, "on_get_time_request: %s", msg.dump().c_str()); -#endif - this->on_get_time_request(msg); - break; - } case GetTimeResponse::MESSAGE_TYPE: { GetTimeResponse msg; msg.decode(msg_data, msg_size); @@ -200,7 +193,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, break; } #endif -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS case ExecuteServiceRequest::MESSAGE_TYPE: { ExecuteServiceRequest msg; msg.decode(msg_data, msg_size); @@ -555,7 +548,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, #ifdef USE_VOICE_ASSISTANT case VoiceAssistantConfigurationRequest::MESSAGE_TYPE: { VoiceAssistantConfigurationRequest msg; - // Empty message: no decode needed + msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_voice_assistant_configuration_request: %s", msg.dump().c_str()); #endif @@ -595,6 +588,39 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, this->on_bluetooth_scanner_set_mode_request(msg); break; } +#endif +#ifdef USE_ZWAVE_PROXY + case ZWaveProxyFrame::MESSAGE_TYPE: { + ZWaveProxyFrame msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "on_z_wave_proxy_frame: %s", msg.dump().c_str()); +#endif + this->on_z_wave_proxy_frame(msg); + break; + } +#endif +#ifdef USE_ZWAVE_PROXY + case ZWaveProxyRequest::MESSAGE_TYPE: { + ZWaveProxyRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "on_z_wave_proxy_request: %s", msg.dump().c_str()); +#endif + this->on_z_wave_proxy_request(msg); + break; + } +#endif +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES + case HomeassistantActionResponse::MESSAGE_TYPE: { + HomeassistantActionResponse msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "on_homeassistant_action_response: %s", msg.dump().c_str()); +#endif + this->on_homeassistant_action_response(msg); + break; + } #endif default: break; @@ -606,11 +632,13 @@ void APIServerConnection::on_hello_request(const HelloRequest &msg) { this->on_fatal_error(); } } -void APIServerConnection::on_connect_request(const ConnectRequest &msg) { - if (!this->send_connect_response(msg)) { +#ifdef USE_API_PASSWORD +void APIServerConnection::on_authentication_request(const AuthenticationRequest &msg) { + if (!this->send_authenticate_response(msg)) { this->on_fatal_error(); } } +#endif void APIServerConnection::on_disconnect_request(const DisconnectRequest &msg) { if (!this->send_disconnect_response(msg)) { this->on_fatal_error(); @@ -622,246 +650,139 @@ void APIServerConnection::on_ping_request(const PingRequest &msg) { } } void APIServerConnection::on_device_info_request(const DeviceInfoRequest &msg) { - if (this->check_connection_setup_() && !this->send_device_info_response(msg)) { + if (!this->send_device_info_response(msg)) { this->on_fatal_error(); } } -void APIServerConnection::on_list_entities_request(const ListEntitiesRequest &msg) { - if (this->check_authenticated_()) { - this->list_entities(msg); - } -} +void APIServerConnection::on_list_entities_request(const ListEntitiesRequest &msg) { this->list_entities(msg); } void APIServerConnection::on_subscribe_states_request(const SubscribeStatesRequest &msg) { - if (this->check_authenticated_()) { - this->subscribe_states(msg); - } -} -void APIServerConnection::on_subscribe_logs_request(const SubscribeLogsRequest &msg) { - if (this->check_authenticated_()) { - this->subscribe_logs(msg); - } + this->subscribe_states(msg); } +void APIServerConnection::on_subscribe_logs_request(const SubscribeLogsRequest &msg) { this->subscribe_logs(msg); } #ifdef USE_API_HOMEASSISTANT_SERVICES void APIServerConnection::on_subscribe_homeassistant_services_request( const SubscribeHomeassistantServicesRequest &msg) { - if (this->check_authenticated_()) { - this->subscribe_homeassistant_services(msg); - } + this->subscribe_homeassistant_services(msg); } #endif #ifdef USE_API_HOMEASSISTANT_STATES void APIServerConnection::on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) { - if (this->check_authenticated_()) { - this->subscribe_home_assistant_states(msg); - } + this->subscribe_home_assistant_states(msg); } #endif -void APIServerConnection::on_get_time_request(const GetTimeRequest &msg) { - if (this->check_connection_setup_() && !this->send_get_time_response(msg)) { - this->on_fatal_error(); - } -} -#ifdef USE_API_SERVICES -void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest &msg) { - if (this->check_authenticated_()) { - this->execute_service(msg); - } -} +#ifdef USE_API_USER_DEFINED_ACTIONS +void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest &msg) { this->execute_service(msg); } #endif #ifdef USE_API_NOISE void APIServerConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) { - if (this->check_authenticated_() && !this->send_noise_encryption_set_key_response(msg)) { + if (!this->send_noise_encryption_set_key_response(msg)) { this->on_fatal_error(); } } #endif #ifdef USE_BUTTON -void APIServerConnection::on_button_command_request(const ButtonCommandRequest &msg) { - if (this->check_authenticated_()) { - this->button_command(msg); - } -} +void APIServerConnection::on_button_command_request(const ButtonCommandRequest &msg) { this->button_command(msg); } #endif #ifdef USE_CAMERA -void APIServerConnection::on_camera_image_request(const CameraImageRequest &msg) { - if (this->check_authenticated_()) { - this->camera_image(msg); - } -} +void APIServerConnection::on_camera_image_request(const CameraImageRequest &msg) { this->camera_image(msg); } #endif #ifdef USE_CLIMATE -void APIServerConnection::on_climate_command_request(const ClimateCommandRequest &msg) { - if (this->check_authenticated_()) { - this->climate_command(msg); - } -} +void APIServerConnection::on_climate_command_request(const ClimateCommandRequest &msg) { this->climate_command(msg); } #endif #ifdef USE_COVER -void APIServerConnection::on_cover_command_request(const CoverCommandRequest &msg) { - if (this->check_authenticated_()) { - this->cover_command(msg); - } -} +void APIServerConnection::on_cover_command_request(const CoverCommandRequest &msg) { this->cover_command(msg); } #endif #ifdef USE_DATETIME_DATE -void APIServerConnection::on_date_command_request(const DateCommandRequest &msg) { - if (this->check_authenticated_()) { - this->date_command(msg); - } -} +void APIServerConnection::on_date_command_request(const DateCommandRequest &msg) { this->date_command(msg); } #endif #ifdef USE_DATETIME_DATETIME void APIServerConnection::on_date_time_command_request(const DateTimeCommandRequest &msg) { - if (this->check_authenticated_()) { - this->datetime_command(msg); - } + this->datetime_command(msg); } #endif #ifdef USE_FAN -void APIServerConnection::on_fan_command_request(const FanCommandRequest &msg) { - if (this->check_authenticated_()) { - this->fan_command(msg); - } -} +void APIServerConnection::on_fan_command_request(const FanCommandRequest &msg) { this->fan_command(msg); } #endif #ifdef USE_LIGHT -void APIServerConnection::on_light_command_request(const LightCommandRequest &msg) { - if (this->check_authenticated_()) { - this->light_command(msg); - } -} +void APIServerConnection::on_light_command_request(const LightCommandRequest &msg) { this->light_command(msg); } #endif #ifdef USE_LOCK -void APIServerConnection::on_lock_command_request(const LockCommandRequest &msg) { - if (this->check_authenticated_()) { - this->lock_command(msg); - } -} +void APIServerConnection::on_lock_command_request(const LockCommandRequest &msg) { this->lock_command(msg); } #endif #ifdef USE_MEDIA_PLAYER void APIServerConnection::on_media_player_command_request(const MediaPlayerCommandRequest &msg) { - if (this->check_authenticated_()) { - this->media_player_command(msg); - } + this->media_player_command(msg); } #endif #ifdef USE_NUMBER -void APIServerConnection::on_number_command_request(const NumberCommandRequest &msg) { - if (this->check_authenticated_()) { - this->number_command(msg); - } -} +void APIServerConnection::on_number_command_request(const NumberCommandRequest &msg) { this->number_command(msg); } #endif #ifdef USE_SELECT -void APIServerConnection::on_select_command_request(const SelectCommandRequest &msg) { - if (this->check_authenticated_()) { - this->select_command(msg); - } -} +void APIServerConnection::on_select_command_request(const SelectCommandRequest &msg) { this->select_command(msg); } #endif #ifdef USE_SIREN -void APIServerConnection::on_siren_command_request(const SirenCommandRequest &msg) { - if (this->check_authenticated_()) { - this->siren_command(msg); - } -} +void APIServerConnection::on_siren_command_request(const SirenCommandRequest &msg) { this->siren_command(msg); } #endif #ifdef USE_SWITCH -void APIServerConnection::on_switch_command_request(const SwitchCommandRequest &msg) { - if (this->check_authenticated_()) { - this->switch_command(msg); - } -} +void APIServerConnection::on_switch_command_request(const SwitchCommandRequest &msg) { this->switch_command(msg); } #endif #ifdef USE_TEXT -void APIServerConnection::on_text_command_request(const TextCommandRequest &msg) { - if (this->check_authenticated_()) { - this->text_command(msg); - } -} +void APIServerConnection::on_text_command_request(const TextCommandRequest &msg) { this->text_command(msg); } #endif #ifdef USE_DATETIME_TIME -void APIServerConnection::on_time_command_request(const TimeCommandRequest &msg) { - if (this->check_authenticated_()) { - this->time_command(msg); - } -} +void APIServerConnection::on_time_command_request(const TimeCommandRequest &msg) { this->time_command(msg); } #endif #ifdef USE_UPDATE -void APIServerConnection::on_update_command_request(const UpdateCommandRequest &msg) { - if (this->check_authenticated_()) { - this->update_command(msg); - } -} +void APIServerConnection::on_update_command_request(const UpdateCommandRequest &msg) { this->update_command(msg); } #endif #ifdef USE_VALVE -void APIServerConnection::on_valve_command_request(const ValveCommandRequest &msg) { - if (this->check_authenticated_()) { - this->valve_command(msg); - } -} +void APIServerConnection::on_valve_command_request(const ValveCommandRequest &msg) { this->valve_command(msg); } #endif #ifdef USE_BLUETOOTH_PROXY void APIServerConnection::on_subscribe_bluetooth_le_advertisements_request( const SubscribeBluetoothLEAdvertisementsRequest &msg) { - if (this->check_authenticated_()) { - this->subscribe_bluetooth_le_advertisements(msg); - } + this->subscribe_bluetooth_le_advertisements(msg); } #endif #ifdef USE_BLUETOOTH_PROXY void APIServerConnection::on_bluetooth_device_request(const BluetoothDeviceRequest &msg) { - if (this->check_authenticated_()) { - this->bluetooth_device_request(msg); - } + this->bluetooth_device_request(msg); } #endif #ifdef USE_BLUETOOTH_PROXY void APIServerConnection::on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &msg) { - if (this->check_authenticated_()) { - this->bluetooth_gatt_get_services(msg); - } + this->bluetooth_gatt_get_services(msg); } #endif #ifdef USE_BLUETOOTH_PROXY void APIServerConnection::on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &msg) { - if (this->check_authenticated_()) { - this->bluetooth_gatt_read(msg); - } + this->bluetooth_gatt_read(msg); } #endif #ifdef USE_BLUETOOTH_PROXY void APIServerConnection::on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &msg) { - if (this->check_authenticated_()) { - this->bluetooth_gatt_write(msg); - } + this->bluetooth_gatt_write(msg); } #endif #ifdef USE_BLUETOOTH_PROXY void APIServerConnection::on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &msg) { - if (this->check_authenticated_()) { - this->bluetooth_gatt_read_descriptor(msg); - } + this->bluetooth_gatt_read_descriptor(msg); } #endif #ifdef USE_BLUETOOTH_PROXY void APIServerConnection::on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &msg) { - if (this->check_authenticated_()) { - this->bluetooth_gatt_write_descriptor(msg); - } + this->bluetooth_gatt_write_descriptor(msg); } #endif #ifdef USE_BLUETOOTH_PROXY void APIServerConnection::on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) { - if (this->check_authenticated_()) { - this->bluetooth_gatt_notify(msg); - } + this->bluetooth_gatt_notify(msg); } #endif #ifdef USE_BLUETOOTH_PROXY void APIServerConnection::on_subscribe_bluetooth_connections_free_request( const SubscribeBluetoothConnectionsFreeRequest &msg) { - if (this->check_authenticated_() && !this->send_subscribe_bluetooth_connections_free_response(msg)) { + if (!this->send_subscribe_bluetooth_connections_free_response(msg)) { this->on_fatal_error(); } } @@ -869,45 +790,68 @@ void APIServerConnection::on_subscribe_bluetooth_connections_free_request( #ifdef USE_BLUETOOTH_PROXY void APIServerConnection::on_unsubscribe_bluetooth_le_advertisements_request( const UnsubscribeBluetoothLEAdvertisementsRequest &msg) { - if (this->check_authenticated_()) { - this->unsubscribe_bluetooth_le_advertisements(msg); - } + this->unsubscribe_bluetooth_le_advertisements(msg); } #endif #ifdef USE_BLUETOOTH_PROXY void APIServerConnection::on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) { - if (this->check_authenticated_()) { - this->bluetooth_scanner_set_mode(msg); - } + this->bluetooth_scanner_set_mode(msg); } #endif #ifdef USE_VOICE_ASSISTANT void APIServerConnection::on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) { - if (this->check_authenticated_()) { - this->subscribe_voice_assistant(msg); - } + this->subscribe_voice_assistant(msg); } #endif #ifdef USE_VOICE_ASSISTANT void APIServerConnection::on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) { - if (this->check_authenticated_() && !this->send_voice_assistant_get_configuration_response(msg)) { + if (!this->send_voice_assistant_get_configuration_response(msg)) { this->on_fatal_error(); } } #endif #ifdef USE_VOICE_ASSISTANT void APIServerConnection::on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) { - if (this->check_authenticated_()) { - this->voice_assistant_set_configuration(msg); - } + this->voice_assistant_set_configuration(msg); } #endif #ifdef USE_ALARM_CONTROL_PANEL void APIServerConnection::on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) { - if (this->check_authenticated_()) { - this->alarm_control_panel_command(msg); - } + this->alarm_control_panel_command(msg); } #endif +#ifdef USE_ZWAVE_PROXY +void APIServerConnection::on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) { this->zwave_proxy_frame(msg); } +#endif +#ifdef USE_ZWAVE_PROXY +void APIServerConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) { this->zwave_proxy_request(msg); } +#endif + +void APIServerConnection::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) { + // Check authentication/connection requirements for messages + switch (msg_type) { + case HelloRequest::MESSAGE_TYPE: // No setup required +#ifdef USE_API_PASSWORD + case AuthenticationRequest::MESSAGE_TYPE: // No setup required +#endif + case DisconnectRequest::MESSAGE_TYPE: // No setup required + case PingRequest::MESSAGE_TYPE: // No setup required + break; // Skip all checks for these messages + case DeviceInfoRequest::MESSAGE_TYPE: // Connection setup only + if (!this->check_connection_setup_()) { + return; // Connection not setup + } + break; + default: + // All other messages require authentication (which includes connection check) + if (!this->check_authenticated_()) { + return; // Authentication failed + } + break; + } + + // Call base implementation to process the message + APIServerConnectionBase::read_message(msg_size, msg_type, msg_data); +} } // namespace esphome::api diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 6172e33bf6..827b89e23c 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -26,7 +26,9 @@ class APIServerConnectionBase : public ProtoService { virtual void on_hello_request(const HelloRequest &value){}; - virtual void on_connect_request(const ConnectRequest &value){}; +#ifdef USE_API_PASSWORD + virtual void on_authentication_request(const AuthenticationRequest &value){}; +#endif virtual void on_disconnect_request(const DisconnectRequest &value){}; virtual void on_disconnect_response(const DisconnectResponse &value){}; @@ -64,6 +66,9 @@ class APIServerConnectionBase : public ProtoService { virtual void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &value){}; #endif +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES + virtual void on_homeassistant_action_response(const HomeassistantActionResponse &value){}; +#endif #ifdef USE_API_HOMEASSISTANT_STATES virtual void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &value){}; #endif @@ -71,10 +76,10 @@ class APIServerConnectionBase : public ProtoService { #ifdef USE_API_HOMEASSISTANT_STATES virtual void on_home_assistant_state_response(const HomeAssistantStateResponse &value){}; #endif - virtual void on_get_time_request(const GetTimeRequest &value){}; + virtual void on_get_time_response(const GetTimeResponse &value){}; -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS virtual void on_execute_service_request(const ExecuteServiceRequest &value){}; #endif @@ -205,6 +210,12 @@ class APIServerConnectionBase : public ProtoService { #ifdef USE_UPDATE virtual void on_update_command_request(const UpdateCommandRequest &value){}; +#endif +#ifdef USE_ZWAVE_PROXY + virtual void on_z_wave_proxy_frame(const ZWaveProxyFrame &value){}; +#endif +#ifdef USE_ZWAVE_PROXY + virtual void on_z_wave_proxy_request(const ZWaveProxyRequest &value){}; #endif protected: void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override; @@ -213,7 +224,9 @@ class APIServerConnectionBase : public ProtoService { class APIServerConnection : public APIServerConnectionBase { public: virtual bool send_hello_response(const HelloRequest &msg) = 0; - virtual bool send_connect_response(const ConnectRequest &msg) = 0; +#ifdef USE_API_PASSWORD + virtual bool send_authenticate_response(const AuthenticationRequest &msg) = 0; +#endif virtual bool send_disconnect_response(const DisconnectRequest &msg) = 0; virtual bool send_ping_response(const PingRequest &msg) = 0; virtual bool send_device_info_response(const DeviceInfoRequest &msg) = 0; @@ -226,8 +239,7 @@ class APIServerConnection : public APIServerConnectionBase { #ifdef USE_API_HOMEASSISTANT_STATES virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0; #endif - virtual bool send_get_time_response(const GetTimeRequest &msg) = 0; -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS virtual void execute_service(const ExecuteServiceRequest &msg) = 0; #endif #ifdef USE_API_NOISE @@ -332,10 +344,18 @@ class APIServerConnection : public APIServerConnectionBase { #endif #ifdef USE_ALARM_CONTROL_PANEL virtual void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) = 0; +#endif +#ifdef USE_ZWAVE_PROXY + virtual void zwave_proxy_frame(const ZWaveProxyFrame &msg) = 0; +#endif +#ifdef USE_ZWAVE_PROXY + virtual void zwave_proxy_request(const ZWaveProxyRequest &msg) = 0; #endif protected: void on_hello_request(const HelloRequest &msg) override; - void on_connect_request(const ConnectRequest &msg) override; +#ifdef USE_API_PASSWORD + void on_authentication_request(const AuthenticationRequest &msg) override; +#endif void on_disconnect_request(const DisconnectRequest &msg) override; void on_ping_request(const PingRequest &msg) override; void on_device_info_request(const DeviceInfoRequest &msg) override; @@ -348,8 +368,7 @@ class APIServerConnection : public APIServerConnectionBase { #ifdef USE_API_HOMEASSISTANT_STATES void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override; #endif - void on_get_time_request(const GetTimeRequest &msg) override; -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS void on_execute_service_request(const ExecuteServiceRequest &msg) override; #endif #ifdef USE_API_NOISE @@ -455,6 +474,13 @@ class APIServerConnection : public APIServerConnectionBase { #ifdef USE_ALARM_CONTROL_PANEL void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) override; #endif +#ifdef USE_ZWAVE_PROXY + void on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) override; +#endif +#ifdef USE_ZWAVE_PROXY + void on_z_wave_proxy_request(const ZWaveProxyRequest &msg) override; +#endif + void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override; }; } // namespace esphome::api diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 1f38f4a31a..64f8751c35 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -5,16 +5,21 @@ #include "esphome/components/network/util.h" #include "esphome/core/application.h" #include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" #include "esphome/core/util.h" #include "esphome/core/version.h" +#ifdef USE_API_HOMEASSISTANT_SERVICES +#include "homeassistant_service.h" +#endif #ifdef USE_LOGGER #include "esphome/components/logger/logger.h" #endif #include +#include namespace esphome::api { @@ -30,19 +35,21 @@ APIServer::APIServer() { } void APIServer::setup() { - this->setup_controller(); + ControllerRegistry::register_controller(this); #ifdef USE_API_NOISE uint32_t hash = 88491486UL; this->noise_pref_ = global_preferences->make_preference(hash, true); +#ifndef USE_API_NOISE_PSK_FROM_YAML + // Only load saved PSK if not set from YAML SavedNoisePsk noise_pref_saved{}; if (this->noise_pref_.load(&noise_pref_saved)) { ESP_LOGD(TAG, "Loaded saved Noise PSK"); - this->set_noise_psk(noise_pref_saved.psk); } +#endif #endif // Schedule reboot if no clients connect within timeout @@ -85,7 +92,7 @@ void APIServer::setup() { return; } - err = this->socket_->listen(4); + err = this->socket_->listen(this->listen_backlog_); if (err != 0) { ESP_LOGW(TAG, "Socket unable to listen: errno %d", errno); this->mark_failed(); @@ -138,9 +145,19 @@ void APIServer::loop() { while (true) { struct sockaddr_storage source_addr; socklen_t addr_len = sizeof(source_addr); + auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len); if (!sock) break; + + // Check if we're at the connection limit + if (this->clients_.size() >= this->max_connections_) { + ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, sock->getpeername().c_str()); + // Immediately close - socket destructor will handle cleanup + sock.reset(); + continue; + } + ESP_LOGD(TAG, "Accept %s", sock->getpeername().c_str()); auto *conn = new APIConnection(std::move(sock), this); @@ -165,7 +182,8 @@ void APIServer::loop() { // Network is down - disconnect all clients for (auto &client : this->clients_) { client->on_fatal_error(); - ESP_LOGW(TAG, "%s: Network down; disconnect", client->get_client_combined_info().c_str()); + ESP_LOGW(TAG, "%s (%s): Network down; disconnect", client->client_info_.name.c_str(), + client->client_info_.peername.c_str()); } // Continue to process and clean up the clients below } @@ -204,11 +222,13 @@ void APIServer::loop() { void APIServer::dump_config() { ESP_LOGCONFIG(TAG, "Server:\n" - " Address: %s:%u", - network::get_use_address().c_str(), this->port_); + " Address: %s:%u\n" + " Listen backlog: %u\n" + " Max connections: %u", + network::get_use_address(), this->port_, this->listen_backlog_, this->max_connections_); #ifdef USE_API_NOISE - ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_->has_psk())); - if (!this->noise_ctx_->has_psk()) { + ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_.has_psk())); + if (!this->noise_ctx_.has_psk()) { ESP_LOGCONFIG(TAG, " Supports encryption: YES"); } #else @@ -217,12 +237,12 @@ void APIServer::dump_config() { } #ifdef USE_API_PASSWORD -bool APIServer::check_password(const std::string &password) const { +bool APIServer::check_password(const uint8_t *password_data, size_t password_len) const { // depend only on input password length const char *a = this->password_.c_str(); uint32_t len_a = this->password_.length(); - const char *b = password.c_str(); - uint32_t len_b = password.length(); + const char *b = reinterpret_cast(password_data); + uint32_t len_b = password_len; // disable optimization with volatile volatile uint32_t length = len_b; @@ -245,11 +265,12 @@ bool APIServer::check_password(const std::string &password) const { return result == 0; } + #endif void APIServer::handle_disconnect(APIConnection *conn) {} -// Macro for entities without extra parameters +// Macro for controller update dispatch #define API_DISPATCH_UPDATE(entity_type, entity_name) \ void APIServer::on_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \ if (obj->is_internal()) \ @@ -258,15 +279,6 @@ void APIServer::handle_disconnect(APIConnection *conn) {} c->send_##entity_name##_state(obj); \ } -// Macro for entities with extra parameters (but parameters not used in send) -#define API_DISPATCH_UPDATE_IGNORE_PARAMS(entity_type, entity_name, ...) \ - void APIServer::on_##entity_name##_update(entity_type *obj, __VA_ARGS__) { /* NOLINT(bugprone-macro-parentheses) */ \ - if (obj->is_internal()) \ - return; \ - for (auto &c : this->clients_) \ - c->send_##entity_name##_state(obj); \ - } - #ifdef USE_BINARY_SENSOR API_DISPATCH_UPDATE(binary_sensor::BinarySensor, binary_sensor) #endif @@ -284,15 +296,15 @@ API_DISPATCH_UPDATE(light::LightState, light) #endif #ifdef USE_SENSOR -API_DISPATCH_UPDATE_IGNORE_PARAMS(sensor::Sensor, sensor, float state) +API_DISPATCH_UPDATE(sensor::Sensor, sensor) #endif #ifdef USE_SWITCH -API_DISPATCH_UPDATE_IGNORE_PARAMS(switch_::Switch, switch, bool state) +API_DISPATCH_UPDATE(switch_::Switch, switch) #endif #ifdef USE_TEXT_SENSOR -API_DISPATCH_UPDATE_IGNORE_PARAMS(text_sensor::TextSensor, text_sensor, const std::string &state) +API_DISPATCH_UPDATE(text_sensor::TextSensor, text_sensor) #endif #ifdef USE_CLIMATE @@ -300,7 +312,7 @@ API_DISPATCH_UPDATE(climate::Climate, climate) #endif #ifdef USE_NUMBER -API_DISPATCH_UPDATE_IGNORE_PARAMS(number::Number, number, float state) +API_DISPATCH_UPDATE(number::Number, number) #endif #ifdef USE_DATETIME_DATE @@ -316,11 +328,11 @@ API_DISPATCH_UPDATE(datetime::DateTimeEntity, datetime) #endif #ifdef USE_TEXT -API_DISPATCH_UPDATE_IGNORE_PARAMS(text::Text, text, const std::string &state) +API_DISPATCH_UPDATE(text::Text, text) #endif #ifdef USE_SELECT -API_DISPATCH_UPDATE_IGNORE_PARAMS(select::Select, select, const std::string &state, size_t index) +API_DISPATCH_UPDATE(select::Select, select) #endif #ifdef USE_LOCK @@ -336,12 +348,13 @@ API_DISPATCH_UPDATE(media_player::MediaPlayer, media_player) #endif #ifdef USE_EVENT -// Event is a special case - it's the only entity that passes extra parameters to the send method -void APIServer::on_event(event::Event *obj, const std::string &event_type) { +// Event is a special case - unlike other entities with simple state fields, +// events store their state in a member accessed via obj->get_last_event_type() +void APIServer::on_event(event::Event *obj) { if (obj->is_internal()) return; for (auto &c : this->clients_) - c->send_event(obj, event_type); + c->send_event(obj, obj->get_last_event_type()); } #endif @@ -355,6 +368,15 @@ void APIServer::on_update(update::UpdateEntity *obj) { } #endif +#ifdef USE_ZWAVE_PROXY +void APIServer::on_zwave_proxy_request(const esphome::api::ProtoMessage &msg) { + // We could add code to manage a second subscription type, but, since this message type is + // very infrequent and small, we simply send it to all clients + for (auto &c : this->clients_) + c->send_message(msg, api::ZWaveProxyRequest::MESSAGE_TYPE); +} +#endif + #ifdef USE_ALARM_CONTROL_PANEL API_DISPATCH_UPDATE(alarm_control_panel::AlarmControlPanel, alarm_control_panel) #endif @@ -370,12 +392,43 @@ void APIServer::set_password(const std::string &password) { this->password_ = pa void APIServer::set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = batch_delay; } #ifdef USE_API_HOMEASSISTANT_SERVICES -void APIServer::send_homeassistant_service_call(const HomeassistantServiceResponse &call) { +void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call) { for (auto &client : this->clients_) { - client->send_homeassistant_service_call(call); + client->send_homeassistant_action(call); } } -#endif +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES +void APIServer::register_action_response_callback(uint32_t call_id, ActionResponseCallback callback) { + this->action_response_callbacks_.push_back({call_id, std::move(callback)}); +} + +void APIServer::handle_action_response(uint32_t call_id, bool success, const std::string &error_message) { + for (auto it = this->action_response_callbacks_.begin(); it != this->action_response_callbacks_.end(); ++it) { + if (it->call_id == call_id) { + auto callback = std::move(it->callback); + this->action_response_callbacks_.erase(it); + ActionResponse response(success, error_message); + callback(response); + return; + } + } +} +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON +void APIServer::handle_action_response(uint32_t call_id, bool success, const std::string &error_message, + const uint8_t *response_data, size_t response_data_len) { + for (auto it = this->action_response_callbacks_.begin(); it != this->action_response_callbacks_.end(); ++it) { + if (it->call_id == call_id) { + auto callback = std::move(it->callback); + this->action_response_callbacks_.erase(it); + ActionResponse response(success, error_message, response_data, response_data_len); + callback(response); + return; + } + } +} +#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON +#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES +#endif // USE_API_HOMEASSISTANT_SERVICES #ifdef USE_API_HOMEASSISTANT_STATES void APIServer::subscribe_home_assistant_state(std::string entity_id, optional attribute, @@ -408,16 +461,10 @@ uint16_t APIServer::get_port() const { return this->port_; } void APIServer::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; } #ifdef USE_API_NOISE -bool APIServer::save_noise_psk(psk_t psk, bool make_active) { - auto &old_psk = this->noise_ctx_->get_psk(); - if (std::equal(old_psk.begin(), old_psk.end(), psk.begin())) { - ESP_LOGW(TAG, "New PSK matches old"); - return true; - } - - SavedNoisePsk new_saved_psk{psk}; - if (!this->noise_pref_.save(&new_saved_psk)) { - ESP_LOGW(TAG, "Failed to save Noise PSK"); +bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, + const LogString *fail_log_msg, const psk_t &active_psk, bool make_active) { + if (!this->noise_pref_.save(&new_psk)) { + ESP_LOGW(TAG, "%s", LOG_STR_ARG(fail_log_msg)); return false; } // ensure it's written immediately @@ -425,11 +472,11 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) { ESP_LOGW(TAG, "Failed to sync preferences"); return false; } - ESP_LOGD(TAG, "Noise PSK saved"); + ESP_LOGD(TAG, "%s", LOG_STR_ARG(save_log_msg)); if (make_active) { - this->set_timeout(100, [this, psk]() { + this->set_timeout(100, [this, active_psk]() { ESP_LOGW(TAG, "Disconnecting all clients to reset PSK"); - this->set_noise_psk(psk); + this->set_noise_psk(active_psk); for (auto &c : this->clients_) { DisconnectRequest req; c->send_message(req, DisconnectRequest::MESSAGE_TYPE); @@ -438,6 +485,38 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) { } return true; } + +bool APIServer::save_noise_psk(psk_t psk, bool make_active) { +#ifdef USE_API_NOISE_PSK_FROM_YAML + // When PSK is set from YAML, this function should never be called + // but if it is, reject the change + ESP_LOGW(TAG, "Key set in YAML"); + return false; +#else + auto &old_psk = this->noise_ctx_.get_psk(); + if (std::equal(old_psk.begin(), old_psk.end(), psk.begin())) { + ESP_LOGW(TAG, "New PSK matches old"); + return true; + } + + SavedNoisePsk new_saved_psk{psk}; + return this->update_noise_psk_(new_saved_psk, LOG_STR("Noise PSK saved"), LOG_STR("Failed to save Noise PSK"), psk, + make_active); +#endif +} +bool APIServer::clear_noise_psk(bool make_active) { +#ifdef USE_API_NOISE_PSK_FROM_YAML + // When PSK is set from YAML, this function should never be called + // but if it is, reject the change + ESP_LOGW(TAG, "Key set in YAML"); + return false; +#else + SavedNoisePsk empty_psk{}; + psk_t empty{}; + return this->update_noise_psk_(empty_psk, LOG_STR("Noise PSK cleared"), LOG_STR("Failed to clear Noise PSK"), empty, + make_active); +#endif +} #endif #ifdef USE_HOMEASSISTANT_TIME @@ -449,7 +528,18 @@ void APIServer::request_time() { } #endif -bool APIServer::is_connected() const { return !this->clients_.empty(); } +bool APIServer::is_connected(bool state_subscription_only) const { + if (!state_subscription_only) { + return !this->clients_.empty(); + } + + for (const auto &client : this->clients_) { + if (client->flags_.state_subscription) { + return true; + } + } + return false; +} void APIServer::on_shutdown() { this->shutting_down_ = true; diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 8b5e624df2..428429418a 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -12,10 +12,11 @@ #include "esphome/core/log.h" #include "list_entities.h" #include "subscribe_state.h" -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS #include "user_services.h" #endif +#include #include namespace esphome::api { @@ -37,21 +38,24 @@ class APIServer : public Component, public Controller { void on_shutdown() override; bool teardown() override; #ifdef USE_API_PASSWORD - bool check_password(const std::string &password) const; + bool check_password(const uint8_t *password_data, size_t password_len) const; void set_password(const std::string &password); #endif void set_port(uint16_t port); void set_reboot_timeout(uint32_t reboot_timeout); void set_batch_delay(uint16_t batch_delay); uint16_t get_batch_delay() const { return batch_delay_; } + void set_listen_backlog(uint8_t listen_backlog) { this->listen_backlog_ = listen_backlog; } + void set_max_connections(uint8_t max_connections) { this->max_connections_ = max_connections; } // Get reference to shared buffer for API connections std::vector &get_shared_buffer_ref() { return shared_write_buffer_; } #ifdef USE_API_NOISE bool save_noise_psk(psk_t psk, bool make_active = true); - void set_noise_psk(psk_t psk) { noise_ctx_->set_psk(psk); } - std::shared_ptr get_noise_ctx() { return noise_ctx_; } + bool clear_noise_psk(bool make_active = true); + void set_noise_psk(psk_t psk) { this->noise_ctx_.set_psk(psk); } + APINoiseContext &get_noise_ctx() { return this->noise_ctx_; } #endif // USE_API_NOISE void handle_disconnect(APIConnection *conn); @@ -68,19 +72,19 @@ class APIServer : public Component, public Controller { void on_light_update(light::LightState *obj) override; #endif #ifdef USE_SENSOR - void on_sensor_update(sensor::Sensor *obj, float state) override; + void on_sensor_update(sensor::Sensor *obj) override; #endif #ifdef USE_SWITCH - void on_switch_update(switch_::Switch *obj, bool state) override; + void on_switch_update(switch_::Switch *obj) override; #endif #ifdef USE_TEXT_SENSOR - void on_text_sensor_update(text_sensor::TextSensor *obj, const std::string &state) override; + void on_text_sensor_update(text_sensor::TextSensor *obj) override; #endif #ifdef USE_CLIMATE void on_climate_update(climate::Climate *obj) override; #endif #ifdef USE_NUMBER - void on_number_update(number::Number *obj, float state) override; + void on_number_update(number::Number *obj) override; #endif #ifdef USE_DATETIME_DATE void on_date_update(datetime::DateEntity *obj) override; @@ -92,10 +96,10 @@ class APIServer : public Component, public Controller { void on_datetime_update(datetime::DateTimeEntity *obj) override; #endif #ifdef USE_TEXT - void on_text_update(text::Text *obj, const std::string &state) override; + void on_text_update(text::Text *obj) override; #endif #ifdef USE_SELECT - void on_select_update(select::Select *obj, const std::string &state, size_t index) override; + void on_select_update(select::Select *obj) override; #endif #ifdef USE_LOCK void on_lock_update(lock::Lock *obj) override; @@ -107,11 +111,28 @@ class APIServer : public Component, public Controller { void on_media_player_update(media_player::MediaPlayer *obj) override; #endif #ifdef USE_API_HOMEASSISTANT_SERVICES - void send_homeassistant_service_call(const HomeassistantServiceResponse &call); -#endif -#ifdef USE_API_SERVICES + void send_homeassistant_action(const HomeassistantActionRequest &call); + +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES + // Action response handling + using ActionResponseCallback = std::function; + void register_action_response_callback(uint32_t call_id, ActionResponseCallback callback); + void handle_action_response(uint32_t call_id, bool success, const std::string &error_message); +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON + void handle_action_response(uint32_t call_id, bool success, const std::string &error_message, + const uint8_t *response_data, size_t response_data_len); +#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON +#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES +#endif // USE_API_HOMEASSISTANT_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS + void initialize_user_services(std::initializer_list services) { + this->user_services_.assign(services); + } +#ifdef USE_API_CUSTOM_SERVICES + // Only compile push_back method when custom_services: true (external components) void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); } #endif +#endif #ifdef USE_HOMEASSISTANT_TIME void request_time(); #endif @@ -120,13 +141,16 @@ class APIServer : public Component, public Controller { void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) override; #endif #ifdef USE_EVENT - void on_event(event::Event *obj, const std::string &event_type) override; + void on_event(event::Event *obj) override; #endif #ifdef USE_UPDATE void on_update(update::UpdateEntity *obj) override; #endif +#ifdef USE_ZWAVE_PROXY + void on_zwave_proxy_request(const esphome::api::ProtoMessage &msg); +#endif - bool is_connected() const; + bool is_connected(bool state_subscription_only = false) const; #ifdef USE_API_HOMEASSISTANT_STATES struct HomeAssistantStateSubscription { @@ -142,7 +166,7 @@ class APIServer : public Component, public Controller { std::function f); const std::vector &get_state_subs() const; #endif -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS const std::vector &get_user_services() const { return this->user_services_; } #endif @@ -157,6 +181,10 @@ class APIServer : public Component, public Controller { protected: void schedule_reboot_timeout_(); +#ifdef USE_API_NOISE + bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg, + const psk_t &active_psk, bool make_active); +#endif // USE_API_NOISE // Pointers and pointer-like types first (4 bytes each) std::unique_ptr socket_ = nullptr; #ifdef USE_API_CLIENT_CONNECTED_TRIGGER @@ -178,18 +206,29 @@ class APIServer : public Component, public Controller { #ifdef USE_API_HOMEASSISTANT_STATES std::vector state_subs_; #endif -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS std::vector user_services_; #endif +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES + struct PendingActionResponse { + uint32_t call_id; + ActionResponseCallback callback; + }; + std::vector action_response_callbacks_; +#endif // Group smaller types together uint16_t port_{6053}; uint16_t batch_delay_{100}; + // Connection limits - these defaults will be overridden by config values + // from cv.SplitDefault in __init__.py which sets platform-specific defaults + uint8_t listen_backlog_{4}; + uint8_t max_connections_{8}; bool shutting_down_ = false; - // 5 bytes used, 3 bytes padding + // 7 bytes used, 1 byte padding #ifdef USE_API_NOISE - std::shared_ptr noise_ctx_ = std::make_shared(); + APINoiseContext noise_ctx_; ESPPreferenceObject noise_pref_; #endif // USE_API_NOISE }; @@ -197,8 +236,11 @@ class APIServer : public Component, public Controller { extern APIServer *global_api_server; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) template class APIConnectedCondition : public Condition { + TEMPLATABLE_VALUE(bool, state_subscription_only) public: - bool check(Ts... x) override { return global_api_server->is_connected(); } + bool check(const Ts &...x) override { + return global_api_server->is_connected(this->state_subscription_only_.value(x...)); + } }; } // namespace esphome::api diff --git a/esphome/components/api/client.py b/esphome/components/api/client.py index 5239e07435..ca1fc089fa 100644 --- a/esphome/components/api/client.py +++ b/esphome/components/api/client.py @@ -30,7 +30,7 @@ if TYPE_CHECKING: _LOGGER = logging.getLogger(__name__) -async def async_run_logs(config: dict[str, Any], address: str) -> None: +async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None: """Run the logs command in the event loop.""" conf = config["api"] name = config["esphome"]["name"] @@ -39,13 +39,21 @@ async def async_run_logs(config: dict[str, Any], address: str) -> None: noise_psk: str | None = None if (encryption := conf.get(CONF_ENCRYPTION)) and (key := encryption.get(CONF_KEY)): noise_psk = key - _LOGGER.info("Starting log output from %s using esphome API", address) + + if len(addresses) == 1: + _LOGGER.info("Starting log output from %s using esphome API", addresses[0]) + else: + _LOGGER.info( + "Starting log output from %s using esphome API", " or ".join(addresses) + ) + cli = APIClient( - address, + addresses[0], # Primary address for compatibility port, password, client_info=f"ESPHome Logs {__version__}", noise_psk=noise_psk, + addresses=addresses, # Pass all addresses for automatic retry ) dashboard = CORE.dashboard @@ -54,9 +62,11 @@ async def async_run_logs(config: dict[str, Any], address: str) -> None: time_ = datetime.now() message: bytes = msg.message text = message.decode("utf8", "backslashreplace") - for parsed_msg in parse_log_message( - text, f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}]" - ): + nanoseconds = time_.microsecond // 1000 + timestamp = ( + f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}.{nanoseconds:03}]" + ) + for parsed_msg in parse_log_message(text, timestamp): print(parsed_msg.replace("\033", "\\033") if dashboard else parsed_msg) stop = await async_run(cli, on_log, name=name) @@ -66,7 +76,7 @@ async def async_run_logs(config: dict[str, Any], address: str) -> None: await stop() -def run_logs(config: dict[str, Any], address: str) -> None: +def run_logs(config: dict[str, Any], addresses: list[str]) -> None: """Run the logs command.""" with contextlib.suppress(KeyboardInterrupt): - asyncio.run(async_run_logs(config, address)) + asyncio.run(async_run_logs(config, addresses)) diff --git a/esphome/components/api/custom_api_device.h b/esphome/components/api/custom_api_device.h index a39947e725..1006d07533 100644 --- a/esphome/components/api/custom_api_device.h +++ b/esphome/components/api/custom_api_device.h @@ -3,17 +3,17 @@ #include #include "api_server.h" #ifdef USE_API -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS #include "user_services.h" #endif namespace esphome::api { -#ifdef USE_API_SERVICES -template class CustomAPIDeviceService : public UserServiceBase { +#ifdef USE_API_USER_DEFINED_ACTIONS +template class CustomAPIDeviceService : public UserServiceDynamic { public: CustomAPIDeviceService(const std::string &name, const std::array &arg_names, T *obj, void (T::*callback)(Ts...)) - : UserServiceBase(name, arg_names), obj_(obj), callback_(callback) {} + : UserServiceDynamic(name, arg_names), obj_(obj), callback_(callback) {} protected: void execute(Ts... x) override { (this->obj_->*this->callback_)(x...); } // NOLINT @@ -21,7 +21,7 @@ template class CustomAPIDeviceService : public UserS T *obj_; void (T::*callback_)(Ts...); }; -#endif // USE_API_SERVICES +#endif // USE_API_USER_DEFINED_ACTIONS class CustomAPIDevice { public: @@ -49,12 +49,26 @@ class CustomAPIDevice { * @param name The name of the service to register. * @param arg_names The name of the arguments for the service, must match the arguments of the function. */ -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS template void register_service(void (T::*callback)(Ts...), const std::string &name, const std::array &arg_names) { +#ifdef USE_API_CUSTOM_SERVICES auto *service = new CustomAPIDeviceService(name, arg_names, (T *) this, callback); // NOLINT global_api_server->register_user_service(service); +#else + static_assert( + sizeof(T) == 0, + "register_service() requires 'custom_services: true' in the 'api:' section of your YAML configuration"); +#endif + } +#else + template + void register_service(void (T::*callback)(Ts...), const std::string &name, + const std::array &arg_names) { + static_assert( + sizeof(T) == 0, + "register_service() requires 'custom_services: true' in the 'api:' section of your YAML configuration"); } #endif @@ -76,10 +90,22 @@ class CustomAPIDevice { * @param callback The member function to call when the service is triggered. * @param name The name of the arguments for the service, must match the arguments of the function. */ -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS template void register_service(void (T::*callback)(), const std::string &name) { +#ifdef USE_API_CUSTOM_SERVICES auto *service = new CustomAPIDeviceService(name, {}, (T *) this, callback); // NOLINT global_api_server->register_user_service(service); +#else + static_assert( + sizeof(T) == 0, + "register_service() requires 'custom_services: true' in the 'api:' section of your YAML configuration"); +#endif + } +#else + template void register_service(void (T::*callback)(), const std::string &name) { + static_assert( + sizeof(T) == 0, + "register_service() requires 'custom_services: true' in the 'api:' section of your YAML configuration"); } #endif @@ -135,6 +161,22 @@ class CustomAPIDevice { auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1); global_api_server->subscribe_home_assistant_state(entity_id, optional(attribute), f); } +#else + template + void subscribe_homeassistant_state(void (T::*callback)(std::string), const std::string &entity_id, + const std::string &attribute = "") { + static_assert(sizeof(T) == 0, + "subscribe_homeassistant_state() requires 'homeassistant_states: true' in the 'api:' section " + "of your YAML configuration"); + } + + template + void subscribe_homeassistant_state(void (T::*callback)(std::string, std::string), const std::string &entity_id, + const std::string &attribute = "") { + static_assert(sizeof(T) == 0, + "subscribe_homeassistant_state() requires 'homeassistant_states: true' in the 'api:' section " + "of your YAML configuration"); + } #endif #ifdef USE_API_HOMEASSISTANT_SERVICES @@ -149,9 +191,9 @@ class CustomAPIDevice { * @param service_name The service to call. */ void call_homeassistant_service(const std::string &service_name) { - HomeassistantServiceResponse resp; + HomeassistantActionRequest resp; resp.set_service(StringRef(service_name)); - global_api_server->send_homeassistant_service_call(resp); + global_api_server->send_homeassistant_action(resp); } /** Call a Home Assistant service from ESPHome. @@ -169,15 +211,15 @@ class CustomAPIDevice { * @param data The data for the service call, mapping from string to string. */ void call_homeassistant_service(const std::string &service_name, const std::map &data) { - HomeassistantServiceResponse resp; + HomeassistantActionRequest resp; resp.set_service(StringRef(service_name)); + resp.data.init(data.size()); for (auto &it : data) { - resp.data.emplace_back(); - auto &kv = resp.data.back(); + auto &kv = resp.data.emplace_back(); kv.set_key(StringRef(it.first)); kv.value = it.second; } - global_api_server->send_homeassistant_service_call(resp); + global_api_server->send_homeassistant_action(resp); } /** Fire an ESPHome event in Home Assistant. @@ -191,10 +233,10 @@ class CustomAPIDevice { * @param event_name The event to fire. */ void fire_homeassistant_event(const std::string &event_name) { - HomeassistantServiceResponse resp; + HomeassistantActionRequest resp; resp.set_service(StringRef(event_name)); resp.is_event = true; - global_api_server->send_homeassistant_service_call(resp); + global_api_server->send_homeassistant_action(resp); } /** Fire an ESPHome event in Home Assistant. @@ -211,16 +253,38 @@ class CustomAPIDevice { * @param data The data for the event, mapping from string to string. */ void fire_homeassistant_event(const std::string &service_name, const std::map &data) { - HomeassistantServiceResponse resp; + HomeassistantActionRequest resp; resp.set_service(StringRef(service_name)); resp.is_event = true; + resp.data.init(data.size()); for (auto &it : data) { - resp.data.emplace_back(); - auto &kv = resp.data.back(); + auto &kv = resp.data.emplace_back(); kv.set_key(StringRef(it.first)); kv.value = it.second; } - global_api_server->send_homeassistant_service_call(resp); + global_api_server->send_homeassistant_action(resp); + } +#else + template void call_homeassistant_service(const std::string &service_name) { + static_assert(sizeof(T) == 0, "call_homeassistant_service() requires 'homeassistant_services: true' in the 'api:' " + "section of your YAML configuration"); + } + + template + void call_homeassistant_service(const std::string &service_name, const std::map &data) { + static_assert(sizeof(T) == 0, "call_homeassistant_service() requires 'homeassistant_services: true' in the 'api:' " + "section of your YAML configuration"); + } + + template void fire_homeassistant_event(const std::string &event_name) { + static_assert(sizeof(T) == 0, "fire_homeassistant_event() requires 'homeassistant_services: true' in the 'api:' " + "section of your YAML configuration"); + } + + template + void fire_homeassistant_event(const std::string &service_name, const std::map &data) { + static_assert(sizeof(T) == 0, "fire_homeassistant_event() requires 'homeassistant_services: true' in the 'api:' " + "section of your YAML configuration"); } #endif }; diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h index 5df9c7c792..d00e9e6257 100644 --- a/esphome/components/api/homeassistant_service.h +++ b/esphome/components/api/homeassistant_service.h @@ -3,10 +3,15 @@ #include "api_server.h" #ifdef USE_API #ifdef USE_API_HOMEASSISTANT_SERVICES +#include +#include +#include #include "api_pb2.h" +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON +#include "esphome/components/json/json_util.h" +#endif #include "esphome/core/automation.h" #include "esphome/core/helpers.h" -#include namespace esphome::api { @@ -36,66 +41,191 @@ template class TemplatableStringValue : public TemplatableValue class TemplatableKeyValuePair { public: + // Default constructor needed for FixedVector::emplace_back() + TemplatableKeyValuePair() = default; + // Keys are always string literals from YAML dictionary keys (e.g., "code", "event") // and never templatable values or lambdas. Only the value parameter can be a lambda/template. // Using pass-by-value with std::move allows optimal performance for both lvalues and rvalues. template TemplatableKeyValuePair(std::string key, T value) : key(std::move(key)), value(value) {} + std::string key; TemplatableStringValue value; }; +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES +// Represents the response data from a Home Assistant action +class ActionResponse { + public: + ActionResponse(bool success, std::string error_message = "") + : success_(success), error_message_(std::move(error_message)) {} + +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON + ActionResponse(bool success, std::string error_message, const uint8_t *data, size_t data_len) + : success_(success), error_message_(std::move(error_message)) { + if (data == nullptr || data_len == 0) + return; + this->json_document_ = json::parse_json(data, data_len); + } +#endif + + bool is_success() const { return this->success_; } + const std::string &get_error_message() const { return this->error_message_; } + +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON + // Get data as parsed JSON object (const version returns read-only view) + JsonObjectConst get_json() const { return this->json_document_.as(); } +#endif + + protected: + bool success_; + std::string error_message_; +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON + JsonDocument json_document_; +#endif +}; + +// Callback type for action responses +template using ActionResponseCallback = std::function; +#endif + template class HomeAssistantServiceCallAction : public Action { public: - explicit HomeAssistantServiceCallAction(APIServer *parent, bool is_event) : parent_(parent), is_event_(is_event) {} + explicit HomeAssistantServiceCallAction(APIServer *parent, bool is_event) : parent_(parent) { + this->flags_.is_event = is_event; + } template void set_service(T service) { this->service_ = service; } + // Initialize FixedVector members - called from Python codegen with compile-time known sizes. + // Must be called before any add_* methods; capacity must match the number of subsequent add_* calls. + void init_data(size_t count) { this->data_.init(count); } + void init_data_template(size_t count) { this->data_template_.init(count); } + void init_variables(size_t count) { this->variables_.init(count); } + // Keys are always string literals from the Python code generation (e.g., cg.add(var.add_data("tag_id", templ))). // The value parameter can be a lambda/template, but keys are never templatable. - // Using pass-by-value allows the compiler to optimize for both lvalues and rvalues. - template void add_data(std::string key, T value) { this->data_.emplace_back(std::move(key), value); } - template void add_data_template(std::string key, T value) { - this->data_template_.emplace_back(std::move(key), value); + template void add_data(K &&key, V &&value) { + this->add_kv_(this->data_, std::forward(key), std::forward(value)); } - template void add_variable(std::string key, T value) { - this->variables_.emplace_back(std::move(key), value); + template void add_data_template(K &&key, V &&value) { + this->add_kv_(this->data_template_, std::forward(key), std::forward(value)); + } + template void add_variable(K &&key, V &&value) { + this->add_kv_(this->variables_, std::forward(key), std::forward(value)); } - void play(Ts... x) override { - HomeassistantServiceResponse resp; +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES + template void set_response_template(T response_template) { + this->response_template_ = response_template; + this->flags_.has_response_template = true; + } + + void set_wants_status() { this->flags_.wants_status = true; } + void set_wants_response() { this->flags_.wants_response = true; } + +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON + Trigger *get_success_trigger_with_response() const { + return this->success_trigger_with_response_; + } +#endif + Trigger *get_success_trigger() const { return this->success_trigger_; } + Trigger *get_error_trigger() const { return this->error_trigger_; } +#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES + + void play(const Ts &...x) override { + HomeassistantActionRequest resp; std::string service_value = this->service_.value(x...); resp.set_service(StringRef(service_value)); - resp.is_event = this->is_event_; - for (auto &it : this->data_) { - resp.data.emplace_back(); - auto &kv = resp.data.back(); - kv.set_key(StringRef(it.key)); - kv.value = it.value.value(x...); + resp.is_event = this->flags_.is_event; + this->populate_service_map(resp.data, this->data_, x...); + this->populate_service_map(resp.data_template, this->data_template_, x...); + this->populate_service_map(resp.variables, this->variables_, x...); + +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES + if (this->flags_.wants_status) { + // Generate a unique call ID for this service call + static uint32_t call_id_counter = 1; + uint32_t call_id = call_id_counter++; + resp.call_id = call_id; +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON + if (this->flags_.wants_response) { + resp.wants_response = true; + // Set response template if provided + if (this->flags_.has_response_template) { + std::string response_template_value = this->response_template_.value(x...); + resp.response_template = response_template_value; + } + } +#endif + + auto captured_args = std::make_tuple(x...); + this->parent_->register_action_response_callback(call_id, [this, captured_args](const ActionResponse &response) { + std::apply( + [this, &response](auto &&...args) { + if (response.is_success()) { +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON + if (this->flags_.wants_response) { + this->success_trigger_with_response_->trigger(response.get_json(), args...); + } else +#endif + { + this->success_trigger_->trigger(args...); + } + } else { + this->error_trigger_->trigger(response.get_error_message(), args...); + } + }, + captured_args); + }); } - for (auto &it : this->data_template_) { - resp.data_template.emplace_back(); - auto &kv = resp.data_template.back(); - kv.set_key(StringRef(it.key)); - kv.value = it.value.value(x...); - } - for (auto &it : this->variables_) { - resp.variables.emplace_back(); - auto &kv = resp.variables.back(); - kv.set_key(StringRef(it.key)); - kv.value = it.value.value(x...); - } - this->parent_->send_homeassistant_service_call(resp); +#endif + + this->parent_->send_homeassistant_action(resp); } protected: + // Helper to add key-value pairs to FixedVectors with perfect forwarding to avoid copies + template void add_kv_(FixedVector> &vec, K &&key, V &&value) { + auto &kv = vec.emplace_back(); + kv.key = std::forward(key); + kv.value = std::forward(value); + } + + template + static void populate_service_map(VectorType &dest, SourceType &source, Ts... x) { + dest.init(source.size()); + for (auto &it : source) { + auto &kv = dest.emplace_back(); + kv.set_key(StringRef(it.key)); + kv.value = it.value.value(x...); + } + } + APIServer *parent_; - bool is_event_; TemplatableStringValue service_{}; - std::vector> data_; - std::vector> data_template_; - std::vector> variables_; + FixedVector> data_; + FixedVector> data_template_; + FixedVector> variables_; +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON + TemplatableStringValue response_template_{""}; + Trigger *success_trigger_with_response_ = new Trigger(); +#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON + Trigger *success_trigger_ = new Trigger(); + Trigger *error_trigger_ = new Trigger(); +#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES + + struct Flags { + uint8_t is_event : 1; + uint8_t wants_status : 1; + uint8_t wants_response : 1; + uint8_t has_response_template : 1; + uint8_t reserved : 5; + } flags_{0}; }; } // namespace esphome::api + #endif #endif diff --git a/esphome/components/api/list_entities.cpp b/esphome/components/api/list_entities.cpp index da4800a45e..e18fc17801 100644 --- a/esphome/components/api/list_entities.cpp +++ b/esphome/components/api/list_entities.cpp @@ -82,7 +82,7 @@ bool ListEntitiesIterator::on_end() { return this->client_->send_list_info_done( ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(client) {} -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) { auto resp = service->encode_list_service_response(); return this->client_->send_message(resp, ListEntitiesServicesResponse::MESSAGE_TYPE); diff --git a/esphome/components/api/list_entities.h b/esphome/components/api/list_entities.h index 769d7b9b6e..4c90dbbad8 100644 --- a/esphome/components/api/list_entities.h +++ b/esphome/components/api/list_entities.h @@ -43,7 +43,7 @@ class ListEntitiesIterator : public ComponentIterator { #ifdef USE_TEXT_SENSOR bool on_text_sensor(text_sensor::TextSensor *entity) override; #endif -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS bool on_service(UserServiceDescriptor *service) override; #endif #ifdef USE_CAMERA diff --git a/esphome/components/api/proto.cpp b/esphome/components/api/proto.cpp index cb6c07ec3c..4f0d0846d7 100644 --- a/esphome/components/api/proto.cpp +++ b/esphome/components/api/proto.cpp @@ -7,75 +7,134 @@ namespace esphome::api { static const char *const TAG = "api.proto"; -void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) { - uint32_t i = 0; - bool error = false; - while (i < length) { +uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size_t length, uint32_t target_field_id) { + uint32_t count = 0; + const uint8_t *ptr = buffer; + const uint8_t *end = buffer + length; + + while (ptr < end) { uint32_t consumed; - auto res = ProtoVarInt::parse(&buffer[i], length - i, &consumed); + + // Parse field header (tag) + auto res = ProtoVarInt::parse(ptr, end - ptr, &consumed); if (!res.has_value()) { - ESP_LOGV(TAG, "Invalid field start at %" PRIu32, i); - break; + break; // Invalid data, stop counting } - uint32_t field_type = (res->as_uint32()) & 0b111; - uint32_t field_id = (res->as_uint32()) >> 3; - i += consumed; + uint32_t tag = res->as_uint32(); + uint32_t field_type = tag & WIRE_TYPE_MASK; + uint32_t field_id = tag >> 3; + ptr += consumed; + + // Count if this is the target field + if (field_id == target_field_id) { + count++; + } + + // Skip field data based on wire type + switch (field_type) { + case WIRE_TYPE_VARINT: { // VarInt - parse and skip + res = ProtoVarInt::parse(ptr, end - ptr, &consumed); + if (!res.has_value()) { + return count; // Invalid data, return what we have + } + ptr += consumed; + break; + } + case WIRE_TYPE_LENGTH_DELIMITED: { // Length-delimited - parse length and skip data + res = ProtoVarInt::parse(ptr, end - ptr, &consumed); + if (!res.has_value()) { + return count; + } + uint32_t field_length = res->as_uint32(); + ptr += consumed; + if (ptr + field_length > end) { + return count; // Out of bounds + } + ptr += field_length; + break; + } + case WIRE_TYPE_FIXED32: { // 32-bit - skip 4 bytes + if (ptr + 4 > end) { + return count; + } + ptr += 4; + break; + } + default: + // Unknown wire type, can't continue + return count; + } + } + + return count; +} + +void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) { + const uint8_t *ptr = buffer; + const uint8_t *end = buffer + length; + + while (ptr < end) { + uint32_t consumed; + + // Parse field header + auto res = ProtoVarInt::parse(ptr, end - ptr, &consumed); + if (!res.has_value()) { + ESP_LOGV(TAG, "Invalid field start at offset %ld", (long) (ptr - buffer)); + return; + } + + uint32_t tag = res->as_uint32(); + uint32_t field_type = tag & WIRE_TYPE_MASK; + uint32_t field_id = tag >> 3; + ptr += consumed; switch (field_type) { - case 0: { // VarInt - res = ProtoVarInt::parse(&buffer[i], length - i, &consumed); + case WIRE_TYPE_VARINT: { // VarInt + res = ProtoVarInt::parse(ptr, end - ptr, &consumed); if (!res.has_value()) { - ESP_LOGV(TAG, "Invalid VarInt at %" PRIu32, i); - error = true; - break; + ESP_LOGV(TAG, "Invalid VarInt at offset %ld", (long) (ptr - buffer)); + return; } if (!this->decode_varint(field_id, *res)) { ESP_LOGV(TAG, "Cannot decode VarInt field %" PRIu32 " with value %" PRIu32 "!", field_id, res->as_uint32()); } - i += consumed; + ptr += consumed; break; } - case 2: { // Length-delimited - res = ProtoVarInt::parse(&buffer[i], length - i, &consumed); + case WIRE_TYPE_LENGTH_DELIMITED: { // Length-delimited + res = ProtoVarInt::parse(ptr, end - ptr, &consumed); if (!res.has_value()) { - ESP_LOGV(TAG, "Invalid Length Delimited at %" PRIu32, i); - error = true; - break; + ESP_LOGV(TAG, "Invalid Length Delimited at offset %ld", (long) (ptr - buffer)); + return; } uint32_t field_length = res->as_uint32(); - i += consumed; - if (field_length > length - i) { - ESP_LOGV(TAG, "Out-of-bounds Length Delimited at %" PRIu32, i); - error = true; - break; + ptr += consumed; + if (ptr + field_length > end) { + ESP_LOGV(TAG, "Out-of-bounds Length Delimited at offset %ld", (long) (ptr - buffer)); + return; } - if (!this->decode_length(field_id, ProtoLengthDelimited(&buffer[i], field_length))) { + if (!this->decode_length(field_id, ProtoLengthDelimited(ptr, field_length))) { ESP_LOGV(TAG, "Cannot decode Length Delimited field %" PRIu32 "!", field_id); } - i += field_length; + ptr += field_length; break; } - case 5: { // 32-bit - if (length - i < 4) { - ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at %" PRIu32, i); - error = true; - break; + case WIRE_TYPE_FIXED32: { // 32-bit + if (ptr + 4 > end) { + ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at offset %ld", (long) (ptr - buffer)); + return; } - uint32_t val = encode_uint32(buffer[i + 3], buffer[i + 2], buffer[i + 1], buffer[i]); + uint32_t val = encode_uint32(ptr[3], ptr[2], ptr[1], ptr[0]); if (!this->decode_32bit(field_id, Proto32Bit(val))) { ESP_LOGV(TAG, "Cannot decode 32-bit field %" PRIu32 " with value %" PRIu32 "!", field_id, val); } - i += 4; + ptr += 4; break; } default: - ESP_LOGV(TAG, "Invalid field type at %" PRIu32, i); - error = true; - break; - } - if (error) { - break; + ESP_LOGV(TAG, "Invalid field type %u at offset %ld", field_type, (long) (ptr - buffer)); + return; } } } diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 5c174b679c..e7585924a5 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -15,6 +15,30 @@ namespace esphome::api { +// Protocol Buffer wire type constants +// See https://protobuf.dev/programming-guides/encoding/#structure +constexpr uint8_t WIRE_TYPE_VARINT = 0; // int32, int64, uint32, uint64, sint32, sint64, bool, enum +constexpr uint8_t WIRE_TYPE_LENGTH_DELIMITED = 2; // string, bytes, embedded messages, packed repeated fields +constexpr uint8_t WIRE_TYPE_FIXED32 = 5; // fixed32, sfixed32, float +constexpr uint8_t WIRE_TYPE_MASK = 0b111; // Mask to extract wire type from tag + +// Helper functions for ZigZag encoding/decoding +inline constexpr uint32_t encode_zigzag32(int32_t value) { + return (static_cast(value) << 1) ^ (static_cast(value >> 31)); +} + +inline constexpr uint64_t encode_zigzag64(int64_t value) { + return (static_cast(value) << 1) ^ (static_cast(value >> 63)); +} + +inline constexpr int32_t decode_zigzag32(uint32_t value) { + return (value & 1) ? static_cast(~(value >> 1)) : static_cast(value >> 1); +} + +inline constexpr int64_t decode_zigzag64(uint64_t value) { + return (value & 1) ? static_cast(~(value >> 1)) : static_cast(value >> 1); +} + /* * StringRef Ownership Model for API Protocol Messages * =================================================== @@ -87,33 +111,25 @@ class ProtoVarInt { return {}; // Incomplete or invalid varint } - uint16_t as_uint16() const { return this->value_; } - uint32_t as_uint32() const { return this->value_; } - uint64_t as_uint64() const { return this->value_; } - bool as_bool() const { return this->value_; } - int32_t as_int32() const { + constexpr uint16_t as_uint16() const { return this->value_; } + constexpr uint32_t as_uint32() const { return this->value_; } + constexpr uint64_t as_uint64() const { return this->value_; } + constexpr bool as_bool() const { return this->value_; } + constexpr int32_t as_int32() const { // Not ZigZag encoded return static_cast(this->as_int64()); } - int64_t as_int64() const { + constexpr int64_t as_int64() const { // Not ZigZag encoded return static_cast(this->value_); } - int32_t as_sint32() const { + constexpr int32_t as_sint32() const { // with ZigZag encoding - if (this->value_ & 1) { - return static_cast(~(this->value_ >> 1)); - } else { - return static_cast(this->value_ >> 1); - } + return decode_zigzag32(static_cast(this->value_)); } - int64_t as_sint64() const { + constexpr int64_t as_sint64() const { // with ZigZag encoding - if (this->value_ & 1) { - return static_cast(~(this->value_ >> 1)); - } else { - return static_cast(this->value_ >> 1); - } + return decode_zigzag64(this->value_); } /** * Encode the varint value to a pre-allocated buffer without bounds checking. @@ -173,6 +189,10 @@ class ProtoLengthDelimited { explicit ProtoLengthDelimited(const uint8_t *value, size_t length) : value_(value), length_(length) {} std::string as_string() const { return std::string(reinterpret_cast(this->value_), this->length_); } + // Direct access to raw data without string allocation + const uint8_t *data() const { return this->value_; } + size_t size() const { return this->length_; } + /** * Decode the length-delimited data into an existing ProtoDecodableMessage instance. * @@ -228,7 +248,7 @@ class ProtoWriteBuffer { * Following https://protobuf.dev/programming-guides/encoding/#structure */ void encode_field_raw(uint32_t field_id, uint32_t type) { - uint32_t val = (field_id << 3) | (type & 0b111); + uint32_t val = (field_id << 3) | (type & WIRE_TYPE_MASK); this->encode_varint_raw(val); } void encode_string(uint32_t field_id, const char *string, size_t len, bool force = false) { @@ -309,22 +329,10 @@ class ProtoWriteBuffer { this->encode_uint64(field_id, static_cast(value), force); } void encode_sint32(uint32_t field_id, int32_t value, bool force = false) { - uint32_t uvalue; - if (value < 0) { - uvalue = ~(value << 1); - } else { - uvalue = value << 1; - } - this->encode_uint32(field_id, uvalue, force); + this->encode_uint32(field_id, encode_zigzag32(value), force); } void encode_sint64(uint32_t field_id, int64_t value, bool force = false) { - uint64_t uvalue; - if (value < 0) { - uvalue = ~(value << 1); - } else { - uvalue = value << 1; - } - this->encode_uint64(field_id, uvalue, force); + this->encode_uint64(field_id, encode_zigzag64(value), force); } void encode_message(uint32_t field_id, const ProtoMessage &value, bool force = false); std::vector *get_buffer() const { return buffer_; } @@ -353,7 +361,18 @@ class ProtoMessage { // Base class for messages that support decoding class ProtoDecodableMessage : public ProtoMessage { public: - void decode(const uint8_t *buffer, size_t length); + virtual void decode(const uint8_t *buffer, size_t length); + + /** + * Count occurrences of a repeated field in a protobuf buffer. + * This is a lightweight scan that only parses tags and skips field data. + * + * @param buffer Pointer to the protobuf buffer + * @param length Length of the buffer in bytes + * @param target_field_id The field ID to count + * @return Number of times the field appears in the buffer + */ + static uint32_t count_repeated_field(const uint8_t *buffer, size_t length, uint32_t target_field_id); protected: virtual bool decode_varint(uint32_t field_id, ProtoVarInt value) { return false; } @@ -395,7 +414,7 @@ class ProtoSize { * @param value The uint32_t value to calculate size for * @return The number of bytes needed to encode the value */ - static inline uint32_t varint(uint32_t value) { + static constexpr uint32_t varint(uint32_t value) { // Optimized varint size calculation using leading zeros // Each 7 bits requires one byte in the varint encoding if (value < 128) @@ -419,7 +438,7 @@ class ProtoSize { * @param value The uint64_t value to calculate size for * @return The number of bytes needed to encode the value */ - static inline uint32_t varint(uint64_t value) { + static constexpr uint32_t varint(uint64_t value) { // Handle common case of values fitting in uint32_t (vast majority of use cases) if (value <= UINT32_MAX) { return varint(static_cast(value)); @@ -450,7 +469,7 @@ class ProtoSize { * @param value The int32_t value to calculate size for * @return The number of bytes needed to encode the value */ - static inline uint32_t varint(int32_t value) { + static constexpr uint32_t varint(int32_t value) { // Negative values are sign-extended to 64 bits in protocol buffers, // which always results in a 10-byte varint for negative int32 if (value < 0) { @@ -466,7 +485,7 @@ class ProtoSize { * @param value The int64_t value to calculate size for * @return The number of bytes needed to encode the value */ - static inline uint32_t varint(int64_t value) { + static constexpr uint32_t varint(int64_t value) { // For int64_t, we convert to uint64_t and calculate the size // This works because the bit pattern determines the encoding size, // and we've handled negative int32 values as a special case above @@ -480,8 +499,8 @@ class ProtoSize { * @param type The wire type value (from the WireType enum in the protobuf spec) * @return The number of bytes needed to encode the field ID and wire type */ - static inline uint32_t field(uint32_t field_id, uint32_t type) { - uint32_t tag = (field_id << 3) | (type & 0b111); + static constexpr uint32_t field(uint32_t field_id, uint32_t type) { + uint32_t tag = (field_id << 3) | (type & WIRE_TYPE_MASK); return varint(tag); } @@ -607,9 +626,8 @@ class ProtoSize { */ inline void add_sint32_force(uint32_t field_id_size, int32_t value) { // Always calculate size when force is true - // ZigZag encoding for sint32: (n << 1) ^ (n >> 31) - uint32_t zigzag = (static_cast(value) << 1) ^ (static_cast(value >> 31)); - total_size_ += field_id_size + varint(zigzag); + // ZigZag encoding for sint32 + total_size_ += field_id_size + varint(encode_zigzag32(value)); } /** @@ -749,13 +767,29 @@ class ProtoSize { template inline void add_repeated_message(uint32_t field_id_size, const std::vector &messages) { // Skip if the vector is empty - if (messages.empty()) { - return; + if (!messages.empty()) { + // Use the force version for all messages in the repeated field + for (const auto &message : messages) { + add_message_object_force(field_id_size, message); + } } + } - // Use the force version for all messages in the repeated field - for (const auto &message : messages) { - add_message_object_force(field_id_size, message); + /** + * @brief Calculates and adds the sizes of all messages in a repeated field to the total message size (FixedVector + * version) + * + * @tparam MessageType The type of the nested messages in the FixedVector + * @param messages FixedVector of message objects + */ + template + inline void add_repeated_message(uint32_t field_id_size, const FixedVector &messages) { + // Skip if the fixed vector is empty + if (!messages.empty()) { + // Use the force version for all messages in the repeated field + for (const auto &message : messages) { + add_message_object_force(field_id_size, message); + } } } }; @@ -831,7 +865,7 @@ class ProtoService { } // Authentication helper methods - bool check_connection_setup_() { + inline bool check_connection_setup_() { if (!this->is_connection_setup()) { this->on_no_setup_connection(); return false; @@ -839,7 +873,7 @@ class ProtoService { return true; } - bool check_authenticated_() { + inline bool check_authenticated_() { #ifdef USE_API_PASSWORD if (!this->check_connection_setup_()) { return false; diff --git a/esphome/components/api/user_services.cpp b/esphome/components/api/user_services.cpp index 27b30eb332..9c2b4aa79a 100644 --- a/esphome/components/api/user_services.cpp +++ b/esphome/components/api/user_services.cpp @@ -11,16 +11,49 @@ template<> int32_t get_execute_arg_value(const ExecuteServiceArgument & } template<> float get_execute_arg_value(const ExecuteServiceArgument &arg) { return arg.float_; } template<> std::string get_execute_arg_value(const ExecuteServiceArgument &arg) { return arg.string_; } + +// Legacy std::vector versions for external components using custom_api_device.h - optimized with reserve template<> std::vector get_execute_arg_value>(const ExecuteServiceArgument &arg) { - return arg.bool_array; + std::vector result; + result.reserve(arg.bool_array.size()); + result.insert(result.end(), arg.bool_array.begin(), arg.bool_array.end()); + return result; } template<> std::vector get_execute_arg_value>(const ExecuteServiceArgument &arg) { - return arg.int_array; + std::vector result; + result.reserve(arg.int_array.size()); + result.insert(result.end(), arg.int_array.begin(), arg.int_array.end()); + return result; } template<> std::vector get_execute_arg_value>(const ExecuteServiceArgument &arg) { - return arg.float_array; + std::vector result; + result.reserve(arg.float_array.size()); + result.insert(result.end(), arg.float_array.begin(), arg.float_array.end()); + return result; } template<> std::vector get_execute_arg_value>(const ExecuteServiceArgument &arg) { + std::vector result; + result.reserve(arg.string_array.size()); + result.insert(result.end(), arg.string_array.begin(), arg.string_array.end()); + return result; +} + +// New FixedVector const reference versions for YAML-generated services - zero-copy +template<> +const FixedVector &get_execute_arg_value &>(const ExecuteServiceArgument &arg) { + return arg.bool_array; +} +template<> +const FixedVector &get_execute_arg_value &>(const ExecuteServiceArgument &arg) { + return arg.int_array; +} +template<> +const FixedVector &get_execute_arg_value &>(const ExecuteServiceArgument &arg) { + return arg.float_array; +} +template<> +const FixedVector &get_execute_arg_value &>( + const ExecuteServiceArgument &arg) { return arg.string_array; } @@ -28,6 +61,8 @@ template<> enums::ServiceArgType to_service_arg_type() { return enums::SER template<> enums::ServiceArgType to_service_arg_type() { return enums::SERVICE_ARG_TYPE_INT; } template<> enums::ServiceArgType to_service_arg_type() { return enums::SERVICE_ARG_TYPE_FLOAT; } template<> enums::ServiceArgType to_service_arg_type() { return enums::SERVICE_ARG_TYPE_STRING; } + +// Legacy std::vector versions for external components using custom_api_device.h template<> enums::ServiceArgType to_service_arg_type>() { return enums::SERVICE_ARG_TYPE_BOOL_ARRAY; } template<> enums::ServiceArgType to_service_arg_type>() { return enums::SERVICE_ARG_TYPE_INT_ARRAY; @@ -39,4 +74,18 @@ template<> enums::ServiceArgType to_service_arg_type>() return enums::SERVICE_ARG_TYPE_STRING_ARRAY; } +// New FixedVector const reference versions for YAML-generated services +template<> enums::ServiceArgType to_service_arg_type &>() { + return enums::SERVICE_ARG_TYPE_BOOL_ARRAY; +} +template<> enums::ServiceArgType to_service_arg_type &>() { + return enums::SERVICE_ARG_TYPE_INT_ARRAY; +} +template<> enums::ServiceArgType to_service_arg_type &>() { + return enums::SERVICE_ARG_TYPE_FLOAT_ARRAY; +} +template<> enums::ServiceArgType to_service_arg_type &>() { + return enums::SERVICE_ARG_TYPE_STRING_ARRAY; +} + } // namespace esphome::api diff --git a/esphome/components/api/user_services.h b/esphome/components/api/user_services.h index 5f040e8433..d9c13c520b 100644 --- a/esphome/components/api/user_services.h +++ b/esphome/components/api/user_services.h @@ -7,7 +7,7 @@ #include "esphome/core/automation.h" #include "api_pb2.h" -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS namespace esphome::api { class UserServiceDescriptor { @@ -23,11 +23,13 @@ template T get_execute_arg_value(const ExecuteServiceArgument &arg); template enums::ServiceArgType to_service_arg_type(); +// Base class for YAML-defined services (most common case) +// Stores only pointers to string literals in flash - no heap allocation template class UserServiceBase : public UserServiceDescriptor { public: - UserServiceBase(std::string name, const std::array &arg_names) - : name_(std::move(name)), arg_names_(arg_names) { - this->key_ = fnv1_hash(this->name_); + UserServiceBase(const char *name, const std::array &arg_names) + : name_(name), arg_names_(arg_names) { + this->key_ = fnv1_hash(name); } ListEntitiesServicesResponse encode_list_service_response() override { @@ -35,9 +37,9 @@ template class UserServiceBase : public UserServiceDescriptor { msg.set_name(StringRef(this->name_)); msg.key = this->key_; std::array arg_types = {to_service_arg_type()...}; - for (int i = 0; i < sizeof...(Ts); i++) { - msg.args.emplace_back(); - auto &arg = msg.args.back(); + msg.args.init(sizeof...(Ts)); + for (size_t i = 0; i < sizeof...(Ts); i++) { + auto &arg = msg.args.emplace_back(); arg.type = arg_types[i]; arg.set_name(StringRef(this->arg_names_[i])); } @@ -47,26 +49,74 @@ template class UserServiceBase : public UserServiceDescriptor { bool execute_service(const ExecuteServiceRequest &req) override { if (req.key != this->key_) return false; - if (req.args.size() != this->arg_names_.size()) + if (req.args.size() != sizeof...(Ts)) return false; - this->execute_(req.args, typename gens::type()); + this->execute_(req.args, std::make_index_sequence{}); return true; } protected: virtual void execute(Ts... x) = 0; - template void execute_(std::vector args, seq type) { + template + void execute_(const ArgsContainer &args, std::index_sequence type) { this->execute((get_execute_arg_value(args[S]))...); } - std::string name_; + // Pointers to string literals in flash - no heap allocation + const char *name_; + std::array arg_names_; uint32_t key_{0}; +}; + +// Separate class for custom_api_device services (rare case) +// Stores copies of runtime-generated names +template class UserServiceDynamic : public UserServiceDescriptor { + public: + UserServiceDynamic(std::string name, const std::array &arg_names) + : name_(std::move(name)), arg_names_(arg_names) { + this->key_ = fnv1_hash(this->name_.c_str()); + } + + ListEntitiesServicesResponse encode_list_service_response() override { + ListEntitiesServicesResponse msg; + msg.set_name(StringRef(this->name_)); + msg.key = this->key_; + std::array arg_types = {to_service_arg_type()...}; + msg.args.init(sizeof...(Ts)); + for (size_t i = 0; i < sizeof...(Ts); i++) { + auto &arg = msg.args.emplace_back(); + arg.type = arg_types[i]; + arg.set_name(StringRef(this->arg_names_[i])); + } + return msg; + } + + bool execute_service(const ExecuteServiceRequest &req) override { + if (req.key != this->key_) + return false; + if (req.args.size() != sizeof...(Ts)) + return false; + this->execute_(req.args, std::make_index_sequence{}); + return true; + } + + protected: + virtual void execute(Ts... x) = 0; + template + void execute_(const ArgsContainer &args, std::index_sequence type) { + this->execute((get_execute_arg_value(args[S]))...); + } + + // Heap-allocated strings for runtime-generated names + std::string name_; std::array arg_names_; + uint32_t key_{0}; }; template class UserServiceTrigger : public UserServiceBase, public Trigger { public: - UserServiceTrigger(const std::string &name, const std::array &arg_names) + // Constructor for static names (YAML-defined services - used by code generator) + UserServiceTrigger(const char *name, const std::array &arg_names) : UserServiceBase(name, arg_names) {} protected: @@ -74,4 +124,4 @@ template class UserServiceTrigger : public UserServiceBase class AT581XResetAction : public Action, public Parented { public: - void play(Ts... x) { this->parent_->reset_hardware_frontend(); } + void play(const Ts &...x) { this->parent_->reset_hardware_frontend(); } }; template class AT581XSettingsAction : public Action, public Parented { @@ -25,7 +25,7 @@ template class AT581XSettingsAction : public Action, publ TEMPLATABLE_VALUE(int, trigger_keep) TEMPLATABLE_VALUE(int, stage_gain) - void play(Ts... x) { + void play(const Ts &...x) { if (this->frequency_.has_value()) { int v = this->frequency_.value(x...); this->parent_->set_frequency(v); diff --git a/esphome/components/atm90e26/sensor.py b/esphome/components/atm90e26/sensor.py index 42ef259100..4522e94846 100644 --- a/esphome/components/atm90e26/sensor.py +++ b/esphome/components/atm90e26/sensor.py @@ -16,6 +16,7 @@ from esphome.const import ( DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, DEVICE_CLASS_POWER_FACTOR, + DEVICE_CLASS_REACTIVE_POWER, DEVICE_CLASS_VOLTAGE, ICON_CURRENT_AC, ICON_LIGHTBULB, @@ -78,6 +79,7 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, icon=ICON_LIGHTBULB, accuracy_decimals=2, + device_class=DEVICE_CLASS_REACTIVE_POWER, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema( diff --git a/esphome/components/atm90e32/atm90e32.cpp b/esphome/components/atm90e32/atm90e32.cpp index a887e7a9e6..634260b5e9 100644 --- a/esphome/components/atm90e32/atm90e32.cpp +++ b/esphome/components/atm90e32/atm90e32.cpp @@ -110,6 +110,8 @@ void ATM90E32Component::update() { void ATM90E32Component::setup() { this->spi_setup(); + this->cs_summary_ = this->cs_->dump_summary(); + const char *cs = this->cs_summary_.c_str(); uint16_t mmode0 = 0x87; // 3P4W 50Hz uint16_t high_thresh = 0; @@ -130,9 +132,9 @@ void ATM90E32Component::setup() { mmode0 |= 0 << 1; // sets 1st bit to 0, phase b is not counted into the all-phase sum energy/power (P/Q/S) } - this->write16_(ATM90E32_REGISTER_SOFTRESET, 0x789A); // Perform soft reset - delay(6); // Wait for the minimum 5ms + 1ms - this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x55AA); // enable register config access + this->write16_(ATM90E32_REGISTER_SOFTRESET, 0x789A, false); // Perform soft reset + delay(6); // Wait for the minimum 5ms + 1ms + this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x55AA); // enable register config access if (!this->validate_spi_read_(0x55AA, "setup()")) { ESP_LOGW(TAG, "Could not initialize ATM90E32 IC, check SPI settings"); this->mark_failed(); @@ -156,16 +158,17 @@ void ATM90E32Component::setup() { if (this->enable_offset_calibration_) { // Initialize flash storage for offset calibrations - uint32_t o_hash = fnv1_hash(std::string("_offset_calibration_") + this->cs_->dump_summary()); + uint32_t o_hash = fnv1_hash(std::string("_offset_calibration_") + this->cs_summary_); this->offset_pref_ = global_preferences->make_preference(o_hash, true); this->restore_offset_calibrations_(); // Initialize flash storage for power offset calibrations - uint32_t po_hash = fnv1_hash(std::string("_power_offset_calibration_") + this->cs_->dump_summary()); + uint32_t po_hash = fnv1_hash(std::string("_power_offset_calibration_") + this->cs_summary_); this->power_offset_pref_ = global_preferences->make_preference(po_hash, true); this->restore_power_offset_calibrations_(); } else { - ESP_LOGI(TAG, "[CALIBRATION] Power & Voltage/Current offset calibration is disabled. Using config file values."); + ESP_LOGI(TAG, "[CALIBRATION][%s] Power & Voltage/Current offset calibration is disabled. Using config file values.", + cs); for (uint8_t phase = 0; phase < 3; ++phase) { this->write16_(this->voltage_offset_registers[phase], static_cast(this->offset_phase_[phase].voltage_offset_)); @@ -180,21 +183,18 @@ void ATM90E32Component::setup() { if (this->enable_gain_calibration_) { // Initialize flash storage for gain calibration - uint32_t g_hash = fnv1_hash(std::string("_gain_calibration_") + this->cs_->dump_summary()); + uint32_t g_hash = fnv1_hash(std::string("_gain_calibration_") + this->cs_summary_); this->gain_calibration_pref_ = global_preferences->make_preference(g_hash, true); this->restore_gain_calibrations_(); - if (this->using_saved_calibrations_) { - ESP_LOGI(TAG, "[CALIBRATION] Successfully restored gain calibration from memory."); - } else { + if (!this->using_saved_calibrations_) { for (uint8_t phase = 0; phase < 3; ++phase) { this->write16_(voltage_gain_registers[phase], this->phase_[phase].voltage_gain_); this->write16_(current_gain_registers[phase], this->phase_[phase].ct_gain_); } } } else { - ESP_LOGI(TAG, "[CALIBRATION] Gain calibration is disabled. Using config file values."); - + ESP_LOGI(TAG, "[CALIBRATION][%s] Gain calibration is disabled. Using config file values.", cs); for (uint8_t phase = 0; phase < 3; ++phase) { this->write16_(voltage_gain_registers[phase], this->phase_[phase].voltage_gain_); this->write16_(current_gain_registers[phase], this->phase_[phase].ct_gain_); @@ -213,6 +213,122 @@ void ATM90E32Component::setup() { this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000); // end configuration } +void ATM90E32Component::log_calibration_status_() { + const char *cs = this->cs_summary_.c_str(); + + bool offset_mismatch = false; + bool power_mismatch = false; + bool gain_mismatch = false; + + for (uint8_t phase = 0; phase < 3; ++phase) { + offset_mismatch |= this->offset_calibration_mismatch_[phase]; + power_mismatch |= this->power_offset_calibration_mismatch_[phase]; + gain_mismatch |= this->gain_calibration_mismatch_[phase]; + } + + if (offset_mismatch) { + ESP_LOGW(TAG, "[CALIBRATION][%s] ", cs); + ESP_LOGW(TAG, + "[CALIBRATION][%s] ===================== Offset mismatch: using flash values =====================", cs); + ESP_LOGW(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------------------", + cs); + ESP_LOGW(TAG, "[CALIBRATION][%s] | Phase | offset_voltage | offset_current |", cs); + ESP_LOGW(TAG, "[CALIBRATION][%s] | | config | flash | config | flash |", cs); + ESP_LOGW(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------------------", + cs); + for (uint8_t phase = 0; phase < 3; ++phase) { + ESP_LOGW(TAG, "[CALIBRATION][%s] | %c | %6d | %6d | %6d | %6d |", cs, 'A' + phase, + this->config_offset_phase_[phase].voltage_offset_, this->offset_phase_[phase].voltage_offset_, + this->config_offset_phase_[phase].current_offset_, this->offset_phase_[phase].current_offset_); + } + ESP_LOGW(TAG, + "[CALIBRATION][%s] ===============================================================================", cs); + } + if (power_mismatch) { + ESP_LOGW(TAG, "[CALIBRATION][%s] ", cs); + ESP_LOGW(TAG, + "[CALIBRATION][%s] ================= Power offset mismatch: using flash values =================", cs); + ESP_LOGW(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------------------", + cs); + ESP_LOGW(TAG, "[CALIBRATION][%s] | Phase | offset_active_power|offset_reactive_power|", cs); + ESP_LOGW(TAG, "[CALIBRATION][%s] | | config | flash | config | flash |", cs); + ESP_LOGW(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------------------", + cs); + for (uint8_t phase = 0; phase < 3; ++phase) { + ESP_LOGW(TAG, "[CALIBRATION][%s] | %c | %6d | %6d | %6d | %6d |", cs, 'A' + phase, + this->config_power_offset_phase_[phase].active_power_offset, + this->power_offset_phase_[phase].active_power_offset, + this->config_power_offset_phase_[phase].reactive_power_offset, + this->power_offset_phase_[phase].reactive_power_offset); + } + ESP_LOGW(TAG, + "[CALIBRATION][%s] ===============================================================================", cs); + } + if (gain_mismatch) { + ESP_LOGW(TAG, "[CALIBRATION][%s] ", cs); + ESP_LOGW(TAG, + "[CALIBRATION][%s] ====================== Gain mismatch: using flash values =====================", cs); + ESP_LOGW(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------------------", + cs); + ESP_LOGW(TAG, "[CALIBRATION][%s] | Phase | voltage_gain | current_gain |", cs); + ESP_LOGW(TAG, "[CALIBRATION][%s] | | config | flash | config | flash |", cs); + ESP_LOGW(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------------------", + cs); + for (uint8_t phase = 0; phase < 3; ++phase) { + ESP_LOGW(TAG, "[CALIBRATION][%s] | %c | %6u | %6u | %6u | %6u |", cs, 'A' + phase, + this->config_gain_phase_[phase].voltage_gain, this->gain_phase_[phase].voltage_gain, + this->config_gain_phase_[phase].current_gain, this->gain_phase_[phase].current_gain); + } + ESP_LOGW(TAG, + "[CALIBRATION][%s] ===============================================================================", cs); + } + if (!this->enable_offset_calibration_) { + ESP_LOGI(TAG, "[CALIBRATION][%s] Power & Voltage/Current offset calibration is disabled. Using config file values.", + cs); + } else if (this->restored_offset_calibration_ && !offset_mismatch) { + ESP_LOGI(TAG, "[CALIBRATION][%s] ", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ============== Restored offset calibration from memory ==============", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_voltage | offset_current |", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs); + for (uint8_t phase = 0; phase < 3; phase++) { + ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, + this->offset_phase_[phase].voltage_offset_, this->offset_phase_[phase].current_offset_); + } + ESP_LOGI(TAG, "[CALIBRATION][%s] ==============================================================\\n", cs); + } + + if (this->restored_power_offset_calibration_ && !power_mismatch) { + ESP_LOGI(TAG, "[CALIBRATION][%s] ", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ============ Restored power offset calibration from memory ============", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_active_power | offset_reactive_power |", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs); + for (uint8_t phase = 0; phase < 3; phase++) { + ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, + this->power_offset_phase_[phase].active_power_offset, + this->power_offset_phase_[phase].reactive_power_offset); + } + ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\n", cs); + } + if (!this->enable_gain_calibration_) { + ESP_LOGI(TAG, "[CALIBRATION][%s] Gain calibration is disabled. Using config file values.", cs); + } else if (this->restored_gain_calibration_ && !gain_mismatch) { + ESP_LOGI(TAG, "[CALIBRATION][%s] ", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ============ Restoring saved gain calibrations to registers ============", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | voltage_gain | current_gain |", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs); + for (uint8_t phase = 0; phase < 3; phase++) { + ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6u | %6u |", cs, 'A' + phase, + this->gain_phase_[phase].voltage_gain, this->gain_phase_[phase].current_gain); + } + ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\\n", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] Gain calibration loaded and verified successfully.\n", cs); + } + this->calibration_message_printed_ = true; +} + void ATM90E32Component::dump_config() { ESP_LOGCONFIG("", "ATM90E32:"); LOG_PIN(" CS Pin: ", this->cs_); @@ -255,6 +371,10 @@ void ATM90E32Component::dump_config() { LOG_SENSOR(" ", "Peak Current C", this->phase_[PHASEC].peak_current_sensor_); LOG_SENSOR(" ", "Frequency", this->freq_sensor_); LOG_SENSOR(" ", "Chip Temp", this->chip_temperature_sensor_); + if (this->restored_offset_calibration_ || this->restored_power_offset_calibration_ || + this->restored_gain_calibration_ || !this->enable_offset_calibration_ || !this->enable_gain_calibration_) { + this->log_calibration_status_(); + } } float ATM90E32Component::get_setup_priority() const { return setup_priority::IO; } @@ -263,19 +383,17 @@ float ATM90E32Component::get_setup_priority() const { return setup_priority::IO; // Peakdetect period: 05H. Bit 15:8 are PeakDet_period in ms. 7:0 are Sag_period // Default is 143FH (20ms, 63ms) uint16_t ATM90E32Component::read16_(uint16_t a_register) { + this->enable(); + delay_microseconds_safe(1); // min delay between CS low and first SCK is 200ns - 1us is plenty uint8_t addrh = (1 << 7) | ((a_register >> 8) & 0x03); uint8_t addrl = (a_register & 0xFF); - uint8_t data[2]; - uint16_t output; - this->enable(); - delay_microseconds_safe(1); // min delay between CS low and first SCK is 200ns - 1ms is plenty - this->write_byte(addrh); - this->write_byte(addrl); - this->read_array(data, 2); - this->disable(); - - output = (uint16_t(data[0] & 0xFF) << 8) | (data[1] & 0xFF); + uint8_t data[4] = {addrh, addrl, 0x00, 0x00}; + this->transfer_array(data, 4); + uint16_t output = encode_uint16(data[2], data[3]); ESP_LOGVV(TAG, "read16_ 0x%04" PRIX16 " output 0x%04" PRIX16, a_register, output); + delay_microseconds_safe(1); // allow the last clock to propagate before releasing CS + this->disable(); + delay_microseconds_safe(1); // meet minimum CS high time before next transaction return output; } @@ -292,13 +410,19 @@ int ATM90E32Component::read32_(uint16_t addr_h, uint16_t addr_l) { return val; } -void ATM90E32Component::write16_(uint16_t a_register, uint16_t val) { +void ATM90E32Component::write16_(uint16_t a_register, uint16_t val, bool validate) { ESP_LOGVV(TAG, "write16_ 0x%04" PRIX16 " val 0x%04" PRIX16, a_register, val); + uint8_t addrh = ((a_register >> 8) & 0x03); + uint8_t addrl = (a_register & 0xFF); + uint8_t data[4] = {addrh, addrl, uint8_t((val >> 8) & 0xFF), uint8_t(val & 0xFF)}; this->enable(); - this->write_byte16(a_register); - this->write_byte16(val); + delay_microseconds_safe(1); // ensure CS setup time + this->write_array(data, 4); + delay_microseconds_safe(1); // allow clock to settle before raising CS this->disable(); - this->validate_spi_read_(val, "write16()"); + delay_microseconds_safe(1); // ensure minimum CS high time + if (validate) + this->validate_spi_read_(val, "write16()"); } float ATM90E32Component::get_local_phase_voltage_(uint8_t phase) { return this->phase_[phase].voltage_; } @@ -441,8 +565,10 @@ float ATM90E32Component::get_chip_temperature_() { } void ATM90E32Component::run_gain_calibrations() { + const char *cs = this->cs_summary_.c_str(); if (!this->enable_gain_calibration_) { - ESP_LOGW(TAG, "[CALIBRATION] Gain calibration is disabled! Enable it first with enable_gain_calibration: true"); + ESP_LOGW(TAG, "[CALIBRATION][%s] Gain calibration is disabled! Enable it first with enable_gain_calibration: true", + cs); return; } @@ -454,12 +580,14 @@ void ATM90E32Component::run_gain_calibrations() { float ref_currents[3] = {this->get_reference_current(0), this->get_reference_current(1), this->get_reference_current(2)}; - ESP_LOGI(TAG, "[CALIBRATION] "); - ESP_LOGI(TAG, "[CALIBRATION] ========================= Gain Calibration ========================="); - ESP_LOGI(TAG, "[CALIBRATION] ---------------------------------------------------------------------"); - ESP_LOGI(TAG, - "[CALIBRATION] | Phase | V_meas (V) | I_meas (A) | V_ref | I_ref | V_gain (old→new) | I_gain (old→new) |"); - ESP_LOGI(TAG, "[CALIBRATION] ---------------------------------------------------------------------"); + ESP_LOGI(TAG, "[CALIBRATION][%s] ", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ========================= Gain Calibration =========================", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs); + ESP_LOGI( + TAG, + "[CALIBRATION][%s] | Phase | V_meas (V) | I_meas (A) | V_ref | I_ref | V_gain (old→new) | I_gain (old→new) |", + cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs); for (uint8_t phase = 0; phase < 3; phase++) { float measured_voltage = this->get_phase_voltage_avg_(phase); @@ -476,22 +604,22 @@ void ATM90E32Component::run_gain_calibrations() { // Voltage calibration if (ref_voltage <= 0.0f) { - ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Skipping voltage calibration: reference voltage is 0.", + ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Skipping voltage calibration: reference voltage is 0.", cs, phase_labels[phase]); } else if (measured_voltage == 0.0f) { - ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Skipping voltage calibration: measured voltage is 0.", + ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Skipping voltage calibration: measured voltage is 0.", cs, phase_labels[phase]); } else { uint32_t new_voltage_gain = static_cast((ref_voltage / measured_voltage) * current_voltage_gain); if (new_voltage_gain == 0) { - ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Voltage gain would be 0. Check reference and measured voltage.", + ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Voltage gain would be 0. Check reference and measured voltage.", cs, phase_labels[phase]); } else { if (new_voltage_gain >= 65535) { - ESP_LOGW( - TAG, - "[CALIBRATION] Phase %s - Voltage gain exceeds 65535. You may need a higher output voltage transformer.", - phase_labels[phase]); + ESP_LOGW(TAG, + "[CALIBRATION][%s] Phase %s - Voltage gain exceeds 65535. You may need a higher output voltage " + "transformer.", + cs, phase_labels[phase]); new_voltage_gain = 65535; } this->gain_phase_[phase].voltage_gain = static_cast(new_voltage_gain); @@ -501,20 +629,20 @@ void ATM90E32Component::run_gain_calibrations() { // Current calibration if (ref_current == 0.0f) { - ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Skipping current calibration: reference current is 0.", + ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Skipping current calibration: reference current is 0.", cs, phase_labels[phase]); } else if (measured_current == 0.0f) { - ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Skipping current calibration: measured current is 0.", + ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Skipping current calibration: measured current is 0.", cs, phase_labels[phase]); } else { uint32_t new_current_gain = static_cast((ref_current / measured_current) * current_current_gain); if (new_current_gain == 0) { - ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Current gain would be 0. Check reference and measured current.", + ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Current gain would be 0. Check reference and measured current.", cs, phase_labels[phase]); } else { if (new_current_gain >= 65535) { - ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Current gain exceeds 65535. You may need to turn up pga gain.", - phase_labels[phase]); + ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Current gain exceeds 65535. You may need to turn up pga gain.", + cs, phase_labels[phase]); new_current_gain = 65535; } this->gain_phase_[phase].current_gain = static_cast(new_current_gain); @@ -523,13 +651,13 @@ void ATM90E32Component::run_gain_calibrations() { } // Final row output - ESP_LOGI(TAG, "[CALIBRATION] | %c | %9.2f | %9.4f | %5.2f | %6.4f | %5u → %-5u | %5u → %-5u |", + ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %9.2f | %9.4f | %5.2f | %6.4f | %5u → %-5u | %5u → %-5u |", cs, 'A' + phase, measured_voltage, measured_current, ref_voltage, ref_current, current_voltage_gain, did_voltage ? this->gain_phase_[phase].voltage_gain : current_voltage_gain, current_current_gain, did_current ? this->gain_phase_[phase].current_gain : current_current_gain); } - ESP_LOGI(TAG, "[CALIBRATION] =====================================================================\n"); + ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\n", cs); this->save_gain_calibration_to_memory_(); this->write_gains_to_registers_(); @@ -537,54 +665,108 @@ void ATM90E32Component::run_gain_calibrations() { } void ATM90E32Component::save_gain_calibration_to_memory_() { + const char *cs = this->cs_summary_.c_str(); bool success = this->gain_calibration_pref_.save(&this->gain_phase_); + global_preferences->sync(); if (success) { this->using_saved_calibrations_ = true; - ESP_LOGI(TAG, "[CALIBRATION] Gain calibration saved to memory."); + ESP_LOGI(TAG, "[CALIBRATION][%s] Gain calibration saved to memory.", cs); } else { this->using_saved_calibrations_ = false; - ESP_LOGE(TAG, "[CALIBRATION] Failed to save gain calibration to memory!"); + ESP_LOGE(TAG, "[CALIBRATION][%s] Failed to save gain calibration to memory!", cs); + } +} + +void ATM90E32Component::save_offset_calibration_to_memory_() { + const char *cs = this->cs_summary_.c_str(); + bool success = this->offset_pref_.save(&this->offset_phase_); + global_preferences->sync(); + if (success) { + this->using_saved_calibrations_ = true; + this->restored_offset_calibration_ = true; + for (bool &phase : this->offset_calibration_mismatch_) + phase = false; + ESP_LOGI(TAG, "[CALIBRATION][%s] Offset calibration saved to memory.", cs); + } else { + this->using_saved_calibrations_ = false; + ESP_LOGE(TAG, "[CALIBRATION][%s] Failed to save offset calibration to memory!", cs); + } +} + +void ATM90E32Component::save_power_offset_calibration_to_memory_() { + const char *cs = this->cs_summary_.c_str(); + bool success = this->power_offset_pref_.save(&this->power_offset_phase_); + global_preferences->sync(); + if (success) { + this->using_saved_calibrations_ = true; + this->restored_power_offset_calibration_ = true; + for (bool &phase : this->power_offset_calibration_mismatch_) + phase = false; + ESP_LOGI(TAG, "[CALIBRATION][%s] Power offset calibration saved to memory.", cs); + } else { + this->using_saved_calibrations_ = false; + ESP_LOGE(TAG, "[CALIBRATION][%s] Failed to save power offset calibration to memory!", cs); } } void ATM90E32Component::run_offset_calibrations() { + const char *cs = this->cs_summary_.c_str(); if (!this->enable_offset_calibration_) { - ESP_LOGW(TAG, "[CALIBRATION] Offset calibration is disabled! Enable it first with enable_offset_calibration: true"); + ESP_LOGW(TAG, + "[CALIBRATION][%s] Offset calibration is disabled! Enable it first with enable_offset_calibration: true", + cs); return; } + ESP_LOGI(TAG, "[CALIBRATION][%s] ", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ======================== Offset Calibration ========================", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_voltage | offset_current |", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------", cs); + for (uint8_t phase = 0; phase < 3; phase++) { int16_t voltage_offset = calibrate_offset(phase, true); int16_t current_offset = calibrate_offset(phase, false); this->write_offsets_to_registers_(phase, voltage_offset, current_offset); - ESP_LOGI(TAG, "[CALIBRATION] Phase %c - offset_voltage: %d, offset_current: %d", 'A' + phase, voltage_offset, + ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, voltage_offset, current_offset); } - this->offset_pref_.save(&this->offset_phase_); // Save to flash + ESP_LOGI(TAG, "[CALIBRATION][%s] ==================================================================\n", cs); + + this->save_offset_calibration_to_memory_(); } void ATM90E32Component::run_power_offset_calibrations() { + const char *cs = this->cs_summary_.c_str(); if (!this->enable_offset_calibration_) { ESP_LOGW( TAG, - "[CALIBRATION] Offset power calibration is disabled! Enable it first with enable_offset_calibration: true"); + "[CALIBRATION][%s] Offset power calibration is disabled! Enable it first with enable_offset_calibration: true", + cs); return; } + ESP_LOGI(TAG, "[CALIBRATION][%s] ", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ===================== Power Offset Calibration =====================", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_active_power | offset_reactive_power |", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs); + for (uint8_t phase = 0; phase < 3; ++phase) { int16_t active_offset = calibrate_power_offset(phase, false); int16_t reactive_offset = calibrate_power_offset(phase, true); this->write_power_offsets_to_registers_(phase, active_offset, reactive_offset); - ESP_LOGI(TAG, "[CALIBRATION] Phase %c - offset_active_power: %d, offset_reactive_power: %d", 'A' + phase, - active_offset, reactive_offset); + ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, active_offset, + reactive_offset); } + ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\n", cs); - this->power_offset_pref_.save(&this->power_offset_phase_); // Save to flash + this->save_power_offset_calibration_to_memory_(); } void ATM90E32Component::write_gains_to_registers_() { @@ -631,102 +813,276 @@ void ATM90E32Component::write_power_offsets_to_registers_(uint8_t phase, int16_t } void ATM90E32Component::restore_gain_calibrations_() { - if (this->gain_calibration_pref_.load(&this->gain_phase_)) { - ESP_LOGI(TAG, "[CALIBRATION] Restoring saved gain calibrations to registers:"); - - for (uint8_t phase = 0; phase < 3; phase++) { - uint16_t v_gain = this->gain_phase_[phase].voltage_gain; - uint16_t i_gain = this->gain_phase_[phase].current_gain; - ESP_LOGI(TAG, "[CALIBRATION] Phase %c - Voltage Gain: %u, Current Gain: %u", 'A' + phase, v_gain, i_gain); - } - - this->write_gains_to_registers_(); - - if (this->verify_gain_writes_()) { - this->using_saved_calibrations_ = true; - ESP_LOGI(TAG, "[CALIBRATION] Gain calibration loaded and verified successfully."); - } else { - this->using_saved_calibrations_ = false; - ESP_LOGE(TAG, "[CALIBRATION] Gain verification failed! Calibration may not be applied correctly."); - } - } else { - this->using_saved_calibrations_ = false; - ESP_LOGW(TAG, "[CALIBRATION] No stored gain calibrations found. Using config file values."); + const char *cs = this->cs_summary_.c_str(); + for (uint8_t i = 0; i < 3; ++i) { + this->config_gain_phase_[i].voltage_gain = this->phase_[i].voltage_gain_; + this->config_gain_phase_[i].current_gain = this->phase_[i].ct_gain_; + this->gain_phase_[i] = this->config_gain_phase_[i]; } + + if (this->gain_calibration_pref_.load(&this->gain_phase_)) { + bool all_zero = true; + bool same_as_config = true; + for (uint8_t phase = 0; phase < 3; ++phase) { + const auto &cfg = this->config_gain_phase_[phase]; + const auto &saved = this->gain_phase_[phase]; + if (saved.voltage_gain != 0 || saved.current_gain != 0) + all_zero = false; + if (saved.voltage_gain != cfg.voltage_gain || saved.current_gain != cfg.current_gain) + same_as_config = false; + } + + if (!all_zero && !same_as_config) { + for (uint8_t phase = 0; phase < 3; ++phase) { + bool mismatch = false; + if (this->has_config_voltage_gain_[phase] && + this->gain_phase_[phase].voltage_gain != this->config_gain_phase_[phase].voltage_gain) + mismatch = true; + if (this->has_config_current_gain_[phase] && + this->gain_phase_[phase].current_gain != this->config_gain_phase_[phase].current_gain) + mismatch = true; + if (mismatch) + this->gain_calibration_mismatch_[phase] = true; + } + + this->write_gains_to_registers_(); + + if (this->verify_gain_writes_()) { + this->using_saved_calibrations_ = true; + this->restored_gain_calibration_ = true; + return; + } + + this->using_saved_calibrations_ = false; + ESP_LOGE(TAG, "[CALIBRATION][%s] Gain verification failed! Calibration may not be applied correctly.", cs); + } + } + + this->using_saved_calibrations_ = false; + for (uint8_t i = 0; i < 3; ++i) + this->gain_phase_[i] = this->config_gain_phase_[i]; + this->write_gains_to_registers_(); + + ESP_LOGW(TAG, "[CALIBRATION][%s] No stored gain calibrations found. Using config file values.", cs); } void ATM90E32Component::restore_offset_calibrations_() { - if (this->offset_pref_.load(&this->offset_phase_)) { - ESP_LOGI(TAG, "[CALIBRATION] Successfully restored offset calibration from memory."); + const char *cs = this->cs_summary_.c_str(); + for (uint8_t i = 0; i < 3; ++i) + this->config_offset_phase_[i] = this->offset_phase_[i]; + bool have_data = this->offset_pref_.load(&this->offset_phase_); + bool all_zero = true; + if (have_data) { + for (auto &phase : this->offset_phase_) { + if (phase.voltage_offset_ != 0 || phase.current_offset_ != 0) { + all_zero = false; + break; + } + } + } + + if (have_data && !all_zero) { + this->restored_offset_calibration_ = true; for (uint8_t phase = 0; phase < 3; phase++) { auto &offset = this->offset_phase_[phase]; - write_offsets_to_registers_(phase, offset.voltage_offset_, offset.current_offset_); - ESP_LOGI(TAG, "[CALIBRATION] Phase %c - offset_voltage:: %d, offset_current: %d", 'A' + phase, - offset.voltage_offset_, offset.current_offset_); + bool mismatch = false; + if (this->has_config_voltage_offset_[phase] && + offset.voltage_offset_ != this->config_offset_phase_[phase].voltage_offset_) + mismatch = true; + if (this->has_config_current_offset_[phase] && + offset.current_offset_ != this->config_offset_phase_[phase].current_offset_) + mismatch = true; + if (mismatch) + this->offset_calibration_mismatch_[phase] = true; } } else { - ESP_LOGW(TAG, "[CALIBRATION] No stored offset calibrations found. Using default values."); + for (uint8_t phase = 0; phase < 3; phase++) + this->offset_phase_[phase] = this->config_offset_phase_[phase]; + ESP_LOGW(TAG, "[CALIBRATION][%s] No stored offset calibrations found. Using default values.", cs); + } + + for (uint8_t phase = 0; phase < 3; phase++) { + write_offsets_to_registers_(phase, this->offset_phase_[phase].voltage_offset_, + this->offset_phase_[phase].current_offset_); } } void ATM90E32Component::restore_power_offset_calibrations_() { - if (this->power_offset_pref_.load(&this->power_offset_phase_)) { - ESP_LOGI(TAG, "[CALIBRATION] Successfully restored power offset calibration from memory."); + const char *cs = this->cs_summary_.c_str(); + for (uint8_t i = 0; i < 3; ++i) + this->config_power_offset_phase_[i] = this->power_offset_phase_[i]; + bool have_data = this->power_offset_pref_.load(&this->power_offset_phase_); + bool all_zero = true; + if (have_data) { + for (auto &phase : this->power_offset_phase_) { + if (phase.active_power_offset != 0 || phase.reactive_power_offset != 0) { + all_zero = false; + break; + } + } + } + + if (have_data && !all_zero) { + this->restored_power_offset_calibration_ = true; for (uint8_t phase = 0; phase < 3; ++phase) { auto &offset = this->power_offset_phase_[phase]; - write_power_offsets_to_registers_(phase, offset.active_power_offset, offset.reactive_power_offset); - ESP_LOGI(TAG, "[CALIBRATION] Phase %c - offset_active_power: %d, offset_reactive_power: %d", 'A' + phase, - offset.active_power_offset, offset.reactive_power_offset); + bool mismatch = false; + if (this->has_config_active_power_offset_[phase] && + offset.active_power_offset != this->config_power_offset_phase_[phase].active_power_offset) + mismatch = true; + if (this->has_config_reactive_power_offset_[phase] && + offset.reactive_power_offset != this->config_power_offset_phase_[phase].reactive_power_offset) + mismatch = true; + if (mismatch) + this->power_offset_calibration_mismatch_[phase] = true; } } else { - ESP_LOGW(TAG, "[CALIBRATION] No stored power offsets found. Using default values."); + for (uint8_t phase = 0; phase < 3; ++phase) + this->power_offset_phase_[phase] = this->config_power_offset_phase_[phase]; + ESP_LOGW(TAG, "[CALIBRATION][%s] No stored power offsets found. Using default values.", cs); + } + + for (uint8_t phase = 0; phase < 3; ++phase) { + write_power_offsets_to_registers_(phase, this->power_offset_phase_[phase].active_power_offset, + this->power_offset_phase_[phase].reactive_power_offset); } } void ATM90E32Component::clear_gain_calibrations() { - ESP_LOGI(TAG, "[CALIBRATION] Clearing stored gain calibrations and restoring config-defined values"); - - for (int phase = 0; phase < 3; phase++) { - gain_phase_[phase].voltage_gain = this->phase_[phase].voltage_gain_; - gain_phase_[phase].current_gain = this->phase_[phase].ct_gain_; + const char *cs = this->cs_summary_.c_str(); + if (!this->using_saved_calibrations_) { + ESP_LOGI(TAG, "[CALIBRATION][%s] No stored gain calibrations to clear. Current values:", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ----------------------------------------------------------", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | voltage_gain | current_gain |", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ----------------------------------------------------------", cs); + for (int phase = 0; phase < 3; phase++) { + ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6u | %6u |", cs, 'A' + phase, + this->gain_phase_[phase].voltage_gain, this->gain_phase_[phase].current_gain); + } + ESP_LOGI(TAG, "[CALIBRATION][%s] ==========================================================\n", cs); + return; } - bool success = this->gain_calibration_pref_.save(&this->gain_phase_); - this->using_saved_calibrations_ = false; + ESP_LOGI(TAG, "[CALIBRATION][%s] Clearing stored gain calibrations and restoring config-defined values", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ----------------------------------------------------------", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | voltage_gain | current_gain |", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ----------------------------------------------------------", cs); - if (success) { - ESP_LOGI(TAG, "[CALIBRATION] Gain calibrations cleared. Config values restored:"); - for (int phase = 0; phase < 3; phase++) { - ESP_LOGI(TAG, "[CALIBRATION] Phase %c - Voltage Gain: %u, Current Gain: %u", 'A' + phase, - gain_phase_[phase].voltage_gain, gain_phase_[phase].current_gain); - } - } else { - ESP_LOGE(TAG, "[CALIBRATION] Failed to clear gain calibrations!"); + for (int phase = 0; phase < 3; phase++) { + uint16_t voltage_gain = this->phase_[phase].voltage_gain_; + uint16_t current_gain = this->phase_[phase].ct_gain_; + + this->config_gain_phase_[phase].voltage_gain = voltage_gain; + this->config_gain_phase_[phase].current_gain = current_gain; + this->gain_phase_[phase].voltage_gain = voltage_gain; + this->gain_phase_[phase].current_gain = current_gain; + + ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6u | %6u |", cs, 'A' + phase, voltage_gain, current_gain); + } + ESP_LOGI(TAG, "[CALIBRATION][%s] ==========================================================\n", cs); + + GainCalibration zero_gains[3]{{0, 0}, {0, 0}, {0, 0}}; + bool success = this->gain_calibration_pref_.save(&zero_gains); + global_preferences->sync(); + + this->using_saved_calibrations_ = false; + this->restored_gain_calibration_ = false; + for (bool &phase : this->gain_calibration_mismatch_) + phase = false; + + if (!success) { + ESP_LOGE(TAG, "[CALIBRATION][%s] Failed to clear gain calibrations!", cs); } this->write_gains_to_registers_(); // Apply them to the chip immediately } void ATM90E32Component::clear_offset_calibrations() { - for (uint8_t phase = 0; phase < 3; phase++) { - this->write_offsets_to_registers_(phase, 0, 0); + const char *cs = this->cs_summary_.c_str(); + if (!this->restored_offset_calibration_) { + ESP_LOGI(TAG, "[CALIBRATION][%s] No stored offset calibrations to clear. Current values:", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_voltage | offset_current |", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs); + for (uint8_t phase = 0; phase < 3; phase++) { + ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, + this->offset_phase_[phase].voltage_offset_, this->offset_phase_[phase].current_offset_); + } + ESP_LOGI(TAG, "[CALIBRATION][%s] ==============================================================\n", cs); + return; } - this->offset_pref_.save(&this->offset_phase_); // Save cleared values to flash memory + ESP_LOGI(TAG, "[CALIBRATION][%s] Clearing stored offset calibrations and restoring config-defined values", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_voltage | offset_current |", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs); - ESP_LOGI(TAG, "[CALIBRATION] Offsets cleared."); + for (uint8_t phase = 0; phase < 3; phase++) { + int16_t voltage_offset = + this->has_config_voltage_offset_[phase] ? this->config_offset_phase_[phase].voltage_offset_ : 0; + int16_t current_offset = + this->has_config_current_offset_[phase] ? this->config_offset_phase_[phase].current_offset_ : 0; + this->write_offsets_to_registers_(phase, voltage_offset, current_offset); + ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, voltage_offset, + current_offset); + } + ESP_LOGI(TAG, "[CALIBRATION][%s] ==============================================================\n", cs); + + OffsetCalibration zero_offsets[3]{{0, 0}, {0, 0}, {0, 0}}; + this->offset_pref_.save(&zero_offsets); // Clear stored values in flash + global_preferences->sync(); + + this->restored_offset_calibration_ = false; + for (bool &phase : this->offset_calibration_mismatch_) + phase = false; + + ESP_LOGI(TAG, "[CALIBRATION][%s] Offsets cleared.", cs); } void ATM90E32Component::clear_power_offset_calibrations() { - for (uint8_t phase = 0; phase < 3; phase++) { - this->write_power_offsets_to_registers_(phase, 0, 0); + const char *cs = this->cs_summary_.c_str(); + if (!this->restored_power_offset_calibration_) { + ESP_LOGI(TAG, "[CALIBRATION][%s] No stored power offsets to clear. Current values:", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_active_power | offset_reactive_power |", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs); + for (uint8_t phase = 0; phase < 3; phase++) { + ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, + this->power_offset_phase_[phase].active_power_offset, + this->power_offset_phase_[phase].reactive_power_offset); + } + ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\n", cs); + return; } - this->power_offset_pref_.save(&this->power_offset_phase_); + ESP_LOGI(TAG, "[CALIBRATION][%s] Clearing stored power offsets and restoring config-defined values", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_active_power | offset_reactive_power |", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs); - ESP_LOGI(TAG, "[CALIBRATION] Power offsets cleared."); + for (uint8_t phase = 0; phase < 3; phase++) { + int16_t active_offset = + this->has_config_active_power_offset_[phase] ? this->config_power_offset_phase_[phase].active_power_offset : 0; + int16_t reactive_offset = this->has_config_reactive_power_offset_[phase] + ? this->config_power_offset_phase_[phase].reactive_power_offset + : 0; + this->write_power_offsets_to_registers_(phase, active_offset, reactive_offset); + ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, active_offset, + reactive_offset); + } + ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\n", cs); + + PowerOffsetCalibration zero_power_offsets[3]{{0, 0}, {0, 0}, {0, 0}}; + this->power_offset_pref_.save(&zero_power_offsets); + global_preferences->sync(); + + this->restored_power_offset_calibration_ = false; + for (bool &phase : this->power_offset_calibration_mismatch_) + phase = false; + + ESP_LOGI(TAG, "[CALIBRATION][%s] Power offsets cleared.", cs); } int16_t ATM90E32Component::calibrate_offset(uint8_t phase, bool voltage) { @@ -747,20 +1103,21 @@ int16_t ATM90E32Component::calibrate_offset(uint8_t phase, bool voltage) { int16_t ATM90E32Component::calibrate_power_offset(uint8_t phase, bool reactive) { const uint8_t num_reads = 5; - uint64_t total_value = 0; + int64_t total_value = 0; for (uint8_t i = 0; i < num_reads; ++i) { - uint32_t reading = reactive ? this->read32_(ATM90E32_REGISTER_QMEAN + phase, ATM90E32_REGISTER_QMEANLSB + phase) - : this->read32_(ATM90E32_REGISTER_PMEAN + phase, ATM90E32_REGISTER_PMEANLSB + phase); + int32_t reading = reactive ? this->read32_(ATM90E32_REGISTER_QMEAN + phase, ATM90E32_REGISTER_QMEANLSB + phase) + : this->read32_(ATM90E32_REGISTER_PMEAN + phase, ATM90E32_REGISTER_PMEANLSB + phase); total_value += reading; } - const uint32_t average_value = total_value / num_reads; - const uint32_t power_offset = ~average_value + 1; + int32_t average_value = total_value / num_reads; + int32_t power_offset = -average_value; return static_cast(power_offset); // Takes the lower 16 bits } bool ATM90E32Component::verify_gain_writes_() { + const char *cs = this->cs_summary_.c_str(); bool success = true; for (uint8_t phase = 0; phase < 3; phase++) { uint16_t read_voltage = this->read16_(voltage_gain_registers[phase]); @@ -768,7 +1125,7 @@ bool ATM90E32Component::verify_gain_writes_() { if (read_voltage != this->gain_phase_[phase].voltage_gain || read_current != this->gain_phase_[phase].current_gain) { - ESP_LOGE(TAG, "[CALIBRATION] Mismatch detected for Phase %s!", phase_labels[phase]); + ESP_LOGE(TAG, "[CALIBRATION][%s] Mismatch detected for Phase %s!", cs, phase_labels[phase]); success = false; } } @@ -791,16 +1148,16 @@ void ATM90E32Component::check_phase_status() { status += "Phase Loss; "; auto *sensor = this->phase_status_text_sensor_[phase]; - const char *phase_name = sensor ? sensor->get_name().c_str() : "Unknown Phase"; + if (sensor == nullptr) + continue; + if (!status.empty()) { status.pop_back(); // remove space status.pop_back(); // remove semicolon - ESP_LOGW(TAG, "%s: %s", phase_name, status.c_str()); - if (sensor != nullptr) - sensor->publish_state(status); + ESP_LOGW(TAG, "%s: %s", sensor->get_name().c_str(), status.c_str()); + sensor->publish_state(status); } else { - if (sensor != nullptr) - sensor->publish_state("Okay"); + sensor->publish_state("Okay"); } } } @@ -817,9 +1174,12 @@ void ATM90E32Component::check_freq_status() { } else { freq_status = "Normal"; } - ESP_LOGW(TAG, "Frequency status: %s", freq_status.c_str()); - if (this->freq_status_text_sensor_ != nullptr) { + if (freq_status == "Normal") { + ESP_LOGD(TAG, "Frequency status: %s", freq_status.c_str()); + } else { + ESP_LOGW(TAG, "Frequency status: %s", freq_status.c_str()); + } this->freq_status_text_sensor_->publish_state(freq_status); } } diff --git a/esphome/components/atm90e32/atm90e32.h b/esphome/components/atm90e32/atm90e32.h index 0703c40ae0..938ce512ce 100644 --- a/esphome/components/atm90e32/atm90e32.h +++ b/esphome/components/atm90e32/atm90e32.h @@ -61,15 +61,29 @@ class ATM90E32Component : public PollingComponent, this->phase_[phase].harmonic_active_power_sensor_ = obj; } void set_peak_current_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].peak_current_sensor_ = obj; } - void set_volt_gain(int phase, uint16_t gain) { this->phase_[phase].voltage_gain_ = gain; } - void set_ct_gain(int phase, uint16_t gain) { this->phase_[phase].ct_gain_ = gain; } - void set_voltage_offset(uint8_t phase, int16_t offset) { this->offset_phase_[phase].voltage_offset_ = offset; } - void set_current_offset(uint8_t phase, int16_t offset) { this->offset_phase_[phase].current_offset_ = offset; } + void set_volt_gain(int phase, uint16_t gain) { + this->phase_[phase].voltage_gain_ = gain; + this->has_config_voltage_gain_[phase] = true; + } + void set_ct_gain(int phase, uint16_t gain) { + this->phase_[phase].ct_gain_ = gain; + this->has_config_current_gain_[phase] = true; + } + void set_voltage_offset(uint8_t phase, int16_t offset) { + this->offset_phase_[phase].voltage_offset_ = offset; + this->has_config_voltage_offset_[phase] = true; + } + void set_current_offset(uint8_t phase, int16_t offset) { + this->offset_phase_[phase].current_offset_ = offset; + this->has_config_current_offset_[phase] = true; + } void set_active_power_offset(uint8_t phase, int16_t offset) { this->power_offset_phase_[phase].active_power_offset = offset; + this->has_config_active_power_offset_[phase] = true; } void set_reactive_power_offset(uint8_t phase, int16_t offset) { this->power_offset_phase_[phase].reactive_power_offset = offset; + this->has_config_reactive_power_offset_[phase] = true; } void set_freq_sensor(sensor::Sensor *freq_sensor) { freq_sensor_ = freq_sensor; } void set_peak_current_signed(bool flag) { peak_current_signed_ = flag; } @@ -127,7 +141,7 @@ class ATM90E32Component : public PollingComponent, #endif uint16_t read16_(uint16_t a_register); int read32_(uint16_t addr_h, uint16_t addr_l); - void write16_(uint16_t a_register, uint16_t val); + void write16_(uint16_t a_register, uint16_t val, bool validate = true); float get_local_phase_voltage_(uint8_t phase); float get_local_phase_current_(uint8_t phase); float get_local_phase_active_power_(uint8_t phase); @@ -159,12 +173,15 @@ class ATM90E32Component : public PollingComponent, void restore_offset_calibrations_(); void restore_power_offset_calibrations_(); void restore_gain_calibrations_(); + void save_offset_calibration_to_memory_(); void save_gain_calibration_to_memory_(); + void save_power_offset_calibration_to_memory_(); void write_offsets_to_registers_(uint8_t phase, int16_t voltage_offset, int16_t current_offset); void write_power_offsets_to_registers_(uint8_t phase, int16_t p_offset, int16_t q_offset); void write_gains_to_registers_(); bool verify_gain_writes_(); bool validate_spi_read_(uint16_t expected, const char *context = nullptr); + void log_calibration_status_(); struct ATM90E32Phase { uint16_t voltage_gain_{0}; @@ -204,19 +221,33 @@ class ATM90E32Component : public PollingComponent, int16_t current_offset_{0}; } offset_phase_[3]; + OffsetCalibration config_offset_phase_[3]; + struct PowerOffsetCalibration { int16_t active_power_offset{0}; int16_t reactive_power_offset{0}; } power_offset_phase_[3]; + PowerOffsetCalibration config_power_offset_phase_[3]; + struct GainCalibration { uint16_t voltage_gain{1}; uint16_t current_gain{1}; } gain_phase_[3]; + GainCalibration config_gain_phase_[3]; + + bool has_config_voltage_offset_[3]{false, false, false}; + bool has_config_current_offset_[3]{false, false, false}; + bool has_config_active_power_offset_[3]{false, false, false}; + bool has_config_reactive_power_offset_[3]{false, false, false}; + bool has_config_voltage_gain_[3]{false, false, false}; + bool has_config_current_gain_[3]{false, false, false}; + ESPPreferenceObject offset_pref_; ESPPreferenceObject power_offset_pref_; ESPPreferenceObject gain_calibration_pref_; + std::string cs_summary_; sensor::Sensor *freq_sensor_{nullptr}; #ifdef USE_TEXT_SENSOR @@ -231,6 +262,13 @@ class ATM90E32Component : public PollingComponent, bool peak_current_signed_{false}; bool enable_offset_calibration_{false}; bool enable_gain_calibration_{false}; + bool restored_offset_calibration_{false}; + bool restored_power_offset_calibration_{false}; + bool restored_gain_calibration_{false}; + bool calibration_message_printed_{false}; + bool offset_calibration_mismatch_[3]{false, false, false}; + bool power_offset_calibration_mismatch_[3]{false, false, false}; + bool gain_calibration_mismatch_[3]{false, false, false}; }; } // namespace atm90e32 diff --git a/esphome/components/atm90e32/sensor.py b/esphome/components/atm90e32/sensor.py index 7cdbd69f56..a510095217 100644 --- a/esphome/components/atm90e32/sensor.py +++ b/esphome/components/atm90e32/sensor.py @@ -17,10 +17,12 @@ from esphome.const import ( CONF_REACTIVE_POWER, CONF_REVERSE_ACTIVE_ENERGY, CONF_VOLTAGE, + DEVICE_CLASS_APPARENT_POWER, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, DEVICE_CLASS_POWER_FACTOR, + DEVICE_CLASS_REACTIVE_POWER, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, ENTITY_CATEGORY_DIAGNOSTIC, @@ -100,13 +102,13 @@ ATM90E32_PHASE_SCHEMA = cv.Schema( unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, icon=ICON_LIGHTBULB, accuracy_decimals=2, - device_class=DEVICE_CLASS_POWER, + device_class=DEVICE_CLASS_REACTIVE_POWER, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_APPARENT_POWER): sensor.sensor_schema( unit_of_measurement=UNIT_VOLT_AMPS, accuracy_decimals=2, - device_class=DEVICE_CLASS_POWER, + device_class=DEVICE_CLASS_APPARENT_POWER, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema( diff --git a/esphome/components/audio/__init__.py b/esphome/components/audio/__init__.py index f657cb5da3..7b03e4b6a7 100644 --- a/esphome/components/audio/__init__.py +++ b/esphome/components/audio/__init__.py @@ -165,4 +165,4 @@ def final_validate_audio_schema( async def to_code(config): - cg.add_library("esphome/esp-audio-libs", "1.1.4") + cg.add_library("esphome/esp-audio-libs", "2.0.1") diff --git a/esphome/components/audio/audio.cpp b/esphome/components/audio/audio.cpp index 2a58c38ac7..9cc9b7d0da 100644 --- a/esphome/components/audio/audio.cpp +++ b/esphome/components/audio/audio.cpp @@ -57,7 +57,7 @@ const char *audio_file_type_to_string(AudioFileType file_type) { void scale_audio_samples(const int16_t *audio_samples, int16_t *output_buffer, int16_t scale_factor, size_t samples_to_scale) { // Note the assembly dsps_mulc function has audio glitches if the input and output buffers are the same. - for (int i = 0; i < samples_to_scale; i++) { + for (size_t i = 0; i < samples_to_scale; i++) { int32_t acc = (int32_t) audio_samples[i] * (int32_t) scale_factor; output_buffer[i] = (int16_t) (acc >> 15); } diff --git a/esphome/components/audio/audio_decoder.cpp b/esphome/components/audio/audio_decoder.cpp index 90ba1aec1e..d1ad571a52 100644 --- a/esphome/components/audio/audio_decoder.cpp +++ b/esphome/components/audio/audio_decoder.cpp @@ -229,18 +229,18 @@ FileDecoderState AudioDecoder::decode_flac_() { auto result = this->flac_decoder_->read_header(this->input_transfer_buffer_->get_buffer_start(), this->input_transfer_buffer_->available()); - if (result == esp_audio_libs::flac::FLAC_DECODER_HEADER_OUT_OF_DATA) { - return FileDecoderState::POTENTIALLY_FAILED; - } - - if (result != esp_audio_libs::flac::FLAC_DECODER_SUCCESS) { - // Couldn't read FLAC header + if (result > esp_audio_libs::flac::FLAC_DECODER_HEADER_OUT_OF_DATA) { + // Serrious error reading FLAC header, there is no recovery return FileDecoderState::FAILED; } size_t bytes_consumed = this->flac_decoder_->get_bytes_index(); this->input_transfer_buffer_->decrease_buffer_length(bytes_consumed); + if (result == esp_audio_libs::flac::FLAC_DECODER_HEADER_OUT_OF_DATA) { + return FileDecoderState::MORE_TO_PROCESS; + } + // Reallocate the output transfer buffer to the smallest necessary size this->free_buffer_required_ = flac_decoder_->get_output_buffer_size_bytes(); if (!this->output_transfer_buffer_->reallocate(this->free_buffer_required_)) { @@ -256,9 +256,9 @@ FileDecoderState AudioDecoder::decode_flac_() { } uint32_t output_samples = 0; - auto result = this->flac_decoder_->decode_frame( - this->input_transfer_buffer_->get_buffer_start(), this->input_transfer_buffer_->available(), - reinterpret_cast(this->output_transfer_buffer_->get_buffer_end()), &output_samples); + auto result = this->flac_decoder_->decode_frame(this->input_transfer_buffer_->get_buffer_start(), + this->input_transfer_buffer_->available(), + this->output_transfer_buffer_->get_buffer_end(), &output_samples); if (result == esp_audio_libs::flac::FLAC_DECODER_ERROR_OUT_OF_DATA) { // Not an issue, just needs more data that we'll get next time. diff --git a/esphome/components/audio_adc/__init__.py b/esphome/components/audio_adc/__init__.py index dd3c958821..2f95a039f5 100644 --- a/esphome/components/audio_adc/__init__.py +++ b/esphome/components/audio_adc/__init__.py @@ -2,7 +2,7 @@ from esphome import automation import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_MIC_GAIN -from esphome.core import coroutine_with_priority +from esphome.core import CoroPriority, coroutine_with_priority CODEOWNERS = ["@kbx81"] IS_PLATFORM_COMPONENT = True @@ -35,7 +35,7 @@ async def audio_adc_set_mic_gain_to_code(config, action_id, template_arg, args): return var -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_define("USE_AUDIO_ADC") cg.add_global(audio_adc_ns.using) diff --git a/esphome/components/audio_adc/automation.h b/esphome/components/audio_adc/automation.h index 1b0bc2a6ad..0c42468479 100644 --- a/esphome/components/audio_adc/automation.h +++ b/esphome/components/audio_adc/automation.h @@ -13,7 +13,7 @@ template class SetMicGainAction : public Action { TEMPLATABLE_VALUE(float, mic_gain) - void play(Ts... x) override { this->audio_adc_->set_mic_gain(this->mic_gain_.value(x...)); } + void play(const Ts &...x) override { this->audio_adc_->set_mic_gain(this->mic_gain_.value(x...)); } protected: AudioAdc *audio_adc_; diff --git a/esphome/components/audio_dac/__init__.py b/esphome/components/audio_dac/__init__.py index 978ed195bd..92e6cb18fa 100644 --- a/esphome/components/audio_dac/__init__.py +++ b/esphome/components/audio_dac/__init__.py @@ -3,7 +3,7 @@ from esphome.automation import maybe_simple_id import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_VOLUME -from esphome.core import coroutine_with_priority +from esphome.core import CoroPriority, coroutine_with_priority CODEOWNERS = ["@kbx81"] IS_PLATFORM_COMPONENT = True @@ -51,7 +51,7 @@ async def audio_dac_set_volume_to_code(config, action_id, template_arg, args): return var -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_define("USE_AUDIO_DAC") cg.add_global(audio_dac_ns.using) diff --git a/esphome/components/audio_dac/automation.h b/esphome/components/audio_dac/automation.h index b6cf2acaf4..3eb3441f3d 100644 --- a/esphome/components/audio_dac/automation.h +++ b/esphome/components/audio_dac/automation.h @@ -11,7 +11,7 @@ template class MuteOffAction : public Action { public: explicit MuteOffAction(AudioDac *audio_dac) : audio_dac_(audio_dac) {} - void play(Ts... x) override { this->audio_dac_->set_mute_off(); } + void play(const Ts &...x) override { this->audio_dac_->set_mute_off(); } protected: AudioDac *audio_dac_; @@ -21,7 +21,7 @@ template class MuteOnAction : public Action { public: explicit MuteOnAction(AudioDac *audio_dac) : audio_dac_(audio_dac) {} - void play(Ts... x) override { this->audio_dac_->set_mute_on(); } + void play(const Ts &...x) override { this->audio_dac_->set_mute_on(); } protected: AudioDac *audio_dac_; @@ -33,7 +33,7 @@ template class SetVolumeAction : public Action { TEMPLATABLE_VALUE(float, volume) - void play(Ts... x) override { this->audio_dac_->set_volume(this->volume_.value(x...)); } + void play(const Ts &...x) override { this->audio_dac_->set_volume(this->volume_.value(x...)); } protected: AudioDac *audio_dac_; diff --git a/esphome/components/axs15231/touchscreen/axs15231_touchscreen.cpp b/esphome/components/axs15231/touchscreen/axs15231_touchscreen.cpp index 4adf0bbbe0..ab3f1dad4f 100644 --- a/esphome/components/axs15231/touchscreen/axs15231_touchscreen.cpp +++ b/esphome/components/axs15231/touchscreen/axs15231_touchscreen.cpp @@ -12,7 +12,7 @@ constexpr static const uint8_t AXS_READ_TOUCHPAD[11] = {0xb5, 0xab, 0xa5, 0x5a, #define ERROR_CHECK(err) \ if ((err) != i2c::ERROR_OK) { \ - this->status_set_warning("Failed to communicate"); \ + this->status_set_warning(LOG_STR("Failed to communicate")); \ return; \ } @@ -41,7 +41,7 @@ void AXS15231Touchscreen::update_touches() { i2c::ErrorCode err; uint8_t data[8]{}; - err = this->write(AXS_READ_TOUCHPAD, sizeof(AXS_READ_TOUCHPAD), false); + err = this->write(AXS_READ_TOUCHPAD, sizeof(AXS_READ_TOUCHPAD)); ERROR_CHECK(err); err = this->read(data, sizeof(data)); ERROR_CHECK(err); diff --git a/esphome/components/bang_bang/bang_bang_climate.cpp b/esphome/components/bang_bang/bang_bang_climate.cpp index bb85b49238..f26377a38a 100644 --- a/esphome/components/bang_bang/bang_bang_climate.cpp +++ b/esphome/components/bang_bang/bang_bang_climate.cpp @@ -6,6 +6,9 @@ namespace bang_bang { static const char *const TAG = "bang_bang.climate"; +BangBangClimate::BangBangClimate() + : idle_trigger_(new Trigger<>()), cool_trigger_(new Trigger<>()), heat_trigger_(new Trigger<>()) {} + void BangBangClimate::setup() { this->sensor_->add_on_state_callback([this](float state) { this->current_temperature = state; @@ -31,53 +34,63 @@ void BangBangClimate::setup() { restore->to_call(this).perform(); } else { // restore from defaults, change_away handles those for us - if (supports_cool_ && supports_heat_) { + if (this->supports_cool_ && this->supports_heat_) { this->mode = climate::CLIMATE_MODE_HEAT_COOL; - } else if (supports_cool_) { + } else if (this->supports_cool_) { this->mode = climate::CLIMATE_MODE_COOL; - } else if (supports_heat_) { + } else if (this->supports_heat_) { this->mode = climate::CLIMATE_MODE_HEAT; } this->change_away_(false); } } + void BangBangClimate::control(const climate::ClimateCall &call) { - if (call.get_mode().has_value()) + if (call.get_mode().has_value()) { this->mode = *call.get_mode(); - if (call.get_target_temperature_low().has_value()) + } + if (call.get_target_temperature_low().has_value()) { this->target_temperature_low = *call.get_target_temperature_low(); - if (call.get_target_temperature_high().has_value()) + } + if (call.get_target_temperature_high().has_value()) { this->target_temperature_high = *call.get_target_temperature_high(); - if (call.get_preset().has_value()) + } + if (call.get_preset().has_value()) { this->change_away_(*call.get_preset() == climate::CLIMATE_PRESET_AWAY); + } this->compute_state_(); this->publish_state(); } + climate::ClimateTraits BangBangClimate::traits() { auto traits = climate::ClimateTraits(); - traits.set_supports_current_temperature(true); - if (this->humidity_sensor_ != nullptr) - traits.set_supports_current_humidity(true); + traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE | + climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE | climate::CLIMATE_SUPPORTS_ACTION); + if (this->humidity_sensor_ != nullptr) { + traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY); + } traits.set_supported_modes({ climate::CLIMATE_MODE_OFF, }); - if (supports_cool_) + if (this->supports_cool_) { traits.add_supported_mode(climate::CLIMATE_MODE_COOL); - if (supports_heat_) + } + if (this->supports_heat_) { traits.add_supported_mode(climate::CLIMATE_MODE_HEAT); - if (supports_cool_ && supports_heat_) + } + if (this->supports_cool_ && this->supports_heat_) { traits.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL); - traits.set_supports_two_point_target_temperature(true); - if (supports_away_) { + } + if (this->supports_away_) { traits.set_supported_presets({ climate::CLIMATE_PRESET_HOME, climate::CLIMATE_PRESET_AWAY, }); } - traits.set_supports_action(true); return traits; } + void BangBangClimate::compute_state_() { if (this->mode == climate::CLIMATE_MODE_OFF) { this->switch_to_action_(climate::CLIMATE_ACTION_OFF); @@ -122,6 +135,7 @@ void BangBangClimate::compute_state_() { this->switch_to_action_(target_action); } + void BangBangClimate::switch_to_action_(climate::ClimateAction action) { if (action == this->action) { // already in target mode @@ -166,6 +180,7 @@ void BangBangClimate::switch_to_action_(climate::ClimateAction action) { this->prev_trigger_ = trig; this->publish_state(); } + void BangBangClimate::change_away_(bool away) { if (!away) { this->target_temperature_low = this->normal_config_.default_temperature_low; @@ -176,22 +191,26 @@ void BangBangClimate::change_away_(bool away) { } this->preset = away ? climate::CLIMATE_PRESET_AWAY : climate::CLIMATE_PRESET_HOME; } + void BangBangClimate::set_normal_config(const BangBangClimateTargetTempConfig &normal_config) { this->normal_config_ = normal_config; } + void BangBangClimate::set_away_config(const BangBangClimateTargetTempConfig &away_config) { this->supports_away_ = true; this->away_config_ = away_config; } -BangBangClimate::BangBangClimate() - : idle_trigger_(new Trigger<>()), cool_trigger_(new Trigger<>()), heat_trigger_(new Trigger<>()) {} + void BangBangClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } void BangBangClimate::set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; } + Trigger<> *BangBangClimate::get_idle_trigger() const { return this->idle_trigger_; } Trigger<> *BangBangClimate::get_cool_trigger() const { return this->cool_trigger_; } -void BangBangClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } Trigger<> *BangBangClimate::get_heat_trigger() const { return this->heat_trigger_; } + +void BangBangClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } void BangBangClimate::set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; } + void BangBangClimate::dump_config() { LOG_CLIMATE("", "Bang Bang Climate", this); ESP_LOGCONFIG(TAG, diff --git a/esphome/components/bang_bang/bang_bang_climate.h b/esphome/components/bang_bang/bang_bang_climate.h index 96368af34c..2e7da93a07 100644 --- a/esphome/components/bang_bang/bang_bang_climate.h +++ b/esphome/components/bang_bang/bang_bang_climate.h @@ -25,14 +25,15 @@ class BangBangClimate : public climate::Climate, public Component { void set_sensor(sensor::Sensor *sensor); void set_humidity_sensor(sensor::Sensor *humidity_sensor); - Trigger<> *get_idle_trigger() const; - Trigger<> *get_cool_trigger() const; void set_supports_cool(bool supports_cool); - Trigger<> *get_heat_trigger() const; void set_supports_heat(bool supports_heat); void set_normal_config(const BangBangClimateTargetTempConfig &normal_config); void set_away_config(const BangBangClimateTargetTempConfig &away_config); + Trigger<> *get_idle_trigger() const; + Trigger<> *get_cool_trigger() const; + Trigger<> *get_heat_trigger() const; + protected: /// Override control to change settings of the climate device. void control(const climate::ClimateCall &call) override; @@ -56,16 +57,10 @@ class BangBangClimate : public climate::Climate, public Component { * * In idle mode, the controller is assumed to have both heating and cooling disabled. */ - Trigger<> *idle_trigger_; + Trigger<> *idle_trigger_{nullptr}; /** The trigger to call when the controller should switch to cooling mode. */ - Trigger<> *cool_trigger_; - /** Whether the controller supports cooling. - * - * A false value for this attribute means that the controller has no cooling action - * (for example a thermostat, where only heating and not-heating is possible). - */ - bool supports_cool_{false}; + Trigger<> *cool_trigger_{nullptr}; /** The trigger to call when the controller should switch to heating mode. * * A null value for this attribute means that the controller has no heating action @@ -73,15 +68,23 @@ class BangBangClimate : public climate::Climate, public Component { * (blinds open) is possible. */ Trigger<> *heat_trigger_{nullptr}; - bool supports_heat_{false}; /** A reference to the trigger that was previously active. * * This is so that the previous trigger can be stopped before enabling a new one. */ Trigger<> *prev_trigger_{nullptr}; - BangBangClimateTargetTempConfig normal_config_{}; + /** Whether the controller supports cooling/heating + * + * A false value for this attribute means that the controller has no respective action + * (for example a thermostat, where only heating and not-heating is possible). + */ + bool supports_cool_{false}; + bool supports_heat_{false}; + bool supports_away_{false}; + + BangBangClimateTargetTempConfig normal_config_{}; BangBangClimateTargetTempConfig away_config_{}; }; diff --git a/esphome/components/bedjet/bedjet_const.h b/esphome/components/bedjet/bedjet_const.h index 7cac1b61ff..10f403dd1a 100644 --- a/esphome/components/bedjet/bedjet_const.h +++ b/esphome/components/bedjet/bedjet_const.h @@ -99,9 +99,7 @@ enum BedjetCommand : uint8_t { static const uint8_t BEDJET_FAN_SPEED_COUNT = 20; -static const char *const BEDJET_FAN_STEP_NAMES[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_; -static const std::string BEDJET_FAN_STEP_NAME_STRINGS[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_; -static const std::set BEDJET_FAN_STEP_NAMES_SET BEDJET_FAN_STEP_NAMES_; +static constexpr const char *const BEDJET_FAN_STEP_NAMES[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_; } // namespace bedjet } // namespace esphome diff --git a/esphome/components/bedjet/bedjet_hub.cpp b/esphome/components/bedjet/bedjet_hub.cpp index 007ca1ca7d..38fcf29b3b 100644 --- a/esphome/components/bedjet/bedjet_hub.cpp +++ b/esphome/components/bedjet/bedjet_hub.cpp @@ -493,7 +493,7 @@ void BedJetHub::dump_config() { " ble_client.app_id: %d\n" " ble_client.conn_id: %d", this->get_name().c_str(), this->parent()->app_id, this->parent()->get_conn_id()); - LOG_UPDATE_INTERVAL(this) + LOG_UPDATE_INTERVAL(this); ESP_LOGCONFIG(TAG, " Child components (%d):", this->children_.size()); for (auto *child : this->children_) { ESP_LOGCONFIG(TAG, " - %s", child->describe().c_str()); diff --git a/esphome/components/bedjet/climate/bedjet_climate.cpp b/esphome/components/bedjet/climate/bedjet_climate.cpp index f22d312b5a..716d4d4241 100644 --- a/esphome/components/bedjet/climate/bedjet_climate.cpp +++ b/esphome/components/bedjet/climate/bedjet_climate.cpp @@ -8,15 +8,15 @@ namespace bedjet { using namespace esphome::climate; -static const std::string *bedjet_fan_step_to_fan_mode(const uint8_t fan_step) { +static const char *bedjet_fan_step_to_fan_mode(const uint8_t fan_step) { if (fan_step < BEDJET_FAN_SPEED_COUNT) - return &BEDJET_FAN_STEP_NAME_STRINGS[fan_step]; + return BEDJET_FAN_STEP_NAMES[fan_step]; return nullptr; } -static uint8_t bedjet_fan_speed_to_step(const std::string &fan_step_percent) { +static uint8_t bedjet_fan_speed_to_step(const char *fan_step_percent) { for (int i = 0; i < BEDJET_FAN_SPEED_COUNT; i++) { - if (fan_step_percent == BEDJET_FAN_STEP_NAME_STRINGS[i]) { + if (strcmp(BEDJET_FAN_STEP_NAMES[i], fan_step_percent) == 0) { return i; } } @@ -48,7 +48,7 @@ void BedJetClimate::dump_config() { ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_fan_mode_to_string(mode))); } for (const auto &mode : traits.get_supported_custom_fan_modes()) { - ESP_LOGCONFIG(TAG, " - %s (c)", mode.c_str()); + ESP_LOGCONFIG(TAG, " - %s (c)", mode); } ESP_LOGCONFIG(TAG, " Supported presets:"); @@ -56,7 +56,7 @@ void BedJetClimate::dump_config() { ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_preset_to_string(preset))); } for (const auto &preset : traits.get_supported_custom_presets()) { - ESP_LOGCONFIG(TAG, " - %s (c)", preset.c_str()); + ESP_LOGCONFIG(TAG, " - %s (c)", preset); } } @@ -79,7 +79,7 @@ void BedJetClimate::reset_state_() { this->target_temperature = NAN; this->current_temperature = NAN; this->preset.reset(); - this->custom_preset.reset(); + this->clear_custom_preset_(); this->publish_state(); } @@ -120,7 +120,7 @@ void BedJetClimate::control(const ClimateCall &call) { if (button_result) { this->mode = mode; // We're using (custom) preset for Turbo, EXT HT, & M1-3 presets, so changing climate mode will clear those - this->custom_preset.reset(); + this->clear_custom_preset_(); this->preset.reset(); } } @@ -144,8 +144,7 @@ void BedJetClimate::control(const ClimateCall &call) { if (result) { this->mode = CLIMATE_MODE_HEAT; - this->preset = CLIMATE_PRESET_BOOST; - this->custom_preset.reset(); + this->set_preset_(CLIMATE_PRESET_BOOST); } } else if (preset == CLIMATE_PRESET_NONE && this->preset.has_value()) { if (this->mode == CLIMATE_MODE_HEAT && this->preset == CLIMATE_PRESET_BOOST) { @@ -153,7 +152,7 @@ void BedJetClimate::control(const ClimateCall &call) { result = this->parent_->send_button(heat_button(this->heating_mode_)); if (result) { this->preset.reset(); - this->custom_preset.reset(); + this->clear_custom_preset_(); } } else { ESP_LOGD(TAG, "Ignoring preset '%s' call; with current mode '%s' and preset '%s'", @@ -164,28 +163,27 @@ void BedJetClimate::control(const ClimateCall &call) { ESP_LOGW(TAG, "Unsupported preset: %d", preset); return; } - } else if (call.get_custom_preset().has_value()) { - std::string preset = *call.get_custom_preset(); + } else if (call.has_custom_preset()) { + const char *preset = call.get_custom_preset(); bool result; - if (preset == "M1") { + if (strcmp(preset, "M1") == 0) { result = this->parent_->button_memory1(); - } else if (preset == "M2") { + } else if (strcmp(preset, "M2") == 0) { result = this->parent_->button_memory2(); - } else if (preset == "M3") { + } else if (strcmp(preset, "M3") == 0) { result = this->parent_->button_memory3(); - } else if (preset == "LTD HT") { + } else if (strcmp(preset, "LTD HT") == 0) { result = this->parent_->button_heat(); - } else if (preset == "EXT HT") { + } else if (strcmp(preset, "EXT HT") == 0) { result = this->parent_->button_ext_heat(); } else { - ESP_LOGW(TAG, "Unsupported preset: %s", preset.c_str()); + ESP_LOGW(TAG, "Unsupported preset: %s", preset); return; } if (result) { - this->custom_preset = preset; - this->preset.reset(); + this->set_custom_preset_(preset); } } @@ -207,19 +205,16 @@ void BedJetClimate::control(const ClimateCall &call) { } if (result) { - this->fan_mode = fan_mode; - this->custom_fan_mode.reset(); + this->set_fan_mode_(fan_mode); } - } else if (call.get_custom_fan_mode().has_value()) { - auto fan_mode = *call.get_custom_fan_mode(); + } else if (call.has_custom_fan_mode()) { + const char *fan_mode = call.get_custom_fan_mode(); auto fan_index = bedjet_fan_speed_to_step(fan_mode); if (fan_index <= 19) { - ESP_LOGV(TAG, "[%s] Converted fan mode %s to bedjet fan step %d", this->get_name().c_str(), fan_mode.c_str(), - fan_index); + ESP_LOGV(TAG, "[%s] Converted fan mode %s to bedjet fan step %d", this->get_name().c_str(), fan_mode, fan_index); bool result = this->parent_->set_fan_index(fan_index); if (result) { - this->custom_fan_mode = fan_mode; - this->fan_mode.reset(); + this->set_custom_fan_mode_(fan_mode); } } } @@ -245,7 +240,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) { const auto *fan_mode_name = bedjet_fan_step_to_fan_mode(data->fan_step); if (fan_mode_name != nullptr) { - this->custom_fan_mode = *fan_mode_name; + this->set_custom_fan_mode_(fan_mode_name); } // TODO: Get biorhythm data to determine which preset (M1-3) is running, if any. @@ -255,7 +250,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) { this->mode = CLIMATE_MODE_OFF; this->action = CLIMATE_ACTION_IDLE; this->fan_mode = CLIMATE_FAN_OFF; - this->custom_preset.reset(); + this->clear_custom_preset_(); this->preset.reset(); break; @@ -266,7 +261,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) { if (this->heating_mode_ == HEAT_MODE_EXTENDED) { this->set_custom_preset_("LTD HT"); } else { - this->custom_preset.reset(); + this->clear_custom_preset_(); } break; @@ -275,7 +270,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) { this->action = CLIMATE_ACTION_HEATING; this->preset.reset(); if (this->heating_mode_ == HEAT_MODE_EXTENDED) { - this->custom_preset.reset(); + this->clear_custom_preset_(); } else { this->set_custom_preset_("EXT HT"); } @@ -284,20 +279,19 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) { case MODE_COOL: this->mode = CLIMATE_MODE_FAN_ONLY; this->action = CLIMATE_ACTION_COOLING; - this->custom_preset.reset(); + this->clear_custom_preset_(); this->preset.reset(); break; case MODE_DRY: this->mode = CLIMATE_MODE_DRY; this->action = CLIMATE_ACTION_DRYING; - this->custom_preset.reset(); + this->clear_custom_preset_(); this->preset.reset(); break; case MODE_TURBO: - this->preset = CLIMATE_PRESET_BOOST; - this->custom_preset.reset(); + this->set_preset_(CLIMATE_PRESET_BOOST); this->mode = CLIMATE_MODE_HEAT; this->action = CLIMATE_ACTION_HEATING; break; diff --git a/esphome/components/bedjet/climate/bedjet_climate.h b/esphome/components/bedjet/climate/bedjet_climate.h index 7eaa735a3f..05f4a849e0 100644 --- a/esphome/components/bedjet/climate/bedjet_climate.h +++ b/esphome/components/bedjet/climate/bedjet_climate.h @@ -33,8 +33,7 @@ class BedJetClimate : public climate::Climate, public BedJetClient, public Polli climate::ClimateTraits traits() override { auto traits = climate::ClimateTraits(); - traits.set_supports_action(true); - traits.set_supports_current_temperature(true); + traits.add_feature_flags(climate::CLIMATE_SUPPORTS_ACTION | climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE); traits.set_supported_modes({ climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_HEAT, @@ -44,28 +43,20 @@ class BedJetClimate : public climate::Climate, public BedJetClient, public Polli }); // It would be better if we had a slider for the fan modes. - traits.set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES_SET); + traits.set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES); traits.set_supported_presets({ // If we support NONE, then have to decide what happens if the user switches to it (turn off?) // climate::CLIMATE_PRESET_NONE, // Climate doesn't have a "TURBO" mode, but we can use the BOOST preset instead. climate::CLIMATE_PRESET_BOOST, }); + // String literals are stored in rodata and valid for program lifetime traits.set_supported_custom_presets({ - // We could fetch biodata from bedjet and set these names that way. - // But then we have to invert the lookup in order to send the right preset. - // For now, we can leave them as M1-3 to match the remote buttons. - // EXT HT added to match remote button. - "EXT HT", + this->heating_mode_ == HEAT_MODE_EXTENDED ? "LTD HT" : "EXT HT", "M1", "M2", "M3", }); - if (this->heating_mode_ == HEAT_MODE_EXTENDED) { - traits.add_supported_custom_preset("LTD HT"); - } else { - traits.add_supported_custom_preset("EXT HT"); - } traits.set_visual_min_temperature(19.0); traits.set_visual_max_temperature(43.0); traits.set_visual_temperature_step(1.0); diff --git a/esphome/components/bh1900nux/__init__.py b/esphome/components/bh1900nux/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/bh1900nux/bh1900nux.cpp b/esphome/components/bh1900nux/bh1900nux.cpp new file mode 100644 index 0000000000..0e71bd6532 --- /dev/null +++ b/esphome/components/bh1900nux/bh1900nux.cpp @@ -0,0 +1,54 @@ +#include "esphome/core/log.h" +#include "bh1900nux.h" + +namespace esphome { +namespace bh1900nux { + +static const char *const TAG = "bh1900nux.sensor"; + +// I2C Registers +static const uint8_t TEMPERATURE_REG = 0x00; +static const uint8_t CONFIG_REG = 0x01; // Not used and supported yet +static const uint8_t TEMPERATURE_LOW_REG = 0x02; // Not used and supported yet +static const uint8_t TEMPERATURE_HIGH_REG = 0x03; // Not used and supported yet +static const uint8_t SOFT_RESET_REG = 0x04; + +// I2C Command payloads +static const uint8_t SOFT_RESET_PAYLOAD = 0x01; // Soft Reset value + +static const float SENSOR_RESOLUTION = 0.0625f; // Sensor resolution per bit in degrees celsius + +void BH1900NUXSensor::setup() { + // Initialize I2C device + i2c::ErrorCode result_code = + this->write_register(SOFT_RESET_REG, &SOFT_RESET_PAYLOAD, 1); // Software Reset to check communication + if (result_code != i2c::ERROR_OK) { + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); + return; + } +} + +void BH1900NUXSensor::update() { + uint8_t temperature_raw[2]; + if (this->read_register(TEMPERATURE_REG, temperature_raw, 2) != i2c::ERROR_OK) { + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); + return; + } + + // Combined raw value, unsigned and unaligned 16 bit + // Temperature is represented in just 12 bits, shift needed + int16_t raw_temperature_register_value = encode_uint16(temperature_raw[0], temperature_raw[1]); + raw_temperature_register_value >>= 4; + float temperature_value = raw_temperature_register_value * SENSOR_RESOLUTION; // Apply sensor resolution + + this->publish_state(temperature_value); +} + +void BH1900NUXSensor::dump_config() { + LOG_SENSOR("", "BH1900NUX", this); + LOG_I2C_DEVICE(this); + LOG_UPDATE_INTERVAL(this); +} + +} // namespace bh1900nux +} // namespace esphome diff --git a/esphome/components/bh1900nux/bh1900nux.h b/esphome/components/bh1900nux/bh1900nux.h new file mode 100644 index 0000000000..fd7f8848d6 --- /dev/null +++ b/esphome/components/bh1900nux/bh1900nux.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace bh1900nux { + +class BH1900NUXSensor : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; + void update() override; + void dump_config() override; +}; + +} // namespace bh1900nux +} // namespace esphome diff --git a/esphome/components/bh1900nux/sensor.py b/esphome/components/bh1900nux/sensor.py new file mode 100644 index 0000000000..5e1c0395af --- /dev/null +++ b/esphome/components/bh1900nux/sensor.py @@ -0,0 +1,34 @@ +import esphome.codegen as cg +from esphome.components import i2c, sensor +import esphome.config_validation as cv +from esphome.const import ( + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, +) + +DEPENDENCIES = ["i2c"] +CODEOWNERS = ["@B48D81EFCC"] + +sensor_ns = cg.esphome_ns.namespace("bh1900nux") +BH1900NUXSensor = sensor_ns.class_( + "BH1900NUXSensor", cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + BH1900NUXSensor, + accuracy_decimals=1, + unit_of_measurement=UNIT_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x48)) +) + + +async def to_code(config): + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index b56fde1ffd..cbf935a501 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -59,7 +59,7 @@ from esphome.const import ( DEVICE_CLASS_VIBRATION, DEVICE_CLASS_WINDOW, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass from esphome.util import Registry @@ -155,6 +155,7 @@ DelayedOffFilter = binary_sensor_ns.class_("DelayedOffFilter", Filter, cg.Compon InvertFilter = binary_sensor_ns.class_("InvertFilter", Filter) AutorepeatFilter = binary_sensor_ns.class_("AutorepeatFilter", Filter, cg.Component) LambdaFilter = binary_sensor_ns.class_("LambdaFilter", Filter) +StatelessLambdaFilter = binary_sensor_ns.class_("StatelessLambdaFilter", Filter) SettleFilter = binary_sensor_ns.class_("SettleFilter", Filter, cg.Component) _LOGGER = getLogger(__name__) @@ -264,20 +265,31 @@ async def delayed_off_filter_to_code(config, filter_id): ), ) async def autorepeat_filter_to_code(config, filter_id): - timings = [] if len(config) > 0: - timings.extend( - (conf[CONF_DELAY], conf[CONF_TIME_OFF], conf[CONF_TIME_ON]) - for conf in config - ) - else: - timings.append( - ( - cv.time_period_str_unit(DEFAULT_DELAY).total_milliseconds, - cv.time_period_str_unit(DEFAULT_TIME_OFF).total_milliseconds, - cv.time_period_str_unit(DEFAULT_TIME_ON).total_milliseconds, + timings = [ + cg.StructInitializer( + cg.MockObj("AutorepeatFilterTiming", "esphome::binary_sensor::"), + ("delay", conf[CONF_DELAY]), + ("time_off", conf[CONF_TIME_OFF]), + ("time_on", conf[CONF_TIME_ON]), ) - ) + for conf in config + ] + else: + timings = [ + cg.StructInitializer( + cg.MockObj("AutorepeatFilterTiming", "esphome::binary_sensor::"), + ("delay", cv.time_period_str_unit(DEFAULT_DELAY).total_milliseconds), + ( + "time_off", + cv.time_period_str_unit(DEFAULT_TIME_OFF).total_milliseconds, + ), + ( + "time_on", + cv.time_period_str_unit(DEFAULT_TIME_ON).total_milliseconds, + ), + ) + ] var = cg.new_Pvariable(filter_id, timings) await cg.register_component(var, {}) return var @@ -288,7 +300,7 @@ async def lambda_filter_to_code(config, filter_id): lambda_ = await cg.process_lambda( config, [(bool, "x")], return_type=cg.optional.template(bool) ) - return cg.new_Pvariable(filter_id, lambda_) + return automation.new_lambda_pvariable(filter_id, lambda_, StatelessLambdaFilter) @register_filter( @@ -536,11 +548,6 @@ def binary_sensor_schema( return _BINARY_SENSOR_SCHEMA.extend(schema) -# Remove before 2025.11.0 -BINARY_SENSOR_SCHEMA = binary_sensor_schema() -BINARY_SENSOR_SCHEMA.add_extra(cv.deprecated_schema_constant("binary_sensor")) - - async def setup_binary_sensor_core_(var, config): await setup_entity(var, config, "binary_sensor") @@ -652,7 +659,7 @@ async def binary_sensor_is_off_to_code(config, condition_id, template_arg, args) return cg.new_Pvariable(condition_id, template_arg, paren, False) -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(binary_sensor_ns.using) diff --git a/esphome/components/binary_sensor/automation.cpp b/esphome/components/binary_sensor/automation.cpp index 64a0d3db8d..66d8d6e90f 100644 --- a/esphome/components/binary_sensor/automation.cpp +++ b/esphome/components/binary_sensor/automation.cpp @@ -1,12 +1,11 @@ #include "automation.h" #include "esphome/core/log.h" -namespace esphome { -namespace binary_sensor { +namespace esphome::binary_sensor { static const char *const TAG = "binary_sensor.automation"; -void binary_sensor::MultiClickTrigger::on_state_(bool state) { +void MultiClickTrigger::on_state_(bool state) { // Handle duplicate events if (state == this->last_state_) { return; @@ -67,7 +66,7 @@ void binary_sensor::MultiClickTrigger::on_state_(bool state) { *this->at_index_ = *this->at_index_ + 1; } -void binary_sensor::MultiClickTrigger::schedule_cooldown_() { +void MultiClickTrigger::schedule_cooldown_() { ESP_LOGV(TAG, "Multi Click: Invalid length of press, starting cooldown of %" PRIu32 " ms", this->invalid_cooldown_); this->is_in_cooldown_ = true; this->set_timeout("cooldown", this->invalid_cooldown_, [this]() { @@ -79,7 +78,7 @@ void binary_sensor::MultiClickTrigger::schedule_cooldown_() { this->cancel_timeout("is_valid"); this->cancel_timeout("is_not_valid"); } -void binary_sensor::MultiClickTrigger::schedule_is_valid_(uint32_t min_length) { +void MultiClickTrigger::schedule_is_valid_(uint32_t min_length) { if (min_length == 0) { this->is_valid_ = true; return; @@ -90,19 +89,19 @@ void binary_sensor::MultiClickTrigger::schedule_is_valid_(uint32_t min_length) { this->is_valid_ = true; }); } -void binary_sensor::MultiClickTrigger::schedule_is_not_valid_(uint32_t max_length) { +void MultiClickTrigger::schedule_is_not_valid_(uint32_t max_length) { this->set_timeout("is_not_valid", max_length, [this]() { ESP_LOGV(TAG, "Multi Click: You waited too long to %s.", this->parent_->state ? "RELEASE" : "PRESS"); this->is_valid_ = false; this->schedule_cooldown_(); }); } -void binary_sensor::MultiClickTrigger::cancel() { +void MultiClickTrigger::cancel() { ESP_LOGV(TAG, "Multi Click: Sequence explicitly cancelled."); this->is_valid_ = false; this->schedule_cooldown_(); } -void binary_sensor::MultiClickTrigger::trigger_() { +void MultiClickTrigger::trigger_() { ESP_LOGV(TAG, "Multi Click: Hooray, multi click is valid. Triggering!"); this->at_index_.reset(); this->cancel_timeout("trigger"); @@ -118,5 +117,4 @@ bool match_interval(uint32_t min_length, uint32_t max_length, uint32_t length) { return length >= min_length && length <= max_length; } } -} // namespace binary_sensor -} // namespace esphome +} // namespace esphome::binary_sensor diff --git a/esphome/components/binary_sensor/automation.h b/esphome/components/binary_sensor/automation.h index b46436dc41..f8b130e08a 100644 --- a/esphome/components/binary_sensor/automation.h +++ b/esphome/components/binary_sensor/automation.h @@ -2,15 +2,14 @@ #include #include -#include #include "esphome/core/component.h" #include "esphome/core/automation.h" #include "esphome/core/hal.h" +#include "esphome/core/helpers.h" #include "esphome/components/binary_sensor/binary_sensor.h" -namespace esphome { -namespace binary_sensor { +namespace esphome::binary_sensor { struct MultiClickTriggerEvent { bool state; @@ -92,8 +91,8 @@ class DoubleClickTrigger : public Trigger<> { class MultiClickTrigger : public Trigger<>, public Component { public: - explicit MultiClickTrigger(BinarySensor *parent, std::vector timing) - : parent_(parent), timing_(std::move(timing)) {} + explicit MultiClickTrigger(BinarySensor *parent, std::initializer_list timing) + : parent_(parent), timing_(timing) {} void setup() override { this->last_state_ = this->parent_->get_state_default(false); @@ -115,7 +114,7 @@ class MultiClickTrigger : public Trigger<>, public Component { void trigger_(); BinarySensor *parent_; - std::vector timing_; + FixedVector timing_; uint32_t invalid_cooldown_{1000}; optional at_index_{}; bool last_state_{false}; @@ -141,7 +140,7 @@ class StateChangeTrigger : public Trigger, optional > { template class BinarySensorCondition : public Condition { public: BinarySensorCondition(BinarySensor *parent, bool state) : parent_(parent), state_(state) {} - bool check(Ts... x) override { return this->parent_->state == this->state_; } + bool check(const Ts &...x) override { return this->parent_->state == this->state_; } protected: BinarySensor *parent_; @@ -153,7 +152,7 @@ template class BinarySensorPublishAction : public Action explicit BinarySensorPublishAction(BinarySensor *sensor) : sensor_(sensor) {} TEMPLATABLE_VALUE(bool, state) - void play(Ts... x) override { + void play(const Ts &...x) override { auto val = this->state_.value(x...); this->sensor_->publish_state(val); } @@ -166,11 +165,10 @@ template class BinarySensorInvalidateAction : public Actionsensor_->invalidate_state(); } + void play(const Ts &...x) override { this->sensor_->invalidate_state(); } protected: BinarySensor *sensor_; }; -} // namespace binary_sensor -} // namespace esphome +} // namespace esphome::binary_sensor diff --git a/esphome/components/binary_sensor/binary_sensor.cpp b/esphome/components/binary_sensor/binary_sensor.cpp index 02b83af552..92b8db5c51 100644 --- a/esphome/components/binary_sensor/binary_sensor.cpp +++ b/esphome/components/binary_sensor/binary_sensor.cpp @@ -1,12 +1,25 @@ #include "binary_sensor.h" +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" -namespace esphome { - -namespace binary_sensor { +namespace esphome::binary_sensor { static const char *const TAG = "binary_sensor"; +// Function implementation of LOG_BINARY_SENSOR macro to reduce code size +void log_binary_sensor(const char *tag, const char *prefix, const char *type, BinarySensor *obj) { + if (obj == nullptr) { + return; + } + + ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str()); + + if (!obj->get_device_class_ref().empty()) { + ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj->get_device_class_ref().c_str()); + } +} + void BinarySensor::publish_state(bool new_state) { if (this->filter_list_ == nullptr) { this->send_state_internal(new_state); @@ -24,6 +37,9 @@ void BinarySensor::send_state_internal(bool new_state) { // Note that set_state_ de-dups and will only trigger callbacks if the state has actually changed if (this->set_state_(new_state)) { ESP_LOGD(TAG, "'%s': New state is %s", this->get_name().c_str(), ONOFF(new_state)); +#if defined(USE_BINARY_SENSOR) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_binary_sensor_update(this); +#endif } } @@ -38,13 +54,11 @@ void BinarySensor::add_filter(Filter *filter) { last_filter->next_ = filter; } } -void BinarySensor::add_filters(const std::vector &filters) { +void BinarySensor::add_filters(std::initializer_list filters) { for (Filter *filter : filters) { this->add_filter(filter); } } bool BinarySensor::is_status_binary_sensor() const { return false; } -} // namespace binary_sensor - -} // namespace esphome +} // namespace esphome::binary_sensor diff --git a/esphome/components/binary_sensor/binary_sensor.h b/esphome/components/binary_sensor/binary_sensor.h index d61be7a49b..0dca3e1520 100644 --- a/esphome/components/binary_sensor/binary_sensor.h +++ b/esphome/components/binary_sensor/binary_sensor.h @@ -4,19 +4,14 @@ #include "esphome/core/helpers.h" #include "esphome/components/binary_sensor/filter.h" -#include +#include -namespace esphome { +namespace esphome::binary_sensor { -namespace binary_sensor { +class BinarySensor; +void log_binary_sensor(const char *tag, const char *prefix, const char *type, BinarySensor *obj); -#define LOG_BINARY_SENSOR(prefix, type, obj) \ - if ((obj) != nullptr) { \ - ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - if (!(obj)->get_device_class().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class().c_str()); \ - } \ - } +#define LOG_BINARY_SENSOR(prefix, type, obj) log_binary_sensor(TAG, prefix, LOG_STR_LITERAL(type), obj) #define SUB_BINARY_SENSOR(name) \ protected: \ @@ -51,7 +46,7 @@ class BinarySensor : public StatefulEntityBase, public EntityBase_DeviceCl void publish_initial_state(bool new_state); void add_filter(Filter *filter); - void add_filters(const std::vector &filters); + void add_filters(std::initializer_list filters); // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) @@ -73,5 +68,4 @@ class BinarySensorInitiallyOff : public BinarySensor { bool has_state() const override { return true; } }; -} // namespace binary_sensor -} // namespace esphome +} // namespace esphome::binary_sensor diff --git a/esphome/components/binary_sensor/filter.cpp b/esphome/components/binary_sensor/filter.cpp index 3567e9c72b..9c7238f6d7 100644 --- a/esphome/components/binary_sensor/filter.cpp +++ b/esphome/components/binary_sensor/filter.cpp @@ -1,11 +1,8 @@ #include "filter.h" #include "binary_sensor.h" -#include -namespace esphome { - -namespace binary_sensor { +namespace esphome::binary_sensor { static const char *const TAG = "sensor.filter"; @@ -68,7 +65,7 @@ float DelayedOffFilter::get_setup_priority() const { return setup_priority::HARD optional InvertFilter::new_value(bool value) { return !value; } -AutorepeatFilter::AutorepeatFilter(std::vector timings) : timings_(std::move(timings)) {} +AutorepeatFilter::AutorepeatFilter(std::initializer_list timings) : timings_(timings) {} optional AutorepeatFilter::new_value(bool value) { if (value) { @@ -133,6 +130,4 @@ optional SettleFilter::new_value(bool value) { float SettleFilter::get_setup_priority() const { return setup_priority::HARDWARE; } -} // namespace binary_sensor - -} // namespace esphome +} // namespace esphome::binary_sensor diff --git a/esphome/components/binary_sensor/filter.h b/esphome/components/binary_sensor/filter.h index 16f44aa5fe..59bc43eeba 100644 --- a/esphome/components/binary_sensor/filter.h +++ b/esphome/components/binary_sensor/filter.h @@ -4,11 +4,7 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" -#include - -namespace esphome { - -namespace binary_sensor { +namespace esphome::binary_sensor { class BinarySensor; @@ -82,11 +78,6 @@ class InvertFilter : public Filter { }; struct AutorepeatFilterTiming { - AutorepeatFilterTiming(uint32_t delay, uint32_t off, uint32_t on) { - this->delay = delay; - this->time_off = off; - this->time_on = on; - } uint32_t delay; uint32_t time_off; uint32_t time_on; @@ -94,7 +85,7 @@ struct AutorepeatFilterTiming { class AutorepeatFilter : public Filter, public Component { public: - explicit AutorepeatFilter(std::vector timings); + explicit AutorepeatFilter(std::initializer_list timings); optional new_value(bool value) override; @@ -104,7 +95,7 @@ class AutorepeatFilter : public Filter, public Component { void next_timing_(); void next_value_(bool val); - std::vector timings_; + FixedVector timings_; uint8_t active_timing_{0}; }; @@ -118,6 +109,21 @@ class LambdaFilter : public Filter { std::function(bool)> f_; }; +/** Optimized lambda filter for stateless lambdas (no capture). + * + * Uses function pointer instead of std::function to reduce memory overhead. + * Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function). + */ +class StatelessLambdaFilter : public Filter { + public: + explicit StatelessLambdaFilter(optional (*f)(bool)) : f_(f) {} + + optional new_value(bool value) override { return this->f_(value); } + + protected: + optional (*f_)(bool); +}; + class SettleFilter : public Filter, public Component { public: optional new_value(bool value) override; @@ -131,6 +137,4 @@ class SettleFilter : public Filter, public Component { bool steady_{true}; }; -} // namespace binary_sensor - -} // namespace esphome +} // namespace esphome::binary_sensor diff --git a/esphome/components/bl0906/bl0906.cpp b/esphome/components/bl0906/bl0906.cpp index e48715010c..c1cd48a1ac 100644 --- a/esphome/components/bl0906/bl0906.cpp +++ b/esphome/components/bl0906/bl0906.cpp @@ -97,10 +97,10 @@ void BL0906::handle_actions_() { return; } ActionCallbackFuncPtr ptr_func = nullptr; - for (int i = 0; i < this->action_queue_.size(); i++) { + for (size_t i = 0; i < this->action_queue_.size(); i++) { ptr_func = this->action_queue_[i]; if (ptr_func) { - ESP_LOGI(TAG, "HandleActionCallback[%d]", i); + ESP_LOGI(TAG, "HandleActionCallback[%zu]", i); (this->*ptr_func)(); } } diff --git a/esphome/components/bl0906/bl0906.h b/esphome/components/bl0906/bl0906.h index 5a9ad0f028..493b645c89 100644 --- a/esphome/components/bl0906/bl0906.h +++ b/esphome/components/bl0906/bl0906.h @@ -89,7 +89,7 @@ class BL0906 : public PollingComponent, public uart::UARTDevice { template class ResetEnergyAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->enqueue_action_(&BL0906::reset_energy_); } + void play(const Ts &...x) override { this->parent_->enqueue_action_(&BL0906::reset_energy_); } }; } // namespace bl0906 diff --git a/esphome/components/bl0940/__init__.py b/esphome/components/bl0940/__init__.py index 087626a4e7..066c2818b6 100644 --- a/esphome/components/bl0940/__init__.py +++ b/esphome/components/bl0940/__init__.py @@ -1 +1,6 @@ -CODEOWNERS = ["@tobias-"] +import esphome.codegen as cg + +CODEOWNERS = ["@tobias-", "@dan-s-github"] + +CONF_BL0940_ID = "bl0940_id" +bl0940_ns = cg.esphome_ns.namespace("bl0940") diff --git a/esphome/components/bl0940/bl0940.cpp b/esphome/components/bl0940/bl0940.cpp index 24990d5482..42e20eb69b 100644 --- a/esphome/components/bl0940/bl0940.cpp +++ b/esphome/components/bl0940/bl0940.cpp @@ -7,28 +7,26 @@ namespace bl0940 { static const char *const TAG = "bl0940"; -static const uint8_t BL0940_READ_COMMAND = 0x50; // 0x58 according to documentation static const uint8_t BL0940_FULL_PACKET = 0xAA; -static const uint8_t BL0940_PACKET_HEADER = 0x55; // 0x58 according to documentation +static const uint8_t BL0940_PACKET_HEADER = 0x55; // 0x58 according to en doc but 0x55 in cn doc -static const uint8_t BL0940_WRITE_COMMAND = 0xA0; // 0xA8 according to documentation static const uint8_t BL0940_REG_I_FAST_RMS_CTRL = 0x10; static const uint8_t BL0940_REG_MODE = 0x18; static const uint8_t BL0940_REG_SOFT_RESET = 0x19; static const uint8_t BL0940_REG_USR_WRPROT = 0x1A; static const uint8_t BL0940_REG_TPS_CTRL = 0x1B; -const uint8_t BL0940_INIT[5][6] = { +static const uint8_t BL0940_INIT[5][5] = { // Reset to default - {BL0940_WRITE_COMMAND, BL0940_REG_SOFT_RESET, 0x5A, 0x5A, 0x5A, 0x38}, + {BL0940_REG_SOFT_RESET, 0x5A, 0x5A, 0x5A, 0x38}, // Enable User Operation Write - {BL0940_WRITE_COMMAND, BL0940_REG_USR_WRPROT, 0x55, 0x00, 0x00, 0xF0}, + {BL0940_REG_USR_WRPROT, 0x55, 0x00, 0x00, 0xF0}, // 0x0100 = CF_UNABLE energy pulse, AC_FREQ_SEL 50Hz, RMS_UPDATE_SEL 800mS - {BL0940_WRITE_COMMAND, BL0940_REG_MODE, 0x00, 0x10, 0x00, 0x37}, + {BL0940_REG_MODE, 0x00, 0x10, 0x00, 0x37}, // 0x47FF = Over-current and leakage alarm on, Automatic temperature measurement, Interval 100mS - {BL0940_WRITE_COMMAND, BL0940_REG_TPS_CTRL, 0xFF, 0x47, 0x00, 0xFE}, + {BL0940_REG_TPS_CTRL, 0xFF, 0x47, 0x00, 0xFE}, // 0x181C = Half cycle, Fast RMS threshold 6172 - {BL0940_WRITE_COMMAND, BL0940_REG_I_FAST_RMS_CTRL, 0x1C, 0x18, 0x00, 0x1B}}; + {BL0940_REG_I_FAST_RMS_CTRL, 0x1C, 0x18, 0x00, 0x1B}}; void BL0940::loop() { DataPacket buffer; @@ -36,8 +34,8 @@ void BL0940::loop() { return; } if (read_array((uint8_t *) &buffer, sizeof(buffer))) { - if (validate_checksum(&buffer)) { - received_package_(&buffer); + if (this->validate_checksum_(&buffer)) { + this->received_package_(&buffer); } } else { ESP_LOGW(TAG, "Junk on wire. Throwing away partial message"); @@ -46,35 +44,151 @@ void BL0940::loop() { } } -bool BL0940::validate_checksum(const DataPacket *data) { - uint8_t checksum = BL0940_READ_COMMAND; +bool BL0940::validate_checksum_(DataPacket *data) { + uint8_t checksum = this->read_command_; // Whole package but checksum - for (uint32_t i = 0; i < sizeof(data->raw) - 1; i++) { - checksum += data->raw[i]; + uint8_t *raw = (uint8_t *) data; + for (uint32_t i = 0; i < sizeof(*data) - 1; i++) { + checksum += raw[i]; } checksum ^= 0xFF; if (checksum != data->checksum) { - ESP_LOGW(TAG, "BL0940 invalid checksum! 0x%02X != 0x%02X", checksum, data->checksum); + ESP_LOGW(TAG, "Invalid checksum! 0x%02X != 0x%02X", checksum, data->checksum); } return checksum == data->checksum; } void BL0940::update() { this->flush(); - this->write_byte(BL0940_READ_COMMAND); + this->write_byte(this->read_command_); this->write_byte(BL0940_FULL_PACKET); } void BL0940::setup() { +#ifdef USE_NUMBER + // add calibration callbacks + if (this->voltage_calibration_number_ != nullptr) { + this->voltage_calibration_number_->add_on_state_callback( + [this](float state) { this->voltage_calibration_callback_(state); }); + if (this->voltage_calibration_number_->has_state()) { + this->voltage_calibration_callback_(this->voltage_calibration_number_->state); + } + } + + if (this->current_calibration_number_ != nullptr) { + this->current_calibration_number_->add_on_state_callback( + [this](float state) { this->current_calibration_callback_(state); }); + if (this->current_calibration_number_->has_state()) { + this->current_calibration_callback_(this->current_calibration_number_->state); + } + } + + if (this->power_calibration_number_ != nullptr) { + this->power_calibration_number_->add_on_state_callback( + [this](float state) { this->power_calibration_callback_(state); }); + if (this->power_calibration_number_->has_state()) { + this->power_calibration_callback_(this->power_calibration_number_->state); + } + } + + if (this->energy_calibration_number_ != nullptr) { + this->energy_calibration_number_->add_on_state_callback( + [this](float state) { this->energy_calibration_callback_(state); }); + if (this->energy_calibration_number_->has_state()) { + this->energy_calibration_callback_(this->energy_calibration_number_->state); + } + } +#endif + + // calculate calibrated reference values + this->voltage_reference_cal_ = this->voltage_reference_ / this->voltage_cal_; + this->current_reference_cal_ = this->current_reference_ / this->current_cal_; + this->power_reference_cal_ = this->power_reference_ / this->power_cal_; + this->energy_reference_cal_ = this->energy_reference_ / this->energy_cal_; + for (auto *i : BL0940_INIT) { - this->write_array(i, 6); + this->write_byte(this->write_command_), this->write_array(i, 5); delay(1); } this->flush(); } -float BL0940::update_temp_(sensor::Sensor *sensor, ube16_t temperature) const { - auto tb = (float) (temperature.h << 8 | temperature.l); +float BL0940::calculate_power_reference_() { + // calculate power reference based on voltage and current reference + return this->voltage_reference_cal_ * this->current_reference_cal_ * 4046 / 324004 / 79931; +} + +float BL0940::calculate_energy_reference_() { + // formula: 3600000 * 4046 * RL * R1 * 1000 / (1638.4 * 256) / Vref² / (R1 + R2) + // or: power_reference_ * 3600000 / (1638.4 * 256) + return this->power_reference_cal_ * 3600000 / (1638.4 * 256); +} + +float BL0940::calculate_calibration_value_(float state) { return (100 + state) / 100; } + +void BL0940::reset_calibration() { +#ifdef USE_NUMBER + if (this->current_calibration_number_ != nullptr && this->current_cal_ != 1) { + this->current_calibration_number_->make_call().set_value(0).perform(); + } + if (this->voltage_calibration_number_ != nullptr && this->voltage_cal_ != 1) { + this->voltage_calibration_number_->make_call().set_value(0).perform(); + } + if (this->power_calibration_number_ != nullptr && this->power_cal_ != 1) { + this->power_calibration_number_->make_call().set_value(0).perform(); + } + if (this->energy_calibration_number_ != nullptr && this->energy_cal_ != 1) { + this->energy_calibration_number_->make_call().set_value(0).perform(); + } +#endif + ESP_LOGD(TAG, "external calibration values restored to initial state"); +} + +void BL0940::current_calibration_callback_(float state) { + this->current_cal_ = this->calculate_calibration_value_(state); + ESP_LOGV(TAG, "update current calibration state: %f", this->current_cal_); + this->recalibrate_(); +} +void BL0940::voltage_calibration_callback_(float state) { + this->voltage_cal_ = this->calculate_calibration_value_(state); + ESP_LOGV(TAG, "update voltage calibration state: %f", this->voltage_cal_); + this->recalibrate_(); +} +void BL0940::power_calibration_callback_(float state) { + this->power_cal_ = this->calculate_calibration_value_(state); + ESP_LOGV(TAG, "update power calibration state: %f", this->power_cal_); + this->recalibrate_(); +} +void BL0940::energy_calibration_callback_(float state) { + this->energy_cal_ = this->calculate_calibration_value_(state); + ESP_LOGV(TAG, "update energy calibration state: %f", this->energy_cal_); + this->recalibrate_(); +} + +void BL0940::recalibrate_() { + ESP_LOGV(TAG, "Recalibrating reference values"); + this->voltage_reference_cal_ = this->voltage_reference_ / this->voltage_cal_; + this->current_reference_cal_ = this->current_reference_ / this->current_cal_; + + if (this->voltage_cal_ != 1 || this->current_cal_ != 1) { + this->power_reference_ = this->calculate_power_reference_(); + } + this->power_reference_cal_ = this->power_reference_ / this->power_cal_; + + if (this->voltage_cal_ != 1 || this->current_cal_ != 1 || this->power_cal_ != 1) { + this->energy_reference_ = this->calculate_energy_reference_(); + } + this->energy_reference_cal_ = this->energy_reference_ / this->energy_cal_; + + ESP_LOGD(TAG, + "Recalibrated reference values:\n" + "Voltage: %f\n, Current: %f\n, Power: %f\n, Energy: %f\n", + this->voltage_reference_cal_, this->current_reference_cal_, this->power_reference_cal_, + this->energy_reference_cal_); +} + +float BL0940::update_temp_(sensor::Sensor *sensor, uint16_le_t temperature) const { + auto tb = (float) temperature; float converted_temp = ((float) 170 / 448) * (tb / 2 - 32) - 45; if (sensor != nullptr) { if (sensor->has_state() && std::abs(converted_temp - sensor->get_state()) > max_temperature_diff_) { @@ -87,33 +201,40 @@ float BL0940::update_temp_(sensor::Sensor *sensor, ube16_t temperature) const { return converted_temp; } -void BL0940::received_package_(const DataPacket *data) const { +void BL0940::received_package_(DataPacket *data) { // Bad header if (data->frame_header != BL0940_PACKET_HEADER) { ESP_LOGI(TAG, "Invalid data. Header mismatch: %d", data->frame_header); return; } - float v_rms = (float) to_uint32_t(data->v_rms) / voltage_reference_; - float i_rms = (float) to_uint32_t(data->i_rms) / current_reference_; - float watt = (float) to_int32_t(data->watt) / power_reference_; - uint32_t cf_cnt = to_uint32_t(data->cf_cnt); - float total_energy_consumption = (float) cf_cnt / energy_reference_; + // cf_cnt is only 24 bits, so track overflows + uint32_t cf_cnt = (uint24_t) data->cf_cnt; + cf_cnt |= this->prev_cf_cnt_ & 0xff000000; + if (cf_cnt < this->prev_cf_cnt_) { + cf_cnt += 0x1000000; + } + this->prev_cf_cnt_ = cf_cnt; - float tps1 = update_temp_(internal_temperature_sensor_, data->tps1); - float tps2 = update_temp_(external_temperature_sensor_, data->tps2); + float v_rms = (uint24_t) data->v_rms / this->voltage_reference_cal_; + float i_rms = (uint24_t) data->i_rms / this->current_reference_cal_; + float watt = (int24_t) data->watt / this->power_reference_cal_; + float total_energy_consumption = cf_cnt / this->energy_reference_cal_; - if (voltage_sensor_ != nullptr) { - voltage_sensor_->publish_state(v_rms); + float tps1 = update_temp_(this->internal_temperature_sensor_, data->tps1); + float tps2 = update_temp_(this->external_temperature_sensor_, data->tps2); + + if (this->voltage_sensor_ != nullptr) { + this->voltage_sensor_->publish_state(v_rms); } - if (current_sensor_ != nullptr) { - current_sensor_->publish_state(i_rms); + if (this->current_sensor_ != nullptr) { + this->current_sensor_->publish_state(i_rms); } - if (power_sensor_ != nullptr) { - power_sensor_->publish_state(watt); + if (this->power_sensor_ != nullptr) { + this->power_sensor_->publish_state(watt); } - if (energy_sensor_ != nullptr) { - energy_sensor_->publish_state(total_energy_consumption); + if (this->energy_sensor_ != nullptr) { + this->energy_sensor_->publish_state(total_energy_consumption); } ESP_LOGV(TAG, "BL0940: U %fV, I %fA, P %fW, Cnt %" PRId32 ", ∫P %fkWh, T1 %f°C, T2 %f°C", v_rms, i_rms, watt, cf_cnt, @@ -121,7 +242,27 @@ void BL0940::received_package_(const DataPacket *data) const { } void BL0940::dump_config() { // NOLINT(readability-function-cognitive-complexity) - ESP_LOGCONFIG(TAG, "BL0940:"); + ESP_LOGCONFIG(TAG, + "BL0940:\n" + " LEGACY MODE: %s\n" + " READ CMD: 0x%02X\n" + " WRITE CMD: 0x%02X\n" + " ------------------\n" + " Current reference: %f\n" + " Energy reference: %f\n" + " Power reference: %f\n" + " Voltage reference: %f\n", + TRUEFALSE(this->legacy_mode_enabled_), this->read_command_, this->write_command_, + this->current_reference_, this->energy_reference_, this->power_reference_, this->voltage_reference_); +#ifdef USE_NUMBER + ESP_LOGCONFIG(TAG, + "BL0940:\n" + " Current calibration: %f\n" + " Energy calibration: %f\n" + " Power calibration: %f\n" + " Voltage calibration: %f\n", + this->current_cal_, this->energy_cal_, this->power_cal_, this->voltage_cal_); +#endif LOG_SENSOR("", "Voltage", this->voltage_sensor_); LOG_SENSOR("", "Current", this->current_sensor_); LOG_SENSOR("", "Power", this->power_sensor_); @@ -130,9 +271,5 @@ void BL0940::dump_config() { // NOLINT(readability-function-cognitive-complexit LOG_SENSOR("", "External temperature", this->external_temperature_sensor_); } -uint32_t BL0940::to_uint32_t(ube24_t input) { return input.h << 16 | input.m << 8 | input.l; } - -int32_t BL0940::to_int32_t(sbe24_t input) { return input.h << 16 | input.m << 8 | input.l; } - } // namespace bl0940 } // namespace esphome diff --git a/esphome/components/bl0940/bl0940.h b/esphome/components/bl0940/bl0940.h index 2d4e7ccaac..93d54003f5 100644 --- a/esphome/components/bl0940/bl0940.h +++ b/esphome/components/bl0940/bl0940.h @@ -1,66 +1,48 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/components/uart/uart.h" +#include "esphome/core/datatypes.h" +#include "esphome/core/defines.h" +#ifdef USE_BUTTON +#include "esphome/components/button/button.h" +#endif +#ifdef USE_NUMBER +#include "esphome/components/number/number.h" +#endif #include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" namespace esphome { namespace bl0940 { -static const float BL0940_PREF = 1430; -static const float BL0940_UREF = 33000; -static const float BL0940_IREF = 275000; // 2750 from tasmota. Seems to generate values 100 times too high - -// Measured to 297J per click according to power consumption of 5 minutes -// Converted to kWh (3.6MJ per kwH). Used to be 256 * 1638.4 -static const float BL0940_EREF = 3.6e6 / 297; - -struct ube24_t { // NOLINT(readability-identifier-naming,altera-struct-pack-align) - uint8_t l; - uint8_t m; - uint8_t h; -} __attribute__((packed)); - -struct ube16_t { // NOLINT(readability-identifier-naming,altera-struct-pack-align) - uint8_t l; - uint8_t h; -} __attribute__((packed)); - -struct sbe24_t { // NOLINT(readability-identifier-naming,altera-struct-pack-align) - uint8_t l; - uint8_t m; - int8_t h; -} __attribute__((packed)); - // Caveat: All these values are big endian (low - middle - high) - -union DataPacket { // NOLINT(altera-struct-pack-align) - uint8_t raw[35]; - struct { - uint8_t frame_header; // value of 0x58 according to docs. 0x55 according to Tasmota real world tests. Reality wins. - ube24_t i_fast_rms; // 0x00 - ube24_t i_rms; // 0x04 - ube24_t RESERVED0; // reserved - ube24_t v_rms; // 0x06 - ube24_t RESERVED1; // reserved - sbe24_t watt; // 0x08 - ube24_t RESERVED2; // reserved - ube24_t cf_cnt; // 0x0A - ube24_t RESERVED3; // reserved - ube16_t tps1; // 0x0c - uint8_t RESERVED4; // value of 0x00 - ube16_t tps2; // 0x0c - uint8_t RESERVED5; // value of 0x00 - uint8_t checksum; // checksum - }; +struct DataPacket { + uint8_t frame_header; // Packet header (0x58 in EN docs, 0x55 in CN docs and Tasmota tests) + uint24_le_t i_fast_rms; // Fast RMS current + uint24_le_t i_rms; // RMS current + uint24_t RESERVED0; // Reserved + uint24_le_t v_rms; // RMS voltage + uint24_t RESERVED1; // Reserved + int24_le_t watt; // Active power (can be negative for bidirectional measurement) + uint24_t RESERVED2; // Reserved + uint24_le_t cf_cnt; // Energy pulse count + uint24_t RESERVED3; // Reserved + uint16_le_t tps1; // Internal temperature sensor 1 + uint8_t RESERVED4; // Reserved (should be 0x00) + uint16_le_t tps2; // Internal temperature sensor 2 + uint8_t RESERVED5; // Reserved (should be 0x00) + uint8_t checksum; // Packet checksum } __attribute__((packed)); class BL0940 : public PollingComponent, public uart::UARTDevice { public: + // Sensor setters void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; } void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; } void set_energy_sensor(sensor::Sensor *energy_sensor) { energy_sensor_ = energy_sensor; } + + // Temperature sensor setters void set_internal_temperature_sensor(sensor::Sensor *internal_temperature_sensor) { internal_temperature_sensor_ = internal_temperature_sensor; } @@ -68,42 +50,105 @@ class BL0940 : public PollingComponent, public uart::UARTDevice { external_temperature_sensor_ = external_temperature_sensor; } - void loop() override; + // Configuration setters + void set_legacy_mode(bool enable) { this->legacy_mode_enabled_ = enable; } + void set_read_command(uint8_t read_command) { this->read_command_ = read_command; } + void set_write_command(uint8_t write_command) { this->write_command_ = write_command; } + // Reference value setters (used for calibration and conversion) + void set_current_reference(float current_ref) { this->current_reference_ = current_ref; } + void set_energy_reference(float energy_ref) { this->energy_reference_ = energy_ref; } + void set_power_reference(float power_ref) { this->power_reference_ = power_ref; } + void set_voltage_reference(float voltage_ref) { this->voltage_reference_ = voltage_ref; } + +#ifdef USE_NUMBER + // Calibration number setters (for Home Assistant number entities) + void set_current_calibration_number(number::Number *num) { this->current_calibration_number_ = num; } + void set_voltage_calibration_number(number::Number *num) { this->voltage_calibration_number_ = num; } + void set_power_calibration_number(number::Number *num) { this->power_calibration_number_ = num; } + void set_energy_calibration_number(number::Number *num) { this->energy_calibration_number_ = num; } +#endif + +#ifdef USE_BUTTON + // Resets all calibration values to defaults (can be triggered by a button) + void reset_calibration(); +#endif + + // Core component methods + void loop() override; void update() override; void setup() override; void dump_config() override; protected: - sensor::Sensor *voltage_sensor_{nullptr}; - sensor::Sensor *current_sensor_{nullptr}; - // NB This may be negative as the circuits is seemingly able to measure - // power in both directions - sensor::Sensor *power_sensor_{nullptr}; - sensor::Sensor *energy_sensor_{nullptr}; - sensor::Sensor *internal_temperature_sensor_{nullptr}; - sensor::Sensor *external_temperature_sensor_{nullptr}; + // --- Sensor pointers --- + sensor::Sensor *voltage_sensor_{nullptr}; // Voltage sensor + sensor::Sensor *current_sensor_{nullptr}; // Current sensor + sensor::Sensor *power_sensor_{nullptr}; // Power sensor (can be negative for bidirectional) + sensor::Sensor *energy_sensor_{nullptr}; // Energy sensor + sensor::Sensor *internal_temperature_sensor_{nullptr}; // Internal temperature sensor + sensor::Sensor *external_temperature_sensor_{nullptr}; // External temperature sensor - // Max difference between two measurements of the temperature. Used to avoid noise. - float max_temperature_diff_{0}; - // Divide by this to turn into Watt - float power_reference_ = BL0940_PREF; - // Divide by this to turn into Volt - float voltage_reference_ = BL0940_UREF; - // Divide by this to turn into Ampere - float current_reference_ = BL0940_IREF; - // Divide by this to turn into kWh - float energy_reference_ = BL0940_EREF; +#ifdef USE_NUMBER + // --- Calibration number entities (for dynamic calibration via HA UI) --- + number::Number *voltage_calibration_number_{nullptr}; + number::Number *current_calibration_number_{nullptr}; + number::Number *power_calibration_number_{nullptr}; + number::Number *energy_calibration_number_{nullptr}; +#endif - float update_temp_(sensor::Sensor *sensor, ube16_t packed_temperature) const; + // --- Internal state --- + uint32_t prev_cf_cnt_ = 0; // Previous energy pulse count (for energy calculation) + float max_temperature_diff_{0}; // Max allowed temperature difference between two measurements (noise filter) - static uint32_t to_uint32_t(ube24_t input); + // --- Reference values for conversion --- + float power_reference_; // Divider for raw power to get Watts + float power_reference_cal_; // Calibrated power reference + float voltage_reference_; // Divider for raw voltage to get Volts + float voltage_reference_cal_; // Calibrated voltage reference + float current_reference_; // Divider for raw current to get Amperes + float current_reference_cal_; // Calibrated current reference + float energy_reference_; // Divider for raw energy to get kWh + float energy_reference_cal_; // Calibrated energy reference - static int32_t to_int32_t(sbe24_t input); + // --- Home Assistant calibration values (multipliers, default 1) --- + float current_cal_{1}; + float voltage_cal_{1}; + float power_cal_{1}; + float energy_cal_{1}; - static bool validate_checksum(const DataPacket *data); + // --- Protocol commands --- + uint8_t read_command_; + uint8_t write_command_; - void received_package_(const DataPacket *data) const; + // --- Mode flags --- + bool legacy_mode_enabled_ = true; + + // --- Methods --- + // Converts packed temperature value to float and updates the sensor + float update_temp_(sensor::Sensor *sensor, uint16_le_t packed_temperature) const; + + // Validates the checksum of a received data packet + bool validate_checksum_(DataPacket *data); + + // Handles a received data packet + void received_package_(DataPacket *data); + + // Calculates reference values for calibration and conversion + float calculate_energy_reference_(); + float calculate_power_reference_(); + float calculate_calibration_value_(float state); + + // Calibration update callbacks (used with number entities) + void current_calibration_callback_(float state); + void voltage_calibration_callback_(float state); + void power_calibration_callback_(float state); + void energy_calibration_callback_(float state); + void reset_calibration_callback_(); + + // Recalculates all reference values after calibration changes + void recalibrate_(); }; + } // namespace bl0940 } // namespace esphome diff --git a/esphome/components/bl0940/button/__init__.py b/esphome/components/bl0940/button/__init__.py new file mode 100644 index 0000000000..04d11e6e30 --- /dev/null +++ b/esphome/components/bl0940/button/__init__.py @@ -0,0 +1,27 @@ +import esphome.codegen as cg +from esphome.components import button +import esphome.config_validation as cv +from esphome.const import ENTITY_CATEGORY_CONFIG, ICON_RESTART + +from .. import CONF_BL0940_ID, bl0940_ns +from ..sensor import BL0940 + +CalibrationResetButton = bl0940_ns.class_( + "CalibrationResetButton", button.Button, cg.Component +) + +CONFIG_SCHEMA = cv.All( + button.button_schema( + CalibrationResetButton, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_RESTART, + ) + .extend({cv.GenerateID(CONF_BL0940_ID): cv.use_id(BL0940)}) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + var = await button.new_button(config) + await cg.register_component(var, config) + await cg.register_parented(var, config[CONF_BL0940_ID]) diff --git a/esphome/components/bl0940/button/calibration_reset_button.cpp b/esphome/components/bl0940/button/calibration_reset_button.cpp new file mode 100644 index 0000000000..79a6b872d8 --- /dev/null +++ b/esphome/components/bl0940/button/calibration_reset_button.cpp @@ -0,0 +1,20 @@ +#include "calibration_reset_button.h" +#include "../bl0940.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" + +namespace esphome { +namespace bl0940 { + +static const char *const TAG = "bl0940.button.calibration_reset"; + +void CalibrationResetButton::dump_config() { LOG_BUTTON("", "Calibration Reset Button", this); } + +void CalibrationResetButton::press_action() { + ESP_LOGI(TAG, "Resetting calibration defaults..."); + this->parent_->reset_calibration(); +} + +} // namespace bl0940 +} // namespace esphome diff --git a/esphome/components/bl0940/button/calibration_reset_button.h b/esphome/components/bl0940/button/calibration_reset_button.h new file mode 100644 index 0000000000..6ea3b35cb4 --- /dev/null +++ b/esphome/components/bl0940/button/calibration_reset_button.h @@ -0,0 +1,19 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/button/button.h" + +namespace esphome { +namespace bl0940 { + +class BL0940; // Forward declaration of BL0940 class + +class CalibrationResetButton : public button::Button, public Component, public Parented { + public: + void dump_config() override; + + void press_action() override; +}; + +} // namespace bl0940 +} // namespace esphome diff --git a/esphome/components/bl0940/number/__init__.py b/esphome/components/bl0940/number/__init__.py new file mode 100644 index 0000000000..a640c2ae08 --- /dev/null +++ b/esphome/components/bl0940/number/__init__.py @@ -0,0 +1,94 @@ +import esphome.codegen as cg +from esphome.components import number +import esphome.config_validation as cv +from esphome.const import ( + CONF_MAX_VALUE, + CONF_MIN_VALUE, + CONF_MODE, + CONF_RESTORE_VALUE, + CONF_STEP, + ENTITY_CATEGORY_CONFIG, + UNIT_PERCENT, +) + +from .. import CONF_BL0940_ID, bl0940_ns +from ..sensor import BL0940 + +# Define calibration types +CONF_CURRENT_CALIBRATION = "current_calibration" +CONF_VOLTAGE_CALIBRATION = "voltage_calibration" +CONF_POWER_CALIBRATION = "power_calibration" +CONF_ENERGY_CALIBRATION = "energy_calibration" + +BL0940Number = bl0940_ns.class_("BL0940Number") + +CalibrationNumber = bl0940_ns.class_( + "CalibrationNumber", number.Number, cg.PollingComponent +) + + +def validate_min_max(config): + if config[CONF_MAX_VALUE] <= config[CONF_MIN_VALUE]: + raise cv.Invalid("max_value must be greater than min_value") + return config + + +CALIBRATION_SCHEMA = cv.All( + number.number_schema( + CalibrationNumber, + entity_category=ENTITY_CATEGORY_CONFIG, + unit_of_measurement=UNIT_PERCENT, + ) + .extend( + { + cv.Optional(CONF_MODE, default="BOX"): cv.enum(number.NUMBER_MODES), + cv.Optional(CONF_MAX_VALUE, default=10): cv.All( + cv.float_, cv.Range(max=50) + ), + cv.Optional(CONF_MIN_VALUE, default=-10): cv.All( + cv.float_, cv.Range(min=-50) + ), + cv.Optional(CONF_STEP, default=0.1): cv.positive_float, + cv.Optional(CONF_RESTORE_VALUE): cv.boolean, + } + ) + .extend(cv.COMPONENT_SCHEMA), + validate_min_max, +) + +# Configuration schema for BL0940 numbers +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(BL0940Number), + cv.GenerateID(CONF_BL0940_ID): cv.use_id(BL0940), + cv.Optional(CONF_CURRENT_CALIBRATION): CALIBRATION_SCHEMA, + cv.Optional(CONF_VOLTAGE_CALIBRATION): CALIBRATION_SCHEMA, + cv.Optional(CONF_POWER_CALIBRATION): CALIBRATION_SCHEMA, + cv.Optional(CONF_ENERGY_CALIBRATION): CALIBRATION_SCHEMA, + } +) + + +async def to_code(config): + # Get the BL0940 component instance + bl0940 = await cg.get_variable(config[CONF_BL0940_ID]) + + # Process all calibration types + for cal_type, setter_method in [ + (CONF_CURRENT_CALIBRATION, "set_current_calibration_number"), + (CONF_VOLTAGE_CALIBRATION, "set_voltage_calibration_number"), + (CONF_POWER_CALIBRATION, "set_power_calibration_number"), + (CONF_ENERGY_CALIBRATION, "set_energy_calibration_number"), + ]: + if conf := config.get(cal_type): + var = await number.new_number( + conf, + min_value=conf.get(CONF_MIN_VALUE), + max_value=conf.get(CONF_MAX_VALUE), + step=conf.get(CONF_STEP), + ) + await cg.register_component(var, conf) + + if restore_value := config.get(CONF_RESTORE_VALUE): + cg.add(var.set_restore_value(restore_value)) + cg.add(getattr(bl0940, setter_method)(var)) diff --git a/esphome/components/bl0940/number/calibration_number.cpp b/esphome/components/bl0940/number/calibration_number.cpp new file mode 100644 index 0000000000..e83c3add1f --- /dev/null +++ b/esphome/components/bl0940/number/calibration_number.cpp @@ -0,0 +1,29 @@ +#include "calibration_number.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace bl0940 { + +static const char *const TAG = "bl0940.number"; + +void CalibrationNumber::setup() { + float value = 0.0f; + if (this->restore_value_) { + this->pref_ = global_preferences->make_preference(this->get_preference_hash()); + if (!this->pref_.load(&value)) { + value = 0.0f; + } + } + this->publish_state(value); +} + +void CalibrationNumber::control(float value) { + this->publish_state(value); + if (this->restore_value_) + this->pref_.save(&value); +} + +void CalibrationNumber::dump_config() { LOG_NUMBER("", "Calibration Number", this); } + +} // namespace bl0940 +} // namespace esphome diff --git a/esphome/components/bl0940/number/calibration_number.h b/esphome/components/bl0940/number/calibration_number.h new file mode 100644 index 0000000000..3a19e36dc9 --- /dev/null +++ b/esphome/components/bl0940/number/calibration_number.h @@ -0,0 +1,26 @@ +#pragma once + +#include "esphome/components/number/number.h" +#include "esphome/core/component.h" +#include "esphome/core/preferences.h" + +namespace esphome { +namespace bl0940 { + +class CalibrationNumber : public number::Number, public Component { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; } + + protected: + void control(float value) override; + bool restore_value_{true}; + + ESPPreferenceObject pref_; +}; + +} // namespace bl0940 +} // namespace esphome diff --git a/esphome/components/bl0940/sensor.py b/esphome/components/bl0940/sensor.py index f49e961f0a..d2e0ea435d 100644 --- a/esphome/components/bl0940/sensor.py +++ b/esphome/components/bl0940/sensor.py @@ -8,6 +8,7 @@ from esphome.const import ( CONF_ID, CONF_INTERNAL_TEMPERATURE, CONF_POWER, + CONF_REFERENCE_VOLTAGE, CONF_VOLTAGE, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, @@ -23,12 +24,133 @@ from esphome.const import ( UNIT_WATT, ) +from . import bl0940_ns + DEPENDENCIES = ["uart"] - -bl0940_ns = cg.esphome_ns.namespace("bl0940") BL0940 = bl0940_ns.class_("BL0940", cg.PollingComponent, uart.UARTDevice) +CONF_LEGACY_MODE = "legacy_mode" + +CONF_READ_COMMAND = "read_command" +CONF_WRITE_COMMAND = "write_command" + +CONF_RESISTOR_SHUNT = "resistor_shunt" +CONF_RESISTOR_ONE = "resistor_one" +CONF_RESISTOR_TWO = "resistor_two" + +CONF_CURRENT_REFERENCE = "current_reference" +CONF_ENERGY_REFERENCE = "energy_reference" +CONF_POWER_REFERENCE = "power_reference" +CONF_VOLTAGE_REFERENCE = "voltage_reference" + +DEFAULT_BL0940_READ_COMMAND = 0x58 +DEFAULT_BL0940_WRITE_COMMAND = 0xA1 + +# Values according to BL0940 application note: +# https://www.belling.com.cn/media/file_object/bel_product/BL0940/guide/BL0940_APPNote_TSSOP14_V1.04_EN.pdf +DEFAULT_BL0940_VREF = 1.218 # Vref = 1.218 +DEFAULT_BL0940_RL = 1 # RL = 1 mΩ +DEFAULT_BL0940_R1 = 0.51 # R1 = 0.51 kΩ +DEFAULT_BL0940_R2 = 1950 # R2 = 5 x 390 kΩ -> 1950 kΩ + +# ---------------------------------------------------- +# values from initial implementation +DEFAULT_BL0940_LEGACY_READ_COMMAND = 0x50 +DEFAULT_BL0940_LEGACY_WRITE_COMMAND = 0xA0 + +DEFAULT_BL0940_LEGACY_UREF = 33000 +DEFAULT_BL0940_LEGACY_IREF = 275000 +DEFAULT_BL0940_LEGACY_PREF = 1430 +# Measured to 297J per click according to power consumption of 5 minutes +# Converted to kWh (3.6MJ per kwH). Used to be 256 * 1638.4 +DEFAULT_BL0940_LEGACY_EREF = 3.6e6 / 297 +# ---------------------------------------------------- + + +# methods to calculate voltage and current reference values +def calculate_voltage_reference(vref, r_one, r_two): + # formula: 79931 / Vref * (R1 * 1000) / (R1 + R2) + return 79931 / vref * (r_one * 1000) / (r_one + r_two) + + +def calculate_current_reference(vref, r_shunt): + # formula: 324004 * RL / Vref + return 324004 * r_shunt / vref + + +def calculate_power_reference(voltage_reference, current_reference): + # calculate power reference based on voltage and current reference + return voltage_reference * current_reference * 4046 / 324004 / 79931 + + +def calculate_energy_reference(power_reference): + # formula: power_reference * 3600000 / (1638.4 * 256) + return power_reference * 3600000 / (1638.4 * 256) + + +def validate_legacy_mode(config): + # Only allow schematic calibration options if legacy_mode is False + if config.get(CONF_LEGACY_MODE, True): + forbidden = [ + CONF_REFERENCE_VOLTAGE, + CONF_RESISTOR_SHUNT, + CONF_RESISTOR_ONE, + CONF_RESISTOR_TWO, + ] + for key in forbidden: + if key in config: + raise cv.Invalid( + f"Option '{key}' is only allowed when legacy_mode: false" + ) + return config + + +def set_command_defaults(config): + # Set defaults for read_command and write_command based on legacy_mode + legacy = config.get(CONF_LEGACY_MODE, True) + if legacy: + config.setdefault(CONF_READ_COMMAND, DEFAULT_BL0940_LEGACY_READ_COMMAND) + config.setdefault(CONF_WRITE_COMMAND, DEFAULT_BL0940_LEGACY_WRITE_COMMAND) + else: + config.setdefault(CONF_READ_COMMAND, DEFAULT_BL0940_READ_COMMAND) + config.setdefault(CONF_WRITE_COMMAND, DEFAULT_BL0940_WRITE_COMMAND) + return config + + +def set_reference_values(config): + # Set default reference values based on legacy_mode + if config.get(CONF_LEGACY_MODE, True): + config.setdefault(CONF_VOLTAGE_REFERENCE, DEFAULT_BL0940_LEGACY_UREF) + config.setdefault(CONF_CURRENT_REFERENCE, DEFAULT_BL0940_LEGACY_IREF) + config.setdefault(CONF_POWER_REFERENCE, DEFAULT_BL0940_LEGACY_PREF) + config.setdefault(CONF_ENERGY_REFERENCE, DEFAULT_BL0940_LEGACY_PREF) + else: + vref = config.get(CONF_VOLTAGE_REFERENCE, DEFAULT_BL0940_VREF) + r_one = config.get(CONF_RESISTOR_ONE, DEFAULT_BL0940_R1) + r_two = config.get(CONF_RESISTOR_TWO, DEFAULT_BL0940_R2) + r_shunt = config.get(CONF_RESISTOR_SHUNT, DEFAULT_BL0940_RL) + + config.setdefault( + CONF_VOLTAGE_REFERENCE, calculate_voltage_reference(vref, r_one, r_two) + ) + config.setdefault( + CONF_CURRENT_REFERENCE, calculate_current_reference(vref, r_shunt) + ) + config.setdefault( + CONF_POWER_REFERENCE, + calculate_power_reference( + config.get(CONF_VOLTAGE_REFERENCE), config.get(CONF_CURRENT_REFERENCE) + ), + ) + config.setdefault( + CONF_ENERGY_REFERENCE, + calculate_energy_reference(config.get(CONF_POWER_REFERENCE)), + ) + + return config + + CONFIG_SCHEMA = ( cv.Schema( { @@ -69,10 +191,24 @@ CONFIG_SCHEMA = ( device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), + cv.Optional(CONF_LEGACY_MODE, default=True): cv.boolean, + cv.Optional(CONF_READ_COMMAND): cv.hex_uint8_t, + cv.Optional(CONF_WRITE_COMMAND): cv.hex_uint8_t, + cv.Optional(CONF_REFERENCE_VOLTAGE): cv.float_, + cv.Optional(CONF_RESISTOR_SHUNT): cv.float_, + cv.Optional(CONF_RESISTOR_ONE): cv.float_, + cv.Optional(CONF_RESISTOR_TWO): cv.float_, + cv.Optional(CONF_CURRENT_REFERENCE): cv.float_, + cv.Optional(CONF_ENERGY_REFERENCE): cv.float_, + cv.Optional(CONF_POWER_REFERENCE): cv.float_, + cv.Optional(CONF_VOLTAGE_REFERENCE): cv.float_, } ) .extend(cv.polling_component_schema("60s")) .extend(uart.UART_DEVICE_SCHEMA) + .add_extra(validate_legacy_mode) + .add_extra(set_command_defaults) + .add_extra(set_reference_values) ) @@ -99,3 +235,16 @@ async def to_code(config): if external_temperature_config := config.get(CONF_EXTERNAL_TEMPERATURE): sens = await sensor.new_sensor(external_temperature_config) cg.add(var.set_external_temperature_sensor(sens)) + + # enable legacy mode + cg.add(var.set_legacy_mode(config.get(CONF_LEGACY_MODE))) + + # Set bl0940 commands after validator has determined which defaults to use if not set + cg.add(var.set_read_command(config.get(CONF_READ_COMMAND))) + cg.add(var.set_write_command(config.get(CONF_WRITE_COMMAND))) + + # Set reference values after validator has set the values either from defaults or calculated + cg.add(var.set_current_reference(config.get(CONF_CURRENT_REFERENCE))) + cg.add(var.set_voltage_reference(config.get(CONF_VOLTAGE_REFERENCE))) + cg.add(var.set_power_reference(config.get(CONF_POWER_REFERENCE))) + cg.add(var.set_energy_reference(config.get(CONF_ENERGY_REFERENCE))) diff --git a/esphome/components/bl0942/bl0942.cpp b/esphome/components/bl0942/bl0942.cpp index 86eff57147..95dd689b07 100644 --- a/esphome/components/bl0942/bl0942.cpp +++ b/esphome/components/bl0942/bl0942.cpp @@ -51,7 +51,7 @@ void BL0942::loop() { if (!avail) { return; } - if (avail < sizeof(buffer)) { + if (static_cast(avail) < sizeof(buffer)) { if (!this->rx_start_) { this->rx_start_ = millis(); } else if (millis() > this->rx_start_ + PKT_TIMEOUT_MS) { @@ -148,8 +148,8 @@ void BL0942::setup() { this->write_reg_(BL0942_REG_USR_WRPROT, 0); - if (this->read_reg_(BL0942_REG_MODE) != mode) - this->status_set_warning("BL0942 setup failed!"); + if (static_cast(this->read_reg_(BL0942_REG_MODE)) != mode) + this->status_set_warning(LOG_STR("BL0942 setup failed!")); this->flush(); } diff --git a/esphome/components/ble_client/__init__.py b/esphome/components/ble_client/__init__.py index 0f3869c23b..37db181584 100644 --- a/esphome/components/ble_client/__init__.py +++ b/esphome/components/ble_client/__init__.py @@ -15,6 +15,7 @@ from esphome.const import ( CONF_TRIGGER_ID, CONF_VALUE, ) +from esphome.core import ID AUTO_LOAD = ["esp32_ble_client"] CODEOWNERS = ["@buxtronix", "@clydebarrow"] @@ -116,7 +117,7 @@ CONFIG_SCHEMA = cv.All( ) .extend(cv.COMPONENT_SCHEMA) .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA), - esp32_ble_tracker.consume_connection_slots(1, "ble_client"), + esp32_ble.consume_connection_slots(1, "ble_client"), ) CONF_BLE_CLIENT_ID = "ble_client_id" @@ -198,7 +199,12 @@ async def ble_write_to_code(config, action_id, template_arg, args): templ = await cg.templatable(value, args, cg.std_vector.template(cg.uint8)) cg.add(var.set_value_template(templ)) else: - cg.add(var.set_value_simple(value)) + # Generate static array in flash to avoid RAM copy + if isinstance(value, bytes): + value = list(value) + arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8) + arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*value)) + cg.add(var.set_value_simple(arr, len(value))) if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): cg.add( @@ -286,6 +292,7 @@ async def remove_bond_to_code(config, action_id, template_arg, args): async def to_code(config): # Register the loggers this component needs esp32_ble.register_bt_logger(BTLoggers.GATT, BTLoggers.SMP) + cg.add_define("USE_ESP32_BLE_UUID") var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/ble_client/automation.h b/esphome/components/ble_client/automation.h index a5c661e2f5..788eac4a57 100644 --- a/esphome/components/ble_client/automation.h +++ b/esphome/components/ble_client/automation.h @@ -106,24 +106,35 @@ template class BLEClientWriteAction : public Action, publ void set_char_uuid32(uint32_t uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } - void set_value_template(std::function(Ts...)> func) { - this->value_template_ = std::move(func); - has_simple_value_ = false; + void set_value_template(std::vector (*func)(Ts...)) { + this->value_.func = func; + this->len_ = -1; // Sentinel value indicates template mode } - void set_value_simple(const std::vector &value) { - this->value_simple_ = value; - has_simple_value_ = true; + // Store pointer to static data in flash (no RAM copy) + void set_value_simple(const uint8_t *data, size_t len) { + this->value_.data = data; + this->len_ = len; // Length >= 0 indicates static mode } - void play(Ts... x) override {} + void play(const Ts &...x) override {} - void play_complex(Ts... x) override { + void play_complex(const Ts &...x) override { this->num_running_++; this->var_ = std::make_tuple(x...); - auto value = this->has_simple_value_ ? this->value_simple_ : this->value_template_(x...); + + bool result; + if (this->len_ >= 0) { + // Static mode: write directly from flash pointer + result = this->write(this->value_.data, this->len_); + } else { + // Template mode: call function and write the vector + std::vector value = this->value_.func(x...); + result = this->write(value); + } + // on write failure, continue the automation chain rather than stopping so that e.g. disconnect can work. - if (!write(value)) + if (!result) this->play_next_(x...); } @@ -136,15 +147,15 @@ template class BLEClientWriteAction : public Action, publ * errors. */ // initiate the write. Return true if all went well, will be followed by a WRITE_CHAR event. - bool write(const std::vector &value) { + bool write(const uint8_t *data, size_t len) { if (this->node_state != espbt::ClientState::ESTABLISHED) { esph_log_w(Automation::TAG, "Cannot write to BLE characteristic - not connected"); return false; } - esph_log_vv(Automation::TAG, "Will write %d bytes: %s", value.size(), format_hex_pretty(value).c_str()); - esp_err_t err = esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), - this->char_handle_, value.size(), const_cast(value.data()), - this->write_type_, ESP_GATT_AUTH_REQ_NONE); + esph_log_vv(Automation::TAG, "Will write %d bytes: %s", len, format_hex_pretty(data, len).c_str()); + esp_err_t err = + esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->char_handle_, len, + const_cast(data), this->write_type_, ESP_GATT_AUTH_REQ_NONE); if (err != ESP_OK) { esph_log_e(Automation::TAG, "Error writing to characteristic: %s!", esp_err_to_name(err)); return false; @@ -152,6 +163,8 @@ template class BLEClientWriteAction : public Action, publ return true; } + bool write(const std::vector &value) { return this->write(value.data(), value.size()); } + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override { switch (event) { @@ -185,7 +198,7 @@ template class BLEClientWriteAction : public Action, publ } this->node_state = espbt::ClientState::ESTABLISHED; esph_log_d(Automation::TAG, "Found characteristic %s on device %s", this->char_uuid_.to_string().c_str(), - ble_client_->address_str().c_str()); + ble_client_->address_str()); break; } default: @@ -195,9 +208,11 @@ template class BLEClientWriteAction : public Action, publ private: BLEClient *ble_client_; - bool has_simple_value_ = true; - std::vector value_simple_; - std::function(Ts...)> value_template_{}; + ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length + union Value { + std::vector (*func)(Ts...); // Function pointer (stateless lambdas) + const uint8_t *data; // Pointer to static data in flash + } value_; espbt::ESPBTUUID service_uuid_; espbt::ESPBTUUID char_uuid_; std::tuple var_{}; @@ -210,12 +225,12 @@ template class BLEClientPasskeyReplyAction : public Actionvalue_simple_; + passkey = this->value_.simple; } else { - passkey = this->value_template_(x...); + passkey = this->value_.template_func(x...); } if (passkey > 999999) return; @@ -224,59 +239,63 @@ template class BLEClientPasskeyReplyAction : public Action func) { - this->value_template_ = std::move(func); - has_simple_value_ = false; + void set_value_template(uint32_t (*func)(Ts...)) { + this->value_.template_func = func; + this->has_simple_value_ = false; } void set_value_simple(const uint32_t &value) { - this->value_simple_ = value; - has_simple_value_ = true; + this->value_.simple = value; + this->has_simple_value_ = true; } private: BLEClient *parent_{nullptr}; bool has_simple_value_ = true; - uint32_t value_simple_{0}; - std::function value_template_{}; + union { + uint32_t simple; + uint32_t (*template_func)(Ts...); + } value_{.simple = 0}; }; template class BLEClientNumericComparisonReplyAction : public Action { public: BLEClientNumericComparisonReplyAction(BLEClient *ble_client) { parent_ = ble_client; } - void play(Ts... x) override { + void play(const Ts &...x) override { esp_bd_addr_t remote_bda; memcpy(remote_bda, parent_->get_remote_bda(), sizeof(esp_bd_addr_t)); if (has_simple_value_) { - esp_ble_confirm_reply(remote_bda, this->value_simple_); + esp_ble_confirm_reply(remote_bda, this->value_.simple); } else { - esp_ble_confirm_reply(remote_bda, this->value_template_(x...)); + esp_ble_confirm_reply(remote_bda, this->value_.template_func(x...)); } } - void set_value_template(std::function func) { - this->value_template_ = std::move(func); - has_simple_value_ = false; + void set_value_template(bool (*func)(Ts...)) { + this->value_.template_func = func; + this->has_simple_value_ = false; } void set_value_simple(const bool &value) { - this->value_simple_ = value; - has_simple_value_ = true; + this->value_.simple = value; + this->has_simple_value_ = true; } private: BLEClient *parent_{nullptr}; bool has_simple_value_ = true; - bool value_simple_{false}; - std::function value_template_{}; + union { + bool simple; + bool (*template_func)(Ts...); + } value_{.simple = false}; }; template class BLEClientRemoveBondAction : public Action { public: BLEClientRemoveBondAction(BLEClient *ble_client) { parent_ = ble_client; } - void play(Ts... x) override { + void play(const Ts &...x) override { esp_bd_addr_t remote_bda; memcpy(remote_bda, parent_->get_remote_bda(), sizeof(esp_bd_addr_t)); esp_ble_remove_bond_device(remote_bda); @@ -311,9 +330,9 @@ template class BLEClientConnectAction : public Action, pu } // not used since we override play_complex_ - void play(Ts... x) override {} + void play(const Ts &...x) override {} - void play_complex(Ts... x) override { + void play_complex(const Ts &...x) override { // it makes no sense to have multiple instances of this running at the same time. // this would occur only if the same automation was re-triggered while still // running. So just cancel the second chain if this is detected. @@ -356,9 +375,9 @@ template class BLEClientDisconnectAction : public Action, } // not used since we override play_complex_ - void play(Ts... x) override {} + void play(const Ts &...x) override {} - void play_complex(Ts... x) override { + void play_complex(const Ts &...x) override { this->num_running_++; if (this->node_state == espbt::ClientState::IDLE) { this->play_next_(x...); diff --git a/esphome/components/ble_client/ble_client.cpp b/esphome/components/ble_client/ble_client.cpp index 5cf096c9d4..b8968fe4ba 100644 --- a/esphome/components/ble_client/ble_client.cpp +++ b/esphome/components/ble_client/ble_client.cpp @@ -39,7 +39,7 @@ void BLEClient::set_enabled(bool enabled) { return; this->enabled = enabled; if (!enabled) { - ESP_LOGI(TAG, "[%s] Disabling BLE client.", this->address_str().c_str()); + ESP_LOGI(TAG, "[%s] Disabling BLE client.", this->address_str()); this->disconnect(); } } diff --git a/esphome/components/ble_client/output/__init__.py b/esphome/components/ble_client/output/__init__.py index 729885eb8b..22a6b29442 100644 --- a/esphome/components/ble_client/output/__init__.py +++ b/esphome/components/ble_client/output/__init__.py @@ -27,7 +27,7 @@ CONFIG_SCHEMA = cv.All( ) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): cg.add( @@ -63,6 +63,6 @@ def to_code(config): ) cg.add(var.set_char_uuid128(uuid128)) cg.add(var.set_require_response(config[CONF_REQUIRE_RESPONSE])) - yield output.register_output(var, config) - yield ble_client.register_ble_node(var, config) - yield cg.register_component(var, config) + await output.register_output(var, config) + await ble_client.register_ble_node(var, config) + await cg.register_component(var, config) diff --git a/esphome/components/ble_client/output/ble_binary_output.cpp b/esphome/components/ble_client/output/ble_binary_output.cpp index ce67193be7..84558717f8 100644 --- a/esphome/components/ble_client/output/ble_binary_output.cpp +++ b/esphome/components/ble_client/output/ble_binary_output.cpp @@ -14,7 +14,7 @@ void BLEBinaryOutput::dump_config() { " MAC address : %s\n" " Service UUID : %s\n" " Characteristic UUID: %s", - this->parent_->address_str().c_str(), this->service_uuid_.to_string().c_str(), + this->parent_->address_str(), this->service_uuid_.to_string().c_str(), this->char_uuid_.to_string().c_str()); LOG_BINARY_OUTPUT(this); } @@ -44,7 +44,7 @@ void BLEBinaryOutput::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_i } this->node_state = espbt::ClientState::ESTABLISHED; ESP_LOGD(TAG, "Found characteristic %s on device %s", this->char_uuid_.to_string().c_str(), - this->parent()->address_str().c_str()); + this->parent()->address_str()); this->node_state = espbt::ClientState::ESTABLISHED; break; } diff --git a/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp b/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp index 663c52ac10..4edcbd3877 100644 --- a/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp +++ b/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp @@ -19,7 +19,7 @@ void BLEClientRSSISensor::loop() { void BLEClientRSSISensor::dump_config() { LOG_SENSOR("", "BLE Client RSSI Sensor", this); - ESP_LOGCONFIG(TAG, " MAC address : %s", this->parent()->address_str().c_str()); + ESP_LOGCONFIG(TAG, " MAC address : %s", this->parent()->address_str()); LOG_UPDATE_INTERVAL(this); } @@ -69,10 +69,10 @@ void BLEClientRSSISensor::update() { this->get_rssi_(); } void BLEClientRSSISensor::get_rssi_() { - ESP_LOGV(TAG, "requesting rssi from %s", this->parent()->address_str().c_str()); + ESP_LOGV(TAG, "requesting rssi from %s", this->parent()->address_str()); auto status = esp_ble_gap_read_rssi(this->parent()->get_remote_bda()); if (status != ESP_OK) { - ESP_LOGW(TAG, "esp_ble_gap_read_rssi error, address=%s, status=%d", this->parent()->address_str().c_str(), status); + ESP_LOGW(TAG, "esp_ble_gap_read_rssi error, address=%s, status=%d", this->parent()->address_str(), status); this->status_set_warning(); this->publish_state(NAN); } diff --git a/esphome/components/ble_client/sensor/ble_sensor.cpp b/esphome/components/ble_client/sensor/ble_sensor.cpp index d0ccfe1f2e..8e3e483003 100644 --- a/esphome/components/ble_client/sensor/ble_sensor.cpp +++ b/esphome/components/ble_client/sensor/ble_sensor.cpp @@ -25,7 +25,7 @@ void BLESensor::dump_config() { " Characteristic UUID: %s\n" " Descriptor UUID : %s\n" " Notifications : %s", - this->parent()->address_str().c_str(), this->service_uuid_.to_string().c_str(), + this->parent()->address_str(), this->service_uuid_.to_string().c_str(), this->char_uuid_.to_string().c_str(), this->descr_uuid_.to_string().c_str(), YESNO(this->notify_)); LOG_UPDATE_INTERVAL(this); } @@ -77,6 +77,9 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga } } else { this->node_state = espbt::ClientState::ESTABLISHED; + // For non-notify characteristics, trigger an immediate read after service discovery + // to avoid peripherals disconnecting due to inactivity + this->update(); } break; } @@ -117,9 +120,9 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga } float BLESensor::parse_data_(uint8_t *value, uint16_t value_len) { - if (this->data_to_value_func_.has_value()) { + if (this->has_data_to_value_) { std::vector data(value, value + value_len); - return (*this->data_to_value_func_)(data); + return this->data_to_value_func_(data); } else { return value[0]; } diff --git a/esphome/components/ble_client/sensor/ble_sensor.h b/esphome/components/ble_client/sensor/ble_sensor.h index 24d1ed2fd2..c6335d5836 100644 --- a/esphome/components/ble_client/sensor/ble_sensor.h +++ b/esphome/components/ble_client/sensor/ble_sensor.h @@ -15,8 +15,6 @@ namespace ble_client { namespace espbt = esphome::esp32_ble_tracker; -using data_to_value_t = std::function)>; - class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClientNode { public: void loop() override; @@ -33,13 +31,17 @@ class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClie void set_descr_uuid16(uint16_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } void set_descr_uuid32(uint32_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } void set_descr_uuid128(uint8_t *uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } - void set_data_to_value(data_to_value_t &&lambda) { this->data_to_value_func_ = lambda; } + void set_data_to_value(float (*lambda)(const std::vector &)) { + this->data_to_value_func_ = lambda; + this->has_data_to_value_ = true; + } void set_enable_notify(bool notify) { this->notify_ = notify; } uint16_t handle; protected: float parse_data_(uint8_t *value, uint16_t value_len); - optional data_to_value_func_{}; + bool has_data_to_value_{false}; + float (*data_to_value_func_)(const std::vector &){}; bool notify_; espbt::ESPBTUUID service_uuid_; espbt::ESPBTUUID char_uuid_; diff --git a/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp b/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp index e7da297fa0..bb771aed99 100644 --- a/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp +++ b/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp @@ -28,7 +28,7 @@ void BLETextSensor::dump_config() { " Characteristic UUID: %s\n" " Descriptor UUID : %s\n" " Notifications : %s", - this->parent()->address_str().c_str(), this->service_uuid_.to_string().c_str(), + this->parent()->address_str(), this->service_uuid_.to_string().c_str(), this->char_uuid_.to_string().c_str(), this->descr_uuid_.to_string().c_str(), YESNO(this->notify_)); LOG_UPDATE_INTERVAL(this); } @@ -79,6 +79,9 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ } } else { this->node_state = espbt::ClientState::ESTABLISHED; + // For non-notify characteristics, trigger an immediate read after service discovery + // to avoid peripherals disconnecting due to inactivity + this->update(); } break; } diff --git a/esphome/components/ble_nus/__init__.py b/esphome/components/ble_nus/__init__.py new file mode 100644 index 0000000000..9570005902 --- /dev/null +++ b/esphome/components/ble_nus/__init__.py @@ -0,0 +1,29 @@ +import esphome.codegen as cg +from esphome.components.zephyr import zephyr_add_prj_conf +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_LOGS, CONF_TYPE + +AUTO_LOAD = ["zephyr_ble_server"] +CODEOWNERS = ["@tomaszduda23"] + +ble_nus_ns = cg.esphome_ns.namespace("ble_nus") +BLENUS = ble_nus_ns.class_("BLENUS", cg.Component) + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(BLENUS), + cv.Optional(CONF_TYPE, default=CONF_LOGS): cv.one_of( + *[CONF_LOGS], lower=True + ), + } + ).extend(cv.COMPONENT_SCHEMA), + cv.only_with_framework("zephyr"), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + zephyr_add_prj_conf("BT_NUS", True) + cg.add(var.set_expose_log(config[CONF_TYPE] == CONF_LOGS)) + await cg.register_component(var, config) diff --git a/esphome/components/ble_nus/ble_nus.cpp b/esphome/components/ble_nus/ble_nus.cpp new file mode 100644 index 0000000000..9c4d0a3938 --- /dev/null +++ b/esphome/components/ble_nus/ble_nus.cpp @@ -0,0 +1,157 @@ +#ifdef USE_ZEPHYR +#include "ble_nus.h" +#include +#include +#include "esphome/core/log.h" +#ifdef USE_LOGGER +#include "esphome/components/logger/logger.h" +#include "esphome/core/application.h" +#endif +#include + +namespace esphome::ble_nus { + +constexpr size_t BLE_TX_BUF_SIZE = 2048; + +// NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables) +BLENUS *global_ble_nus; +RING_BUF_DECLARE(global_ble_tx_ring_buf, BLE_TX_BUF_SIZE); +// NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables) + +static const char *const TAG = "ble_nus"; + +size_t BLENUS::write_array(const uint8_t *data, size_t len) { + if (atomic_get(&this->tx_status_) == TX_DISABLED) { + return 0; + } + return ring_buf_put(&global_ble_tx_ring_buf, data, len); +} + +void BLENUS::connected(bt_conn *conn, uint8_t err) { + if (err == 0) { + global_ble_nus->conn_.store(bt_conn_ref(conn)); + } +} + +void BLENUS::disconnected(bt_conn *conn, uint8_t reason) { + if (global_ble_nus->conn_) { + bt_conn_unref(global_ble_nus->conn_.load()); + // Connection array is global static. + // Reference can be kept even if disconnected. + } +} + +void BLENUS::tx_callback(bt_conn *conn) { + atomic_cas(&global_ble_nus->tx_status_, TX_BUSY, TX_ENABLED); + ESP_LOGVV(TAG, "Sent operation completed"); +} + +void BLENUS::send_enabled_callback(bt_nus_send_status status) { + switch (status) { + case BT_NUS_SEND_STATUS_ENABLED: + atomic_set(&global_ble_nus->tx_status_, TX_ENABLED); +#ifdef USE_LOGGER + if (global_ble_nus->expose_log_) { + App.schedule_dump_config(); + } +#endif + ESP_LOGD(TAG, "NUS notification has been enabled"); + break; + case BT_NUS_SEND_STATUS_DISABLED: + atomic_set(&global_ble_nus->tx_status_, TX_DISABLED); + ESP_LOGD(TAG, "NUS notification has been disabled"); + break; + } +} + +void BLENUS::rx_callback(bt_conn *conn, const uint8_t *const data, uint16_t len) { + ESP_LOGD(TAG, "Received %d bytes.", len); +} + +void BLENUS::setup() { + bt_nus_cb callbacks = { + .received = rx_callback, + .sent = tx_callback, + .send_enabled = send_enabled_callback, + }; + + bt_nus_init(&callbacks); + + static bt_conn_cb conn_callbacks = { + .connected = BLENUS::connected, + .disconnected = BLENUS::disconnected, + }; + + bt_conn_cb_register(&conn_callbacks); + + global_ble_nus = this; +#ifdef USE_LOGGER + if (logger::global_logger != nullptr && this->expose_log_) { + logger::global_logger->add_on_log_callback( + [this](int level, const char *tag, const char *message, size_t message_len) { + this->write_array(reinterpret_cast(message), message_len); + const char c = '\n'; + this->write_array(reinterpret_cast(&c), 1); + }); + } + +#endif +} + +void BLENUS::dump_config() { + ESP_LOGCONFIG(TAG, "ble nus:"); + ESP_LOGCONFIG(TAG, " log: %s", YESNO(this->expose_log_)); + uint32_t mtu = 0; + bt_conn *conn = this->conn_.load(); + if (conn) { + mtu = bt_nus_get_mtu(conn); + } + ESP_LOGCONFIG(TAG, " MTU: %u", mtu); +} + +void BLENUS::loop() { + if (ring_buf_is_empty(&global_ble_tx_ring_buf)) { + return; + } + + if (!atomic_cas(&this->tx_status_, TX_ENABLED, TX_BUSY)) { + if (atomic_get(&this->tx_status_) == TX_DISABLED) { + ring_buf_reset(&global_ble_tx_ring_buf); + } + return; + } + + bt_conn *conn = this->conn_.load(); + if (conn) { + conn = bt_conn_ref(conn); + } + + if (nullptr == conn) { + atomic_cas(&this->tx_status_, TX_BUSY, TX_ENABLED); + return; + } + + uint32_t req_len = bt_nus_get_mtu(conn); + + uint8_t *buf; + uint32_t size = ring_buf_get_claim(&global_ble_tx_ring_buf, &buf, req_len); + + int err, err2; + + err = bt_nus_send(conn, buf, size); + err2 = ring_buf_get_finish(&global_ble_tx_ring_buf, size); + if (err2) { + // It should no happen. + ESP_LOGE(TAG, "Size %u exceeds valid bytes in the ring buffer (%d error)", size, err2); + } + if (err == 0) { + ESP_LOGVV(TAG, "Sent %d bytes", size); + } else { + ESP_LOGE(TAG, "Failed to send %d bytes (%d error)", size, err); + atomic_cas(&this->tx_status_, TX_BUSY, TX_ENABLED); + } + bt_conn_unref(conn); +} + +} // namespace esphome::ble_nus +#endif diff --git a/esphome/components/ble_nus/ble_nus.h b/esphome/components/ble_nus/ble_nus.h new file mode 100644 index 0000000000..e8cba32b4c --- /dev/null +++ b/esphome/components/ble_nus/ble_nus.h @@ -0,0 +1,37 @@ +#pragma once +#ifdef USE_ZEPHYR +#include "esphome/core/defines.h" +#include "esphome/core/component.h" +#include +#include + +namespace esphome::ble_nus { + +class BLENUS : public Component { + enum TxStatus { + TX_DISABLED, + TX_ENABLED, + TX_BUSY, + }; + + public: + void setup() override; + void dump_config() override; + void loop() override; + size_t write_array(const uint8_t *data, size_t len); + void set_expose_log(bool expose_log) { this->expose_log_ = expose_log; } + + protected: + static void send_enabled_callback(bt_nus_send_status status); + static void tx_callback(bt_conn *conn); + static void rx_callback(bt_conn *conn, const uint8_t *data, uint16_t len); + static void connected(bt_conn *conn, uint8_t err); + static void disconnected(bt_conn *conn, uint8_t reason); + + std::atomic conn_ = nullptr; + bool expose_log_ = false; + atomic_t tx_status_ = ATOMIC_INIT(TX_DISABLED); +}; + +} // namespace esphome::ble_nus +#endif diff --git a/esphome/components/bluetooth_proxy/__init__.py b/esphome/components/bluetooth_proxy/__init__.py index ec1df6a06c..ad7528c156 100644 --- a/esphome/components/bluetooth_proxy/__init__.py +++ b/esphome/components/bluetooth_proxy/__init__.py @@ -1,3 +1,5 @@ +import logging + import esphome.codegen as cg from esphome.components import esp32_ble, esp32_ble_client, esp32_ble_tracker from esphome.components.esp32 import add_idf_sdkconfig_option @@ -7,7 +9,9 @@ from esphome.const import CONF_ACTIVE, CONF_ID AUTO_LOAD = ["esp32_ble_client", "esp32_ble_tracker"] DEPENDENCIES = ["api", "esp32"] -CODEOWNERS = ["@jesserockz"] +CODEOWNERS = ["@jesserockz", "@bdraco"] + +_LOGGER = logging.getLogger(__name__) CONF_CONNECTION_SLOTS = "connection_slots" CONF_CACHE_SERVICES = "cache_services" @@ -38,9 +42,8 @@ def validate_connections(config): ) elif config[CONF_ACTIVE]: connection_slots: int = config[CONF_CONNECTION_SLOTS] - esp32_ble_tracker.consume_connection_slots(connection_slots, "bluetooth_proxy")( - config - ) + esp32_ble.consume_connection_slots(connection_slots, "bluetooth_proxy")(config) + return { **config, CONF_CONNECTIONS: [CONNECTION_SCHEMA({}) for _ in range(connection_slots)], @@ -53,20 +56,18 @@ CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(BluetoothProxy), - cv.Optional(CONF_ACTIVE, default=False): cv.boolean, - cv.SplitDefault(CONF_CACHE_SERVICES, esp32_idf=True): cv.All( - cv.only_with_esp_idf, cv.boolean - ), + cv.Optional(CONF_ACTIVE, default=True): cv.boolean, + cv.Optional(CONF_CACHE_SERVICES, default=True): cv.boolean, cv.Optional( CONF_CONNECTION_SLOTS, default=DEFAULT_CONNECTION_SLOTS, ): cv.All( cv.positive_int, - cv.Range(min=1, max=esp32_ble_tracker.max_connections()), + cv.Range(min=1, max=esp32_ble.IDF_MAX_CONNECTIONS), ), cv.Optional(CONF_CONNECTIONS): cv.All( cv.ensure_list(CONNECTION_SCHEMA), - cv.Length(min=1, max=esp32_ble_tracker.max_connections()), + cv.Length(min=1, max=esp32_ble.IDF_MAX_CONNECTIONS), ), } ) @@ -91,6 +92,12 @@ async def to_code(config): connection_count = len(config.get(CONF_CONNECTIONS, [])) cg.add_define("BLUETOOTH_PROXY_MAX_CONNECTIONS", connection_count) + # Define batch size for BLE advertisements + # Each advertisement is up to 80 bytes when packaged (including protocol overhead) + # 16 advertisements × 80 bytes (worst case) = 1280 bytes out of ~1320 bytes usable payload + # This achieves ~97% WiFi MTU utilization while staying under the limit + cg.add_define("BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE", 16) + for connection_conf in config.get(CONF_CONNECTIONS, []): connection_var = cg.new_Pvariable(connection_conf[CONF_ID]) await cg.register_component(connection_var, connection_conf) diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp index 01c2aa3d22..1d6f7e23b3 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp @@ -12,16 +12,30 @@ namespace esphome::bluetooth_proxy { static const char *const TAG = "bluetooth_proxy.connection"; +// This function is allocation-free and directly packs UUIDs into the output array +// using precalculated constants for the Bluetooth base UUID static void fill_128bit_uuid_array(std::array &out, esp_bt_uuid_t uuid_source) { - esp_bt_uuid_t uuid = espbt::ESPBTUUID::from_uuid(uuid_source).as_128bit().get_uuid(); - out[0] = ((uint64_t) uuid.uuid.uuid128[15] << 56) | ((uint64_t) uuid.uuid.uuid128[14] << 48) | - ((uint64_t) uuid.uuid.uuid128[13] << 40) | ((uint64_t) uuid.uuid.uuid128[12] << 32) | - ((uint64_t) uuid.uuid.uuid128[11] << 24) | ((uint64_t) uuid.uuid.uuid128[10] << 16) | - ((uint64_t) uuid.uuid.uuid128[9] << 8) | ((uint64_t) uuid.uuid.uuid128[8]); - out[1] = ((uint64_t) uuid.uuid.uuid128[7] << 56) | ((uint64_t) uuid.uuid.uuid128[6] << 48) | - ((uint64_t) uuid.uuid.uuid128[5] << 40) | ((uint64_t) uuid.uuid.uuid128[4] << 32) | - ((uint64_t) uuid.uuid.uuid128[3] << 24) | ((uint64_t) uuid.uuid.uuid128[2] << 16) | - ((uint64_t) uuid.uuid.uuid128[1] << 8) | ((uint64_t) uuid.uuid.uuid128[0]); + // Bluetooth base UUID: 00000000-0000-1000-8000-00805F9B34FB + // out[0] = bytes 8-15 (big-endian) + // - For 128-bit UUIDs: use bytes 8-15 as-is + // - For 16/32-bit UUIDs: insert into bytes 12-15, use 0x00001000 for bytes 8-11 + out[0] = uuid_source.len == ESP_UUID_LEN_128 + ? (((uint64_t) uuid_source.uuid.uuid128[15] << 56) | ((uint64_t) uuid_source.uuid.uuid128[14] << 48) | + ((uint64_t) uuid_source.uuid.uuid128[13] << 40) | ((uint64_t) uuid_source.uuid.uuid128[12] << 32) | + ((uint64_t) uuid_source.uuid.uuid128[11] << 24) | ((uint64_t) uuid_source.uuid.uuid128[10] << 16) | + ((uint64_t) uuid_source.uuid.uuid128[9] << 8) | ((uint64_t) uuid_source.uuid.uuid128[8])) + : (((uint64_t) (uuid_source.len == ESP_UUID_LEN_16 ? uuid_source.uuid.uuid16 : uuid_source.uuid.uuid32) + << 32) | + 0x00001000ULL); // Base UUID bytes 8-11 + // out[1] = bytes 0-7 (big-endian) + // - For 128-bit UUIDs: use bytes 0-7 as-is + // - For 16/32-bit UUIDs: use precalculated base UUID constant + out[1] = uuid_source.len == ESP_UUID_LEN_128 + ? ((uint64_t) uuid_source.uuid.uuid128[7] << 56) | ((uint64_t) uuid_source.uuid.uuid128[6] << 48) | + ((uint64_t) uuid_source.uuid.uuid128[5] << 40) | ((uint64_t) uuid_source.uuid.uuid128[4] << 32) | + ((uint64_t) uuid_source.uuid.uuid128[3] << 24) | ((uint64_t) uuid_source.uuid.uuid128[2] << 16) | + ((uint64_t) uuid_source.uuid.uuid128[1] << 8) | ((uint64_t) uuid_source.uuid.uuid128[0]) + : 0x800000805F9B34FBULL; // Base UUID bytes 0-7: 80-00-00-80-5F-9B-34-FB } // Helper to fill UUID in the appropriate format based on client support and UUID type @@ -80,9 +94,11 @@ void BluetoothConnection::dump_config() { void BluetoothConnection::update_allocated_slot_(uint64_t find_value, uint64_t set_value) { auto &allocated = this->proxy_->connections_free_response_.allocated; - auto *it = std::find(allocated.begin(), allocated.end(), find_value); - if (it != allocated.end()) { - *it = set_value; + for (auto &slot : allocated) { + if (slot == find_value) { + slot = set_value; + return; + } } } @@ -105,13 +121,24 @@ void BluetoothConnection::set_address(uint64_t address) { void BluetoothConnection::loop() { BLEClientBase::loop(); - // Early return if no active connection or not in service discovery phase - if (this->address_ == 0 || this->send_service_ < 0 || this->send_service_ > this->service_count_) { + // Early return if no active connection + if (this->address_ == 0) { return; } - // Handle service discovery - this->send_service_for_discovery_(); + // Handle service discovery if in valid range + if (this->send_service_ >= 0 && this->send_service_ <= this->service_count_) { + this->send_service_for_discovery_(); + } + + // Check if we should disable the loop + // - For V3_WITH_CACHE: Services are never sent, disable after INIT state + // - For V3_WITHOUT_CACHE: Disable only after service discovery is complete + // (send_service_ == DONE_SENDING_SERVICES, which is only set after services are sent) + if (this->state_ != espbt::ClientState::INIT && (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE || + this->send_service_ == DONE_SENDING_SERVICES)) { + this->disable_loop(); + } } void BluetoothConnection::reset_connection_(esp_err_t reason) { @@ -125,7 +152,7 @@ void BluetoothConnection::reset_connection_(esp_err_t reason) { // to detect incomplete service discovery rather than relying on us to // tell them about a partial list. this->set_address(0); - this->send_service_ = DONE_SENDING_SERVICES; + this->send_service_ = INIT_SENDING_SERVICES; this->proxy_->send_connections_free(); } @@ -133,10 +160,7 @@ void BluetoothConnection::send_service_for_discovery_() { if (this->send_service_ >= this->service_count_) { this->send_service_ = DONE_SENDING_SERVICES; this->proxy_->send_gatt_services_done(this->address_); - if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE || - this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) { - this->release_services(); - } + this->release_services(); return; } @@ -172,8 +196,8 @@ void BluetoothConnection::send_service_for_discovery_() { if (service_status != ESP_GATT_OK || service_count == 0) { ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_service %s, status=%d, service_count=%d, offset=%d", - this->connection_index_, this->address_str().c_str(), - service_status != ESP_GATT_OK ? "error" : "missing", service_status, service_count, this->send_service_); + this->connection_index_, this->address_str(), service_status != ESP_GATT_OK ? "error" : "missing", + service_status, service_count, this->send_service_); this->send_service_ = DONE_SENDING_SERVICES; return; } @@ -185,8 +209,7 @@ void BluetoothConnection::send_service_for_discovery_() { service_result.start_handle, service_result.end_handle, 0, &total_char_count); if (char_count_status != ESP_GATT_OK) { - ESP_LOGE(TAG, "[%d] [%s] Error getting characteristic count, status=%d", this->connection_index_, - this->address_str().c_str(), char_count_status); + this->log_connection_error_("esp_ble_gattc_get_attr_count", char_count_status); this->send_service_ = DONE_SENDING_SERVICES; return; } @@ -207,8 +230,8 @@ void BluetoothConnection::send_service_for_discovery_() { service_resp.handle = service_result.start_handle; if (total_char_count > 0) { - // Reserve space and process characteristics - service_resp.characteristics.reserve(total_char_count); + // Initialize FixedVector with exact count and process characteristics + service_resp.characteristics.init(total_char_count); uint16_t char_offset = 0; esp_gattc_char_elem_t char_result; while (true) { // characteristics @@ -220,8 +243,7 @@ void BluetoothConnection::send_service_for_discovery_() { break; } if (char_status != ESP_GATT_OK) { - ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_char error, status=%d", this->connection_index_, - this->address_str().c_str(), char_status); + this->log_connection_error_("esp_ble_gattc_get_all_char", char_status); this->send_service_ = DONE_SENDING_SERVICES; return; } @@ -231,9 +253,7 @@ void BluetoothConnection::send_service_for_discovery_() { service_resp.characteristics.emplace_back(); auto &characteristic_resp = service_resp.characteristics.back(); - fill_gatt_uuid(characteristic_resp.uuid, characteristic_resp.short_uuid, char_result.uuid, use_efficient_uuids); - characteristic_resp.handle = char_result.char_handle; characteristic_resp.properties = char_result.properties; char_offset++; @@ -244,18 +264,16 @@ void BluetoothConnection::send_service_for_discovery_() { this->gattc_if_, this->conn_id_, ESP_GATT_DB_DESCRIPTOR, 0, 0, char_result.char_handle, &total_desc_count); if (desc_count_status != ESP_GATT_OK) { - ESP_LOGE(TAG, "[%d] [%s] Error getting descriptor count for char handle %d, status=%d", - this->connection_index_, this->address_str().c_str(), char_result.char_handle, desc_count_status); + this->log_connection_error_("esp_ble_gattc_get_attr_count", desc_count_status); this->send_service_ = DONE_SENDING_SERVICES; return; } if (total_desc_count == 0) { - // No descriptors, continue to next characteristic continue; } - // Reserve space and process descriptors - characteristic_resp.descriptors.reserve(total_desc_count); + // Initialize FixedVector with exact count and process descriptors + characteristic_resp.descriptors.init(total_desc_count); uint16_t desc_offset = 0; esp_gattc_descr_elem_t desc_result; while (true) { // descriptors @@ -266,8 +284,7 @@ void BluetoothConnection::send_service_for_discovery_() { break; } if (desc_status != ESP_GATT_OK) { - ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_descr error, status=%d", this->connection_index_, - this->address_str().c_str(), desc_status); + this->log_connection_error_("esp_ble_gattc_get_all_descr", desc_status); this->send_service_ = DONE_SENDING_SERVICES; return; } @@ -277,9 +294,7 @@ void BluetoothConnection::send_service_for_discovery_() { characteristic_resp.descriptors.emplace_back(); auto &descriptor_resp = characteristic_resp.descriptors.back(); - fill_gatt_uuid(descriptor_resp.uuid, descriptor_resp.short_uuid, desc_result.uuid, use_efficient_uuids); - descriptor_resp.handle = desc_result.handle; desc_offset++; } @@ -297,13 +312,13 @@ void BluetoothConnection::send_service_for_discovery_() { if (resp.services.size() > 1) { resp.services.pop_back(); ESP_LOGD(TAG, "[%d] [%s] Service %d would exceed limit (current: %d + service: %d > %d), sending current batch", - this->connection_index_, this->address_str().c_str(), this->send_service_, current_size, service_size, + this->connection_index_, this->address_str(), this->send_service_, current_size, service_size, MAX_PACKET_SIZE); // Don't increment send_service_ - we'll retry this service in next batch } else { // This single service is too large, but we have to send it anyway ESP_LOGV(TAG, "[%d] [%s] Service %d is too large (%d bytes) but sending anyway", this->connection_index_, - this->address_str().c_str(), this->send_service_, service_size); + this->address_str(), this->send_service_, service_size); // Increment so we don't get stuck this->send_service_++; } @@ -321,6 +336,32 @@ void BluetoothConnection::send_service_for_discovery_() { api_conn->send_message(resp, api::BluetoothGATTGetServicesResponse::MESSAGE_TYPE); } +void BluetoothConnection::log_connection_error_(const char *operation, esp_gatt_status_t status) { + ESP_LOGE(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str(), operation, status); +} + +void BluetoothConnection::log_connection_warning_(const char *operation, esp_err_t err) { + ESP_LOGW(TAG, "[%d] [%s] %s failed, err=%d", this->connection_index_, this->address_str(), operation, err); +} + +void BluetoothConnection::log_gatt_not_connected_(const char *action, const char *type) { + ESP_LOGW(TAG, "[%d] [%s] Cannot %s GATT %s, not connected.", this->connection_index_, this->address_str(), action, + type); +} + +void BluetoothConnection::log_gatt_operation_error_(const char *operation, uint16_t handle, esp_gatt_status_t status) { + ESP_LOGW(TAG, "[%d] [%s] Error %s for handle 0x%2X, status=%d", this->connection_index_, this->address_str(), + operation, handle, status); +} + +esp_err_t BluetoothConnection::check_and_log_error_(const char *operation, esp_err_t err) { + if (err != ESP_OK) { + this->log_connection_warning_(operation, err); + return err; + } + return ESP_OK; +} + bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { if (!BLEClientBase::gattc_event_handler(event, gattc_if, param)) @@ -328,10 +369,19 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga switch (event) { case ESP_GATTC_DISCONNECT_EVT: { - this->reset_connection_(param->disconnect.reason); + // Don't reset connection yet - wait for CLOSE_EVT to ensure controller has freed resources + // This prevents race condition where we mark slot as free before controller cleanup is complete + ESP_LOGD(TAG, "[%d] [%s] Disconnect, reason=0x%02x", this->connection_index_, this->address_str_, + param->disconnect.reason); + // Send disconnection notification but don't free the slot yet + this->proxy_->send_device_connection(this->address_, false, 0, param->disconnect.reason); break; } case ESP_GATTC_CLOSE_EVT: { + ESP_LOGD(TAG, "[%d] [%s] Close, reason=0x%02x, freeing slot", this->connection_index_, this->address_str_, + param->close.reason); + // Now the GATT connection is fully closed and controller resources are freed + // Safe to mark the connection slot as available this->reset_connection_(param->close.reason); break; } @@ -361,8 +411,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga case ESP_GATTC_READ_DESCR_EVT: case ESP_GATTC_READ_CHAR_EVT: { if (param->read.status != ESP_GATT_OK) { - ESP_LOGW(TAG, "[%d] [%s] Error reading char/descriptor at handle 0x%2X, status=%d", this->connection_index_, - this->address_str_.c_str(), param->read.handle, param->read.status); + this->log_gatt_operation_error_("reading char/descriptor", param->read.handle, param->read.status); this->proxy_->send_gatt_error(this->address_, param->read.handle, param->read.status); break; } @@ -376,8 +425,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga case ESP_GATTC_WRITE_CHAR_EVT: case ESP_GATTC_WRITE_DESCR_EVT: { if (param->write.status != ESP_GATT_OK) { - ESP_LOGW(TAG, "[%d] [%s] Error writing char/descriptor at handle 0x%2X, status=%d", this->connection_index_, - this->address_str_.c_str(), param->write.handle, param->write.status); + this->log_gatt_operation_error_("writing char/descriptor", param->write.handle, param->write.status); this->proxy_->send_gatt_error(this->address_, param->write.handle, param->write.status); break; } @@ -389,9 +437,8 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga } case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: { if (param->unreg_for_notify.status != ESP_GATT_OK) { - ESP_LOGW(TAG, "[%d] [%s] Error unregistering notifications for handle 0x%2X, status=%d", - this->connection_index_, this->address_str_.c_str(), param->unreg_for_notify.handle, - param->unreg_for_notify.status); + this->log_gatt_operation_error_("unregistering notifications", param->unreg_for_notify.handle, + param->unreg_for_notify.status); this->proxy_->send_gatt_error(this->address_, param->unreg_for_notify.handle, param->unreg_for_notify.status); break; } @@ -403,8 +450,8 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga } case ESP_GATTC_REG_FOR_NOTIFY_EVT: { if (param->reg_for_notify.status != ESP_GATT_OK) { - ESP_LOGW(TAG, "[%d] [%s] Error registering notifications for handle 0x%2X, status=%d", this->connection_index_, - this->address_str_.c_str(), param->reg_for_notify.handle, param->reg_for_notify.status); + this->log_gatt_operation_error_("registering notifications", param->reg_for_notify.handle, + param->reg_for_notify.status); this->proxy_->send_gatt_error(this->address_, param->reg_for_notify.handle, param->reg_for_notify.status); break; } @@ -415,7 +462,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga break; } case ESP_GATTC_NOTIFY_EVT: { - ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_NOTIFY_EVT: handle=0x%2X", this->connection_index_, this->address_str_.c_str(), + ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_NOTIFY_EVT: handle=0x%2X", this->connection_index_, this->address_str_, param->notify.handle); api::BluetoothGATTNotifyDataResponse resp; resp.address = this->address_; @@ -450,108 +497,77 @@ void BluetoothConnection::gap_event_handler(esp_gap_ble_cb_event_t event, esp_bl esp_err_t BluetoothConnection::read_characteristic(uint16_t handle) { if (!this->connected()) { - ESP_LOGW(TAG, "[%d] [%s] Cannot read GATT characteristic, not connected.", this->connection_index_, - this->address_str_.c_str()); + this->log_gatt_not_connected_("read", "characteristic"); return ESP_GATT_NOT_CONNECTED; } - ESP_LOGV(TAG, "[%d] [%s] Reading GATT characteristic handle %d", this->connection_index_, this->address_str_.c_str(), - handle); + ESP_LOGV(TAG, "[%d] [%s] Reading GATT characteristic handle %d", this->connection_index_, this->address_str_, handle); esp_err_t err = esp_ble_gattc_read_char(this->gattc_if_, this->conn_id_, handle, ESP_GATT_AUTH_REQ_NONE); - if (err != ERR_OK) { - ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_read_char error, err=%d", this->connection_index_, - this->address_str_.c_str(), err); - return err; - } - return ESP_OK; + return this->check_and_log_error_("esp_ble_gattc_read_char", err); } -esp_err_t BluetoothConnection::write_characteristic(uint16_t handle, const std::string &data, bool response) { +esp_err_t BluetoothConnection::write_characteristic(uint16_t handle, const uint8_t *data, size_t length, + bool response) { if (!this->connected()) { - ESP_LOGW(TAG, "[%d] [%s] Cannot write GATT characteristic, not connected.", this->connection_index_, - this->address_str_.c_str()); + this->log_gatt_not_connected_("write", "characteristic"); return ESP_GATT_NOT_CONNECTED; } - ESP_LOGV(TAG, "[%d] [%s] Writing GATT characteristic handle %d", this->connection_index_, this->address_str_.c_str(), - handle); + ESP_LOGV(TAG, "[%d] [%s] Writing GATT characteristic handle %d", this->connection_index_, this->address_str_, handle); + // ESP-IDF's API requires a non-const uint8_t* but it doesn't modify the data + // The BTC layer immediately copies the data to its own buffer (see btc_gattc.c) + // const_cast is safe here and was previously hidden by a C-style cast esp_err_t err = - esp_ble_gattc_write_char(this->gattc_if_, this->conn_id_, handle, data.size(), (uint8_t *) data.data(), + esp_ble_gattc_write_char(this->gattc_if_, this->conn_id_, handle, length, const_cast(data), response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); - if (err != ERR_OK) { - ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_write_char error, err=%d", this->connection_index_, - this->address_str_.c_str(), err); - return err; - } - return ESP_OK; + return this->check_and_log_error_("esp_ble_gattc_write_char", err); } esp_err_t BluetoothConnection::read_descriptor(uint16_t handle) { if (!this->connected()) { - ESP_LOGW(TAG, "[%d] [%s] Cannot read GATT descriptor, not connected.", this->connection_index_, - this->address_str_.c_str()); + this->log_gatt_not_connected_("read", "descriptor"); return ESP_GATT_NOT_CONNECTED; } - ESP_LOGV(TAG, "[%d] [%s] Reading GATT descriptor handle %d", this->connection_index_, this->address_str_.c_str(), - handle); + ESP_LOGV(TAG, "[%d] [%s] Reading GATT descriptor handle %d", this->connection_index_, this->address_str_, handle); esp_err_t err = esp_ble_gattc_read_char_descr(this->gattc_if_, this->conn_id_, handle, ESP_GATT_AUTH_REQ_NONE); - if (err != ERR_OK) { - ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_read_char_descr error, err=%d", this->connection_index_, - this->address_str_.c_str(), err); - return err; - } - return ESP_OK; + return this->check_and_log_error_("esp_ble_gattc_read_char_descr", err); } -esp_err_t BluetoothConnection::write_descriptor(uint16_t handle, const std::string &data, bool response) { +esp_err_t BluetoothConnection::write_descriptor(uint16_t handle, const uint8_t *data, size_t length, bool response) { if (!this->connected()) { - ESP_LOGW(TAG, "[%d] [%s] Cannot write GATT descriptor, not connected.", this->connection_index_, - this->address_str_.c_str()); + this->log_gatt_not_connected_("write", "descriptor"); return ESP_GATT_NOT_CONNECTED; } - ESP_LOGV(TAG, "[%d] [%s] Writing GATT descriptor handle %d", this->connection_index_, this->address_str_.c_str(), - handle); + ESP_LOGV(TAG, "[%d] [%s] Writing GATT descriptor handle %d", this->connection_index_, this->address_str_, handle); + // ESP-IDF's API requires a non-const uint8_t* but it doesn't modify the data + // The BTC layer immediately copies the data to its own buffer (see btc_gattc.c) + // const_cast is safe here and was previously hidden by a C-style cast esp_err_t err = esp_ble_gattc_write_char_descr( - this->gattc_if_, this->conn_id_, handle, data.size(), (uint8_t *) data.data(), + this->gattc_if_, this->conn_id_, handle, length, const_cast(data), response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); - if (err != ERR_OK) { - ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_write_char_descr error, err=%d", this->connection_index_, - this->address_str_.c_str(), err); - return err; - } - return ESP_OK; + return this->check_and_log_error_("esp_ble_gattc_write_char_descr", err); } esp_err_t BluetoothConnection::notify_characteristic(uint16_t handle, bool enable) { if (!this->connected()) { - ESP_LOGW(TAG, "[%d] [%s] Cannot notify GATT characteristic, not connected.", this->connection_index_, - this->address_str_.c_str()); + this->log_gatt_not_connected_("notify", "characteristic"); return ESP_GATT_NOT_CONNECTED; } if (enable) { ESP_LOGV(TAG, "[%d] [%s] Registering for GATT characteristic notifications handle %d", this->connection_index_, - this->address_str_.c_str(), handle); + this->address_str_, handle); esp_err_t err = esp_ble_gattc_register_for_notify(this->gattc_if_, this->remote_bda_, handle); - if (err != ESP_OK) { - ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_register_for_notify failed, err=%d", this->connection_index_, - this->address_str_.c_str(), err); - return err; - } - } else { - ESP_LOGV(TAG, "[%d] [%s] Unregistering for GATT characteristic notifications handle %d", this->connection_index_, - this->address_str_.c_str(), handle); - esp_err_t err = esp_ble_gattc_unregister_for_notify(this->gattc_if_, this->remote_bda_, handle); - if (err != ESP_OK) { - ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_unregister_for_notify failed, err=%d", this->connection_index_, - this->address_str_.c_str(), err); - return err; - } + return this->check_and_log_error_("esp_ble_gattc_register_for_notify", err); } - return ESP_OK; + + ESP_LOGV(TAG, "[%d] [%s] Unregistering for GATT characteristic notifications handle %d", this->connection_index_, + this->address_str_, handle); + esp_err_t err = esp_ble_gattc_unregister_for_notify(this->gattc_if_, this->remote_bda_, handle); + return this->check_and_log_error_("esp_ble_gattc_unregister_for_notify", err); } esp32_ble_tracker::AdvertisementParserType BluetoothConnection::get_advertisement_parser_type() { diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.h b/esphome/components/bluetooth_proxy/bluetooth_connection.h index 042868e7a4..60bbc93e8b 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.h +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.h @@ -8,7 +8,7 @@ namespace esphome::bluetooth_proxy { class BluetoothProxy; -class BluetoothConnection : public esp32_ble_client::BLEClientBase { +class BluetoothConnection final : public esp32_ble_client::BLEClientBase { public: void dump_config() override; void loop() override; @@ -18,9 +18,9 @@ class BluetoothConnection : public esp32_ble_client::BLEClientBase { esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override; esp_err_t read_characteristic(uint16_t handle); - esp_err_t write_characteristic(uint16_t handle, const std::string &data, bool response); + esp_err_t write_characteristic(uint16_t handle, const uint8_t *data, size_t length, bool response); esp_err_t read_descriptor(uint16_t handle); - esp_err_t write_descriptor(uint16_t handle, const std::string &data, bool response); + esp_err_t write_descriptor(uint16_t handle, const uint8_t *data, size_t length, bool response); esp_err_t notify_characteristic(uint16_t handle, bool enable); @@ -33,13 +33,18 @@ class BluetoothConnection : public esp32_ble_client::BLEClientBase { void send_service_for_discovery_(); void reset_connection_(esp_err_t reason); void update_allocated_slot_(uint64_t find_value, uint64_t set_value); + void log_connection_error_(const char *operation, esp_gatt_status_t status); + void log_connection_warning_(const char *operation, esp_err_t err); + void log_gatt_not_connected_(const char *action, const char *type); + void log_gatt_operation_error_(const char *operation, uint16_t handle, esp_gatt_status_t status); + esp_err_t check_and_log_error_(const char *operation, esp_err_t err); // Memory optimized layout for 32-bit systems // Group 1: Pointers (4 bytes each, naturally aligned) BluetoothProxy *proxy_; // Group 2: 2-byte types - int16_t send_service_{-2}; // Needs to handle negative values and service count + int16_t send_service_{-3}; // -3 = INIT_SENDING_SERVICES, -2 = DONE_SENDING_SERVICES, >=0 = service index // Group 3: 1-byte types bool seen_mtu_or_services_{false}; diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index a59a33117a..71f8da75a7 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -11,12 +11,8 @@ namespace esphome::bluetooth_proxy { static const char *const TAG = "bluetooth_proxy"; -// Batch size for BLE advertisements to maximize WiFi efficiency -// Each advertisement is up to 80 bytes when packaged (including protocol overhead) -// Most advertisements are 20-30 bytes, allowing even more to fit per packet -// 16 advertisements × 80 bytes (worst case) = 1280 bytes out of ~1320 bytes usable payload -// This achieves ~97% WiFi MTU utilization while staying under the limit -static constexpr size_t FLUSH_BATCH_SIZE = 16; +// BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE is defined during code generation +// It sets the batch size for BLE advertisements to maximize WiFi efficiency // Verify BLE advertisement data array size matches the BLE specification (31 bytes adv + 31 bytes scan response) static_assert(sizeof(((api::BluetoothLERawAdvertisement *) nullptr)->data) == 62, @@ -25,18 +21,11 @@ static_assert(sizeof(((api::BluetoothLERawAdvertisement *) nullptr)->data) == 62 BluetoothProxy::BluetoothProxy() { global_bluetooth_proxy = this; } void BluetoothProxy::setup() { - // Pre-allocate response object - this->response_ = std::make_unique(); + this->connections_free_response_.limit = BLUETOOTH_PROXY_MAX_CONNECTIONS; + this->connections_free_response_.free = BLUETOOTH_PROXY_MAX_CONNECTIONS; - // Reserve capacity but start with size 0 - // Reserve 50% since we'll grow naturally and flush at FLUSH_BATCH_SIZE - this->response_->advertisements.reserve(FLUSH_BATCH_SIZE / 2); - - // Don't pre-allocate pool - let it grow only if needed in busy environments - // Many devices in quiet areas will never need the overflow pool - - this->connections_free_response_.limit = this->connections_.size(); - this->connections_free_response_.free = this->connections_.size(); + // Capture the configured scan mode from YAML before any API changes + this->configured_scan_active_ = this->parent_->get_scan_active(); this->parent_->add_scanner_state_callback([this](esp32_ble_tracker::ScannerState state) { if (this->api_connection_ != nullptr) { @@ -50,9 +39,31 @@ void BluetoothProxy::send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerSta resp.state = static_cast(state); resp.mode = this->parent_->get_scan_active() ? api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE : api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_PASSIVE; + resp.configured_mode = this->configured_scan_active_ + ? api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE + : api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_PASSIVE; this->api_connection_->send_message(resp, api::BluetoothScannerStateResponse::MESSAGE_TYPE); } +void BluetoothProxy::log_connection_request_ignored_(BluetoothConnection *connection, espbt::ClientState state) { + ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, state: %s", connection->get_connection_index(), + connection->address_str(), espbt::client_state_to_string(state)); +} + +void BluetoothProxy::log_connection_info_(BluetoothConnection *connection, const char *message) { + ESP_LOGI(TAG, "[%d] [%s] Connecting %s", connection->get_connection_index(), connection->address_str(), message); +} + +void BluetoothProxy::log_not_connected_gatt_(const char *action, const char *type) { + ESP_LOGW(TAG, "Cannot %s GATT %s, not connected", action, type); +} + +void BluetoothProxy::handle_gatt_not_connected_(uint64_t address, uint16_t handle, const char *action, + const char *type) { + this->log_not_connected_gatt_(action, type); + this->send_gatt_error(address, handle, ESP_GATT_NOT_CONNECTED); +} + #ifdef USE_ESP32_BLE_DEVICE bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { // This method should never be called since bluetooth_proxy always uses raw advertisements @@ -65,39 +76,27 @@ bool BluetoothProxy::parse_devices(const esp32_ble::BLEScanResult *scan_results, if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr) return false; - auto &advertisements = this->response_->advertisements; + auto &advertisements = this->response_.advertisements; for (size_t i = 0; i < count; i++) { auto &result = scan_results[i]; uint8_t length = result.adv_data_len + result.scan_rsp_len; - // Check if we need to expand the vector - if (this->advertisement_count_ >= advertisements.size()) { - if (this->advertisement_pool_.empty()) { - // No room in pool, need to allocate - advertisements.emplace_back(); - } else { - // Pull from pool - advertisements.push_back(std::move(this->advertisement_pool_.back())); - this->advertisement_pool_.pop_back(); - } - } - // Fill in the data directly at current position - auto &adv = advertisements[this->advertisement_count_]; + auto &adv = advertisements[this->response_.advertisements_len]; adv.address = esp32_ble::ble_addr_to_uint64(result.bda); adv.rssi = result.rssi; adv.address_type = result.ble_addr_type; adv.data_len = length; std::memcpy(adv.data, result.ble_adv, length); - this->advertisement_count_++; + this->response_.advertisements_len++; ESP_LOGV(TAG, "Queuing raw packet from %02X:%02X:%02X:%02X:%02X:%02X, length %d. RSSI: %d dB", result.bda[0], result.bda[1], result.bda[2], result.bda[3], result.bda[4], result.bda[5], length, result.rssi); - // Flush if we have reached FLUSH_BATCH_SIZE - if (this->advertisement_count_ >= FLUSH_BATCH_SIZE) { + // Flush if we have reached BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE + if (this->response_.advertisements_len >= BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE) { this->flush_pending_advertisements(); } } @@ -106,40 +105,31 @@ bool BluetoothProxy::parse_devices(const esp32_ble::BLEScanResult *scan_results, } void BluetoothProxy::flush_pending_advertisements() { - if (this->advertisement_count_ == 0 || !api::global_api_server->is_connected() || this->api_connection_ == nullptr) + if (this->response_.advertisements_len == 0 || !api::global_api_server->is_connected() || + this->api_connection_ == nullptr) return; - auto &advertisements = this->response_->advertisements; - - // Return any items beyond advertisement_count_ to the pool - if (advertisements.size() > this->advertisement_count_) { - // Move unused items back to pool - this->advertisement_pool_.insert(this->advertisement_pool_.end(), - std::make_move_iterator(advertisements.begin() + this->advertisement_count_), - std::make_move_iterator(advertisements.end())); - - // Resize to actual count - advertisements.resize(this->advertisement_count_); - } - // Send the message - this->api_connection_->send_message(*this->response_, api::BluetoothLERawAdvertisementsResponse::MESSAGE_TYPE); + this->api_connection_->send_message(this->response_, api::BluetoothLERawAdvertisementsResponse::MESSAGE_TYPE); - // Reset count - existing items will be overwritten in next batch - this->advertisement_count_ = 0; + ESP_LOGV(TAG, "Sent batch of %u BLE advertisements", this->response_.advertisements_len); + + // Reset the length for the next batch + this->response_.advertisements_len = 0; } void BluetoothProxy::dump_config() { - ESP_LOGCONFIG(TAG, "Bluetooth Proxy:"); ESP_LOGCONFIG(TAG, + "Bluetooth Proxy:\n" " Active: %s\n" " Connections: %d", - YESNO(this->active_), this->connections_.size()); + YESNO(this->active_), this->connection_count_); } void BluetoothProxy::loop() { if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr) { - for (auto *connection : this->connections_) { + for (uint8_t i = 0; i < this->connection_count_; i++) { + auto *connection = this->connections_[i]; if (connection->get_address() != 0 && !connection->disconnect_pending()) { connection->disconnect(); } @@ -162,17 +152,15 @@ esp32_ble_tracker::AdvertisementParserType BluetoothProxy::get_advertisement_par } BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool reserve) { - for (auto *connection : this->connections_) { - if (connection->get_address() == address) + for (uint8_t i = 0; i < this->connection_count_; i++) { + auto *connection = this->connections_[i]; + uint64_t conn_addr = connection->get_address(); + + if (conn_addr == address) return connection; - } - if (!reserve) - return nullptr; - - for (auto *connection : this->connections_) { - if (connection->get_address() == 0) { - connection->send_service_ = DONE_SENDING_SERVICES; + if (reserve && conn_addr == 0) { + connection->send_service_ = INIT_SENDING_SERVICES; connection->set_address(address); // All connections must start at INIT // We only set the state if we allocate the connection @@ -182,78 +170,54 @@ BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool rese return connection; } } - return nullptr; } void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest &msg) { switch (msg.request_type) { case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITH_CACHE: - case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE: - case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT: { + case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE: { auto *connection = this->get_connection_(msg.address, true); if (connection == nullptr) { ESP_LOGW(TAG, "No free connections available"); this->send_device_connection(msg.address, false); return; } + if (!msg.has_address_type) { + ESP_LOGE(TAG, "[%d] [%s] Missing address type in connect request", connection->get_connection_index(), + connection->address_str()); + this->send_device_connection(msg.address, false); + return; + } if (connection->state() == espbt::ClientState::CONNECTED || connection->state() == espbt::ClientState::ESTABLISHED) { - ESP_LOGW(TAG, "[%d] [%s] Connection already established", connection->get_connection_index(), - connection->address_str().c_str()); + this->log_connection_request_ignored_(connection, connection->state()); this->send_device_connection(msg.address, true); this->send_connections_free(); return; - } else if (connection->state() == espbt::ClientState::SEARCHING) { - ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, already searching for device", - connection->get_connection_index(), connection->address_str().c_str()); - return; - } else if (connection->state() == espbt::ClientState::DISCOVERED) { - ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, device already discovered", - connection->get_connection_index(), connection->address_str().c_str()); - return; - } else if (connection->state() == espbt::ClientState::READY_TO_CONNECT) { - ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, waiting in line to connect", - connection->get_connection_index(), connection->address_str().c_str()); - return; } else if (connection->state() == espbt::ClientState::CONNECTING) { if (connection->disconnect_pending()) { ESP_LOGW(TAG, "[%d] [%s] Connection request while pending disconnect, cancelling pending disconnect", - connection->get_connection_index(), connection->address_str().c_str()); + connection->get_connection_index(), connection->address_str()); connection->cancel_pending_disconnect(); return; } - ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, already connecting", connection->get_connection_index(), - connection->address_str().c_str()); - return; - } else if (connection->state() == espbt::ClientState::DISCONNECTING) { - ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, device is disconnecting", - connection->get_connection_index(), connection->address_str().c_str()); + this->log_connection_request_ignored_(connection, connection->state()); return; } else if (connection->state() != espbt::ClientState::INIT) { - ESP_LOGW(TAG, "[%d] [%s] Connection already in progress", connection->get_connection_index(), - connection->address_str().c_str()); + this->log_connection_request_ignored_(connection, connection->state()); return; } if (msg.request_type == api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITH_CACHE) { connection->set_connection_type(espbt::ConnectionType::V3_WITH_CACHE); - ESP_LOGI(TAG, "[%d] [%s] Connecting v3 with cache", connection->get_connection_index(), - connection->address_str().c_str()); - } else if (msg.request_type == api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE) { + this->log_connection_info_(connection, "v3 with cache"); + } else { // BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE connection->set_connection_type(espbt::ConnectionType::V3_WITHOUT_CACHE); - ESP_LOGI(TAG, "[%d] [%s] Connecting v3 without cache", connection->get_connection_index(), - connection->address_str().c_str()); - } else { - connection->set_connection_type(espbt::ConnectionType::V1); - ESP_LOGI(TAG, "[%d] [%s] Connecting v1", connection->get_connection_index(), connection->address_str().c_str()); - } - if (msg.has_address_type) { - uint64_to_bd_addr(msg.address, connection->remote_bda_); - connection->set_remote_addr_type(static_cast(msg.address_type)); - connection->set_state(espbt::ClientState::DISCOVERED); - } else { - connection->set_state(espbt::ClientState::SEARCHING); + this->log_connection_info_(connection, "v3 without cache"); } + uint64_to_bd_addr(msg.address, connection->remote_bda_); + connection->set_remote_addr_type(static_cast(msg.address_type)); + connection->set_state(espbt::ClientState::DISCOVERED); this->send_connections_free(); break; } @@ -307,14 +271,18 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest break; } + case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT: { + ESP_LOGE(TAG, "V1 connections removed"); + this->send_device_connection(msg.address, false); + break; + } } } void BluetoothProxy::bluetooth_gatt_read(const api::BluetoothGATTReadRequest &msg) { auto *connection = this->get_connection_(msg.address, false); if (connection == nullptr) { - ESP_LOGW(TAG, "Cannot read GATT characteristic, not connected"); - this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); + this->handle_gatt_not_connected_(msg.address, msg.handle, "read", "characteristic"); return; } @@ -327,12 +295,11 @@ void BluetoothProxy::bluetooth_gatt_read(const api::BluetoothGATTReadRequest &ms void BluetoothProxy::bluetooth_gatt_write(const api::BluetoothGATTWriteRequest &msg) { auto *connection = this->get_connection_(msg.address, false); if (connection == nullptr) { - ESP_LOGW(TAG, "Cannot write GATT characteristic, not connected"); - this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); + this->handle_gatt_not_connected_(msg.address, msg.handle, "write", "characteristic"); return; } - auto err = connection->write_characteristic(msg.handle, msg.data, msg.response); + auto err = connection->write_characteristic(msg.handle, msg.data, msg.data_len, msg.response); if (err != ESP_OK) { this->send_gatt_error(msg.address, msg.handle, err); } @@ -341,8 +308,7 @@ void BluetoothProxy::bluetooth_gatt_write(const api::BluetoothGATTWriteRequest & void BluetoothProxy::bluetooth_gatt_read_descriptor(const api::BluetoothGATTReadDescriptorRequest &msg) { auto *connection = this->get_connection_(msg.address, false); if (connection == nullptr) { - ESP_LOGW(TAG, "Cannot read GATT descriptor, not connected"); - this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); + this->handle_gatt_not_connected_(msg.address, msg.handle, "read", "descriptor"); return; } @@ -355,12 +321,11 @@ void BluetoothProxy::bluetooth_gatt_read_descriptor(const api::BluetoothGATTRead void BluetoothProxy::bluetooth_gatt_write_descriptor(const api::BluetoothGATTWriteDescriptorRequest &msg) { auto *connection = this->get_connection_(msg.address, false); if (connection == nullptr) { - ESP_LOGW(TAG, "Cannot write GATT descriptor, not connected"); - this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); + this->handle_gatt_not_connected_(msg.address, msg.handle, "write", "descriptor"); return; } - auto err = connection->write_descriptor(msg.handle, msg.data, true); + auto err = connection->write_descriptor(msg.handle, msg.data, msg.data_len, true); if (err != ESP_OK) { this->send_gatt_error(msg.address, msg.handle, err); } @@ -369,25 +334,22 @@ void BluetoothProxy::bluetooth_gatt_write_descriptor(const api::BluetoothGATTWri void BluetoothProxy::bluetooth_gatt_send_services(const api::BluetoothGATTGetServicesRequest &msg) { auto *connection = this->get_connection_(msg.address, false); if (connection == nullptr || !connection->connected()) { - ESP_LOGW(TAG, "Cannot get GATT services, not connected"); - this->send_gatt_error(msg.address, 0, ESP_GATT_NOT_CONNECTED); + this->handle_gatt_not_connected_(msg.address, 0, "get", "services"); return; } if (!connection->service_count_) { - ESP_LOGW(TAG, "[%d] [%s] No GATT services found", connection->connection_index_, connection->address_str().c_str()); + ESP_LOGW(TAG, "[%d] [%s] No GATT services found", connection->connection_index_, connection->address_str()); this->send_gatt_services_done(msg.address); return; } - if (connection->send_service_ == - DONE_SENDING_SERVICES) // Only start sending services if we're not already sending them + if (connection->send_service_ == INIT_SENDING_SERVICES) // Start sending services if not started yet connection->send_service_ = 0; } void BluetoothProxy::bluetooth_gatt_notify(const api::BluetoothGATTNotifyRequest &msg) { auto *connection = this->get_connection_(msg.address, false); if (connection == nullptr) { - ESP_LOGW(TAG, "Cannot notify GATT characteristic, not connected"); - this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); + this->handle_gatt_not_connected_(msg.address, msg.handle, "notify", "characteristic"); return; } diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index 70deef1ebd..4363c508ec 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -2,6 +2,7 @@ #ifdef USE_ESP32 +#include #include #include @@ -15,13 +16,16 @@ #include "bluetooth_connection.h" +#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID #include +#endif #include namespace esphome::bluetooth_proxy { static const esp_err_t ESP_GATT_NOT_CONNECTED = -1; static const int DONE_SENDING_SERVICES = -2; +static const int INIT_SENDING_SERVICES = -3; using namespace esp32_ble_client; @@ -48,7 +52,7 @@ enum BluetoothProxySubscriptionFlag : uint32_t { SUBSCRIPTION_RAW_ADVERTISEMENTS = 1 << 0, }; -class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Component { +class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, public Component { friend class BluetoothConnection; // Allow connection to update connections_free_response_ public: BluetoothProxy(); @@ -63,8 +67,10 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override; void register_connection(BluetoothConnection *connection) { - this->connections_.push_back(connection); - connection->proxy_ = this; + if (this->connection_count_ < BLUETOOTH_PROXY_MAX_CONNECTIONS) { + this->connections_[this->connection_count_++] = connection; + connection->proxy_ = this; + } } void bluetooth_device_request(const api::BluetoothDeviceRequest &msg); @@ -124,26 +130,33 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com return flags; } - std::string get_bluetooth_mac_address_pretty() { + void get_bluetooth_mac_address_pretty(std::span output) { const uint8_t *mac = esp_bt_dev_get_address(); - return str_snprintf("%02X:%02X:%02X:%02X:%02X:%02X", 17, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + if (mac != nullptr) { + format_mac_addr_upper(mac, output.data()); + } else { + output[0] = '\0'; + } } protected: void send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerState state); BluetoothConnection *get_connection_(uint64_t address, bool reserve); + void log_connection_request_ignored_(BluetoothConnection *connection, espbt::ClientState state); + void log_connection_info_(BluetoothConnection *connection, const char *message); + void log_not_connected_gatt_(const char *action, const char *type); + void handle_gatt_not_connected_(uint64_t address, uint16_t handle, const char *action, const char *type); // Memory optimized layout for 32-bit systems // Group 1: Pointers (4 bytes each, naturally aligned) api::APIConnection *api_connection_{nullptr}; - // Group 2: Container types (typically 12 bytes on 32-bit) - std::vector connections_{}; + // Group 2: Fixed-size array of connection pointers + std::array connections_{}; // BLE advertisement batching - std::vector advertisement_pool_; - std::unique_ptr response_; + api::BluetoothLERawAdvertisementsResponse response_; // Group 3: 4-byte types uint32_t last_advertisement_flush_time_{0}; @@ -153,8 +166,9 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com // Group 4: 1-byte types grouped together bool active_; - uint8_t advertisement_count_{0}; - // 2 bytes used, 2 bytes padding + uint8_t connection_count_{0}; + bool configured_scan_active_{false}; // Configured scan mode from YAML + // 3 bytes used, 1 byte padding }; extern BluetoothProxy *global_bluetooth_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/bm8563/__init__.py b/esphome/components/bm8563/__init__.py new file mode 100644 index 0000000000..20254a8b00 --- /dev/null +++ b/esphome/components/bm8563/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@abmantis"] diff --git a/esphome/components/bm8563/bm8563.cpp b/esphome/components/bm8563/bm8563.cpp new file mode 100644 index 0000000000..07831485c1 --- /dev/null +++ b/esphome/components/bm8563/bm8563.cpp @@ -0,0 +1,198 @@ +#include "bm8563.h" +#include "esphome/core/log.h" + +namespace esphome::bm8563 { + +static const char *const TAG = "bm8563"; + +static constexpr uint8_t CONTROL_STATUS_1_REG = 0x00; +static constexpr uint8_t CONTROL_STATUS_2_REG = 0x01; +static constexpr uint8_t TIME_FIRST_REG = 0x02; // Time uses reg 2, 3, 4 +static constexpr uint8_t DATE_FIRST_REG = 0x05; // Date uses reg 5, 6, 7, 8 +static constexpr uint8_t TIMER_CONTROL_REG = 0x0E; +static constexpr uint8_t TIMER_VALUE_REG = 0x0F; +static constexpr uint8_t CLOCK_1_HZ = 0x82; +static constexpr uint8_t CLOCK_1_60_HZ = 0x83; +// Maximum duration: 255 minutes (at 1/60 Hz) = 15300 seconds +static constexpr uint32_t MAX_TIMER_DURATION_S = 255 * 60; + +void BM8563::setup() { + if (!this->write_byte_16(CONTROL_STATUS_1_REG, 0)) { + this->mark_failed(); + return; + } +} + +void BM8563::update() { this->read_time(); } + +void BM8563::dump_config() { + ESP_LOGCONFIG(TAG, "BM8563:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); + } +} + +void BM8563::start_timer(uint32_t duration_s) { + this->clear_irq_(); + this->set_timer_irq_(duration_s); +} + +void BM8563::write_time() { + auto now = time::RealTimeClock::utcnow(); + if (!now.is_valid()) { + ESP_LOGE(TAG, "Invalid system time, not syncing to RTC."); + return; + } + + ESP_LOGD(TAG, "Writing time: %i-%i-%i %i, %i:%i:%i", now.year, now.month, now.day_of_month, now.day_of_week, now.hour, + now.minute, now.second); + + this->set_time_(now); + this->set_date_(now); +} + +void BM8563::read_time() { + ESPTime rtc_time; + this->get_time_(rtc_time); + this->get_date_(rtc_time); + rtc_time.day_of_year = 1; // unused by recalc_timestamp_utc, but needs to be valid + ESP_LOGD(TAG, "Read time: %i-%i-%i %i, %i:%i:%i", rtc_time.year, rtc_time.month, rtc_time.day_of_month, + rtc_time.day_of_week, rtc_time.hour, rtc_time.minute, rtc_time.second); + + rtc_time.recalc_timestamp_utc(false); + if (!rtc_time.is_valid()) { + ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock."); + return; + } + time::RealTimeClock::synchronize_epoch_(rtc_time.timestamp); +} + +uint8_t BM8563::bcd2_to_byte_(uint8_t value) { + const uint8_t tmp = ((value & 0xF0) >> 0x4) * 10; + return tmp + (value & 0x0F); +} + +uint8_t BM8563::byte_to_bcd2_(uint8_t value) { + const uint8_t bcdhigh = value / 10; + value -= bcdhigh * 10; + return (bcdhigh << 4) | value; +} + +void BM8563::get_time_(ESPTime &time) { + uint8_t buf[3] = {0}; + this->read_register(TIME_FIRST_REG, buf, 3); + + time.second = this->bcd2_to_byte_(buf[0] & 0x7f); + time.minute = this->bcd2_to_byte_(buf[1] & 0x7f); + time.hour = this->bcd2_to_byte_(buf[2] & 0x3f); +} + +void BM8563::set_time_(const ESPTime &time) { + uint8_t buf[3] = {this->byte_to_bcd2_(time.second), this->byte_to_bcd2_(time.minute), this->byte_to_bcd2_(time.hour)}; + this->write_register_(TIME_FIRST_REG, buf, 3); +} + +void BM8563::get_date_(ESPTime &time) { + uint8_t buf[4] = {0}; + this->read_register(DATE_FIRST_REG, buf, sizeof(buf)); + + time.day_of_month = this->bcd2_to_byte_(buf[0] & 0x3f); + time.day_of_week = this->bcd2_to_byte_(buf[1] & 0x07); + time.month = this->bcd2_to_byte_(buf[2] & 0x1f); + + uint8_t year_byte = this->bcd2_to_byte_(buf[3] & 0xff); + + if (buf[2] & 0x80) { + time.year = 1900 + year_byte; + } else { + time.year = 2000 + year_byte; + } +} + +void BM8563::set_date_(const ESPTime &time) { + uint8_t buf[4] = { + this->byte_to_bcd2_(time.day_of_month), + this->byte_to_bcd2_(time.day_of_week), + this->byte_to_bcd2_(time.month), + this->byte_to_bcd2_(time.year % 100), + }; + + if (time.year < 2000) { + buf[2] = buf[2] | 0x80; + } + + this->write_register_(DATE_FIRST_REG, buf, 4); +} + +void BM8563::write_byte_(uint8_t reg, uint8_t value) { + if (!this->write_byte(reg, value)) { + ESP_LOGE(TAG, "Failed to write byte 0x%02X with value 0x%02X", reg, value); + } +} + +void BM8563::write_register_(uint8_t reg, const uint8_t *data, size_t len) { + if (auto error = this->write_register(reg, data, len); error != i2c::ErrorCode::NO_ERROR) { + ESP_LOGE(TAG, "Failed to write register 0x%02X with %zu bytes", reg, len); + } +} + +optional BM8563::read_register_(uint8_t reg) { + uint8_t data; + if (auto error = this->read_register(reg, &data, 1); error != i2c::ErrorCode::NO_ERROR) { + ESP_LOGE(TAG, "Failed to read register 0x%02X", reg); + return {}; + } + return data; +} + +void BM8563::set_timer_irq_(uint32_t duration_s) { + ESP_LOGI(TAG, "Timer Duration: %u s", duration_s); + + if (duration_s > MAX_TIMER_DURATION_S) { + ESP_LOGW(TAG, "Timer duration %u s exceeds maximum %u s", duration_s, MAX_TIMER_DURATION_S); + return; + } + + if (duration_s > 255) { + uint8_t duration_minutes = duration_s / 60; + this->write_byte_(TIMER_VALUE_REG, duration_minutes); + this->write_byte_(TIMER_CONTROL_REG, CLOCK_1_60_HZ); + } else { + this->write_byte_(TIMER_VALUE_REG, duration_s); + this->write_byte_(TIMER_CONTROL_REG, CLOCK_1_HZ); + } + + auto maybe_ctrl_status_2 = this->read_register_(CONTROL_STATUS_2_REG); + if (!maybe_ctrl_status_2.has_value()) { + ESP_LOGE(TAG, "Failed to read CONTROL_STATUS_2_REG"); + return; + } + uint8_t ctrl_status_2_reg_value = maybe_ctrl_status_2.value(); + ctrl_status_2_reg_value |= (1 << 0); + ctrl_status_2_reg_value &= ~(1 << 7); + this->write_byte_(CONTROL_STATUS_2_REG, ctrl_status_2_reg_value); +} + +void BM8563::clear_irq_() { + auto maybe_data = this->read_register_(CONTROL_STATUS_2_REG); + if (!maybe_data.has_value()) { + ESP_LOGE(TAG, "Failed to read CONTROL_STATUS_2_REG"); + return; + } + uint8_t data = maybe_data.value(); + this->write_byte_(CONTROL_STATUS_2_REG, data & 0xf3); +} + +void BM8563::disable_irq_() { + this->clear_irq_(); + auto maybe_data = this->read_register_(CONTROL_STATUS_2_REG); + if (!maybe_data.has_value()) { + ESP_LOGE(TAG, "Failed to read CONTROL_STATUS_2_REG"); + return; + } + uint8_t data = maybe_data.value(); + this->write_byte_(CONTROL_STATUS_2_REG, data & 0xfc); +} + +} // namespace esphome::bm8563 diff --git a/esphome/components/bm8563/bm8563.h b/esphome/components/bm8563/bm8563.h new file mode 100644 index 0000000000..eda2d1b3c0 --- /dev/null +++ b/esphome/components/bm8563/bm8563.h @@ -0,0 +1,57 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/time/real_time_clock.h" + +namespace esphome::bm8563 { + +class BM8563 : public time::RealTimeClock, public i2c::I2CDevice { + public: + void setup() override; + void update() override; + void dump_config() override; + + void write_time(); + void read_time(); + void start_timer(uint32_t duration_s); + + private: + void get_time_(ESPTime &time); + void get_date_(ESPTime &time); + + void set_time_(const ESPTime &time); + void set_date_(const ESPTime &time); + + void set_timer_irq_(uint32_t duration_s); + void clear_irq_(); + void disable_irq_(); + + void write_byte_(uint8_t reg, uint8_t value); + void write_register_(uint8_t reg, const uint8_t *data, size_t len); + optional read_register_(uint8_t reg); + + uint8_t bcd2_to_byte_(uint8_t value); + uint8_t byte_to_bcd2_(uint8_t value); +}; + +template class WriteAction : public Action, public Parented { + public: + void play(const Ts &...x) override { this->parent_->write_time(); } +}; + +template class ReadAction : public Action, public Parented { + public: + void play(const Ts &...x) override { this->parent_->read_time(); } +}; + +template class TimerAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint32_t, duration) + + void play(const Ts &...x) override { + auto duration = this->duration_.value(x...); + this->parent_->start_timer(duration); + } +}; + +} // namespace esphome::bm8563 diff --git a/esphome/components/bm8563/time.py b/esphome/components/bm8563/time.py new file mode 100644 index 0000000000..2785315af2 --- /dev/null +++ b/esphome/components/bm8563/time.py @@ -0,0 +1,80 @@ +from esphome import automation +import esphome.codegen as cg +from esphome.components import i2c, time +import esphome.config_validation as cv +from esphome.const import CONF_DURATION, CONF_ID + +DEPENDENCIES = ["i2c"] + +I2C_ADDR = 0x51 + +bm8563_ns = cg.esphome_ns.namespace("bm8563") +BM8563 = bm8563_ns.class_("BM8563", time.RealTimeClock, i2c.I2CDevice) +WriteAction = bm8563_ns.class_("WriteAction", automation.Action) +ReadAction = bm8563_ns.class_("ReadAction", automation.Action) +TimerAction = bm8563_ns.class_("TimerAction", automation.Action) + +CONFIG_SCHEMA = ( + time.TIME_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(BM8563), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(I2C_ADDR)) +) + + +@automation.register_action( + "bm8563.write_time", + WriteAction, + automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(BM8563), + } + ), +) +async def bm8563_write_time_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +@automation.register_action( + "bm8563.start_timer", + TimerAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(BM8563), + cv.Required(CONF_DURATION): cv.templatable(cv.positive_time_period_seconds), + } + ), +) +async def bm8563_start_timer_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + template_ = await cg.templatable(config[CONF_DURATION], args, cg.uint32) + cg.add(var.set_duration(template_)) + return var + + +@automation.register_action( + "bm8563.read_time", + ReadAction, + automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(BM8563), + } + ), +) +async def bm8563_read_time_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + await time.register_time(var, config) diff --git a/esphome/components/bme280_base/bme280_base.cpp b/esphome/components/bme280_base/bme280_base.cpp index e5cea0d06d..c5d4c9c0a5 100644 --- a/esphome/components/bme280_base/bme280_base.cpp +++ b/esphome/components/bme280_base/bme280_base.cpp @@ -7,6 +7,8 @@ #include #include +#define BME280_ERROR_WRONG_CHIP_ID "Wrong chip ID" + namespace esphome { namespace bme280_base { @@ -98,18 +100,18 @@ void BME280Component::setup() { if (!this->read_byte(BME280_REGISTER_CHIPID, &chip_id)) { this->error_code_ = COMMUNICATION_FAILED; - this->mark_failed(); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); return; } if (chip_id != 0x60) { this->error_code_ = WRONG_CHIP_ID; - this->mark_failed(); + this->mark_failed(LOG_STR(BME280_ERROR_WRONG_CHIP_ID)); return; } // Send a soft reset. if (!this->write_byte(BME280_REGISTER_RESET, BME280_SOFT_RESET)) { - this->mark_failed(); + this->mark_failed(LOG_STR("Reset failed")); return; } // Wait until the NVM data has finished loading. @@ -118,14 +120,12 @@ void BME280Component::setup() { do { // NOLINT delay(2); if (!this->read_byte(BME280_REGISTER_STATUS, &status)) { - ESP_LOGW(TAG, "Error reading status register."); - this->mark_failed(); + this->mark_failed(LOG_STR("Error reading status register")); return; } } while ((status & BME280_STATUS_IM_UPDATE) && (--retry)); if (status & BME280_STATUS_IM_UPDATE) { - ESP_LOGW(TAG, "Timeout loading NVM."); - this->mark_failed(); + this->mark_failed(LOG_STR("Timeout loading NVM")); return; } @@ -153,26 +153,26 @@ void BME280Component::setup() { uint8_t humid_control_val = 0; if (!this->read_byte(BME280_REGISTER_CONTROLHUMID, &humid_control_val)) { - this->mark_failed(); + this->mark_failed(LOG_STR("Read humidity control")); return; } humid_control_val &= ~0b00000111; humid_control_val |= this->humidity_oversampling_ & 0b111; if (!this->write_byte(BME280_REGISTER_CONTROLHUMID, humid_control_val)) { - this->mark_failed(); + this->mark_failed(LOG_STR("Write humidity control")); return; } uint8_t config_register = 0; if (!this->read_byte(BME280_REGISTER_CONFIG, &config_register)) { - this->mark_failed(); + this->mark_failed(LOG_STR("Read config")); return; } config_register &= ~0b11111100; config_register |= 0b101 << 5; // 1000 ms standby time config_register |= (this->iir_filter_ & 0b111) << 2; if (!this->write_byte(BME280_REGISTER_CONFIG, config_register)) { - this->mark_failed(); + this->mark_failed(LOG_STR("Write config")); return; } } @@ -183,7 +183,7 @@ void BME280Component::dump_config() { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); break; case WRONG_CHIP_ID: - ESP_LOGE(TAG, "BME280 has wrong chip ID! Is it a BME280?"); + ESP_LOGE(TAG, BME280_ERROR_WRONG_CHIP_ID); break; case NONE: default: @@ -223,21 +223,21 @@ void BME280Component::update() { this->set_timeout("data", uint32_t(ceilf(meas_time)), [this]() { uint8_t data[8]; if (!this->read_bytes(BME280_REGISTER_MEASUREMENTS, data, 8)) { - ESP_LOGW(TAG, "Error reading registers."); + ESP_LOGW(TAG, "Error reading registers"); this->status_set_warning(); return; } int32_t t_fine = 0; float const temperature = this->read_temperature_(data, &t_fine); if (std::isnan(temperature)) { - ESP_LOGW(TAG, "Invalid temperature, cannot read pressure & humidity values."); + ESP_LOGW(TAG, "Invalid temperature"); this->status_set_warning(); return; } float const pressure = this->read_pressure_(data, t_fine); float const humidity = this->read_humidity_(data, t_fine); - ESP_LOGV(TAG, "Got temperature=%.1f°C pressure=%.1fhPa humidity=%.1f%%", temperature, pressure, humidity); + ESP_LOGV(TAG, "Temperature=%.1f°C Pressure=%.1fhPa Humidity=%.1f%%", temperature, pressure, humidity); if (this->temperature_sensor_ != nullptr) this->temperature_sensor_->publish_state(temperature); if (this->pressure_sensor_ != nullptr) diff --git a/esphome/components/bme680/bme680.cpp b/esphome/components/bme680/bme680.cpp index c5c4829985..16435ccfee 100644 --- a/esphome/components/bme680/bme680.cpp +++ b/esphome/components/bme680/bme680.cpp @@ -28,7 +28,7 @@ const float BME680_GAS_LOOKUP_TABLE_1[16] PROGMEM = {0.0, 0.0, 0.0, 0.0, 0.0, const float BME680_GAS_LOOKUP_TABLE_2[16] PROGMEM = {0.0, 0.0, 0.0, 0.0, 0.1, 0.7, 0.0, -0.8, -0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; -static const char *oversampling_to_str(BME680Oversampling oversampling) { +[[maybe_unused]] static const char *oversampling_to_str(BME680Oversampling oversampling) { switch (oversampling) { case BME680_OVERSAMPLING_NONE: return "None"; @@ -47,7 +47,7 @@ static const char *oversampling_to_str(BME680Oversampling oversampling) { } } -static const char *iir_filter_to_str(BME680IIRFilter filter) { +[[maybe_unused]] static const char *iir_filter_to_str(BME680IIRFilter filter) { switch (filter) { case BME680_IIR_FILTER_OFF: return "OFF"; diff --git a/esphome/components/bme680_bsec/__init__.py b/esphome/components/bme680_bsec/__init__.py index 330dc4dd9c..8a8d74b5f3 100644 --- a/esphome/components/bme680_bsec/__init__.py +++ b/esphome/components/bme680_bsec/__init__.py @@ -41,7 +41,7 @@ CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(BME680BSECComponent), - cv.Optional(CONF_TEMPERATURE_OFFSET, default=0): cv.temperature, + cv.Optional(CONF_TEMPERATURE_OFFSET, default=0): cv.temperature_delta, cv.Optional(CONF_IAQ_MODE, default="STATIC"): cv.enum( IAQ_MODE_OPTIONS, upper=True ), diff --git a/esphome/components/bme68x_bsec2/__init__.py b/esphome/components/bme68x_bsec2/__init__.py index f4235b31b4..e421efb2d6 100644 --- a/esphome/components/bme68x_bsec2/__init__.py +++ b/esphome/components/bme68x_bsec2/__init__.py @@ -139,7 +139,7 @@ CONFIG_SCHEMA_BASE = ( cv.Optional(CONF_SUPPLY_VOLTAGE, default="3.3V"): cv.enum( VOLTAGE_OPTIONS, upper=True ), - cv.Optional(CONF_TEMPERATURE_OFFSET, default=0): cv.temperature, + cv.Optional(CONF_TEMPERATURE_OFFSET, default=0): cv.temperature_delta, cv.Optional( CONF_STATE_SAVE_INTERVAL, default="6hours" ): cv.positive_time_period_minutes, diff --git a/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp index f5dcfd65a1..91383c8d45 100644 --- a/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp +++ b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp @@ -70,6 +70,9 @@ void BME68xBSEC2Component::dump_config() { if (this->is_failed()) { ESP_LOGE(TAG, "Communication failed (BSEC2 status: %d, BME68X status: %d)", this->bsec_status_, this->bme68x_status_); + if (this->bsec_status_ == BSEC_I_SU_SUBSCRIBEDOUTPUTGATES) { + ESP_LOGE(TAG, "No sensors, add at least one sensor to the config"); + } } if (this->algorithm_output_ != ALGORITHM_OUTPUT_IAQ) { diff --git a/esphome/components/bmi160/bmi160.cpp b/esphome/components/bmi160/bmi160.cpp index b041c7c2dc..4fcc3edb82 100644 --- a/esphome/components/bmi160/bmi160.cpp +++ b/esphome/components/bmi160/bmi160.cpp @@ -203,7 +203,7 @@ void BMI160Component::dump_config() { i2c::ErrorCode BMI160Component::read_le_int16_(uint8_t reg, int16_t *value, uint8_t len) { uint8_t raw_data[len * 2]; // read using read_register because we have little-endian data, and read_bytes_16 will swap it - i2c::ErrorCode err = this->read_register(reg, raw_data, len * 2, true); + i2c::ErrorCode err = this->read_register(reg, raw_data, len * 2); if (err != i2c::ERROR_OK) { return err; } diff --git a/esphome/components/bmp280_base/bmp280_base.cpp b/esphome/components/bmp280_base/bmp280_base.cpp index 6b5f98b9ce..728eead521 100644 --- a/esphome/components/bmp280_base/bmp280_base.cpp +++ b/esphome/components/bmp280_base/bmp280_base.cpp @@ -2,6 +2,8 @@ #include "esphome/core/hal.h" #include "esphome/core/log.h" +#define BMP280_ERROR_WRONG_CHIP_ID "Wrong chip ID" + namespace esphome { namespace bmp280_base { @@ -61,25 +63,25 @@ void BMP280Component::setup() { // Read the chip id twice, to work around a bug where the first read is 0. // https://community.st.com/t5/stm32-mcus-products/issue-with-reading-bmp280-chip-id-using-spi/td-p/691855 - if (!this->read_byte(0xD0, &chip_id)) { + if (!this->bmp_read_byte(0xD0, &chip_id)) { this->error_code_ = COMMUNICATION_FAILED; - this->mark_failed(); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); return; } - if (!this->read_byte(0xD0, &chip_id)) { + if (!this->bmp_read_byte(0xD0, &chip_id)) { this->error_code_ = COMMUNICATION_FAILED; - this->mark_failed(); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); return; } if (chip_id != 0x58) { this->error_code_ = WRONG_CHIP_ID; - this->mark_failed(); + this->mark_failed(LOG_STR(BMP280_ERROR_WRONG_CHIP_ID)); return; } // Send a soft reset. - if (!this->write_byte(BMP280_REGISTER_RESET, BMP280_SOFT_RESET)) { - this->mark_failed(); + if (!this->bmp_write_byte(BMP280_REGISTER_RESET, BMP280_SOFT_RESET)) { + this->mark_failed(LOG_STR("Reset failed")); return; } // Wait until the NVM data has finished loading. @@ -87,15 +89,13 @@ void BMP280Component::setup() { uint8_t retry = 5; do { delay(2); - if (!this->read_byte(BMP280_REGISTER_STATUS, &status)) { - ESP_LOGW(TAG, "Error reading status register."); - this->mark_failed(); + if (!this->bmp_read_byte(BMP280_REGISTER_STATUS, &status)) { + this->mark_failed(LOG_STR("Error reading status register")); return; } } while ((status & BMP280_STATUS_IM_UPDATE) && (--retry)); if (status & BMP280_STATUS_IM_UPDATE) { - ESP_LOGW(TAG, "Timeout loading NVM."); - this->mark_failed(); + this->mark_failed(LOG_STR("Timeout loading NVM")); return; } @@ -115,15 +115,15 @@ void BMP280Component::setup() { this->calibration_.p9 = this->read_s16_le_(0x9E); uint8_t config_register = 0; - if (!this->read_byte(BMP280_REGISTER_CONFIG, &config_register)) { - this->mark_failed(); + if (!this->bmp_read_byte(BMP280_REGISTER_CONFIG, &config_register)) { + this->mark_failed(LOG_STR("Read config")); return; } config_register &= ~0b11111100; config_register |= 0b000 << 5; // 0.5 ms standby time config_register |= (this->iir_filter_ & 0b111) << 2; - if (!this->write_byte(BMP280_REGISTER_CONFIG, config_register)) { - this->mark_failed(); + if (!this->bmp_write_byte(BMP280_REGISTER_CONFIG, config_register)) { + this->mark_failed(LOG_STR("Write config")); return; } } @@ -134,7 +134,7 @@ void BMP280Component::dump_config() { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); break; case WRONG_CHIP_ID: - ESP_LOGE(TAG, "BMP280 has wrong chip ID! Is it a BME280?"); + ESP_LOGE(TAG, BMP280_ERROR_WRONG_CHIP_ID); break; case NONE: default: @@ -159,7 +159,7 @@ void BMP280Component::update() { meas_value |= (this->temperature_oversampling_ & 0b111) << 5; meas_value |= (this->pressure_oversampling_ & 0b111) << 2; meas_value |= 0b01; // Forced mode - if (!this->write_byte(BMP280_REGISTER_CONTROL, meas_value)) { + if (!this->bmp_write_byte(BMP280_REGISTER_CONTROL, meas_value)) { this->status_set_warning(); return; } @@ -172,13 +172,13 @@ void BMP280Component::update() { int32_t t_fine = 0; float temperature = this->read_temperature_(&t_fine); if (std::isnan(temperature)) { - ESP_LOGW(TAG, "Invalid temperature, cannot read pressure values."); + ESP_LOGW(TAG, "Invalid temperature"); this->status_set_warning(); return; } float pressure = this->read_pressure_(t_fine); - ESP_LOGD(TAG, "Got temperature=%.1f°C pressure=%.1fhPa", temperature, pressure); + ESP_LOGV(TAG, "Temperature=%.1f°C Pressure=%.1fhPa", temperature, pressure); if (this->temperature_sensor_ != nullptr) this->temperature_sensor_->publish_state(temperature); if (this->pressure_sensor_ != nullptr) @@ -188,9 +188,10 @@ void BMP280Component::update() { } float BMP280Component::read_temperature_(int32_t *t_fine) { - uint8_t data[3]; - if (!this->read_bytes(BMP280_REGISTER_TEMPDATA, data, 3)) + uint8_t data[3]{}; + if (!this->bmp_read_bytes(BMP280_REGISTER_TEMPDATA, data, 3)) return NAN; + ESP_LOGV(TAG, "Read temperature data, raw: %02X %02X %02X", data[0], data[1], data[2]); int32_t adc = ((data[0] & 0xFF) << 16) | ((data[1] & 0xFF) << 8) | (data[2] & 0xFF); adc >>= 4; if (adc == 0x80000) { @@ -212,7 +213,7 @@ float BMP280Component::read_temperature_(int32_t *t_fine) { float BMP280Component::read_pressure_(int32_t t_fine) { uint8_t data[3]; - if (!this->read_bytes(BMP280_REGISTER_PRESSUREDATA, data, 3)) + if (!this->bmp_read_bytes(BMP280_REGISTER_PRESSUREDATA, data, 3)) return NAN; int32_t adc = ((data[0] & 0xFF) << 16) | ((data[1] & 0xFF) << 8) | (data[2] & 0xFF); adc >>= 4; @@ -258,12 +259,12 @@ void BMP280Component::set_pressure_oversampling(BMP280Oversampling pressure_over void BMP280Component::set_iir_filter(BMP280IIRFilter iir_filter) { this->iir_filter_ = iir_filter; } uint8_t BMP280Component::read_u8_(uint8_t a_register) { uint8_t data = 0; - this->read_byte(a_register, &data); + this->bmp_read_byte(a_register, &data); return data; } uint16_t BMP280Component::read_u16_le_(uint8_t a_register) { uint16_t data = 0; - this->read_byte_16(a_register, &data); + this->bmp_read_byte_16(a_register, &data); return (data >> 8) | (data << 8); } int16_t BMP280Component::read_s16_le_(uint8_t a_register) { return this->read_u16_le_(a_register); } diff --git a/esphome/components/bmp280_base/bmp280_base.h b/esphome/components/bmp280_base/bmp280_base.h index 4b22e98f13..a47a794e96 100644 --- a/esphome/components/bmp280_base/bmp280_base.h +++ b/esphome/components/bmp280_base/bmp280_base.h @@ -67,12 +67,12 @@ class BMP280Component : public PollingComponent { float get_setup_priority() const override; void update() override; - virtual bool read_byte(uint8_t a_register, uint8_t *data) = 0; - virtual bool write_byte(uint8_t a_register, uint8_t data) = 0; - virtual bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0; - virtual bool read_byte_16(uint8_t a_register, uint16_t *data) = 0; - protected: + virtual bool bmp_read_byte(uint8_t a_register, uint8_t *data) = 0; + virtual bool bmp_write_byte(uint8_t a_register, uint8_t data) = 0; + virtual bool bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0; + virtual bool bmp_read_byte_16(uint8_t a_register, uint16_t *data) = 0; + /// Read the temperature value and store the calculated ambient temperature in t_fine. float read_temperature_(int32_t *t_fine); /// Read the pressure value in hPa using the provided t_fine value. diff --git a/esphome/components/bmp280_i2c/bmp280_i2c.cpp b/esphome/components/bmp280_i2c/bmp280_i2c.cpp index 04b8bd8b10..75d899008d 100644 --- a/esphome/components/bmp280_i2c/bmp280_i2c.cpp +++ b/esphome/components/bmp280_i2c/bmp280_i2c.cpp @@ -5,19 +5,6 @@ namespace esphome { namespace bmp280_i2c { -bool BMP280I2CComponent::read_byte(uint8_t a_register, uint8_t *data) { - return I2CDevice::read_byte(a_register, data); -}; -bool BMP280I2CComponent::write_byte(uint8_t a_register, uint8_t data) { - return I2CDevice::write_byte(a_register, data); -}; -bool BMP280I2CComponent::read_bytes(uint8_t a_register, uint8_t *data, size_t len) { - return I2CDevice::read_bytes(a_register, data, len); -}; -bool BMP280I2CComponent::read_byte_16(uint8_t a_register, uint16_t *data) { - return I2CDevice::read_byte_16(a_register, data); -}; - void BMP280I2CComponent::dump_config() { LOG_I2C_DEVICE(this); BMP280Component::dump_config(); diff --git a/esphome/components/bmp280_i2c/bmp280_i2c.h b/esphome/components/bmp280_i2c/bmp280_i2c.h index 66d78d788b..0ac956202b 100644 --- a/esphome/components/bmp280_i2c/bmp280_i2c.h +++ b/esphome/components/bmp280_i2c/bmp280_i2c.h @@ -11,10 +11,12 @@ static const char *const TAG = "bmp280_i2c.sensor"; /// This class implements support for the BMP280 Temperature+Pressure i2c sensor. class BMP280I2CComponent : public esphome::bmp280_base::BMP280Component, public i2c::I2CDevice { public: - bool read_byte(uint8_t a_register, uint8_t *data) override; - bool write_byte(uint8_t a_register, uint8_t data) override; - bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) override; - bool read_byte_16(uint8_t a_register, uint16_t *data) override; + bool bmp_read_byte(uint8_t a_register, uint8_t *data) override { return read_byte(a_register, data); } + bool bmp_write_byte(uint8_t a_register, uint8_t data) override { return write_byte(a_register, data); } + bool bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) override { + return read_bytes(a_register, data, len); + } + bool bmp_read_byte_16(uint8_t a_register, uint16_t *data) override { return read_byte_16(a_register, data); } void dump_config() override; }; diff --git a/esphome/components/bmp280_spi/bmp280_spi.cpp b/esphome/components/bmp280_spi/bmp280_spi.cpp index a35e829432..88983e77c3 100644 --- a/esphome/components/bmp280_spi/bmp280_spi.cpp +++ b/esphome/components/bmp280_spi/bmp280_spi.cpp @@ -28,7 +28,7 @@ void BMP280SPIComponent::setup() { // 0x77 is transferred, for read access, the byte 0xF7 is transferred. // https://www.bosch-sensortec.com/media/boschsensortec/downloads/datasheets/bst-bmp280-ds001.pdf -bool BMP280SPIComponent::read_byte(uint8_t a_register, uint8_t *data) { +bool BMP280SPIComponent::bmp_read_byte(uint8_t a_register, uint8_t *data) { this->enable(); this->transfer_byte(set_bit(a_register, 7)); *data = this->transfer_byte(0); @@ -36,7 +36,7 @@ bool BMP280SPIComponent::read_byte(uint8_t a_register, uint8_t *data) { return true; } -bool BMP280SPIComponent::write_byte(uint8_t a_register, uint8_t data) { +bool BMP280SPIComponent::bmp_write_byte(uint8_t a_register, uint8_t data) { this->enable(); this->transfer_byte(clear_bit(a_register, 7)); this->transfer_byte(data); @@ -44,7 +44,7 @@ bool BMP280SPIComponent::write_byte(uint8_t a_register, uint8_t data) { return true; } -bool BMP280SPIComponent::read_bytes(uint8_t a_register, uint8_t *data, size_t len) { +bool BMP280SPIComponent::bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) { this->enable(); this->transfer_byte(set_bit(a_register, 7)); this->read_array(data, len); @@ -52,7 +52,7 @@ bool BMP280SPIComponent::read_bytes(uint8_t a_register, uint8_t *data, size_t le return true; } -bool BMP280SPIComponent::read_byte_16(uint8_t a_register, uint16_t *data) { +bool BMP280SPIComponent::bmp_read_byte_16(uint8_t a_register, uint16_t *data) { this->enable(); this->transfer_byte(set_bit(a_register, 7)); ((uint8_t *) data)[1] = this->transfer_byte(0); diff --git a/esphome/components/bmp280_spi/bmp280_spi.h b/esphome/components/bmp280_spi/bmp280_spi.h index dd226502f6..1bb7678e55 100644 --- a/esphome/components/bmp280_spi/bmp280_spi.h +++ b/esphome/components/bmp280_spi/bmp280_spi.h @@ -10,10 +10,10 @@ class BMP280SPIComponent : public esphome::bmp280_base::BMP280Component, public spi::SPIDevice { void setup() override; - bool read_byte(uint8_t a_register, uint8_t *data) override; - bool write_byte(uint8_t a_register, uint8_t data) override; - bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) override; - bool read_byte_16(uint8_t a_register, uint16_t *data) override; + bool bmp_read_byte(uint8_t a_register, uint8_t *data) override; + bool bmp_write_byte(uint8_t a_register, uint8_t data) override; + bool bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) override; + bool bmp_read_byte_16(uint8_t a_register, uint16_t *data) override; }; } // namespace bmp280_spi diff --git a/esphome/components/button/__init__.py b/esphome/components/button/__init__.py index a23958989e..d2f143b97e 100644 --- a/esphome/components/button/__init__.py +++ b/esphome/components/button/__init__.py @@ -17,7 +17,7 @@ from esphome.const import ( DEVICE_CLASS_RESTART, DEVICE_CLASS_UPDATE, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass @@ -84,11 +84,6 @@ def button_schema( return _BUTTON_SCHEMA.extend(schema) -# Remove before 2025.11.0 -BUTTON_SCHEMA = button_schema(Button) -BUTTON_SCHEMA.add_extra(cv.deprecated_schema_constant("button")) - - async def setup_button_core_(var, config): await setup_entity(var, config, "button") @@ -134,6 +129,6 @@ async def button_press_to_code(config, action_id, template_arg, args): return cg.new_Pvariable(action_id, template_arg, paren) -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(button_ns.using) diff --git a/esphome/components/button/automation.h b/esphome/components/button/automation.h index a5fb9f35b7..3b792eb5d7 100644 --- a/esphome/components/button/automation.h +++ b/esphome/components/button/automation.h @@ -11,7 +11,7 @@ template class PressAction : public Action { public: explicit PressAction(Button *button) : button_(button) {} - void play(Ts... x) override { this->button_->press(); } + void play(const Ts &...x) override { this->button_->press(); } protected: Button *button_; diff --git a/esphome/components/button/button.cpp b/esphome/components/button/button.cpp index 4c4cb7740c..c968d31088 100644 --- a/esphome/components/button/button.cpp +++ b/esphome/components/button/button.cpp @@ -6,6 +6,19 @@ namespace button { static const char *const TAG = "button"; +// Function implementation of LOG_BUTTON macro to reduce code size +void log_button(const char *tag, const char *prefix, const char *type, Button *obj) { + if (obj == nullptr) { + return; + } + + ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str()); + + if (!obj->get_icon_ref().empty()) { + ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj->get_icon_ref().c_str()); + } +} + void Button::press() { ESP_LOGD(TAG, "'%s' Pressed.", this->get_name().c_str()); this->press_action(); diff --git a/esphome/components/button/button.h b/esphome/components/button/button.h index 9488eca221..75b76f9dcf 100644 --- a/esphome/components/button/button.h +++ b/esphome/components/button/button.h @@ -7,13 +7,10 @@ namespace esphome { namespace button { -#define LOG_BUTTON(prefix, type, obj) \ - if ((obj) != nullptr) { \ - ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - if (!(obj)->get_icon().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \ - } \ - } +class Button; +void log_button(const char *tag, const char *prefix, const char *type, Button *obj); + +#define LOG_BUTTON(prefix, type, obj) log_button(TAG, prefix, LOG_STR_LITERAL(type), obj) #define SUB_BUTTON(name) \ protected: \ diff --git a/esphome/components/camera/buffer.h b/esphome/components/camera/buffer.h new file mode 100644 index 0000000000..f860877b94 --- /dev/null +++ b/esphome/components/camera/buffer.h @@ -0,0 +1,18 @@ +#pragma once + +#include +#include + +namespace esphome::camera { + +/// Interface for a generic buffer that stores image data. +class Buffer { + public: + /// Returns a pointer to the buffer's data. + virtual uint8_t *get_data_buffer() = 0; + /// Returns the length of the buffer in bytes. + virtual size_t get_data_length() = 0; + virtual ~Buffer() = default; +}; + +} // namespace esphome::camera diff --git a/esphome/components/camera/buffer_impl.cpp b/esphome/components/camera/buffer_impl.cpp new file mode 100644 index 0000000000..d17a4e2707 --- /dev/null +++ b/esphome/components/camera/buffer_impl.cpp @@ -0,0 +1,20 @@ +#include "buffer_impl.h" + +namespace esphome::camera { + +BufferImpl::BufferImpl(size_t size) { + this->data_ = this->allocator_.allocate(size); + this->size_ = size; +} + +BufferImpl::BufferImpl(CameraImageSpec *spec) { + this->data_ = this->allocator_.allocate(spec->bytes_per_image()); + this->size_ = spec->bytes_per_image(); +} + +BufferImpl::~BufferImpl() { + if (this->data_ != nullptr) + this->allocator_.deallocate(this->data_, this->size_); +} + +} // namespace esphome::camera diff --git a/esphome/components/camera/buffer_impl.h b/esphome/components/camera/buffer_impl.h new file mode 100644 index 0000000000..46398295fa --- /dev/null +++ b/esphome/components/camera/buffer_impl.h @@ -0,0 +1,26 @@ +#pragma once + +#include "buffer.h" +#include "camera.h" + +namespace esphome::camera { + +/// Default implementation of Buffer Interface. +/// Uses a RAMAllocator for memory reservation. +class BufferImpl : public Buffer { + public: + explicit BufferImpl(size_t size); + explicit BufferImpl(CameraImageSpec *spec); + // -------- Buffer -------- + uint8_t *get_data_buffer() override { return data_; } + size_t get_data_length() override { return size_; } + // ------------------------ + ~BufferImpl() override; + + protected: + RAMAllocator allocator_; + size_t size_{}; + uint8_t *data_{}; +}; + +} // namespace esphome::camera diff --git a/esphome/components/camera/camera.cpp b/esphome/components/camera/camera.cpp index 3bd632af5c..66b8138f38 100644 --- a/esphome/components/camera/camera.cpp +++ b/esphome/components/camera/camera.cpp @@ -8,7 +8,7 @@ Camera *Camera::global_camera = nullptr; Camera::Camera() { if (global_camera != nullptr) { - this->status_set_error("Multiple cameras are configured, but only one is supported."); + this->status_set_error(LOG_STR("Multiple cameras are configured, but only one is supported.")); this->mark_failed(); return; } diff --git a/esphome/components/camera/camera.h b/esphome/components/camera/camera.h index fb9da58cc1..c28a756a06 100644 --- a/esphome/components/camera/camera.h +++ b/esphome/components/camera/camera.h @@ -15,6 +15,26 @@ namespace camera { */ enum CameraRequester : uint8_t { IDLE, API_REQUESTER, WEB_REQUESTER }; +/// Enumeration of different pixel formats. +enum PixelFormat : uint8_t { + PIXEL_FORMAT_GRAYSCALE = 0, ///< 8-bit grayscale. + PIXEL_FORMAT_RGB565, ///< 16-bit RGB (5-6-5). + PIXEL_FORMAT_BGR888, ///< RGB pixel data in 8-bit format, stored as B, G, R (1 byte each). +}; + +/// Returns string name for a given PixelFormat. +inline const char *to_string(PixelFormat format) { + switch (format) { + case PIXEL_FORMAT_GRAYSCALE: + return "PIXEL_FORMAT_GRAYSCALE"; + case PIXEL_FORMAT_RGB565: + return "PIXEL_FORMAT_RGB565"; + case PIXEL_FORMAT_BGR888: + return "PIXEL_FORMAT_BGR888"; + } + return "PIXEL_FORMAT_UNKNOWN"; +} + /** Abstract camera image base class. * Encapsulates the JPEG encoded data and it is shared among * all connected clients. @@ -43,6 +63,29 @@ class CameraImageReader { virtual ~CameraImageReader() {} }; +/// Specification of a caputured camera image. +/// This struct defines the format and size details for images captured +/// or processed by a camera component. +struct CameraImageSpec { + uint16_t width; + uint16_t height; + PixelFormat format; + size_t bytes_per_pixel() { + switch (format) { + case PIXEL_FORMAT_GRAYSCALE: + return 1; + case PIXEL_FORMAT_RGB565: + return 2; + case PIXEL_FORMAT_BGR888: + return 3; + } + + return 1; + } + size_t bytes_per_row() { return bytes_per_pixel() * width; } + size_t bytes_per_image() { return bytes_per_pixel() * width * height; } +}; + /** Abstract camera base class. Collaborates with API. * 1) API server starts and installs callback (add_image_callback) * which is called by the camera when a new image is available. diff --git a/esphome/components/camera/encoder.h b/esphome/components/camera/encoder.h new file mode 100644 index 0000000000..17ce828d23 --- /dev/null +++ b/esphome/components/camera/encoder.h @@ -0,0 +1,69 @@ +#pragma once + +#include "buffer.h" +#include "camera.h" + +namespace esphome::camera { + +/// Result codes from the encoder used to control camera pipeline flow. +enum EncoderError : uint8_t { + ENCODER_ERROR_SUCCESS = 0, ///< Encoding succeeded, continue pipeline normally. + ENCODER_ERROR_SKIP_FRAME, ///< Skip current frame, try again on next frame. + ENCODER_ERROR_RETRY_FRAME, ///< Retry current frame, after buffer growth or for incremental encoding. + ENCODER_ERROR_CONFIGURATION ///< Fatal config error, shut down pipeline. +}; + +/// Converts EncoderError to string. +inline const char *to_string(EncoderError error) { + switch (error) { + case ENCODER_ERROR_SUCCESS: + return "ENCODER_ERROR_SUCCESS"; + case ENCODER_ERROR_SKIP_FRAME: + return "ENCODER_ERROR_SKIP_FRAME"; + case ENCODER_ERROR_RETRY_FRAME: + return "ENCODER_ERROR_RETRY_FRAME"; + case ENCODER_ERROR_CONFIGURATION: + return "ENCODER_ERROR_CONFIGURATION"; + } + return "ENCODER_ERROR_INVALID"; +} + +/// Interface for an encoder buffer supporting resizing and variable-length data. +class EncoderBuffer { + public: + /// Sets logical buffer size, reallocates if needed. + /// @param size Required size in bytes. + /// @return true on success, false on allocation failure. + virtual bool set_buffer_size(size_t size) = 0; + + /// Returns a pointer to the buffer data. + virtual uint8_t *get_data() const = 0; + + /// Returns number of bytes currently used. + virtual size_t get_size() const = 0; + + /// Returns total allocated buffer size. + virtual size_t get_max_size() const = 0; + + virtual ~EncoderBuffer() = default; +}; + +/// Interface for image encoders used in a camera pipeline. +class Encoder { + public: + /// Encodes pixel data from a previous camera pipeline stage. + /// @param spec Specification of the input pixel data. + /// @param pixels Image pixels in RGB or grayscale format, as specified in @p spec. + /// @return EncoderError Indicating the result of the encoding operation. + virtual EncoderError encode_pixels(CameraImageSpec *spec, Buffer *pixels) = 0; + + /// Returns the encoder's output buffer. + /// @return Pointer to an EncoderBuffer containing encoded data. + virtual EncoderBuffer *get_output_buffer() = 0; + + /// Prints the encoder's configuration to the log. + virtual void dump_config() = 0; + virtual ~Encoder() = default; +}; + +} // namespace esphome::camera diff --git a/esphome/components/camera_encoder/__init__.py b/esphome/components/camera_encoder/__init__.py new file mode 100644 index 0000000000..89181d27b4 --- /dev/null +++ b/esphome/components/camera_encoder/__init__.py @@ -0,0 +1,60 @@ +import esphome.codegen as cg +from esphome.components.esp32 import add_idf_component +import esphome.config_validation as cv +from esphome.const import CONF_BUFFER_SIZE, CONF_ID, CONF_TYPE +from esphome.types import ConfigType + +CODEOWNERS = ["@DT-art1"] + +AUTO_LOAD = ["camera"] + +CONF_BUFFER_EXPAND_SIZE = "buffer_expand_size" +CONF_ENCODER_BUFFER_ID = "encoder_buffer_id" +CONF_QUALITY = "quality" + +ESP32_CAMERA_ENCODER = "esp32_camera" + +camera_ns = cg.esphome_ns.namespace("camera") +camera_encoder_ns = cg.esphome_ns.namespace("camera_encoder") + +Encoder = camera_ns.class_("Encoder") +EncoderBufferImpl = camera_encoder_ns.class_("EncoderBufferImpl") + +ESP32CameraJPEGEncoder = camera_encoder_ns.class_("ESP32CameraJPEGEncoder", Encoder) + +MAX_JPEG_BUFFER_SIZE_2MB = 2 * 1024 * 1024 + +ESP32_CAMERA_ENCODER_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(ESP32CameraJPEGEncoder), + cv.Optional(CONF_QUALITY, default=80): cv.int_range(1, 100), + cv.Optional(CONF_BUFFER_SIZE, default=4096): cv.int_range( + 1024, MAX_JPEG_BUFFER_SIZE_2MB + ), + cv.Optional(CONF_BUFFER_EXPAND_SIZE, default=1024): cv.int_range( + 0, MAX_JPEG_BUFFER_SIZE_2MB + ), + cv.GenerateID(CONF_ENCODER_BUFFER_ID): cv.declare_id(EncoderBufferImpl), + } +) + +CONFIG_SCHEMA = cv.typed_schema( + { + ESP32_CAMERA_ENCODER: ESP32_CAMERA_ENCODER_SCHEMA, + }, + default_type=ESP32_CAMERA_ENCODER, +) + + +async def to_code(config: ConfigType) -> None: + buffer = cg.new_Pvariable(config[CONF_ENCODER_BUFFER_ID]) + cg.add(buffer.set_buffer_size(config[CONF_BUFFER_SIZE])) + if config[CONF_TYPE] == ESP32_CAMERA_ENCODER: + add_idf_component(name="espressif/esp32-camera", ref="2.1.1") + cg.add_define("USE_ESP32_CAMERA_JPEG_ENCODER") + var = cg.new_Pvariable( + config[CONF_ID], + config[CONF_QUALITY], + buffer, + ) + cg.add(var.set_buffer_expand_size(config[CONF_BUFFER_EXPAND_SIZE])) diff --git a/esphome/components/camera_encoder/encoder_buffer_impl.cpp b/esphome/components/camera_encoder/encoder_buffer_impl.cpp new file mode 100644 index 0000000000..db84026496 --- /dev/null +++ b/esphome/components/camera_encoder/encoder_buffer_impl.cpp @@ -0,0 +1,23 @@ +#include "encoder_buffer_impl.h" + +namespace esphome::camera_encoder { + +bool EncoderBufferImpl::set_buffer_size(size_t size) { + if (size > this->capacity_) { + uint8_t *p = this->allocator_.reallocate(this->data_, size); + if (p == nullptr) + return false; + + this->data_ = p; + this->capacity_ = size; + } + this->size_ = size; + return true; +} + +EncoderBufferImpl::~EncoderBufferImpl() { + if (this->data_ != nullptr) + this->allocator_.deallocate(this->data_, this->capacity_); +} + +} // namespace esphome::camera_encoder diff --git a/esphome/components/camera_encoder/encoder_buffer_impl.h b/esphome/components/camera_encoder/encoder_buffer_impl.h new file mode 100644 index 0000000000..13eccb7d56 --- /dev/null +++ b/esphome/components/camera_encoder/encoder_buffer_impl.h @@ -0,0 +1,25 @@ +#pragma once + +#include "esphome/components/camera/encoder.h" +#include "esphome/core/helpers.h" + +namespace esphome::camera_encoder { + +class EncoderBufferImpl : public camera::EncoderBuffer { + public: + // --- EncoderBuffer --- + bool set_buffer_size(size_t size) override; + uint8_t *get_data() const override { return this->data_; } + size_t get_size() const override { return this->size_; } + size_t get_max_size() const override { return this->capacity_; } + // ---------------------- + ~EncoderBufferImpl() override; + + protected: + RAMAllocator allocator_; + size_t capacity_{}; + size_t size_{}; + uint8_t *data_{}; +}; + +} // namespace esphome::camera_encoder diff --git a/esphome/components/camera_encoder/esp32_camera_jpeg_encoder.cpp b/esphome/components/camera_encoder/esp32_camera_jpeg_encoder.cpp new file mode 100644 index 0000000000..55a3f0b96c --- /dev/null +++ b/esphome/components/camera_encoder/esp32_camera_jpeg_encoder.cpp @@ -0,0 +1,84 @@ +#include "esphome/core/defines.h" + +#ifdef USE_ESP32_CAMERA_JPEG_ENCODER + +#include "esp32_camera_jpeg_encoder.h" + +namespace esphome::camera_encoder { + +static const char *const TAG = "camera_encoder"; + +ESP32CameraJPEGEncoder::ESP32CameraJPEGEncoder(uint8_t quality, camera::EncoderBuffer *output) { + this->quality_ = quality; + this->output_ = output; +} + +camera::EncoderError ESP32CameraJPEGEncoder::encode_pixels(camera::CameraImageSpec *spec, camera::Buffer *pixels) { + this->bytes_written_ = 0; + this->out_of_output_memory_ = false; + bool success = fmt2jpg_cb(pixels->get_data_buffer(), pixels->get_data_length(), spec->width, spec->height, + to_internal_(spec->format), this->quality_, callback, this); + + if (!success) + return camera::ENCODER_ERROR_CONFIGURATION; + + if (this->out_of_output_memory_) { + if (this->buffer_expand_size_ <= 0) + return camera::ENCODER_ERROR_SKIP_FRAME; + + size_t current_size = this->output_->get_max_size(); + size_t new_size = this->output_->get_max_size() + this->buffer_expand_size_; + if (!this->output_->set_buffer_size(new_size)) { + ESP_LOGE(TAG, "Failed to expand output buffer."); + this->buffer_expand_size_ = 0; + return camera::ENCODER_ERROR_SKIP_FRAME; + } + + ESP_LOGD(TAG, "Output buffer expanded (%u -> %u).", current_size, this->output_->get_max_size()); + return camera::ENCODER_ERROR_RETRY_FRAME; + } + + this->output_->set_buffer_size(this->bytes_written_); + return camera::ENCODER_ERROR_SUCCESS; +} + +void ESP32CameraJPEGEncoder::dump_config() { + ESP_LOGCONFIG(TAG, + "ESP32 Camera JPEG Encoder:\n" + " Size: %zu\n" + " Quality: %d\n" + " Expand: %d\n", + this->output_->get_max_size(), this->quality_, this->buffer_expand_size_); +} + +size_t ESP32CameraJPEGEncoder::callback(void *arg, size_t index, const void *data, size_t len) { + ESP32CameraJPEGEncoder *that = reinterpret_cast(arg); + uint8_t *buffer = that->output_->get_data(); + size_t buffer_length = that->output_->get_max_size(); + if (index + len > buffer_length) { + that->out_of_output_memory_ = true; + return 0; + } + + std::memcpy(&buffer[index], data, len); + that->bytes_written_ += len; + return len; +} + +pixformat_t ESP32CameraJPEGEncoder::to_internal_(camera::PixelFormat format) { + switch (format) { + case camera::PIXEL_FORMAT_GRAYSCALE: + return PIXFORMAT_GRAYSCALE; + case camera::PIXEL_FORMAT_RGB565: + return PIXFORMAT_RGB565; + // Internal representation for RGB is in byte order: B, G, R + case camera::PIXEL_FORMAT_BGR888: + return PIXFORMAT_RGB888; + } + + return PIXFORMAT_GRAYSCALE; +} + +} // namespace esphome::camera_encoder + +#endif diff --git a/esphome/components/camera_encoder/esp32_camera_jpeg_encoder.h b/esphome/components/camera_encoder/esp32_camera_jpeg_encoder.h new file mode 100644 index 0000000000..0ede366e73 --- /dev/null +++ b/esphome/components/camera_encoder/esp32_camera_jpeg_encoder.h @@ -0,0 +1,41 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_ESP32_CAMERA_JPEG_ENCODER + +#include + +#include "esphome/components/camera/encoder.h" + +namespace esphome::camera_encoder { + +/// Encoder that uses the software-based JPEG implementation from Espressif's esp32-camera component. +class ESP32CameraJPEGEncoder : public camera::Encoder { + public: + /// Constructs a ESP32CameraJPEGEncoder instance. + /// @param quality Sets the quality of the encoded image (1-100). + /// @param output Pointer to preallocated output buffer. + ESP32CameraJPEGEncoder(uint8_t quality, camera::EncoderBuffer *output); + /// Sets the number of bytes to expand the output buffer on underflow during encoding. + /// @param buffer_expand_size Number of bytes to expand the buffer. + void set_buffer_expand_size(size_t buffer_expand_size) { this->buffer_expand_size_ = buffer_expand_size; } + // -------- Encoder -------- + camera::EncoderError encode_pixels(camera::CameraImageSpec *spec, camera::Buffer *pixels) override; + camera::EncoderBuffer *get_output_buffer() override { return output_; } + void dump_config() override; + // ------------------------- + protected: + static size_t callback(void *arg, size_t index, const void *data, size_t len); + pixformat_t to_internal_(camera::PixelFormat format); + + camera::EncoderBuffer *output_{}; + size_t buffer_expand_size_{}; + size_t bytes_written_{}; + uint8_t quality_{}; + bool out_of_output_memory_{}; +}; + +} // namespace esphome::camera_encoder + +#endif diff --git a/esphome/components/canbus/__init__.py b/esphome/components/canbus/__init__.py index e1de1eb2f2..7b51c2c45c 100644 --- a/esphome/components/canbus/__init__.py +++ b/esphome/components/canbus/__init__.py @@ -4,7 +4,7 @@ from esphome import automation import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_DATA, CONF_ID, CONF_TRIGGER_ID -from esphome.core import CORE +from esphome.core import CORE, ID CODEOWNERS = ["@mvturnho", "@danielschramm"] IS_PLATFORM_COMPONENT = True @@ -176,5 +176,8 @@ async def canbus_action_to_code(config, action_id, template_arg, args): else: if isinstance(data, bytes): data = [int(x) for x in data] - cg.add(var.set_data_static(data)) + # Generate static array in flash to avoid RAM copy + arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8) + arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data)) + cg.add(var.set_data_static(arr, len(data))) return var diff --git a/esphome/components/canbus/canbus.cpp b/esphome/components/canbus/canbus.cpp index 6e61f05be7..e208b0fd66 100644 --- a/esphome/components/canbus/canbus.cpp +++ b/esphome/components/canbus/canbus.cpp @@ -21,8 +21,8 @@ void Canbus::dump_config() { } } -void Canbus::send_data(uint32_t can_id, bool use_extended_id, bool remote_transmission_request, - const std::vector &data) { +canbus::Error Canbus::send_data(uint32_t can_id, bool use_extended_id, bool remote_transmission_request, + const std::vector &data) { struct CanFrame can_message; uint8_t size = static_cast(data.size()); @@ -45,13 +45,15 @@ void Canbus::send_data(uint32_t can_id, bool use_extended_id, bool remote_transm ESP_LOGVV(TAG, " data[%d]=%02x", i, can_message.data[i]); } - if (this->send_message(&can_message) != canbus::ERROR_OK) { + canbus::Error error = this->send_message(&can_message); + if (error != canbus::ERROR_OK) { if (use_extended_id) { - ESP_LOGW(TAG, "send to extended id=0x%08" PRIx32 " failed!", can_id); + ESP_LOGW(TAG, "send to extended id=0x%08" PRIx32 " failed with error %d!", can_id, error); } else { - ESP_LOGW(TAG, "send to standard id=0x%03" PRIx32 " failed!", can_id); + ESP_LOGW(TAG, "send to standard id=0x%03" PRIx32 " failed with error %d!", can_id, error); } } + return error; } void Canbus::add_trigger(CanbusTrigger *trigger) { diff --git a/esphome/components/canbus/canbus.h b/esphome/components/canbus/canbus.h index 7319bfb4ad..f7b84111bd 100644 --- a/esphome/components/canbus/canbus.h +++ b/esphome/components/canbus/canbus.h @@ -70,11 +70,11 @@ class Canbus : public Component { float get_setup_priority() const override { return setup_priority::HARDWARE; } void loop() override; - void send_data(uint32_t can_id, bool use_extended_id, bool remote_transmission_request, - const std::vector &data); - void send_data(uint32_t can_id, bool use_extended_id, const std::vector &data) { + canbus::Error send_data(uint32_t can_id, bool use_extended_id, bool remote_transmission_request, + const std::vector &data); + canbus::Error send_data(uint32_t can_id, bool use_extended_id, const std::vector &data) { // for backwards compatibility only - this->send_data(can_id, use_extended_id, false, data); + return this->send_data(can_id, use_extended_id, false, data); } void set_can_id(uint32_t can_id) { this->can_id_ = can_id; } void set_use_extended_id(bool use_extended_id) { this->use_extended_id_ = use_extended_id; } @@ -105,20 +105,23 @@ class Canbus : public Component { CallbackManager &data)> callback_manager_{}; - virtual bool setup_internal(); - virtual Error send_message(struct CanFrame *frame); - virtual Error read_message(struct CanFrame *frame); + virtual bool setup_internal() = 0; + virtual Error send_message(struct CanFrame *frame) = 0; + virtual Error read_message(struct CanFrame *frame) = 0; }; template class CanbusSendAction : public Action, public Parented { public: - void set_data_template(const std::function(Ts...)> func) { - this->data_func_ = func; - this->static_ = false; + void set_data_template(std::vector (*func)(Ts...)) { + // Stateless lambdas (generated by ESPHome) implicitly convert to function pointers + this->data_.func = func; + this->len_ = -1; // Sentinel value indicates template mode } - void set_data_static(const std::vector &data) { - this->data_static_ = data; - this->static_ = true; + + // Store pointer to static data in flash (no RAM copy) + void set_data_static(const uint8_t *data, size_t len) { + this->data_.data = data; + this->len_ = len; // Length >= 0 indicates static mode } void set_can_id(uint32_t can_id) { this->can_id_ = can_id; } @@ -129,25 +132,30 @@ template class CanbusSendAction : public Action, public P this->remote_transmission_request_ = remote_transmission_request; } - void play(Ts... x) override { + void play(const Ts &...x) override { auto can_id = this->can_id_.has_value() ? *this->can_id_ : this->parent_->can_id_; auto use_extended_id = this->use_extended_id_.has_value() ? *this->use_extended_id_ : this->parent_->use_extended_id_; - if (this->static_) { - this->parent_->send_data(can_id, use_extended_id, this->remote_transmission_request_, this->data_static_); + std::vector data; + if (this->len_ >= 0) { + // Static mode: copy from flash to vector + data.assign(this->data_.data, this->data_.data + this->len_); } else { - auto val = this->data_func_(x...); - this->parent_->send_data(can_id, use_extended_id, this->remote_transmission_request_, val); + // Template mode: call function + data = this->data_.func(x...); } + this->parent_->send_data(can_id, use_extended_id, this->remote_transmission_request_, data); } protected: optional can_id_{}; optional use_extended_id_{}; bool remote_transmission_request_{false}; - bool static_{false}; - std::function(Ts...)> data_func_{}; - std::vector data_static_{}; + ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length + union Data { + std::vector (*func)(Ts...); // Function pointer (stateless lambdas) + const uint8_t *data; // Pointer to static data in flash + } data_; }; class CanbusTrigger : public Trigger, uint32_t, bool>, public Component { diff --git a/esphome/components/cap1188/cap1188.cpp b/esphome/components/cap1188/cap1188.cpp index 584ff896c5..683e5cf487 100644 --- a/esphome/components/cap1188/cap1188.cpp +++ b/esphome/components/cap1188/cap1188.cpp @@ -8,17 +8,30 @@ namespace cap1188 { static const char *const TAG = "cap1188"; void CAP1188Component::setup() { - // Reset device using the reset pin - if (this->reset_pin_ != nullptr) { - this->reset_pin_->setup(); - this->reset_pin_->digital_write(false); - delay(100); // NOLINT - this->reset_pin_->digital_write(true); - delay(100); // NOLINT - this->reset_pin_->digital_write(false); - delay(100); // NOLINT + this->disable_loop(); + + // no reset pin + if (this->reset_pin_ == nullptr) { + this->finish_setup_(); + return; } + // reset pin configured so reset before finishing setup + this->reset_pin_->setup(); + this->reset_pin_->digital_write(false); + // delay after reset pin write + this->set_timeout(100, [this]() { + this->reset_pin_->digital_write(true); + // delay after reset pin write + this->set_timeout(100, [this]() { + this->reset_pin_->digital_write(false); + // delay after reset pin write + this->set_timeout(100, [this]() { this->finish_setup_(); }); + }); + }); +} + +void CAP1188Component::finish_setup_() { // Check if CAP1188 is actually connected this->read_byte(CAP1188_PRODUCT_ID, &this->cap1188_product_id_); this->read_byte(CAP1188_MANUFACTURE_ID, &this->cap1188_manufacture_id_); @@ -44,6 +57,9 @@ void CAP1188Component::setup() { // Speed up a bit this->write_byte(CAP1188_STAND_BY_CONFIGURATION, 0x30); + + // Setup successful, so enable loop + this->enable_loop(); } void CAP1188Component::dump_config() { diff --git a/esphome/components/cap1188/cap1188.h b/esphome/components/cap1188/cap1188.h index baefd1c48f..297c601b05 100644 --- a/esphome/components/cap1188/cap1188.h +++ b/esphome/components/cap1188/cap1188.h @@ -49,6 +49,8 @@ class CAP1188Component : public Component, public i2c::I2CDevice { void loop() override; protected: + void finish_setup_(); + std::vector channels_{}; uint8_t touch_threshold_{0x20}; uint8_t allow_multiple_touches_{0x80}; diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py index 7e8afd8fab..25d0a22083 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -1,20 +1,37 @@ +import logging + import esphome.codegen as cg from esphome.components import web_server_base from esphome.components.web_server_base import CONF_WEB_SERVER_BASE_ID +from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( + CONF_AP, CONF_ID, PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_LN882X, PLATFORM_RTL87XX, + PlatformFramework, ) from esphome.core import CORE, coroutine_with_priority +from esphome.coroutine import CoroPriority +import esphome.final_validate as fv +from esphome.types import ConfigType + +_LOGGER = logging.getLogger(__name__) + + +def AUTO_LOAD() -> list[str]: + auto_load = ["web_server_base", "ota.web_server"] + if CORE.using_esp_idf: + auto_load.append("socket") + return auto_load + -AUTO_LOAD = ["web_server_base", "ota.web_server"] DEPENDENCIES = ["wifi"] -CODEOWNERS = ["@OttoWinter"] +CODEOWNERS = ["@esphome/core"] captive_portal_ns = cg.esphome_ns.namespace("captive_portal") CaptivePortal = captive_portal_ns.class_("CaptivePortal", cg.Component) @@ -40,7 +57,38 @@ CONFIG_SCHEMA = cv.All( ) -@coroutine_with_priority(64.0) +def _final_validate(config: ConfigType) -> ConfigType: + full_config = fv.full_config.get() + wifi_conf = full_config.get("wifi") + + if wifi_conf is None: + # This shouldn't happen due to DEPENDENCIES = ["wifi"], but check anyway + raise cv.Invalid("Captive portal requires the wifi component to be configured") + + if CONF_AP not in wifi_conf: + _LOGGER.warning( + "Captive portal is enabled but no WiFi AP is configured. " + "The captive portal will not be accessible. " + "Add 'ap:' to your WiFi configuration to enable the captive portal." + ) + + # Register socket needs for DNS server and additional HTTP connections + # - 1 UDP socket for DNS server + # - 3 additional TCP sockets for captive portal detection probes + configuration requests + # OS captive portal detection makes multiple probe requests that stay in TIME_WAIT. + # Need headroom for actual user configuration requests. + # LRU purging will reclaim idle sockets to prevent exhaustion from repeated attempts. + from esphome.components import socket + + socket.consume_sockets(4, "captive_portal")(config) + + return config + + +FINAL_VALIDATE_SCHEMA = _final_validate + + +@coroutine_with_priority(CoroPriority.CAPTIVE_PORTAL) async def to_code(config): paren = await cg.get_variable(config[CONF_WEB_SERVER_BASE_ID]) @@ -57,3 +105,11 @@ async def to_code(config): cg.add_library("DNSServer", None) if CORE.is_libretiny: cg.add_library("DNSServer", None) + + +# Only compile the ESP-IDF DNS server when using ESP-IDF framework +FILTER_SOURCE_FILES = filter_source_files_from_platform( + { + "dns_server_esp32_idf.cpp": {PlatformFramework.ESP32_IDF}, + } +) diff --git a/esphome/components/captive_portal/captive_index.h b/esphome/components/captive_portal/captive_index.h index 8835762fb3..3122f27558 100644 --- a/esphome/components/captive_portal/captive_index.h +++ b/esphome/components/captive_portal/captive_index.h @@ -7,103 +7,83 @@ namespace esphome { namespace captive_portal { const uint8_t INDEX_GZ[] PROGMEM = { - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xdd, 0x58, 0x6d, 0x6f, 0xdb, 0x38, 0x12, 0xfe, 0xde, - 0x5f, 0x31, 0xa7, 0x36, 0x6b, 0x6b, 0x1b, 0x51, 0x22, 0xe5, 0xb7, 0xd8, 0x92, 0x16, 0x69, 0xae, 0x8b, 0x5d, 0xa0, - 0xdd, 0x2d, 0x90, 0x6c, 0xef, 0x43, 0x51, 0x20, 0xb4, 0x34, 0xb2, 0xd8, 0x48, 0xa4, 0x4e, 0xa4, 0x5f, 0x52, 0xc3, - 0xf7, 0xdb, 0x0f, 0x94, 0x6c, 0xc7, 0xe9, 0x35, 0x87, 0xeb, 0xe2, 0x0e, 0x87, 0xdd, 0x18, 0x21, 0x86, 0xe4, 0xcc, - 0x70, 0xe6, 0xf1, 0x0c, 0x67, 0xcc, 0xe8, 0x2f, 0x99, 0x4a, 0xcd, 0x7d, 0x8d, 0x50, 0x98, 0xaa, 0x4c, 0x22, 0x3b, - 0x42, 0xc9, 0xe5, 0x22, 0x46, 0x99, 0x44, 0x05, 0xf2, 0x2c, 0x89, 0x2a, 0x34, 0x1c, 0xd2, 0x82, 0x37, 0x1a, 0x4d, - 0xfc, 0xdb, 0xcd, 0x8f, 0xde, 0x04, 0xfc, 0x24, 0x2a, 0x85, 0xbc, 0x83, 0x06, 0xcb, 0x58, 0xa4, 0x4a, 0x42, 0xd1, - 0x60, 0x1e, 0x67, 0xdc, 0xf0, 0xa9, 0xa8, 0xf8, 0x02, 0x2d, 0x43, 0x2b, 0x26, 0x79, 0x85, 0xf1, 0x4a, 0xe0, 0xba, - 0x56, 0x8d, 0x81, 0x54, 0x49, 0x83, 0xd2, 0xc4, 0xce, 0x5a, 0x64, 0xa6, 0x88, 0x33, 0x5c, 0x89, 0x14, 0xbd, 0x76, - 0x72, 0x2e, 0xa4, 0x30, 0x82, 0x97, 0x9e, 0x4e, 0x79, 0x89, 0x31, 0x3d, 0x5f, 0x6a, 0x6c, 0xda, 0x09, 0x9f, 0x97, - 0x18, 0x4b, 0xe5, 0xf8, 0x49, 0xa4, 0xd3, 0x46, 0xd4, 0x06, 0xac, 0xbd, 0x71, 0xa5, 0xb2, 0x65, 0x89, 0x89, 0xef, - 0x73, 0xad, 0xd1, 0x68, 0x5f, 0xc8, 0x0c, 0x37, 0x64, 0x14, 0x86, 0x29, 0xe3, 0xe3, 0x9c, 0x7c, 0xd2, 0xcf, 0x32, - 0x95, 0x2e, 0x2b, 0x94, 0x86, 0x94, 0x2a, 0xe5, 0x46, 0x28, 0x49, 0x34, 0xf2, 0x26, 0x2d, 0xe2, 0x38, 0x76, 0x7e, - 0xd0, 0x7c, 0x85, 0xce, 0x77, 0xdf, 0xf5, 0x8f, 0x4c, 0x0b, 0x34, 0xaf, 0x4b, 0xb4, 0xa4, 0x7e, 0x75, 0x7f, 0xc3, - 0x17, 0xbf, 0xf0, 0x0a, 0xfb, 0x0e, 0xd7, 0x22, 0x43, 0xc7, 0xfd, 0x10, 0x7c, 0x24, 0xda, 0xdc, 0x97, 0x48, 0x32, - 0xa1, 0xeb, 0x92, 0xdf, 0xc7, 0xce, 0xbc, 0x54, 0xe9, 0x9d, 0xe3, 0xce, 0xf2, 0xa5, 0x4c, 0xad, 0x72, 0xd0, 0x7d, - 0x74, 0xb7, 0x25, 0x1a, 0x30, 0xf1, 0x5b, 0x6e, 0x0a, 0x52, 0xf1, 0x4d, 0xbf, 0x23, 0x84, 0xec, 0xb3, 0xef, 0xfb, - 0xf8, 0x92, 0x06, 0x81, 0x7b, 0xde, 0x0e, 0x81, 0xeb, 0xd3, 0x20, 0x98, 0x35, 0x68, 0x96, 0x8d, 0x04, 0xde, 0xbf, - 0x8d, 0x6a, 0x6e, 0x0a, 0xc8, 0x62, 0xa7, 0xa2, 0x8c, 0x04, 0xc1, 0x04, 0xe8, 0x05, 0x61, 0x43, 0x8f, 0x52, 0x12, - 0x7a, 0x74, 0x98, 0x8e, 0xbd, 0x21, 0xd0, 0x81, 0x37, 0x04, 0xc6, 0xc8, 0x10, 0x82, 0xcf, 0x0e, 0xe4, 0xa2, 0x2c, - 0x63, 0x47, 0x2a, 0x89, 0x0e, 0x68, 0xd3, 0xa8, 0x3b, 0x8c, 0x9d, 0x74, 0xd9, 0x34, 0x28, 0xcd, 0x95, 0x2a, 0x55, - 0xe3, 0xf8, 0xc9, 0x33, 0x78, 0xf4, 0xf7, 0xcd, 0x47, 0x98, 0x86, 0x4b, 0x9d, 0xab, 0xa6, 0x8a, 0x9d, 0xf6, 0x4b, - 0xe9, 0xbf, 0xd8, 0x9a, 0x1d, 0xd8, 0xc1, 0x3d, 0xd9, 0xf4, 0x54, 0x23, 0x16, 0x42, 0xc6, 0x0e, 0x65, 0x40, 0x27, - 0x8e, 0x9f, 0xdc, 0xba, 0xbb, 0x23, 0x26, 0xdc, 0x62, 0xb2, 0xf7, 0x52, 0xf5, 0x3f, 0xdc, 0x46, 0x7a, 0xb5, 0x80, - 0x4d, 0x55, 0x4a, 0x1d, 0x3b, 0x85, 0x31, 0xf5, 0xd4, 0xf7, 0xd7, 0xeb, 0x35, 0x59, 0x87, 0x44, 0x35, 0x0b, 0x9f, - 0x05, 0x41, 0xe0, 0xeb, 0xd5, 0xc2, 0x81, 0x2e, 0x3e, 0x1c, 0x36, 0x70, 0xa0, 0x40, 0xb1, 0x28, 0x4c, 0x4b, 0x27, - 0x2f, 0xb6, 0xb8, 0x8b, 0x2c, 0x47, 0x72, 0xfb, 0xf1, 0xe4, 0x14, 0x71, 0x72, 0x0a, 0xfe, 0x70, 0x82, 0x66, 0xef, - 0xad, 0x35, 0x6a, 0xcc, 0x19, 0x30, 0x08, 0xda, 0x0f, 0xf3, 0x2c, 0xbd, 0x9f, 0x79, 0x5f, 0xcc, 0xe0, 0x64, 0x06, - 0x0c, 0x9e, 0x01, 0xb0, 0x6a, 0xe4, 0x5d, 0x1c, 0xc5, 0xa9, 0xdd, 0x5e, 0xd1, 0xe0, 0x61, 0xc1, 0xca, 0xfc, 0x34, - 0x3a, 0x9d, 0x7b, 0xec, 0xbd, 0x65, 0xb0, 0xd8, 0x1f, 0x85, 0x3c, 0x56, 0xd0, 0xf7, 0x23, 0x3e, 0x84, 0xe1, 0x7e, - 0x65, 0xe8, 0x59, 0xfa, 0x38, 0xb3, 0x27, 0xc1, 0x70, 0xc5, 0x0a, 0x5a, 0x79, 0x23, 0x6f, 0xc8, 0x43, 0x08, 0xf7, - 0x26, 0x85, 0x10, 0xae, 0x58, 0x31, 0x7a, 0x3f, 0x3a, 0x5d, 0xf3, 0xc2, 0xcf, 0x3d, 0x0b, 0xf3, 0xd4, 0x71, 0x1e, - 0x30, 0x50, 0xa7, 0x18, 0x90, 0x4f, 0x4a, 0xc8, 0xbe, 0xe3, 0xb8, 0xbb, 0x1c, 0x4d, 0x5a, 0xf4, 0x1d, 0x3f, 0x55, - 0x32, 0x17, 0x0b, 0xf2, 0x49, 0x2b, 0xe9, 0xb8, 0xc4, 0x14, 0x28, 0xfb, 0x07, 0x51, 0x2b, 0x88, 0xed, 0x4e, 0xff, - 0xcb, 0x1d, 0xe3, 0x6e, 0x8f, 0xf9, 0x61, 0x84, 0x29, 0x31, 0x36, 0xc4, 0x66, 0xf4, 0xf9, 0x71, 0x75, 0xae, 0xb2, - 0xfb, 0x27, 0x52, 0xa7, 0xa0, 0x5d, 0xde, 0x08, 0x29, 0xb1, 0xb9, 0xc1, 0x8d, 0x89, 0x9d, 0xb7, 0x97, 0x57, 0x70, - 0x99, 0x65, 0x0d, 0x6a, 0x3d, 0x05, 0xe7, 0xa5, 0x21, 0x15, 0x4f, 0xff, 0x73, 0x5d, 0xf4, 0x91, 0xae, 0xbf, 0x89, - 0x1f, 0x05, 0xfc, 0x82, 0x66, 0xad, 0x9a, 0xbb, 0xbd, 0x36, 0x6b, 0xda, 0xcc, 0x66, 0x60, 0x13, 0x1b, 0xc2, 0x6b, - 0x4d, 0x74, 0x29, 0x52, 0xec, 0x53, 0x97, 0x54, 0xbc, 0x7e, 0xf0, 0x4a, 0x1e, 0x80, 0xba, 0x8d, 0x32, 0xb1, 0x82, - 0xb4, 0xe4, 0x5a, 0xc7, 0x8e, 0xec, 0x54, 0x39, 0xb0, 0x4f, 0x1b, 0x25, 0xd3, 0x52, 0xa4, 0x77, 0xb1, 0xf3, 0x95, - 0x1b, 0xe2, 0xd5, 0xfd, 0xcf, 0x59, 0xbf, 0xa7, 0xb5, 0xc8, 0x7a, 0x2e, 0x59, 0xf1, 0x72, 0x89, 0x10, 0x83, 0x29, - 0x84, 0x7e, 0x30, 0x70, 0xf6, 0xa4, 0x58, 0xad, 0xef, 0x7a, 0x2e, 0xc9, 0x55, 0xba, 0xd4, 0x7d, 0xd7, 0x39, 0x64, - 0x69, 0xc4, 0xbb, 0x3b, 0xd4, 0x79, 0xee, 0x7c, 0x61, 0x91, 0x57, 0x62, 0x6e, 0x9c, 0x87, 0x6c, 0x7e, 0xb1, 0xd5, - 0x7d, 0x49, 0x1a, 0xad, 0x85, 0xbb, 0x3b, 0x2e, 0x46, 0xba, 0xe6, 0xf2, 0x4b, 0x41, 0x6b, 0xa0, 0x4d, 0x1a, 0x49, - 0x2c, 0x65, 0x33, 0xa7, 0xe6, 0xf2, 0x78, 0xa0, 0xcf, 0x0f, 0xe4, 0x8b, 0xad, 0xe8, 0x4b, 0x7b, 0x4b, 0xde, 0x1d, - 0x35, 0x46, 0x7e, 0x26, 0x56, 0xc9, 0xed, 0xce, 0x7d, 0xf0, 0xe3, 0xef, 0x4b, 0x6c, 0xee, 0xaf, 0xb1, 0xc4, 0xd4, - 0xa8, 0xa6, 0xef, 0x3c, 0x97, 0x68, 0x1c, 0xb7, 0x73, 0xf8, 0xa7, 0x9b, 0xb7, 0x6f, 0x62, 0xd5, 0x6f, 0xdc, 0xf3, - 0xa7, 0xb8, 0x6d, 0xb5, 0xf8, 0xd0, 0x60, 0xf9, 0x8f, 0xb8, 0x67, 0xeb, 0x45, 0xef, 0xa3, 0xe3, 0x92, 0xd6, 0xdf, - 0xdb, 0x87, 0xa2, 0x61, 0x13, 0xfb, 0xe5, 0xa6, 0x2a, 0xcf, 0xad, 0x87, 0xde, 0x68, 0xe8, 0xee, 0x6e, 0x77, 0xee, - 0xce, 0x9d, 0x45, 0x7e, 0x77, 0xef, 0x27, 0x51, 0x7b, 0x05, 0x27, 0xdf, 0x6f, 0xe7, 0x6a, 0xe3, 0x69, 0xf1, 0x59, - 0xc8, 0xc5, 0x54, 0xc8, 0x02, 0x1b, 0x61, 0x76, 0x99, 0x58, 0x9d, 0x0b, 0x59, 0x2f, 0xcd, 0xb6, 0xe6, 0x59, 0x66, - 0x77, 0x86, 0xf5, 0x66, 0x96, 0x2b, 0x69, 0x2c, 0x27, 0x4e, 0x29, 0x56, 0xbb, 0x6e, 0xbf, 0xbd, 0x5b, 0xa6, 0x17, - 0xc3, 0xb3, 0x9d, 0x0d, 0xb8, 0xad, 0xc1, 0x8d, 0xf1, 0x78, 0x29, 0x16, 0x72, 0x9a, 0xa2, 0x34, 0xd8, 0x74, 0x42, - 0x39, 0xaf, 0x44, 0x79, 0x3f, 0xd5, 0x5c, 0x6a, 0x4f, 0x63, 0x23, 0xf2, 0xdd, 0x7c, 0x69, 0x8c, 0x92, 0xdb, 0xb9, - 0x6a, 0x32, 0x6c, 0xa6, 0xc1, 0xac, 0x23, 0xbc, 0x86, 0x67, 0x62, 0xa9, 0xa7, 0x24, 0x6c, 0xb0, 0x9a, 0xcd, 0x79, - 0x7a, 0xb7, 0x68, 0xd4, 0x52, 0x66, 0x5e, 0x6a, 0x6f, 0xe1, 0xe9, 0x73, 0x9a, 0xf3, 0x10, 0xd3, 0xd9, 0x7e, 0x96, - 0xe7, 0xf9, 0xac, 0x14, 0x12, 0xbd, 0xee, 0x56, 0x9b, 0x32, 0x32, 0xb0, 0x62, 0x27, 0x66, 0x12, 0x66, 0x17, 0x3a, - 0x1b, 0x69, 0x10, 0x9c, 0xcd, 0x0e, 0xee, 0x04, 0xb3, 0x74, 0xd9, 0x68, 0xd5, 0x4c, 0x6b, 0x25, 0xac, 0x99, 0xbb, - 0x8a, 0x0b, 0x79, 0x6a, 0xbd, 0x0d, 0x93, 0xd9, 0xbe, 0x3c, 0x4d, 0x85, 0x6c, 0x8f, 0x69, 0x8b, 0xd4, 0xac, 0x12, - 0xb2, 0x2b, 0xb2, 0x53, 0x36, 0x0a, 0xea, 0xcd, 0x8e, 0xec, 0x03, 0x64, 0x7b, 0xe0, 0xce, 0x4b, 0xdc, 0xcc, 0x3e, - 0x2d, 0xb5, 0x11, 0xf9, 0xbd, 0xb7, 0x2f, 0xd2, 0x53, 0x5d, 0xf3, 0x14, 0xbd, 0x39, 0x9a, 0x35, 0xa2, 0x9c, 0xb5, - 0x67, 0x78, 0xc2, 0x60, 0xa5, 0xf7, 0x38, 0x1d, 0xd5, 0xb4, 0x01, 0xfa, 0x58, 0xd7, 0xbf, 0xe3, 0xb6, 0xb1, 0xb8, - 0xad, 0x78, 0xb3, 0x10, 0xd2, 0x9b, 0x2b, 0x63, 0x54, 0x35, 0xf5, 0xc6, 0xf5, 0x66, 0xb6, 0x5f, 0xb2, 0xca, 0xa6, - 0xd4, 0x9a, 0xd9, 0xd6, 0xde, 0x03, 0xde, 0xb4, 0xde, 0x80, 0x56, 0xa5, 0xc8, 0xf6, 0x7c, 0x2d, 0x0b, 0x04, 0x47, - 0x78, 0xe8, 0xb0, 0xde, 0x80, 0x5d, 0x3b, 0x40, 0x3d, 0xc8, 0x27, 0x9c, 0x06, 0x5f, 0xf9, 0x46, 0xb2, 0x3c, 0x67, - 0xf3, 0xfc, 0x88, 0x94, 0x2d, 0xa1, 0x3b, 0xb1, 0x8f, 0x0a, 0x36, 0xa8, 0x37, 0xb3, 0xc3, 0x77, 0x33, 0xa8, 0x37, - 0x3b, 0xd1, 0xa6, 0xc5, 0xf6, 0x44, 0x4b, 0x1b, 0xaa, 0xd3, 0x65, 0x53, 0xf6, 0x9d, 0xaf, 0x84, 0xee, 0x59, 0x78, - 0xf5, 0x50, 0xe2, 0x7a, 0x4f, 0x97, 0xb8, 0x1e, 0xd8, 0xa6, 0xe8, 0x95, 0xda, 0xc4, 0xbd, 0xb6, 0xd8, 0x0c, 0x80, - 0x0d, 0x7a, 0x67, 0xe1, 0xeb, 0xb3, 0xf0, 0xea, 0xbf, 0x52, 0xbb, 0x7e, 0x77, 0xe1, 0xfa, 0x86, 0xaa, 0xf5, 0x8d, - 0x15, 0xab, 0xf3, 0xce, 0x3a, 0x7f, 0x16, 0xbe, 0x76, 0xdc, 0x9d, 0x20, 0x5a, 0x2c, 0xe8, 0xff, 0x02, 0xda, 0x7f, - 0xc5, 0x31, 0xbc, 0xa4, 0x13, 0x72, 0x01, 0xed, 0xd0, 0x41, 0x44, 0xc2, 0x09, 0x8c, 0xaf, 0x06, 0x64, 0x40, 0xc1, - 0xb6, 0x43, 0x23, 0x18, 0x93, 0xc9, 0x05, 0xd0, 0x11, 0x09, 0xc7, 0x40, 0x19, 0x30, 0x4a, 0x86, 0x6f, 0x58, 0x48, - 0x46, 0x43, 0x18, 0x5f, 0xb1, 0x80, 0x84, 0x0c, 0x3a, 0xde, 0x11, 0x61, 0x0c, 0x42, 0xcb, 0x12, 0x56, 0x01, 0xb0, - 0x34, 0x24, 0xc1, 0x18, 0x02, 0x18, 0x91, 0xe0, 0x82, 0x4c, 0x46, 0x30, 0x21, 0x63, 0x0a, 0x8c, 0x0c, 0x86, 0xa5, - 0x37, 0x24, 0x14, 0x46, 0x24, 0x1c, 0xf1, 0x09, 0x19, 0x84, 0xd0, 0x0e, 0x1d, 0x1c, 0x63, 0xc2, 0x98, 0x47, 0x02, - 0xfa, 0x26, 0x24, 0x6c, 0x0c, 0x63, 0x32, 0x18, 0x5c, 0xd2, 0x11, 0xb9, 0x18, 0x40, 0x37, 0x76, 0xf0, 0x52, 0x06, - 0xc3, 0xa7, 0x40, 0x63, 0x7f, 0x5e, 0xd0, 0x42, 0xc2, 0x28, 0x84, 0xe4, 0x62, 0xc2, 0x6d, 0x5f, 0xca, 0xa0, 0x1b, - 0x3b, 0xdc, 0x28, 0x85, 0xe0, 0x77, 0x63, 0x16, 0xfe, 0x79, 0x31, 0xa3, 0x16, 0x01, 0x46, 0x06, 0xe1, 0x25, 0x0d, - 0xc9, 0x08, 0xda, 0xa1, 0x3b, 0x9b, 0x32, 0x98, 0x5c, 0x5d, 0xc0, 0x04, 0x46, 0x64, 0x34, 0x81, 0x0b, 0x18, 0x5a, - 0x74, 0x2f, 0xc8, 0x64, 0xd0, 0x09, 0x79, 0x8c, 0x7c, 0x2b, 0x8c, 0x83, 0x3f, 0x30, 0x8c, 0x4f, 0xf9, 0xf4, 0x07, - 0x76, 0xe9, 0xff, 0x71, 0x05, 0x45, 0x7e, 0xd7, 0x86, 0x45, 0x7e, 0xf7, 0x3c, 0x60, 0xbb, 0xa8, 0x24, 0xb2, 0xdd, - 0x48, 0x12, 0x15, 0x14, 0x44, 0x16, 0x57, 0x3c, 0x4d, 0x4e, 0x5a, 0xfd, 0xc8, 0x2f, 0xe8, 0x61, 0xab, 0xa0, 0xc9, - 0xa3, 0xc6, 0xbd, 0xdb, 0x6b, 0x2b, 0x7d, 0x72, 0x53, 0x20, 0xbc, 0xbe, 0x7e, 0x07, 0x6b, 0x51, 0x96, 0x20, 0xd5, - 0x1a, 0x4c, 0x73, 0x0f, 0x46, 0xd9, 0x57, 0x03, 0x89, 0xa9, 0xb1, 0xa4, 0x29, 0x10, 0xf6, 0x7d, 0x04, 0x21, 0x24, - 0x9a, 0x37, 0xc9, 0xbb, 0x12, 0xb9, 0x46, 0x58, 0x88, 0x15, 0x82, 0x30, 0xa0, 0x55, 0x85, 0x60, 0x84, 0x1d, 0x8e, - 0x82, 0x2d, 0x5f, 0xe4, 0x77, 0x87, 0x74, 0x8d, 0xb2, 0xc8, 0x62, 0x89, 0x26, 0xd9, 0x77, 0xc4, 0x51, 0x11, 0x76, - 0x56, 0x5d, 0xa3, 0x31, 0x42, 0x2e, 0xac, 0x55, 0x61, 0x12, 0xd9, 0x5f, 0xb7, 0xc0, 0xdb, 0xdf, 0x0c, 0xb1, 0xbf, - 0x16, 0xb9, 0xb0, 0x6f, 0x06, 0x49, 0xd4, 0x76, 0x91, 0x56, 0x83, 0x6d, 0x64, 0xba, 0x07, 0x8e, 0x96, 0x2a, 0x51, - 0x2e, 0x4c, 0x11, 0x87, 0x0c, 0xea, 0x92, 0xa7, 0x58, 0xa8, 0x32, 0xc3, 0x26, 0xbe, 0xbe, 0xfe, 0xf9, 0xaf, 0xf6, - 0x35, 0xc4, 0x9a, 0x70, 0x94, 0xac, 0xf5, 0x5d, 0x27, 0x68, 0x89, 0xbd, 0xdc, 0x68, 0xd0, 0xbd, 0x6b, 0xd4, 0x5c, - 0xeb, 0xb5, 0x6a, 0xb2, 0x47, 0x5a, 0xde, 0x1d, 0x16, 0xf7, 0x9a, 0xda, 0xff, 0xb6, 0x1f, 0xed, 0x84, 0xf4, 0x72, - 0x5e, 0x09, 0x93, 0x5c, 0xf3, 0x15, 0x46, 0x7e, 0xb7, 0x91, 0x44, 0xbe, 0x75, 0xa0, 0xe3, 0x2d, 0xf6, 0x32, 0x05, - 0x4d, 0x7e, 0xbd, 0xb9, 0x84, 0xdf, 0xea, 0x8c, 0x1b, 0xec, 0xb0, 0x6f, 0xbd, 0xac, 0xd0, 0x14, 0x2a, 0x8b, 0xdf, - 0xfd, 0x7a, 0x7d, 0x73, 0xf4, 0x78, 0xd9, 0x32, 0x01, 0xca, 0xb4, 0x7b, 0x6f, 0x59, 0x96, 0x46, 0xd4, 0xbc, 0x31, - 0xad, 0x5a, 0xcf, 0x66, 0xc7, 0xc1, 0xa3, 0x76, 0x3f, 0x17, 0x25, 0x76, 0x4e, 0xed, 0x05, 0xfd, 0x04, 0xbe, 0x66, - 0xe3, 0xe1, 0xec, 0x2f, 0xac, 0xf4, 0xbb, 0x00, 0xf2, 0xbb, 0x68, 0xf2, 0xdb, 0xd7, 0xa8, 0x7f, 0x02, 0x14, 0xee, - 0xbc, 0x64, 0x9d, 0x12, 0x00, 0x00}; + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x95, 0x16, 0x6b, 0x8f, 0xdb, 0x36, 0xf2, 0x7b, 0x7e, + 0x05, 0x8f, 0x49, 0xbb, 0x52, 0xb3, 0x7a, 0x7a, 0xed, 0x6c, 0x24, 0x51, 0x45, 0x9a, 0xbb, 0xa2, 0x05, 0x9a, 0x36, + 0xc0, 0x6e, 0x73, 0x1f, 0x82, 0x00, 0x4b, 0x53, 0x23, 0x8b, 0x31, 0x45, 0xea, 0x48, 0xca, 0x8f, 0x18, 0xbe, 0xdf, + 0x7e, 0xa0, 0x24, 0x7b, 0x9d, 0x45, 0x73, 0xb8, 0xb3, 0x60, 0x61, 0x38, 0xef, 0x19, 0xcd, 0x83, 0xc5, 0xdf, 0x2a, + 0xc5, 0xec, 0xbe, 0x03, 0xd4, 0xd8, 0x56, 0x94, 0x85, 0x7b, 0x23, 0x41, 0xe5, 0x8a, 0x80, 0x2c, 0x8b, 0x06, 0x68, + 0x55, 0x16, 0x2d, 0x58, 0x8a, 0x58, 0x43, 0xb5, 0x01, 0x4b, 0xfe, 0xbc, 0xff, 0x39, 0xb8, 0x2d, 0x0b, 0xc1, 0xe5, + 0x1a, 0x69, 0x10, 0x84, 0x33, 0x25, 0x51, 0xa3, 0xa1, 0x26, 0x15, 0xb5, 0x34, 0xe3, 0x2d, 0x5d, 0xc1, 0x24, 0x22, + 0x69, 0x0b, 0x64, 0xc3, 0x61, 0xdb, 0x29, 0x6d, 0x11, 0x53, 0xd2, 0x82, 0xb4, 0x04, 0x6f, 0x79, 0x65, 0x1b, 0x52, + 0xc1, 0x86, 0x33, 0x08, 0x86, 0xc3, 0x35, 0x97, 0xdc, 0x72, 0x2a, 0x02, 0xc3, 0xa8, 0x00, 0x92, 0x5c, 0xf7, 0x06, + 0xf4, 0x70, 0xa0, 0x4b, 0x01, 0x44, 0x2a, 0x5c, 0x16, 0x86, 0x69, 0xde, 0x59, 0xe4, 0x5c, 0x25, 0xad, 0xaa, 0x7a, + 0x01, 0x65, 0x14, 0x51, 0x63, 0xc0, 0x9a, 0x88, 0xcb, 0x0a, 0x76, 0xe1, 0x32, 0x66, 0x2c, 0x86, 0xdb, 0xdb, 0xf0, + 0xb3, 0x79, 0x56, 0x29, 0xd6, 0xb7, 0x20, 0x6d, 0x28, 0x14, 0xa3, 0x96, 0x2b, 0x19, 0x1a, 0xa0, 0x9a, 0x35, 0x84, + 0x10, 0xfc, 0xa3, 0xa1, 0x1b, 0xc0, 0xdf, 0x7f, 0xef, 0x9d, 0x99, 0x56, 0x60, 0xff, 0x21, 0xc0, 0x81, 0xe6, 0xa7, + 0xfd, 0x3d, 0x5d, 0xfd, 0x4e, 0x5b, 0xf0, 0x30, 0x35, 0xbc, 0x02, 0xec, 0x7f, 0x8c, 0x3f, 0x85, 0xc6, 0xee, 0x05, + 0x84, 0x15, 0x37, 0x9d, 0xa0, 0x7b, 0x82, 0x97, 0x42, 0xb1, 0x35, 0xf6, 0xf3, 0xba, 0x97, 0xcc, 0x29, 0x47, 0xc6, + 0x03, 0xff, 0x20, 0xc0, 0x22, 0x4b, 0xde, 0x51, 0xdb, 0x84, 0x2d, 0xdd, 0x79, 0x23, 0xc0, 0xa5, 0x97, 0xfe, 0xe0, + 0xc1, 0xcb, 0x24, 0x8e, 0xfd, 0xeb, 0xe1, 0x15, 0xfb, 0x51, 0x12, 0xc7, 0xb9, 0x06, 0xdb, 0x6b, 0x89, 0xa8, 0xf7, + 0x50, 0x74, 0xd4, 0x36, 0xa8, 0x22, 0xf8, 0x5d, 0x92, 0xa2, 0xe4, 0x75, 0x98, 0xce, 0x7f, 0x0b, 0x5f, 0xa1, 0x9b, + 0x30, 0x9d, 0xb3, 0x57, 0xc1, 0x1c, 0x25, 0x37, 0xc1, 0x1c, 0xa5, 0x69, 0x38, 0x47, 0xf1, 0x17, 0x8c, 0x6a, 0x2e, + 0x04, 0xc1, 0x52, 0x49, 0xc0, 0xc8, 0x58, 0xad, 0xd6, 0x40, 0x30, 0xeb, 0xb5, 0x06, 0x69, 0xdf, 0x2a, 0xa1, 0x34, + 0x8e, 0xca, 0x67, 0xff, 0x97, 0x42, 0xab, 0xa9, 0x34, 0xb5, 0xd2, 0x2d, 0xc1, 0x43, 0xf6, 0xbd, 0x17, 0x07, 0x7b, + 0x44, 0xee, 0xe5, 0x5f, 0x10, 0x03, 0xa5, 0xf9, 0x8a, 0x4b, 0x82, 0x9d, 0xc6, 0x5b, 0x1c, 0x95, 0x0f, 0xfe, 0xf1, + 0x1c, 0x3d, 0x75, 0xd1, 0x4f, 0xf1, 0x28, 0xef, 0xe3, 0x43, 0x61, 0x36, 0x2b, 0xb4, 0x6b, 0x85, 0x34, 0x04, 0x37, + 0xd6, 0x76, 0x59, 0x14, 0x6d, 0xb7, 0xdb, 0x70, 0x3b, 0x0b, 0x95, 0x5e, 0x45, 0x69, 0x1c, 0xc7, 0x91, 0xd9, 0xac, + 0x30, 0x1a, 0x0b, 0x01, 0xa7, 0x37, 0x18, 0x35, 0xc0, 0x57, 0x8d, 0x1d, 0xe0, 0xf2, 0xc5, 0x01, 0x8e, 0x85, 0xe3, + 0x28, 0x1f, 0x3e, 0x5d, 0x58, 0xe1, 0x17, 0x56, 0xe0, 0x47, 0xea, 0xe1, 0x53, 0x98, 0x57, 0x43, 0x98, 0xaf, 0x68, + 0x8a, 0x52, 0x14, 0x0f, 0x4f, 0x1a, 0x38, 0x78, 0x3a, 0x05, 0x4f, 0x4e, 0xe8, 0xe2, 0xe4, 0xa0, 0x76, 0x11, 0xbc, + 0x3e, 0xcb, 0x26, 0x0e, 0xb3, 0x49, 0xe2, 0x47, 0x84, 0x13, 0xf8, 0x65, 0x71, 0x79, 0x0e, 0xd2, 0x0f, 0x97, 0x0c, + 0xce, 0x5a, 0x93, 0x7c, 0x58, 0xd0, 0x39, 0x9a, 0x4f, 0x98, 0x79, 0xe0, 0xe0, 0xf3, 0x09, 0xcd, 0x37, 0x69, 0x93, + 0xb4, 0xc1, 0x22, 0x98, 0xd3, 0x19, 0x9a, 0x4d, 0x8e, 0xcc, 0xd0, 0x6c, 0x93, 0x36, 0x8b, 0x0f, 0x8b, 0x4b, 0x5c, + 0x30, 0xfb, 0x72, 0x15, 0x95, 0xd8, 0xcf, 0x30, 0x7e, 0x8c, 0x5c, 0x5d, 0x46, 0x1e, 0x7e, 0x56, 0x5c, 0x7a, 0x18, + 0xfb, 0xc7, 0x1a, 0x2c, 0x6b, 0x3c, 0x1c, 0x31, 0x25, 0x6b, 0xbe, 0x0a, 0x3f, 0x1b, 0x25, 0xb1, 0x1f, 0xda, 0x06, + 0xa4, 0x77, 0x12, 0x75, 0x82, 0x30, 0x50, 0xbc, 0xa7, 0x14, 0xeb, 0x1f, 0xce, 0xf5, 0x6f, 0xb9, 0x15, 0x40, 0x6c, + 0xe8, 0x1a, 0xf6, 0xfa, 0x8c, 0x5d, 0xaa, 0x6a, 0xff, 0x8d, 0xd6, 0x68, 0x92, 0xb1, 0x2f, 0xb8, 0x94, 0xa0, 0xef, + 0x61, 0x67, 0x09, 0x7e, 0xf7, 0xe6, 0x2d, 0x7a, 0x53, 0x55, 0x1a, 0x8c, 0xc9, 0x10, 0x7e, 0x69, 0xc3, 0x96, 0xb2, + 0xff, 0x5d, 0x57, 0xf2, 0x95, 0xae, 0x7f, 0xf2, 0x9f, 0x39, 0xfa, 0x1d, 0xec, 0x56, 0xe9, 0xf5, 0xa4, 0xcd, 0xb9, + 0x96, 0xbb, 0x0e, 0xd3, 0xc4, 0x86, 0xb4, 0x33, 0xa1, 0x11, 0x9c, 0x81, 0x97, 0xf8, 0x61, 0x4b, 0xbb, 0xc7, 0xa8, + 0xe4, 0x29, 0x51, 0x0f, 0x45, 0xc5, 0x37, 0x88, 0x09, 0x6a, 0x0c, 0xc1, 0x72, 0x54, 0x85, 0xd1, 0x33, 0x34, 0xfc, + 0x94, 0x64, 0x82, 0xb3, 0x35, 0xc1, 0x7f, 0x31, 0x01, 0x7e, 0xda, 0xff, 0x5a, 0x79, 0x57, 0xc6, 0xf0, 0xea, 0xca, + 0x0f, 0x37, 0x54, 0xf4, 0x80, 0x08, 0xb2, 0x0d, 0x37, 0x8f, 0x0e, 0xe6, 0xdf, 0x14, 0xeb, 0xcc, 0xfa, 0xca, 0x0f, + 0x6b, 0xc5, 0x7a, 0xe3, 0xf9, 0xb8, 0x9c, 0xcc, 0x15, 0x74, 0x1c, 0x90, 0xf8, 0x39, 0x7e, 0xe2, 0x51, 0x20, 0xa0, + 0xb6, 0x67, 0x3e, 0x84, 0x5e, 0x1c, 0x8c, 0x27, 0x43, 0x6d, 0x0c, 0xf7, 0x8f, 0x67, 0x64, 0x61, 0x3a, 0x2a, 0x9f, + 0x0a, 0x3a, 0x07, 0x5d, 0xab, 0xc8, 0xd0, 0x41, 0xae, 0x5f, 0x3a, 0x2a, 0xcf, 0x06, 0x23, 0x7a, 0x02, 0x5f, 0x1c, + 0xb8, 0x27, 0xdd, 0x14, 0x5c, 0x9f, 0x35, 0x16, 0x51, 0xc5, 0x37, 0xe5, 0xc3, 0xd1, 0x7f, 0x8c, 0xe3, 0x5f, 0x3d, + 0xe8, 0xfd, 0x1d, 0x08, 0x60, 0x56, 0x69, 0x0f, 0x3f, 0x97, 0x60, 0xb1, 0x3f, 0x06, 0xfc, 0xcb, 0xfd, 0xbb, 0xdf, + 0x88, 0xf2, 0xb4, 0x7f, 0xfd, 0x2d, 0x6e, 0xb7, 0x0a, 0x3e, 0x6a, 0x10, 0xff, 0x26, 0x57, 0x6e, 0x19, 0x5c, 0x7d, + 0xc2, 0x7e, 0x38, 0xc4, 0xfb, 0xf0, 0xb8, 0x11, 0x5c, 0x3b, 0xbf, 0xdc, 0xb5, 0xe2, 0xda, 0x45, 0x18, 0x2c, 0xe6, + 0xfe, 0xf1, 0xe1, 0xe8, 0x1f, 0xfd, 0xbc, 0x88, 0xc6, 0xb9, 0x5e, 0x16, 0xc3, 0x88, 0x2d, 0x7f, 0x38, 0x2c, 0xd5, + 0x2e, 0x30, 0xfc, 0x0b, 0x97, 0xab, 0x8c, 0xcb, 0x06, 0x34, 0xb7, 0xc7, 0x8a, 0x6f, 0xae, 0xb9, 0xec, 0x7a, 0x7b, + 0xe8, 0x68, 0x55, 0x39, 0xca, 0xbc, 0xdb, 0xe5, 0xb5, 0x92, 0xd6, 0x71, 0x42, 0x96, 0x40, 0x7b, 0x1c, 0xe9, 0xc3, + 0x44, 0xc9, 0x5e, 0xcf, 0xbf, 0x3b, 0xba, 0x82, 0x3b, 0x58, 0xd8, 0xd9, 0x80, 0x0a, 0xbe, 0x92, 0x19, 0x03, 0x69, + 0x41, 0x8f, 0x42, 0x35, 0x6d, 0xb9, 0xd8, 0x67, 0x86, 0x4a, 0x13, 0x18, 0xd0, 0xbc, 0x3e, 0x2e, 0x7b, 0x6b, 0x95, + 0x3c, 0x2c, 0x95, 0xae, 0x40, 0x67, 0x71, 0x3e, 0x02, 0x81, 0xa6, 0x15, 0xef, 0x4d, 0x16, 0xce, 0x34, 0xb4, 0xf9, + 0x92, 0xb2, 0xf5, 0x4a, 0xab, 0x5e, 0x56, 0x01, 0x73, 0x93, 0x36, 0x7b, 0x9e, 0xd4, 0x74, 0x06, 0x2c, 0x9f, 0x4e, + 0x75, 0x5d, 0xe7, 0x82, 0x4b, 0x08, 0xc6, 0x59, 0x96, 0xa5, 0xe1, 0x8d, 0x13, 0xbb, 0x70, 0x33, 0x4c, 0x1d, 0x62, + 0xf4, 0x31, 0x89, 0xe3, 0xef, 0xf2, 0x53, 0x38, 0x71, 0xce, 0x7a, 0x6d, 0x94, 0xce, 0x3a, 0xc5, 0x9d, 0x9b, 0xc7, + 0x96, 0x72, 0x79, 0xe9, 0xbd, 0x2b, 0x93, 0x7c, 0x5a, 0x3f, 0x19, 0x97, 0x83, 0x99, 0x61, 0x09, 0xe5, 0x2d, 0x97, + 0xe3, 0x0e, 0xcd, 0xd2, 0x45, 0xdc, 0xed, 0x8e, 0xe1, 0x54, 0x20, 0x87, 0x13, 0x77, 0x2d, 0x60, 0x97, 0x7f, 0xee, + 0x8d, 0xe5, 0xf5, 0x3e, 0x98, 0x76, 0x70, 0x66, 0x3a, 0xca, 0x20, 0x58, 0x82, 0xdd, 0x02, 0xc8, 0x7c, 0xb0, 0x11, + 0x70, 0x0b, 0xad, 0x99, 0xf2, 0x74, 0x56, 0x33, 0x14, 0xe8, 0xd7, 0xba, 0xfe, 0x1b, 0xb7, 0xab, 0xc5, 0x43, 0x4b, + 0xf5, 0x8a, 0xcb, 0x60, 0xa9, 0xac, 0x55, 0x6d, 0x16, 0xbc, 0xea, 0x76, 0xf9, 0x84, 0x72, 0xca, 0xb2, 0xc4, 0xb9, + 0x39, 0xec, 0xd6, 0x53, 0xbe, 0x93, 0x6e, 0x87, 0x8c, 0x12, 0xbc, 0x9a, 0xf8, 0x06, 0x16, 0x14, 0x9f, 0xd3, 0x93, + 0xcc, 0xbb, 0x1d, 0x72, 0xb8, 0x53, 0xaa, 0x6f, 0xea, 0x5b, 0x9a, 0xc4, 0x7f, 0xf1, 0x45, 0xaa, 0xba, 0x4e, 0x97, + 0xf5, 0x39, 0x53, 0x6e, 0x4d, 0xba, 0xd6, 0x18, 0x4a, 0xab, 0x88, 0xc6, 0xdb, 0x8c, 0xab, 0x8c, 0xb2, 0x70, 0x19, + 0x2e, 0x8b, 0x26, 0x41, 0xbc, 0x22, 0x2d, 0x65, 0xe5, 0xc5, 0xf8, 0x2a, 0xa2, 0x26, 0x39, 0x91, 0x9a, 0xa4, 0xfc, + 0x6a, 0x18, 0x8d, 0xb4, 0xc1, 0xfb, 0xf2, 0xad, 0x92, 0x12, 0x98, 0xe5, 0x72, 0x85, 0xac, 0x42, 0x53, 0x0a, 0xc2, + 0x30, 0x2c, 0x96, 0xba, 0x7c, 0x2f, 0x80, 0x1a, 0x40, 0x5b, 0xca, 0x6d, 0x58, 0x44, 0x23, 0xff, 0xd8, 0xc7, 0xbc, + 0x22, 0x12, 0x6c, 0x39, 0x35, 0x6c, 0xd1, 0xcc, 0x46, 0x03, 0x77, 0x60, 0x9d, 0x26, 0x67, 0x60, 0x56, 0x16, 0x6e, + 0xe5, 0x22, 0x3a, 0x8c, 0x34, 0x12, 0x6d, 0x79, 0xcd, 0xdd, 0x95, 0xa5, 0x2c, 0x86, 0x22, 0x77, 0x1a, 0x5c, 0x9e, + 0xc7, 0xeb, 0xd5, 0x00, 0x09, 0x90, 0x2b, 0xdb, 0x90, 0x59, 0x8a, 0x3a, 0x41, 0x19, 0x34, 0x4a, 0x54, 0xa0, 0xc9, + 0xdd, 0xdd, 0xaf, 0x7f, 0x2f, 0x9d, 0x33, 0x8f, 0x72, 0x9d, 0x59, 0x8f, 0x62, 0x0e, 0x98, 0xa4, 0x16, 0x37, 0xe3, + 0xa5, 0xaa, 0xa3, 0xc6, 0x6c, 0x95, 0xae, 0xbe, 0xd2, 0xf1, 0x7e, 0x42, 0x8e, 0x7a, 0x86, 0xff, 0xd0, 0x2a, 0xe5, + 0x1d, 0xdd, 0x40, 0x11, 0x4d, 0x87, 0x22, 0x72, 0x0e, 0x8f, 0xf4, 0x66, 0xe2, 0x6b, 0x92, 0xf2, 0x8f, 0xfb, 0x37, + 0xe8, 0xcf, 0xae, 0xa2, 0x16, 0xc6, 0xb4, 0x0d, 0x51, 0xb5, 0x60, 0x1b, 0x55, 0x91, 0xf7, 0x7f, 0xdc, 0xdd, 0x9f, + 0x23, 0xec, 0x07, 0x26, 0x04, 0x92, 0x8d, 0xd7, 0xbb, 0x5e, 0x58, 0xde, 0x51, 0x6d, 0x07, 0xb5, 0x81, 0x9b, 0x22, + 0xa7, 0x18, 0x06, 0x7a, 0xcd, 0x05, 0x8c, 0x61, 0x8c, 0x82, 0x25, 0x3a, 0x79, 0x75, 0xb2, 0xf6, 0xc4, 0xaf, 0x68, + 0xfc, 0xda, 0xd1, 0xf8, 0xe9, 0xa3, 0xe1, 0xa6, 0xfb, 0x1f, 0x53, 0x58, 0x46, 0xb2, 0xf9, 0x0a, 0x00, 0x00}; } // namespace captive_portal } // namespace esphome diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index 25179fdacc..4eb00835b1 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -11,84 +11,109 @@ namespace captive_portal { static const char *const TAG = "captive_portal"; void CaptivePortal::handle_config(AsyncWebServerRequest *request) { - AsyncResponseStream *stream = request->beginResponseStream("application/json"); - stream->addHeader("cache-control", "public, max-age=0, must-revalidate"); - stream->printf(R"({"mac":"%s","name":"%s","aps":[{})", get_mac_address_pretty().c_str(), App.get_name().c_str()); + AsyncResponseStream *stream = request->beginResponseStream(ESPHOME_F("application/json")); + stream->addHeader(ESPHOME_F("cache-control"), ESPHOME_F("public, max-age=0, must-revalidate")); + char mac_s[18]; + const char *mac_str = get_mac_address_pretty_into_buffer(mac_s); +#ifdef USE_ESP8266 + stream->print(ESPHOME_F("{\"mac\":\"")); + stream->print(mac_str); + stream->print(ESPHOME_F("\",\"name\":\"")); + stream->print(App.get_name().c_str()); + stream->print(ESPHOME_F("\",\"aps\":[{}")); +#else + stream->printf(R"({"mac":"%s","name":"%s","aps":[{})", mac_str, App.get_name().c_str()); +#endif for (auto &scan : wifi::global_wifi_component->get_scan_result()) { if (scan.get_is_hidden()) continue; - // Assumes no " in ssid, possible unicode isses? + // Assumes no " in ssid, possible unicode isses? +#ifdef USE_ESP8266 + stream->print(ESPHOME_F(",{\"ssid\":\"")); + stream->print(scan.get_ssid().c_str()); + stream->print(ESPHOME_F("\",\"rssi\":")); + stream->print(scan.get_rssi()); + stream->print(ESPHOME_F(",\"lock\":")); + stream->print(scan.get_with_auth()); + stream->print(ESPHOME_F("}")); +#else stream->printf(R"(,{"ssid":"%s","rssi":%d,"lock":%d})", scan.get_ssid().c_str(), scan.get_rssi(), scan.get_with_auth()); +#endif } - stream->print(F("]}")); + stream->print(ESPHOME_F("]}")); request->send(stream); } void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { - std::string ssid = request->arg("ssid").c_str(); - std::string psk = request->arg("psk").c_str(); + std::string ssid = request->arg("ssid").c_str(); // NOLINT(readability-redundant-string-cstr) + std::string psk = request->arg("psk").c_str(); // NOLINT(readability-redundant-string-cstr) ESP_LOGI(TAG, "Requested WiFi Settings Change:"); ESP_LOGI(TAG, " SSID='%s'", ssid.c_str()); ESP_LOGI(TAG, " Password=" LOG_SECRET("'%s'"), psk.c_str()); - wifi::global_wifi_component->save_wifi_sta(ssid, psk); - wifi::global_wifi_component->start_scanning(); - request->redirect("/?save"); + // Defer save to main loop thread to avoid NVS operations from HTTP thread + this->defer([ssid, psk]() { wifi::global_wifi_component->save_wifi_sta(ssid, psk); }); + request->redirect(ESPHOME_F("/?save")); } void CaptivePortal::setup() { -#ifndef USE_ARDUINO - // No DNS server needed for non-Arduino frameworks + // Disable loop by default - will be enabled when captive portal starts this->disable_loop(); -#endif } void CaptivePortal::start() { this->base_->init(); if (!this->initialized_) { this->base_->add_handler(this); +#ifdef USE_ESP32 + // Enable LRU socket purging to handle captive portal detection probe bursts + // OS captive portal detection makes many simultaneous HTTP requests which can + // exhaust sockets. LRU purging automatically closes oldest idle connections. + this->base_->get_server()->set_lru_purge_enable(true); +#endif } + network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip(); + +#ifdef USE_ESP_IDF + // Create DNS server instance for ESP-IDF + this->dns_server_ = make_unique(); + this->dns_server_->start(ip); +#endif #ifdef USE_ARDUINO this->dns_server_ = make_unique(); this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError); - network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip(); - this->dns_server_->start(53, "*", ip); - // Re-enable loop() when DNS server is started - this->enable_loop(); + this->dns_server_->start(53, ESPHOME_F("*"), ip); #endif - this->base_->get_server()->onNotFound([this](AsyncWebServerRequest *req) { - if (!this->active_ || req->host().c_str() == wifi::global_wifi_component->wifi_soft_ap_ip().str()) { - req->send(404, "text/html", "File not found"); - return; - } - - auto url = "http://" + wifi::global_wifi_component->wifi_soft_ap_ip().str(); - req->redirect(url.c_str()); - }); - this->initialized_ = true; this->active_ = true; + + // Enable loop() now that captive portal is active + this->enable_loop(); + + ESP_LOGV(TAG, "Captive portal started"); } void CaptivePortal::handleRequest(AsyncWebServerRequest *req) { - if (req->url() == "/") { -#ifndef USE_ESP8266 - auto *response = req->beginResponse(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ)); -#else - auto *response = req->beginResponse_P(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ)); -#endif - response->addHeader("Content-Encoding", "gzip"); - req->send(response); - return; - } else if (req->url() == "/config.json") { + if (req->url() == ESPHOME_F("/config.json")) { this->handle_config(req); return; - } else if (req->url() == "/wifisave") { + } else if (req->url() == ESPHOME_F("/wifisave")) { this->handle_wifisave(req); return; } + + // All other requests get the captive portal page + // This includes OS captive portal detection endpoints which will trigger + // the captive portal when they don't receive their expected responses +#ifndef USE_ESP8266 + auto *response = req->beginResponse(200, ESPHOME_F("text/html"), INDEX_GZ, sizeof(INDEX_GZ)); +#else + auto *response = req->beginResponse_P(200, ESPHOME_F("text/html"), INDEX_GZ, sizeof(INDEX_GZ)); +#endif + response->addHeader(ESPHOME_F("Content-Encoding"), ESPHOME_F("gzip")); + req->send(response); } CaptivePortal::CaptivePortal(web_server_base::WebServerBase *base) : base_(base) { global_captive_portal = this; } diff --git a/esphome/components/captive_portal/captive_portal.h b/esphome/components/captive_portal/captive_portal.h index c78fff824a..ae9b9dfba0 100644 --- a/esphome/components/captive_portal/captive_portal.h +++ b/esphome/components/captive_portal/captive_portal.h @@ -5,6 +5,9 @@ #ifdef USE_ARDUINO #include #endif +#ifdef USE_ESP_IDF +#include "dns_server_esp32_idf.h" +#endif #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "esphome/core/preferences.h" @@ -19,41 +22,40 @@ class CaptivePortal : public AsyncWebHandler, public Component { CaptivePortal(web_server_base::WebServerBase *base); void setup() override; void dump_config() override; -#ifdef USE_ARDUINO void loop() override { +#ifdef USE_ARDUINO if (this->dns_server_ != nullptr) { this->dns_server_->processNextRequest(); - } else { - this->disable_loop(); } - } #endif +#ifdef USE_ESP_IDF + if (this->dns_server_ != nullptr) { + this->dns_server_->process_next_request(); + } +#endif + } float get_setup_priority() const override; void start(); bool is_active() const { return this->active_; } void end() { this->active_ = false; - this->base_->deinit(); -#ifdef USE_ARDUINO - this->dns_server_->stop(); - this->dns_server_ = nullptr; + this->disable_loop(); // Stop processing DNS requests +#ifdef USE_ESP32 + // Disable LRU socket purging now that captive portal is done + this->base_->get_server()->set_lru_purge_enable(false); #endif + this->base_->deinit(); + if (this->dns_server_ != nullptr) { + this->dns_server_->stop(); + this->dns_server_ = nullptr; + } } bool canHandle(AsyncWebServerRequest *request) const override { - if (!this->active_) - return false; - - if (request->method() == HTTP_GET) { - if (request->url() == "/") - return true; - if (request->url() == "/config.json") - return true; - if (request->url() == "/wifisave") - return true; - } - - return false; + // Handle all GET requests when captive portal is active + // This allows us to respond with the portal page for any URL, + // triggering OS captive portal detection + return this->active_ && request->method() == HTTP_GET; } void handle_config(AsyncWebServerRequest *request); @@ -66,7 +68,7 @@ class CaptivePortal : public AsyncWebHandler, public Component { web_server_base::WebServerBase *base_; bool initialized_{false}; bool active_{false}; -#ifdef USE_ARDUINO +#if defined(USE_ARDUINO) || defined(USE_ESP_IDF) std::unique_ptr dns_server_{nullptr}; #endif }; diff --git a/esphome/components/captive_portal/dns_server_esp32_idf.cpp b/esphome/components/captive_portal/dns_server_esp32_idf.cpp new file mode 100644 index 0000000000..740107400a --- /dev/null +++ b/esphome/components/captive_portal/dns_server_esp32_idf.cpp @@ -0,0 +1,205 @@ +#include "dns_server_esp32_idf.h" +#ifdef USE_ESP_IDF + +#include "esphome/core/log.h" +#include "esphome/core/hal.h" +#include "esphome/components/socket/socket.h" +#include +#include + +namespace esphome::captive_portal { + +static const char *const TAG = "captive_portal.dns"; + +// DNS constants +static constexpr uint16_t DNS_PORT = 53; +static constexpr uint16_t DNS_QR_FLAG = 1 << 15; +static constexpr uint16_t DNS_OPCODE_MASK = 0x7800; +static constexpr uint16_t DNS_QTYPE_A = 0x0001; +static constexpr uint16_t DNS_QCLASS_IN = 0x0001; +static constexpr uint16_t DNS_ANSWER_TTL = 300; + +// DNS Header structure +struct DNSHeader { + uint16_t id; + uint16_t flags; + uint16_t qd_count; + uint16_t an_count; + uint16_t ns_count; + uint16_t ar_count; +} __attribute__((packed)); + +// DNS Question structure +struct DNSQuestion { + uint16_t type; + uint16_t dns_class; +} __attribute__((packed)); + +// DNS Answer structure +struct DNSAnswer { + uint16_t ptr_offset; + uint16_t type; + uint16_t dns_class; + uint32_t ttl; + uint16_t addr_len; + uint32_t ip_addr; +} __attribute__((packed)); + +void DNSServer::start(const network::IPAddress &ip) { + this->server_ip_ = ip; + ESP_LOGV(TAG, "Starting DNS server on %s", ip.str().c_str()); + + // Create loop-monitored UDP socket + this->socket_ = socket::socket_ip_loop_monitored(SOCK_DGRAM, IPPROTO_UDP); + if (this->socket_ == nullptr) { + ESP_LOGE(TAG, "Socket create failed"); + return; + } + + // Set socket options + int enable = 1; + this->socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(enable)); + + // Bind to port 53 + struct sockaddr_storage server_addr = {}; + socklen_t addr_len = socket::set_sockaddr_any((struct sockaddr *) &server_addr, sizeof(server_addr), DNS_PORT); + + int err = this->socket_->bind((struct sockaddr *) &server_addr, addr_len); + if (err != 0) { + ESP_LOGE(TAG, "Bind failed: %d", errno); + this->socket_ = nullptr; + return; + } + ESP_LOGV(TAG, "Bound to port %d", DNS_PORT); +} + +void DNSServer::stop() { + if (this->socket_ != nullptr) { + this->socket_->close(); + this->socket_ = nullptr; + } + ESP_LOGV(TAG, "Stopped"); +} + +void DNSServer::process_next_request() { + // Process one request if socket is valid and data is available + if (this->socket_ == nullptr || !this->socket_->ready()) { + return; + } + struct sockaddr_in client_addr; + socklen_t client_addr_len = sizeof(client_addr); + + // Receive DNS request using raw fd for recvfrom + int fd = this->socket_->get_fd(); + if (fd < 0) { + return; + } + + ssize_t len = recvfrom(fd, this->buffer_, sizeof(this->buffer_), MSG_DONTWAIT, (struct sockaddr *) &client_addr, + &client_addr_len); + + if (len < 0) { + if (errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) { + ESP_LOGE(TAG, "recvfrom failed: %d", errno); + } + return; + } + + ESP_LOGVV(TAG, "Received %d bytes from %s:%d", len, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); + + if (len < static_cast(sizeof(DNSHeader) + 1)) { + ESP_LOGV(TAG, "Request too short: %d", len); + return; + } + + // Parse DNS header + DNSHeader *header = (DNSHeader *) this->buffer_; + uint16_t flags = ntohs(header->flags); + uint16_t qd_count = ntohs(header->qd_count); + + // Check if it's a standard query + if ((flags & DNS_QR_FLAG) || (flags & DNS_OPCODE_MASK) || qd_count != 1) { + ESP_LOGV(TAG, "Not a standard query: flags=0x%04X, qd_count=%d", flags, qd_count); + return; // Not a standard query + } + + // Parse domain name (we don't actually care about it - redirect everything) + uint8_t *ptr = this->buffer_ + sizeof(DNSHeader); + uint8_t *end = this->buffer_ + len; + + while (ptr < end && *ptr != 0) { + uint8_t label_len = *ptr; + if (label_len > 63) { // Check for invalid label length + return; + } + // Check if we have room for this label plus the length byte + if (ptr + label_len + 1 > end) { + return; // Would overflow + } + ptr += label_len + 1; + } + + // Check if we reached a proper null terminator + if (ptr >= end || *ptr != 0) { + return; // Name not terminated or truncated + } + ptr++; // Skip the null terminator + + // Check we have room for the question + if (ptr + sizeof(DNSQuestion) > end) { + return; // Request truncated + } + + // Parse DNS question + DNSQuestion *question = (DNSQuestion *) ptr; + uint16_t qtype = ntohs(question->type); + uint16_t qclass = ntohs(question->dns_class); + + // We only handle A queries + if (qtype != DNS_QTYPE_A || qclass != DNS_QCLASS_IN) { + ESP_LOGV(TAG, "Not an A query: type=0x%04X, class=0x%04X", qtype, qclass); + return; // Not an A query + } + + // Build DNS response by modifying the request in-place + header->flags = htons(DNS_QR_FLAG | 0x8000); // Response + Authoritative + header->an_count = htons(1); // One answer + + // Add answer section after the question + size_t question_len = (ptr + sizeof(DNSQuestion)) - this->buffer_ - sizeof(DNSHeader); + size_t answer_offset = sizeof(DNSHeader) + question_len; + + // Check if we have room for the answer + if (answer_offset + sizeof(DNSAnswer) > sizeof(this->buffer_)) { + ESP_LOGW(TAG, "Response too large"); + return; + } + + DNSAnswer *answer = (DNSAnswer *) (this->buffer_ + answer_offset); + + // Pointer to name in question (offset from start of packet) + answer->ptr_offset = htons(0xC000 | sizeof(DNSHeader)); + answer->type = htons(DNS_QTYPE_A); + answer->dns_class = htons(DNS_QCLASS_IN); + answer->ttl = htonl(DNS_ANSWER_TTL); + answer->addr_len = htons(4); + + // Get the raw IP address + ip4_addr_t addr = this->server_ip_; + answer->ip_addr = addr.addr; + + size_t response_len = answer_offset + sizeof(DNSAnswer); + + // Send response + ssize_t sent = + this->socket_->sendto(this->buffer_, response_len, 0, (struct sockaddr *) &client_addr, client_addr_len); + if (sent < 0) { + ESP_LOGV(TAG, "Send failed: %d", errno); + } else { + ESP_LOGV(TAG, "Sent %d bytes", sent); + } +} + +} // namespace esphome::captive_portal + +#endif // USE_ESP_IDF diff --git a/esphome/components/captive_portal/dns_server_esp32_idf.h b/esphome/components/captive_portal/dns_server_esp32_idf.h new file mode 100644 index 0000000000..13d9def8e3 --- /dev/null +++ b/esphome/components/captive_portal/dns_server_esp32_idf.h @@ -0,0 +1,27 @@ +#pragma once +#ifdef USE_ESP_IDF + +#include +#include "esphome/core/helpers.h" +#include "esphome/components/network/ip_address.h" +#include "esphome/components/socket/socket.h" + +namespace esphome::captive_portal { + +class DNSServer { + public: + void start(const network::IPAddress &ip); + void stop(); + void process_next_request(); + + protected: + static constexpr size_t DNS_BUFFER_SIZE = 192; + + std::unique_ptr socket_{nullptr}; + network::IPAddress server_ip_; + uint8_t buffer_[DNS_BUFFER_SIZE]; +}; + +} // namespace esphome::captive_portal + +#endif // USE_ESP_IDF diff --git a/esphome/components/ccs811/ccs811.cpp b/esphome/components/ccs811/ccs811.cpp index cecb92b3df..84355f2793 100644 --- a/esphome/components/ccs811/ccs811.cpp +++ b/esphome/components/ccs811/ccs811.cpp @@ -152,10 +152,10 @@ void CCS811Component::send_env_data_() { void CCS811Component::dump_config() { ESP_LOGCONFIG(TAG, "CCS811"); LOG_I2C_DEVICE(this) - LOG_UPDATE_INTERVAL(this) - LOG_SENSOR(" ", "CO2 Sensor", this->co2_) - LOG_SENSOR(" ", "TVOC Sensor", this->tvoc_) - LOG_TEXT_SENSOR(" ", "Firmware Version Sensor", this->version_) + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "CO2 Sensor", this->co2_); + LOG_SENSOR(" ", "TVOC Sensor", this->tvoc_); + LOG_TEXT_SENSOR(" ", "Firmware Version Sensor", this->version_); if (this->baseline_) { ESP_LOGCONFIG(TAG, " Baseline: %04X", *this->baseline_); } else { diff --git a/esphome/components/ch422g/ch422g.cpp b/esphome/components/ch422g/ch422g.cpp index 6f652cb0c6..9a4e342525 100644 --- a/esphome/components/ch422g/ch422g.cpp +++ b/esphome/components/ch422g/ch422g.cpp @@ -91,7 +91,7 @@ bool CH422GComponent::read_inputs_() { // Write a register. Can't use the standard write_byte() method because there is no single pre-configured i2c address. bool CH422GComponent::write_reg_(uint8_t reg, uint8_t value) { - auto err = this->bus_->write(reg, &value, 1); + auto err = this->bus_->write_readv(reg, &value, 1, nullptr, 0); if (err != i2c::ERROR_OK) { this->status_set_warning(str_sprintf("write failed for register 0x%X, error %d", reg, err).c_str()); return false; @@ -102,7 +102,7 @@ bool CH422GComponent::write_reg_(uint8_t reg, uint8_t value) { uint8_t CH422GComponent::read_reg_(uint8_t reg) { uint8_t value; - auto err = this->bus_->read(reg, &value, 1); + auto err = this->bus_->write_readv(reg, nullptr, 0, &value, 1); if (err != i2c::ERROR_OK) { this->status_set_warning(str_sprintf("read failed for register 0x%X, error %d", reg, err).c_str()); return 0; diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index 4af3a619b5..5824e68141 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -47,7 +47,7 @@ from esphome.const import ( CONF_VISUAL, CONF_WEB_SERVER, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass @@ -270,11 +270,6 @@ def climate_schema( return _CLIMATE_SCHEMA.extend(schema) -# Remove before 2025.11.0 -CLIMATE_SCHEMA = climate_schema(Climate) -CLIMATE_SCHEMA.add_extra(cv.deprecated_schema_constant("climate")) - - async def setup_climate_core_(var, config): await setup_entity(var, config, "climate") @@ -517,6 +512,6 @@ async def climate_control_to_code(config, action_id, template_arg, args): return var -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(climate_ns.using) diff --git a/esphome/components/climate/automation.h b/esphome/components/climate/automation.h index a4d13ade58..36cc8f4f21 100644 --- a/esphome/components/climate/automation.h +++ b/esphome/components/climate/automation.h @@ -22,7 +22,7 @@ template class ControlAction : public Action { TEMPLATABLE_VALUE(std::string, custom_preset) TEMPLATABLE_VALUE(ClimateSwingMode, swing_mode) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->climate_->make_call(); call.set_mode(this->mode_.optional_value(x...)); call.set_target_temperature(this->target_temperature_.optional_value(x...)); diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index edebc0de69..82b75660ba 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -1,4 +1,6 @@ #include "climate.h" +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/macros.h" namespace esphome { @@ -6,6 +8,42 @@ namespace climate { static const char *const TAG = "climate"; +// Memory-efficient lookup tables +struct StringToUint8 { + const char *str; + const uint8_t value; +}; + +constexpr StringToUint8 CLIMATE_MODES_BY_STR[] = { + {"OFF", CLIMATE_MODE_OFF}, + {"AUTO", CLIMATE_MODE_AUTO}, + {"COOL", CLIMATE_MODE_COOL}, + {"HEAT", CLIMATE_MODE_HEAT}, + {"FAN_ONLY", CLIMATE_MODE_FAN_ONLY}, + {"DRY", CLIMATE_MODE_DRY}, + {"HEAT_COOL", CLIMATE_MODE_HEAT_COOL}, +}; + +constexpr StringToUint8 CLIMATE_FAN_MODES_BY_STR[] = { + {"ON", CLIMATE_FAN_ON}, {"OFF", CLIMATE_FAN_OFF}, {"AUTO", CLIMATE_FAN_AUTO}, + {"LOW", CLIMATE_FAN_LOW}, {"MEDIUM", CLIMATE_FAN_MEDIUM}, {"HIGH", CLIMATE_FAN_HIGH}, + {"MIDDLE", CLIMATE_FAN_MIDDLE}, {"FOCUS", CLIMATE_FAN_FOCUS}, {"DIFFUSE", CLIMATE_FAN_DIFFUSE}, + {"QUIET", CLIMATE_FAN_QUIET}, +}; + +constexpr StringToUint8 CLIMATE_PRESETS_BY_STR[] = { + {"ECO", CLIMATE_PRESET_ECO}, {"AWAY", CLIMATE_PRESET_AWAY}, {"BOOST", CLIMATE_PRESET_BOOST}, + {"COMFORT", CLIMATE_PRESET_COMFORT}, {"HOME", CLIMATE_PRESET_HOME}, {"SLEEP", CLIMATE_PRESET_SLEEP}, + {"ACTIVITY", CLIMATE_PRESET_ACTIVITY}, {"NONE", CLIMATE_PRESET_NONE}, +}; + +constexpr StringToUint8 CLIMATE_SWING_MODES_BY_STR[] = { + {"OFF", CLIMATE_SWING_OFF}, + {"BOTH", CLIMATE_SWING_BOTH}, + {"VERTICAL", CLIMATE_SWING_VERTICAL}, + {"HORIZONTAL", CLIMATE_SWING_HORIZONTAL}, +}; + void ClimateCall::perform() { this->parent_->control_callback_.call(*this); ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); @@ -14,21 +52,21 @@ void ClimateCall::perform() { const LogString *mode_s = climate_mode_to_string(*this->mode_); ESP_LOGD(TAG, " Mode: %s", LOG_STR_ARG(mode_s)); } - if (this->custom_fan_mode_.has_value()) { + if (this->custom_fan_mode_ != nullptr) { this->fan_mode_.reset(); - ESP_LOGD(TAG, " Custom Fan: %s", this->custom_fan_mode_.value().c_str()); + ESP_LOGD(TAG, " Custom Fan: %s", this->custom_fan_mode_); } if (this->fan_mode_.has_value()) { - this->custom_fan_mode_.reset(); + this->custom_fan_mode_ = nullptr; const LogString *fan_mode_s = climate_fan_mode_to_string(*this->fan_mode_); ESP_LOGD(TAG, " Fan: %s", LOG_STR_ARG(fan_mode_s)); } - if (this->custom_preset_.has_value()) { + if (this->custom_preset_ != nullptr) { this->preset_.reset(); - ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset_.value().c_str()); + ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset_); } if (this->preset_.has_value()) { - this->custom_preset_.reset(); + this->custom_preset_ = nullptr; const LogString *preset_s = climate_preset_to_string(*this->preset_); ESP_LOGD(TAG, " Preset: %s", LOG_STR_ARG(preset_s)); } @@ -50,206 +88,179 @@ void ClimateCall::perform() { } this->parent_->control(*this); } + void ClimateCall::validate_() { auto traits = this->parent_->get_traits(); if (this->mode_.has_value()) { auto mode = *this->mode_; if (!traits.supports_mode(mode)) { - ESP_LOGW(TAG, " Mode %s is not supported by this device!", LOG_STR_ARG(climate_mode_to_string(mode))); + ESP_LOGW(TAG, " Mode %s not supported", LOG_STR_ARG(climate_mode_to_string(mode))); this->mode_.reset(); } } - if (this->custom_fan_mode_.has_value()) { - auto custom_fan_mode = *this->custom_fan_mode_; - if (!traits.supports_custom_fan_mode(custom_fan_mode)) { - ESP_LOGW(TAG, " Fan Mode %s is not supported by this device!", custom_fan_mode.c_str()); - this->custom_fan_mode_.reset(); + if (this->custom_fan_mode_ != nullptr) { + if (!traits.supports_custom_fan_mode(this->custom_fan_mode_)) { + ESP_LOGW(TAG, " Fan Mode %s not supported", this->custom_fan_mode_); + this->custom_fan_mode_ = nullptr; } } else if (this->fan_mode_.has_value()) { auto fan_mode = *this->fan_mode_; if (!traits.supports_fan_mode(fan_mode)) { - ESP_LOGW(TAG, " Fan Mode %s is not supported by this device!", - LOG_STR_ARG(climate_fan_mode_to_string(fan_mode))); + ESP_LOGW(TAG, " Fan Mode %s not supported", LOG_STR_ARG(climate_fan_mode_to_string(fan_mode))); this->fan_mode_.reset(); } } - if (this->custom_preset_.has_value()) { - auto custom_preset = *this->custom_preset_; - if (!traits.supports_custom_preset(custom_preset)) { - ESP_LOGW(TAG, " Preset %s is not supported by this device!", custom_preset.c_str()); - this->custom_preset_.reset(); + if (this->custom_preset_ != nullptr) { + if (!traits.supports_custom_preset(this->custom_preset_)) { + ESP_LOGW(TAG, " Preset %s not supported", this->custom_preset_); + this->custom_preset_ = nullptr; } } else if (this->preset_.has_value()) { auto preset = *this->preset_; if (!traits.supports_preset(preset)) { - ESP_LOGW(TAG, " Preset %s is not supported by this device!", LOG_STR_ARG(climate_preset_to_string(preset))); + ESP_LOGW(TAG, " Preset %s not supported", LOG_STR_ARG(climate_preset_to_string(preset))); this->preset_.reset(); } } if (this->swing_mode_.has_value()) { auto swing_mode = *this->swing_mode_; if (!traits.supports_swing_mode(swing_mode)) { - ESP_LOGW(TAG, " Swing Mode %s is not supported by this device!", - LOG_STR_ARG(climate_swing_mode_to_string(swing_mode))); + ESP_LOGW(TAG, " Swing Mode %s not supported", LOG_STR_ARG(climate_swing_mode_to_string(swing_mode))); this->swing_mode_.reset(); } } if (this->target_temperature_.has_value()) { auto target = *this->target_temperature_; - if (traits.get_supports_two_point_target_temperature()) { + if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | + CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) { ESP_LOGW(TAG, " Cannot set target temperature for climate device " - "with two-point target temperature!"); + "with two-point target temperature"); this->target_temperature_.reset(); } else if (std::isnan(target)) { - ESP_LOGW(TAG, " Target temperature must not be NAN!"); + ESP_LOGW(TAG, " Target temperature must not be NAN"); this->target_temperature_.reset(); } } if (this->target_temperature_low_.has_value() || this->target_temperature_high_.has_value()) { - if (!traits.get_supports_two_point_target_temperature()) { - ESP_LOGW(TAG, " Cannot set low/high target temperature for this device!"); + if (!traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | + CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) { + ESP_LOGW(TAG, " Cannot set low/high target temperature"); this->target_temperature_low_.reset(); this->target_temperature_high_.reset(); } } if (this->target_temperature_low_.has_value() && std::isnan(*this->target_temperature_low_)) { - ESP_LOGW(TAG, " Target temperature low must not be NAN!"); + ESP_LOGW(TAG, " Target temperature low must not be NAN"); this->target_temperature_low_.reset(); } if (this->target_temperature_high_.has_value() && std::isnan(*this->target_temperature_high_)) { - ESP_LOGW(TAG, " Target temperature low must not be NAN!"); + ESP_LOGW(TAG, " Target temperature high must not be NAN"); this->target_temperature_high_.reset(); } if (this->target_temperature_low_.has_value() && this->target_temperature_high_.has_value()) { float low = *this->target_temperature_low_; float high = *this->target_temperature_high_; if (low > high) { - ESP_LOGW(TAG, " Target temperature low %.2f must be smaller than target temperature high %.2f!", low, high); + ESP_LOGW(TAG, " Target temperature low %.2f must be less than target temperature high %.2f", low, high); this->target_temperature_low_.reset(); this->target_temperature_high_.reset(); } } } + ClimateCall &ClimateCall::set_mode(ClimateMode mode) { this->mode_ = mode; return *this; } + ClimateCall &ClimateCall::set_mode(const std::string &mode) { - if (str_equals_case_insensitive(mode, "OFF")) { - this->set_mode(CLIMATE_MODE_OFF); - } else if (str_equals_case_insensitive(mode, "AUTO")) { - this->set_mode(CLIMATE_MODE_AUTO); - } else if (str_equals_case_insensitive(mode, "COOL")) { - this->set_mode(CLIMATE_MODE_COOL); - } else if (str_equals_case_insensitive(mode, "HEAT")) { - this->set_mode(CLIMATE_MODE_HEAT); - } else if (str_equals_case_insensitive(mode, "FAN_ONLY")) { - this->set_mode(CLIMATE_MODE_FAN_ONLY); - } else if (str_equals_case_insensitive(mode, "DRY")) { - this->set_mode(CLIMATE_MODE_DRY); - } else if (str_equals_case_insensitive(mode, "HEAT_COOL")) { - this->set_mode(CLIMATE_MODE_HEAT_COOL); - } else { - ESP_LOGW(TAG, "'%s' - Unrecognized mode %s", this->parent_->get_name().c_str(), mode.c_str()); - } - return *this; -} -ClimateCall &ClimateCall::set_fan_mode(ClimateFanMode fan_mode) { - this->fan_mode_ = fan_mode; - this->custom_fan_mode_.reset(); - return *this; -} -ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) { - if (str_equals_case_insensitive(fan_mode, "ON")) { - this->set_fan_mode(CLIMATE_FAN_ON); - } else if (str_equals_case_insensitive(fan_mode, "OFF")) { - this->set_fan_mode(CLIMATE_FAN_OFF); - } else if (str_equals_case_insensitive(fan_mode, "AUTO")) { - this->set_fan_mode(CLIMATE_FAN_AUTO); - } else if (str_equals_case_insensitive(fan_mode, "LOW")) { - this->set_fan_mode(CLIMATE_FAN_LOW); - } else if (str_equals_case_insensitive(fan_mode, "MEDIUM")) { - this->set_fan_mode(CLIMATE_FAN_MEDIUM); - } else if (str_equals_case_insensitive(fan_mode, "HIGH")) { - this->set_fan_mode(CLIMATE_FAN_HIGH); - } else if (str_equals_case_insensitive(fan_mode, "MIDDLE")) { - this->set_fan_mode(CLIMATE_FAN_MIDDLE); - } else if (str_equals_case_insensitive(fan_mode, "FOCUS")) { - this->set_fan_mode(CLIMATE_FAN_FOCUS); - } else if (str_equals_case_insensitive(fan_mode, "DIFFUSE")) { - this->set_fan_mode(CLIMATE_FAN_DIFFUSE); - } else if (str_equals_case_insensitive(fan_mode, "QUIET")) { - this->set_fan_mode(CLIMATE_FAN_QUIET); - } else { - if (this->parent_->get_traits().supports_custom_fan_mode(fan_mode)) { - this->custom_fan_mode_ = fan_mode; - this->fan_mode_.reset(); - } else { - ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), fan_mode.c_str()); + for (const auto &mode_entry : CLIMATE_MODES_BY_STR) { + if (str_equals_case_insensitive(mode, mode_entry.str)) { + this->set_mode(static_cast(mode_entry.value)); + return *this; } } + ESP_LOGW(TAG, "'%s' - Unrecognized mode %s", this->parent_->get_name().c_str(), mode.c_str()); return *this; } + +ClimateCall &ClimateCall::set_fan_mode(ClimateFanMode fan_mode) { + this->fan_mode_ = fan_mode; + this->custom_fan_mode_ = nullptr; + return *this; +} + +ClimateCall &ClimateCall::set_fan_mode(const char *custom_fan_mode) { + // Check if it's a standard enum mode first + for (const auto &mode_entry : CLIMATE_FAN_MODES_BY_STR) { + if (str_equals_case_insensitive(custom_fan_mode, mode_entry.str)) { + return this->set_fan_mode(static_cast(mode_entry.value)); + } + } + // Find the matching pointer from parent climate device + if (const char *mode_ptr = this->parent_->find_custom_fan_mode_(custom_fan_mode)) { + this->custom_fan_mode_ = mode_ptr; + this->fan_mode_.reset(); + return *this; + } + ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), custom_fan_mode); + return *this; +} + +ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) { return this->set_fan_mode(fan_mode.c_str()); } + ClimateCall &ClimateCall::set_fan_mode(optional fan_mode) { if (fan_mode.has_value()) { this->set_fan_mode(fan_mode.value()); } return *this; } + ClimateCall &ClimateCall::set_preset(ClimatePreset preset) { this->preset_ = preset; - this->custom_preset_.reset(); + this->custom_preset_ = nullptr; return *this; } -ClimateCall &ClimateCall::set_preset(const std::string &preset) { - if (str_equals_case_insensitive(preset, "ECO")) { - this->set_preset(CLIMATE_PRESET_ECO); - } else if (str_equals_case_insensitive(preset, "AWAY")) { - this->set_preset(CLIMATE_PRESET_AWAY); - } else if (str_equals_case_insensitive(preset, "BOOST")) { - this->set_preset(CLIMATE_PRESET_BOOST); - } else if (str_equals_case_insensitive(preset, "COMFORT")) { - this->set_preset(CLIMATE_PRESET_COMFORT); - } else if (str_equals_case_insensitive(preset, "HOME")) { - this->set_preset(CLIMATE_PRESET_HOME); - } else if (str_equals_case_insensitive(preset, "SLEEP")) { - this->set_preset(CLIMATE_PRESET_SLEEP); - } else if (str_equals_case_insensitive(preset, "ACTIVITY")) { - this->set_preset(CLIMATE_PRESET_ACTIVITY); - } else if (str_equals_case_insensitive(preset, "NONE")) { - this->set_preset(CLIMATE_PRESET_NONE); - } else { - if (this->parent_->get_traits().supports_custom_preset(preset)) { - this->custom_preset_ = preset; - this->preset_.reset(); - } else { - ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), preset.c_str()); + +ClimateCall &ClimateCall::set_preset(const char *custom_preset) { + // Check if it's a standard enum preset first + for (const auto &preset_entry : CLIMATE_PRESETS_BY_STR) { + if (str_equals_case_insensitive(custom_preset, preset_entry.str)) { + return this->set_preset(static_cast(preset_entry.value)); } } + // Find the matching pointer from parent climate device + if (const char *preset_ptr = this->parent_->find_custom_preset_(custom_preset)) { + this->custom_preset_ = preset_ptr; + this->preset_.reset(); + return *this; + } + ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), custom_preset); return *this; } + +ClimateCall &ClimateCall::set_preset(const std::string &preset) { return this->set_preset(preset.c_str()); } + ClimateCall &ClimateCall::set_preset(optional preset) { if (preset.has_value()) { this->set_preset(preset.value()); } return *this; } + ClimateCall &ClimateCall::set_swing_mode(ClimateSwingMode swing_mode) { this->swing_mode_ = swing_mode; return *this; } + ClimateCall &ClimateCall::set_swing_mode(const std::string &swing_mode) { - if (str_equals_case_insensitive(swing_mode, "OFF")) { - this->set_swing_mode(CLIMATE_SWING_OFF); - } else if (str_equals_case_insensitive(swing_mode, "BOTH")) { - this->set_swing_mode(CLIMATE_SWING_BOTH); - } else if (str_equals_case_insensitive(swing_mode, "VERTICAL")) { - this->set_swing_mode(CLIMATE_SWING_VERTICAL); - } else if (str_equals_case_insensitive(swing_mode, "HORIZONTAL")) { - this->set_swing_mode(CLIMATE_SWING_HORIZONTAL); - } else { - ESP_LOGW(TAG, "'%s' - Unrecognized swing mode %s", this->parent_->get_name().c_str(), swing_mode.c_str()); + for (const auto &mode_entry : CLIMATE_SWING_MODES_BY_STR) { + if (str_equals_case_insensitive(swing_mode, mode_entry.str)) { + this->set_swing_mode(static_cast(mode_entry.value)); + return *this; + } } + ESP_LOGW(TAG, "'%s' - Unrecognized swing mode %s", this->parent_->get_name().c_str(), swing_mode.c_str()); return *this; } @@ -257,59 +268,69 @@ ClimateCall &ClimateCall::set_target_temperature(float target_temperature) { this->target_temperature_ = target_temperature; return *this; } + ClimateCall &ClimateCall::set_target_temperature_low(float target_temperature_low) { this->target_temperature_low_ = target_temperature_low; return *this; } + ClimateCall &ClimateCall::set_target_temperature_high(float target_temperature_high) { this->target_temperature_high_ = target_temperature_high; return *this; } + ClimateCall &ClimateCall::set_target_humidity(float target_humidity) { this->target_humidity_ = target_humidity; return *this; } -const optional &ClimateCall::get_mode() const { return this->mode_; } const optional &ClimateCall::get_target_temperature() const { return this->target_temperature_; } const optional &ClimateCall::get_target_temperature_low() const { return this->target_temperature_low_; } const optional &ClimateCall::get_target_temperature_high() const { return this->target_temperature_high_; } const optional &ClimateCall::get_target_humidity() const { return this->target_humidity_; } + +const optional &ClimateCall::get_mode() const { return this->mode_; } const optional &ClimateCall::get_fan_mode() const { return this->fan_mode_; } -const optional &ClimateCall::get_custom_fan_mode() const { return this->custom_fan_mode_; } -const optional &ClimateCall::get_preset() const { return this->preset_; } -const optional &ClimateCall::get_custom_preset() const { return this->custom_preset_; } const optional &ClimateCall::get_swing_mode() const { return this->swing_mode_; } +const optional &ClimateCall::get_preset() const { return this->preset_; } + ClimateCall &ClimateCall::set_target_temperature_high(optional target_temperature_high) { this->target_temperature_high_ = target_temperature_high; return *this; } + ClimateCall &ClimateCall::set_target_temperature_low(optional target_temperature_low) { this->target_temperature_low_ = target_temperature_low; return *this; } + ClimateCall &ClimateCall::set_target_temperature(optional target_temperature) { this->target_temperature_ = target_temperature; return *this; } + ClimateCall &ClimateCall::set_target_humidity(optional target_humidity) { this->target_humidity_ = target_humidity; return *this; } + ClimateCall &ClimateCall::set_mode(optional mode) { this->mode_ = mode; return *this; } + ClimateCall &ClimateCall::set_fan_mode(optional fan_mode) { this->fan_mode_ = fan_mode; - this->custom_fan_mode_.reset(); + this->custom_fan_mode_ = nullptr; return *this; } + ClimateCall &ClimateCall::set_preset(optional preset) { this->preset_ = preset; - this->custom_preset_.reset(); + this->custom_preset_ = nullptr; return *this; } + ClimateCall &ClimateCall::set_swing_mode(optional swing_mode) { this->swing_mode_ = swing_mode; return *this; @@ -327,13 +348,14 @@ void Climate::add_on_control_callback(std::function &&callb static const uint32_t RESTORE_STATE_VERSION = 0x848EA6ADUL; optional Climate::restore_state_() { - this->rtc_ = global_preferences->make_preference(this->get_object_id_hash() ^ + this->rtc_ = global_preferences->make_preference(this->get_preference_hash() ^ RESTORE_STATE_VERSION); ClimateDeviceRestoreState recovered{}; if (!this->rtc_.load(&recovered)) return {}; return recovered; } + void Climate::save_state_() { #if (defined(USE_ESP_IDF) || (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0))) && \ !defined(CLANG_TIDY) @@ -350,40 +372,48 @@ void Climate::save_state_() { state.mode = this->mode; auto traits = this->get_traits(); - if (traits.get_supports_two_point_target_temperature()) { + if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | + CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) { state.target_temperature_low = this->target_temperature_low; state.target_temperature_high = this->target_temperature_high; } else { state.target_temperature = this->target_temperature; } - if (traits.get_supports_target_humidity()) { + if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) { state.target_humidity = this->target_humidity; } if (traits.get_supports_fan_modes() && fan_mode.has_value()) { state.uses_custom_fan_mode = false; state.fan_mode = this->fan_mode.value(); } - if (!traits.get_supported_custom_fan_modes().empty() && custom_fan_mode.has_value()) { + if (!traits.get_supported_custom_fan_modes().empty() && this->has_custom_fan_mode()) { state.uses_custom_fan_mode = true; const auto &supported = traits.get_supported_custom_fan_modes(); - std::vector vec{supported.begin(), supported.end()}; - auto it = std::find(vec.begin(), vec.end(), custom_fan_mode); - if (it != vec.end()) { - state.custom_fan_mode = std::distance(vec.begin(), it); + // std::vector maintains insertion order + size_t i = 0; + for (const char *mode : supported) { + if (strcmp(mode, this->custom_fan_mode_) == 0) { + state.custom_fan_mode = i; + break; + } + i++; } } if (traits.get_supports_presets() && preset.has_value()) { state.uses_custom_preset = false; state.preset = this->preset.value(); } - if (!traits.get_supported_custom_presets().empty() && custom_preset.has_value()) { + if (!traits.get_supported_custom_presets().empty() && this->has_custom_preset()) { state.uses_custom_preset = true; const auto &supported = traits.get_supported_custom_presets(); - std::vector vec{supported.begin(), supported.end()}; - auto it = std::find(vec.begin(), vec.end(), custom_preset); - // only set custom preset if value exists, otherwise leave it as is - if (it != vec.cend()) { - state.custom_preset = std::distance(vec.begin(), it); + // std::vector maintains insertion order + size_t i = 0; + for (const char *preset : supported) { + if (strcmp(preset, this->custom_preset_) == 0) { + state.custom_preset = i; + break; + } + i++; } } if (traits.get_supports_swing_modes()) { @@ -392,47 +422,52 @@ void Climate::save_state_() { this->rtc_.save(&state); } + void Climate::publish_state() { ESP_LOGD(TAG, "'%s' - Sending state:", this->name_.c_str()); auto traits = this->get_traits(); ESP_LOGD(TAG, " Mode: %s", LOG_STR_ARG(climate_mode_to_string(this->mode))); - if (traits.get_supports_action()) { + if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) { ESP_LOGD(TAG, " Action: %s", LOG_STR_ARG(climate_action_to_string(this->action))); } if (traits.get_supports_fan_modes() && this->fan_mode.has_value()) { ESP_LOGD(TAG, " Fan Mode: %s", LOG_STR_ARG(climate_fan_mode_to_string(this->fan_mode.value()))); } - if (!traits.get_supported_custom_fan_modes().empty() && this->custom_fan_mode.has_value()) { - ESP_LOGD(TAG, " Custom Fan Mode: %s", this->custom_fan_mode.value().c_str()); + if (!traits.get_supported_custom_fan_modes().empty() && this->has_custom_fan_mode()) { + ESP_LOGD(TAG, " Custom Fan Mode: %s", this->custom_fan_mode_); } if (traits.get_supports_presets() && this->preset.has_value()) { ESP_LOGD(TAG, " Preset: %s", LOG_STR_ARG(climate_preset_to_string(this->preset.value()))); } - if (!traits.get_supported_custom_presets().empty() && this->custom_preset.has_value()) { - ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset.value().c_str()); + if (!traits.get_supported_custom_presets().empty() && this->has_custom_preset()) { + ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset_); } if (traits.get_supports_swing_modes()) { ESP_LOGD(TAG, " Swing Mode: %s", LOG_STR_ARG(climate_swing_mode_to_string(this->swing_mode))); } - if (traits.get_supports_current_temperature()) { + if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) { ESP_LOGD(TAG, " Current Temperature: %.2f°C", this->current_temperature); } - if (traits.get_supports_two_point_target_temperature()) { + if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | + CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) { ESP_LOGD(TAG, " Target Temperature: Low: %.2f°C High: %.2f°C", this->target_temperature_low, this->target_temperature_high); } else { ESP_LOGD(TAG, " Target Temperature: %.2f°C", this->target_temperature); } - if (traits.get_supports_current_humidity()) { + if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) { ESP_LOGD(TAG, " Current Humidity: %.0f%%", this->current_humidity); } - if (traits.get_supports_target_humidity()) { + if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) { ESP_LOGD(TAG, " Target Humidity: %.0f%%", this->target_humidity); } // Send state to frontend this->state_callback_.call(*this); +#if defined(USE_CLIMATE) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_climate_update(this); +#endif // Save state this->save_state_(); } @@ -462,16 +497,20 @@ ClimateTraits Climate::get_traits() { void Climate::set_visual_min_temperature_override(float visual_min_temperature_override) { this->visual_min_temperature_override_ = visual_min_temperature_override; } + void Climate::set_visual_max_temperature_override(float visual_max_temperature_override) { this->visual_max_temperature_override_ = visual_max_temperature_override; } + void Climate::set_visual_temperature_step_override(float target, float current) { this->visual_target_temperature_step_override_ = target; this->visual_current_temperature_step_override_ = current; } + void Climate::set_visual_min_humidity_override(float visual_min_humidity_override) { this->visual_min_humidity_override_ = visual_min_humidity_override; } + void Climate::set_visual_max_humidity_override(float visual_max_humidity_override) { this->visual_max_humidity_override_ = visual_max_humidity_override; } @@ -482,154 +521,244 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) { auto call = climate->make_call(); auto traits = climate->get_traits(); call.set_mode(this->mode); - if (traits.get_supports_two_point_target_temperature()) { + if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | + CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) { call.set_target_temperature_low(this->target_temperature_low); call.set_target_temperature_high(this->target_temperature_high); } else { call.set_target_temperature(this->target_temperature); } - if (traits.get_supports_target_humidity()) { + if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) { call.set_target_humidity(this->target_humidity); } - if (traits.get_supports_fan_modes() || !traits.get_supported_custom_fan_modes().empty()) { + if (this->uses_custom_fan_mode) { + if (this->custom_fan_mode < traits.get_supported_custom_fan_modes().size()) { + call.fan_mode_.reset(); + call.custom_fan_mode_ = traits.get_supported_custom_fan_modes()[this->custom_fan_mode]; + } + } else if (traits.supports_fan_mode(this->fan_mode)) { call.set_fan_mode(this->fan_mode); } - if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) { + if (this->uses_custom_preset) { + if (this->custom_preset < traits.get_supported_custom_presets().size()) { + call.preset_.reset(); + call.custom_preset_ = traits.get_supported_custom_presets()[this->custom_preset]; + } + } else if (traits.supports_preset(this->preset)) { call.set_preset(this->preset); } - if (traits.get_supports_swing_modes()) { + if (traits.supports_swing_mode(this->swing_mode)) { call.set_swing_mode(this->swing_mode); } return call; } + void ClimateDeviceRestoreState::apply(Climate *climate) { auto traits = climate->get_traits(); climate->mode = this->mode; - if (traits.get_supports_two_point_target_temperature()) { + if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | + CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) { climate->target_temperature_low = this->target_temperature_low; climate->target_temperature_high = this->target_temperature_high; } else { climate->target_temperature = this->target_temperature; } - if (traits.get_supports_target_humidity()) { + if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) { climate->target_humidity = this->target_humidity; } - if (traits.get_supports_fan_modes() && !this->uses_custom_fan_mode) { + if (this->uses_custom_fan_mode) { + if (this->custom_fan_mode < traits.get_supported_custom_fan_modes().size()) { + climate->fan_mode.reset(); + climate->custom_fan_mode_ = traits.get_supported_custom_fan_modes()[this->custom_fan_mode]; + } + } else if (traits.supports_fan_mode(this->fan_mode)) { climate->fan_mode = this->fan_mode; + climate->clear_custom_fan_mode_(); } - if (!traits.get_supported_custom_fan_modes().empty() && this->uses_custom_fan_mode) { - // std::set has consistent order (lexicographic for strings), so this is ok - const auto &modes = traits.get_supported_custom_fan_modes(); - std::vector modes_vec{modes.begin(), modes.end()}; - if (custom_fan_mode < modes_vec.size()) { - climate->custom_fan_mode = modes_vec[this->custom_fan_mode]; + if (this->uses_custom_preset) { + if (this->custom_preset < traits.get_supported_custom_presets().size()) { + climate->preset.reset(); + climate->custom_preset_ = traits.get_supported_custom_presets()[this->custom_preset]; } - } - if (traits.get_supports_presets() && !this->uses_custom_preset) { + } else if (traits.supports_preset(this->preset)) { climate->preset = this->preset; + climate->clear_custom_preset_(); } - if (!traits.get_supported_custom_presets().empty() && uses_custom_preset) { - // std::set has consistent order (lexicographic for strings), so this is ok - const auto &presets = traits.get_supported_custom_presets(); - std::vector presets_vec{presets.begin(), presets.end()}; - if (custom_preset < presets_vec.size()) { - climate->custom_preset = presets_vec[this->custom_preset]; - } - } - if (traits.get_supports_swing_modes()) { + if (traits.supports_swing_mode(this->swing_mode)) { climate->swing_mode = this->swing_mode; } climate->publish_state(); } -template bool set_alternative(optional &dst, optional &alt, const T1 &src) { - bool is_changed = alt.has_value(); - alt.reset(); - if (is_changed || dst != src) { - dst = src; - is_changed = true; +/** Template helper for setting primary modes (fan_mode, preset) with mutual exclusion. + * + * Climate devices have mutually exclusive mode pairs: + * - fan_mode (enum) vs custom_fan_mode_ (const char*) + * - preset (enum) vs custom_preset_ (const char*) + * + * Only one mode in each pair can be active at a time. This helper ensures setting a primary + * mode automatically clears its corresponding custom mode. + * + * Example state transitions: + * Before: custom_fan_mode_="Turbo", fan_mode=nullopt + * Call: set_fan_mode_(CLIMATE_FAN_HIGH) + * After: custom_fan_mode_=nullptr, fan_mode=CLIMATE_FAN_HIGH + * + * @param primary The primary mode optional (fan_mode or preset) + * @param custom_ptr Reference to the custom mode pointer (custom_fan_mode_ or custom_preset_) + * @param value The new primary mode value to set + * @return true if state changed, false if already set to this value + */ +template bool set_primary_mode(optional &primary, const char *&custom_ptr, T value) { + // Clear the custom mode (mutual exclusion) + bool changed = custom_ptr != nullptr; + custom_ptr = nullptr; + // Set the primary mode + if (changed || !primary.has_value() || primary.value() != value) { + primary = value; + return true; } - return is_changed; + return false; +} + +/** Template helper for setting custom modes (custom_fan_mode_, custom_preset_) with mutual exclusion. + * + * This helper ensures setting a custom mode automatically clears its corresponding primary mode. + * It also validates that the custom mode exists in the device's supported modes (lifetime safety). + * + * Example state transitions: + * Before: fan_mode=CLIMATE_FAN_HIGH, custom_fan_mode_=nullptr + * Call: set_custom_fan_mode_("Turbo") + * After: fan_mode=nullopt, custom_fan_mode_="Turbo" (pointer from traits) + * + * Lifetime Safety: + * - found_ptr must come from traits.find_custom_*_mode_() + * - Only pointers found in traits are stored, ensuring they remain valid + * - Prevents dangling pointers from temporary strings + * + * @param custom_ptr Reference to the custom mode pointer to set + * @param primary The primary mode optional to clear + * @param found_ptr The validated pointer from traits (nullptr if not found) + * @param has_custom Whether a custom mode is currently active + * @return true if state changed, false otherwise + */ +template +bool set_custom_mode(const char *&custom_ptr, optional &primary, const char *found_ptr, bool has_custom) { + if (found_ptr != nullptr) { + // Clear the primary mode (mutual exclusion) + bool changed = primary.has_value(); + primary.reset(); + // Set the custom mode (pointer is validated by caller from traits) + if (changed || custom_ptr != found_ptr) { + custom_ptr = found_ptr; + return true; + } + return false; + } + // Mode not found in supported modes, clear it if currently set + if (has_custom) { + custom_ptr = nullptr; + return true; + } + return false; } bool Climate::set_fan_mode_(ClimateFanMode mode) { - return set_alternative(this->fan_mode, this->custom_fan_mode, mode); + return set_primary_mode(this->fan_mode, this->custom_fan_mode_, mode); } -bool Climate::set_custom_fan_mode_(const std::string &mode) { - return set_alternative(this->custom_fan_mode, this->fan_mode, mode); +bool Climate::set_custom_fan_mode_(const char *mode) { + auto traits = this->get_traits(); + return set_custom_mode(this->custom_fan_mode_, this->fan_mode, traits.find_custom_fan_mode_(mode), + this->has_custom_fan_mode()); } -bool Climate::set_preset_(ClimatePreset preset) { return set_alternative(this->preset, this->custom_preset, preset); } +void Climate::clear_custom_fan_mode_() { this->custom_fan_mode_ = nullptr; } -bool Climate::set_custom_preset_(const std::string &preset) { - return set_alternative(this->custom_preset, this->preset, preset); +bool Climate::set_preset_(ClimatePreset preset) { return set_primary_mode(this->preset, this->custom_preset_, preset); } + +bool Climate::set_custom_preset_(const char *preset) { + auto traits = this->get_traits(); + return set_custom_mode(this->custom_preset_, this->preset, traits.find_custom_preset_(preset), + this->has_custom_preset()); +} + +void Climate::clear_custom_preset_() { this->custom_preset_ = nullptr; } + +const char *Climate::find_custom_fan_mode_(const char *custom_fan_mode) { + return this->get_traits().find_custom_fan_mode_(custom_fan_mode); +} + +const char *Climate::find_custom_preset_(const char *custom_preset) { + return this->get_traits().find_custom_preset_(custom_preset); } void Climate::dump_traits_(const char *tag) { auto traits = this->get_traits(); ESP_LOGCONFIG(tag, "ClimateTraits:"); ESP_LOGCONFIG(tag, - " [x] Visual settings:\n" - " - Min temperature: %.1f\n" - " - Max temperature: %.1f\n" - " - Temperature step:\n" - " Target: %.1f", + " Visual settings:\n" + " - Min temperature: %.1f\n" + " - Max temperature: %.1f\n" + " - Temperature step:\n" + " Target: %.1f", traits.get_visual_min_temperature(), traits.get_visual_max_temperature(), traits.get_visual_target_temperature_step()); - if (traits.get_supports_current_temperature()) { - ESP_LOGCONFIG(tag, " Current: %.1f", traits.get_visual_current_temperature_step()); + if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) { + ESP_LOGCONFIG(tag, " Current: %.1f", traits.get_visual_current_temperature_step()); } - if (traits.get_supports_target_humidity() || traits.get_supports_current_humidity()) { + if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY | + climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) { ESP_LOGCONFIG(tag, - " - Min humidity: %.0f\n" - " - Max humidity: %.0f", + " - Min humidity: %.0f\n" + " - Max humidity: %.0f", traits.get_visual_min_humidity(), traits.get_visual_max_humidity()); } - if (traits.get_supports_two_point_target_temperature()) { - ESP_LOGCONFIG(tag, " [x] Supports two-point target temperature"); + if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | + CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) { + ESP_LOGCONFIG(tag, " Supports two-point target temperature"); } - if (traits.get_supports_current_temperature()) { - ESP_LOGCONFIG(tag, " [x] Supports current temperature"); + if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) { + ESP_LOGCONFIG(tag, " Supports current temperature"); } - if (traits.get_supports_target_humidity()) { - ESP_LOGCONFIG(tag, " [x] Supports target humidity"); + if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) { + ESP_LOGCONFIG(tag, " Supports target humidity"); } - if (traits.get_supports_current_humidity()) { - ESP_LOGCONFIG(tag, " [x] Supports current humidity"); + if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) { + ESP_LOGCONFIG(tag, " Supports current humidity"); } - if (traits.get_supports_action()) { - ESP_LOGCONFIG(tag, " [x] Supports action"); + if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) { + ESP_LOGCONFIG(tag, " Supports action"); } if (!traits.get_supported_modes().empty()) { - ESP_LOGCONFIG(tag, " [x] Supported modes:"); + ESP_LOGCONFIG(tag, " Supported modes:"); for (ClimateMode m : traits.get_supported_modes()) - ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_mode_to_string(m))); + ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_mode_to_string(m))); } if (!traits.get_supported_fan_modes().empty()) { - ESP_LOGCONFIG(tag, " [x] Supported fan modes:"); + ESP_LOGCONFIG(tag, " Supported fan modes:"); for (ClimateFanMode m : traits.get_supported_fan_modes()) - ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_fan_mode_to_string(m))); + ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_fan_mode_to_string(m))); } if (!traits.get_supported_custom_fan_modes().empty()) { - ESP_LOGCONFIG(tag, " [x] Supported custom fan modes:"); - for (const std::string &s : traits.get_supported_custom_fan_modes()) - ESP_LOGCONFIG(tag, " - %s", s.c_str()); + ESP_LOGCONFIG(tag, " Supported custom fan modes:"); + for (const char *s : traits.get_supported_custom_fan_modes()) + ESP_LOGCONFIG(tag, " - %s", s); } if (!traits.get_supported_presets().empty()) { - ESP_LOGCONFIG(tag, " [x] Supported presets:"); + ESP_LOGCONFIG(tag, " Supported presets:"); for (ClimatePreset p : traits.get_supported_presets()) - ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_preset_to_string(p))); + ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_preset_to_string(p))); } if (!traits.get_supported_custom_presets().empty()) { - ESP_LOGCONFIG(tag, " [x] Supported custom presets:"); - for (const std::string &s : traits.get_supported_custom_presets()) - ESP_LOGCONFIG(tag, " - %s", s.c_str()); + ESP_LOGCONFIG(tag, " Supported custom presets:"); + for (const char *s : traits.get_supported_custom_presets()) + ESP_LOGCONFIG(tag, " - %s", s); } if (!traits.get_supported_swing_modes().empty()) { - ESP_LOGCONFIG(tag, " [x] Supported swing modes:"); + ESP_LOGCONFIG(tag, " Supported swing modes:"); for (ClimateSwingMode m : traits.get_supported_swing_modes()) - ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_swing_mode_to_string(m))); + ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_swing_mode_to_string(m))); } } diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index b31a2eedf6..b277877c3e 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -33,6 +33,7 @@ class Climate; class ClimateCall { public: explicit ClimateCall(Climate *parent) : parent_(parent) {} + friend struct ClimateDeviceRestoreState; /// Set the mode of the climate device. ClimateCall &set_mode(ClimateMode mode); @@ -76,6 +77,8 @@ class ClimateCall { ClimateCall &set_fan_mode(const std::string &fan_mode); /// Set the fan mode of the climate device based on a string. ClimateCall &set_fan_mode(optional fan_mode); + /// Set the custom fan mode of the climate device. + ClimateCall &set_fan_mode(const char *custom_fan_mode); /// Set the swing mode of the climate device. ClimateCall &set_swing_mode(ClimateSwingMode swing_mode); /// Set the swing mode of the climate device. @@ -90,34 +93,41 @@ class ClimateCall { ClimateCall &set_preset(const std::string &preset); /// Set the preset of the climate device based on a string. ClimateCall &set_preset(optional preset); + /// Set the custom preset of the climate device. + ClimateCall &set_preset(const char *custom_preset); void perform(); - const optional &get_mode() const; const optional &get_target_temperature() const; const optional &get_target_temperature_low() const; const optional &get_target_temperature_high() const; const optional &get_target_humidity() const; + + const optional &get_mode() const; const optional &get_fan_mode() const; const optional &get_swing_mode() const; - const optional &get_custom_fan_mode() const; const optional &get_preset() const; - const optional &get_custom_preset() const; + const char *get_custom_fan_mode() const { return this->custom_fan_mode_; } + const char *get_custom_preset() const { return this->custom_preset_; } + bool has_custom_fan_mode() const { return this->custom_fan_mode_ != nullptr; } + bool has_custom_preset() const { return this->custom_preset_ != nullptr; } protected: void validate_(); Climate *const parent_; - optional mode_; optional target_temperature_; optional target_temperature_low_; optional target_temperature_high_; optional target_humidity_; + optional mode_; optional fan_mode_; optional swing_mode_; - optional custom_fan_mode_; optional preset_; - optional custom_preset_; + + private: + const char *custom_fan_mode_{nullptr}; + const char *custom_preset_{nullptr}; }; /// Struct used to save the state of the climate device in restore memory. @@ -169,47 +179,6 @@ class Climate : public EntityBase { public: Climate() {} - /// The active mode of the climate device. - ClimateMode mode{CLIMATE_MODE_OFF}; - - /// The active state of the climate device. - ClimateAction action{CLIMATE_ACTION_OFF}; - - /// The current temperature of the climate device, as reported from the integration. - float current_temperature{NAN}; - - /// The current humidity of the climate device, as reported from the integration. - float current_humidity{NAN}; - - union { - /// The target temperature of the climate device. - float target_temperature; - struct { - /// The minimum target temperature of the climate device, for climate devices with split target temperature. - float target_temperature_low{NAN}; - /// The maximum target temperature of the climate device, for climate devices with split target temperature. - float target_temperature_high{NAN}; - }; - }; - - /// The target humidity of the climate device. - float target_humidity; - - /// The active fan mode of the climate device. - optional fan_mode; - - /// The active swing mode of the climate device. - ClimateSwingMode swing_mode; - - /// The active custom fan mode of the climate device. - optional custom_fan_mode; - - /// The active preset of the climate device. - optional preset; - - /// The active custom preset mode of the climate device. - optional custom_preset; - /** Add a callback for the climate device state, each time the state of the climate device is updated * (using publish_state), this callback will be called. * @@ -251,20 +220,78 @@ class Climate : public EntityBase { void set_visual_min_humidity_override(float visual_min_humidity_override); void set_visual_max_humidity_override(float visual_max_humidity_override); + /// Check if a custom fan mode is currently active. + bool has_custom_fan_mode() const { return this->custom_fan_mode_ != nullptr; } + + /// Check if a custom preset is currently active. + bool has_custom_preset() const { return this->custom_preset_ != nullptr; } + + /// The current temperature of the climate device, as reported from the integration. + float current_temperature{NAN}; + + /// The current humidity of the climate device, as reported from the integration. + float current_humidity{NAN}; + + union { + /// The target temperature of the climate device. + float target_temperature; + struct { + /// The minimum target temperature of the climate device, for climate devices with split target temperature. + float target_temperature_low{NAN}; + /// The maximum target temperature of the climate device, for climate devices with split target temperature. + float target_temperature_high{NAN}; + }; + }; + + /// The target humidity of the climate device. + float target_humidity; + + /// The active fan mode of the climate device. + optional fan_mode; + + /// The active preset of the climate device. + optional preset; + + /// The active mode of the climate device. + ClimateMode mode{CLIMATE_MODE_OFF}; + + /// The active state of the climate device. + ClimateAction action{CLIMATE_ACTION_OFF}; + + /// The active swing mode of the climate device. + ClimateSwingMode swing_mode{CLIMATE_SWING_OFF}; + + /// Get the active custom fan mode (read-only access). + const char *get_custom_fan_mode() const { return this->custom_fan_mode_; } + + /// Get the active custom preset (read-only access). + const char *get_custom_preset() const { return this->custom_preset_; } + protected: friend ClimateCall; + friend struct ClimateDeviceRestoreState; /// Set fan mode. Reset custom fan mode. Return true if fan mode has been changed. bool set_fan_mode_(ClimateFanMode mode); /// Set custom fan mode. Reset primary fan mode. Return true if fan mode has been changed. - bool set_custom_fan_mode_(const std::string &mode); + bool set_custom_fan_mode_(const char *mode); + /// Clear custom fan mode. + void clear_custom_fan_mode_(); /// Set preset. Reset custom preset. Return true if preset has been changed. bool set_preset_(ClimatePreset preset); /// Set custom preset. Reset primary preset. Return true if preset has been changed. - bool set_custom_preset_(const std::string &preset); + bool set_custom_preset_(const char *preset); + /// Clear custom preset. + void clear_custom_preset_(); + + /// Find and return the matching custom fan mode pointer from traits, or nullptr if not found. + const char *find_custom_fan_mode_(const char *custom_fan_mode); + + /// Find and return the matching custom preset pointer from traits, or nullptr if not found. + const char *find_custom_preset_(const char *custom_preset); /** Get the default traits of this climate device. * @@ -301,6 +328,21 @@ class Climate : public EntityBase { optional visual_current_temperature_step_override_{}; optional visual_min_humidity_override_{}; optional visual_max_humidity_override_{}; + + private: + /** The active custom fan mode (private - enforces use of safe setters). + * + * Points to an entry in traits.supported_custom_fan_modes_ or nullptr. + * Use get_custom_fan_mode() to read, set_custom_fan_mode_() to modify. + */ + const char *custom_fan_mode_{nullptr}; + + /** The active custom preset (private - enforces use of safe setters). + * + * Points to an entry in traits.supported_custom_presets_ or nullptr. + * Use get_custom_preset() to read, set_custom_preset_() to modify. + */ + const char *custom_preset_{nullptr}; }; } // namespace climate diff --git a/esphome/components/climate/climate_mode.h b/esphome/components/climate/climate_mode.h index 80efb4c048..44423d2f22 100644 --- a/esphome/components/climate/climate_mode.h +++ b/esphome/components/climate/climate_mode.h @@ -7,6 +7,7 @@ namespace esphome { namespace climate { /// Enum for all modes a climate device can be in. +/// NOTE: If adding values, update ClimateModeMask in climate_traits.h to use the new last value enum ClimateMode : uint8_t { /// The climate device is off CLIMATE_MODE_OFF = 0, @@ -24,7 +25,7 @@ enum ClimateMode : uint8_t { * For example, the target temperature can be adjusted based on a schedule, or learned behavior. * The target temperature can't be adjusted when in this mode. */ - CLIMATE_MODE_AUTO = 6 + CLIMATE_MODE_AUTO = 6 // Update ClimateModeMask in climate_traits.h if adding values after this }; /// Enum for the current action of the climate device. Values match those of ClimateMode. @@ -43,6 +44,7 @@ enum ClimateAction : uint8_t { CLIMATE_ACTION_FAN = 6, }; +/// NOTE: If adding values, update ClimateFanModeMask in climate_traits.h to use the new last value enum ClimateFanMode : uint8_t { /// The fan mode is set to On CLIMATE_FAN_ON = 0, @@ -63,10 +65,11 @@ enum ClimateFanMode : uint8_t { /// The fan mode is set to Diffuse CLIMATE_FAN_DIFFUSE = 8, /// The fan mode is set to Quiet - CLIMATE_FAN_QUIET = 9, + CLIMATE_FAN_QUIET = 9, // Update ClimateFanModeMask in climate_traits.h if adding values after this }; /// Enum for all modes a climate swing can be in +/// NOTE: If adding values, update ClimateSwingModeMask in climate_traits.h to use the new last value enum ClimateSwingMode : uint8_t { /// The swing mode is set to Off CLIMATE_SWING_OFF = 0, @@ -75,10 +78,11 @@ enum ClimateSwingMode : uint8_t { /// The fan mode is set to Vertical CLIMATE_SWING_VERTICAL = 2, /// The fan mode is set to Horizontal - CLIMATE_SWING_HORIZONTAL = 3, + CLIMATE_SWING_HORIZONTAL = 3, // Update ClimateSwingModeMask in climate_traits.h if adding values after this }; /// Enum for all preset modes +/// NOTE: If adding values, update ClimatePresetMask in climate_traits.h to use the new last value enum ClimatePreset : uint8_t { /// No preset is active CLIMATE_PRESET_NONE = 0, @@ -95,7 +99,22 @@ enum ClimatePreset : uint8_t { /// Device is prepared for sleep CLIMATE_PRESET_SLEEP = 6, /// Device is reacting to activity (e.g., movement sensors) - CLIMATE_PRESET_ACTIVITY = 7, + CLIMATE_PRESET_ACTIVITY = 7, // Update ClimatePresetMask in climate_traits.h if adding values after this +}; + +enum ClimateFeature : uint32_t { + // Reporting current temperature is supported + CLIMATE_SUPPORTS_CURRENT_TEMPERATURE = 1 << 0, + // Setting two target temperatures is supported (used in conjunction with CLIMATE_MODE_HEAT_COOL) + CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE = 1 << 1, + // Single-point mode is NOT supported (UI always displays two handles, setting 'target_temperature' is not supported) + CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE = 1 << 2, + // Reporting current humidity is supported + CLIMATE_SUPPORTS_CURRENT_HUMIDITY = 1 << 3, + // Setting a target humidity is supported + CLIMATE_SUPPORTS_TARGET_HUMIDITY = 1 << 4, + // Reporting current climate action is supported + CLIMATE_SUPPORTS_ACTION = 1 << 5, }; /// Convert the given ClimateMode to a human-readable string. diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 8bd4714753..0eecf9789f 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -1,19 +1,43 @@ #pragma once -#include "esphome/core/helpers.h" +#include +#include #include "climate_mode.h" -#include +#include "esphome/core/finite_set_mask.h" +#include "esphome/core/helpers.h" namespace esphome { - -#ifdef USE_API -namespace api { -class APIConnection; -} // namespace api -#endif - namespace climate { +// Type aliases for climate enum bitmasks +// These replace std::set to eliminate red-black tree overhead +// For contiguous enums starting at 0, DefaultBitPolicy provides 1:1 mapping (enum value = bit position) +// Bitmask size is automatically calculated from the last enum value +using ClimateModeMask = FiniteSetMask>; +using ClimateFanModeMask = FiniteSetMask>; +using ClimateSwingModeMask = + FiniteSetMask>; +using ClimatePresetMask = FiniteSetMask>; + +// Lightweight linear search for small vectors (1-20 items) of const char* pointers +// Avoids std::find template overhead +inline bool vector_contains(const std::vector &vec, const char *value) { + for (const char *item : vec) { + if (strcmp(item, value) == 0) + return true; + } + return false; +} + +// Find and return matching pointer from vector, or nullptr if not found +inline const char *vector_find(const std::vector &vec, const char *value) { + for (const char *item : vec) { + if (strcmp(item, value) == 0) + return item; + } + return nullptr; +} + /** This class contains all static data for climate devices. * * All climate devices must support these features: @@ -21,135 +45,164 @@ namespace climate { * - Target Temperature * * All other properties and modes are optional and the integration must mark - * each of them as supported by setting the appropriate flag here. + * each of them as supported by setting the appropriate flag(s) here. * - * - supports current temperature - if the climate device supports reporting a current temperature - * - supports two point target temperature - if the climate device's target temperature should be - * split in target_temperature_low and target_temperature_high instead of just the single target_temperature + * - feature flags: see ClimateFeatures enum in climate_mode.h * - supports modes: * - auto mode (automatic control) * - cool mode (lowers current temperature) * - heat mode (increases current temperature) * - dry mode (removes humidity from air) * - fan mode (only turns on fan) - * - supports action - if the climate device supports reporting the active - * current action of the device with the action property. * - supports fan modes - optionally, if it has a fan which can be configured in different ways: * - on, off, auto, high, medium, low, middle, focus, diffuse, quiet * - supports swing modes - optionally, if it has a swing which can be configured in different ways: * - off, both, vertical, horizontal * * This class also contains static data for the climate device display: - * - visual min/max temperature - tells the frontend what range of temperatures the climate device - * should display (gauge min/max values) + * - visual min/max temperature/humidity - tells the frontend what range of temperature/humidity the + * climate device should display (gauge min/max values) * - temperature step - the step with which to increase/decrease target temperature. * This also affects with how many decimal places the temperature is shown */ +class Climate; // Forward declaration + class ClimateTraits { + friend class Climate; // Allow Climate to access protected find methods + public: - bool get_supports_current_temperature() const { return this->supports_current_temperature_; } + /// Get/set feature flags (see ClimateFeatures enum in climate_mode.h) + uint32_t get_feature_flags() const { return this->feature_flags_; } + void add_feature_flags(uint32_t feature_flags) { this->feature_flags_ |= feature_flags; } + void clear_feature_flags(uint32_t feature_flags) { this->feature_flags_ &= ~feature_flags; } + bool has_feature_flags(uint32_t feature_flags) const { return this->feature_flags_ & feature_flags; } + void set_feature_flags(uint32_t feature_flags) { this->feature_flags_ = feature_flags; } + + ESPDEPRECATED("This method is deprecated, use get_feature_flags() instead", "2025.11.0") + bool get_supports_current_temperature() const { + return this->has_feature_flags(CLIMATE_SUPPORTS_CURRENT_TEMPERATURE); + } + ESPDEPRECATED("This method is deprecated, use add_feature_flags() instead", "2025.11.0") void set_supports_current_temperature(bool supports_current_temperature) { - this->supports_current_temperature_ = supports_current_temperature; + if (supports_current_temperature) { + this->add_feature_flags(CLIMATE_SUPPORTS_CURRENT_TEMPERATURE); + } else { + this->clear_feature_flags(CLIMATE_SUPPORTS_CURRENT_TEMPERATURE); + } } - bool get_supports_current_humidity() const { return this->supports_current_humidity_; } + ESPDEPRECATED("This method is deprecated, use get_feature_flags() instead", "2025.11.0") + bool get_supports_current_humidity() const { return this->has_feature_flags(CLIMATE_SUPPORTS_CURRENT_HUMIDITY); } + ESPDEPRECATED("This method is deprecated, use add_feature_flags() instead", "2025.11.0") void set_supports_current_humidity(bool supports_current_humidity) { - this->supports_current_humidity_ = supports_current_humidity; + if (supports_current_humidity) { + this->add_feature_flags(CLIMATE_SUPPORTS_CURRENT_HUMIDITY); + } else { + this->clear_feature_flags(CLIMATE_SUPPORTS_CURRENT_HUMIDITY); + } } - bool get_supports_two_point_target_temperature() const { return this->supports_two_point_target_temperature_; } + ESPDEPRECATED("This method is deprecated, use get_feature_flags() instead", "2025.11.0") + bool get_supports_two_point_target_temperature() const { + return this->has_feature_flags(CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE); + } + ESPDEPRECATED("This method is deprecated, use add_feature_flags() instead", "2025.11.0") void set_supports_two_point_target_temperature(bool supports_two_point_target_temperature) { - this->supports_two_point_target_temperature_ = supports_two_point_target_temperature; + if (supports_two_point_target_temperature) + // Use CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE to mimic previous behavior + { + this->add_feature_flags(CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE); + } else { + this->clear_feature_flags(CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE); + } } - bool get_supports_target_humidity() const { return this->supports_target_humidity_; } + ESPDEPRECATED("This method is deprecated, use get_feature_flags() instead", "2025.11.0") + bool get_supports_target_humidity() const { return this->has_feature_flags(CLIMATE_SUPPORTS_TARGET_HUMIDITY); } + ESPDEPRECATED("This method is deprecated, use add_feature_flags() instead", "2025.11.0") void set_supports_target_humidity(bool supports_target_humidity) { - this->supports_target_humidity_ = supports_target_humidity; + if (supports_target_humidity) { + this->add_feature_flags(CLIMATE_SUPPORTS_TARGET_HUMIDITY); + } else { + this->clear_feature_flags(CLIMATE_SUPPORTS_TARGET_HUMIDITY); + } } - void set_supported_modes(std::set modes) { this->supported_modes_ = std::move(modes); } + ESPDEPRECATED("This method is deprecated, use get_feature_flags() instead", "2025.11.0") + bool get_supports_action() const { return this->has_feature_flags(CLIMATE_SUPPORTS_ACTION); } + ESPDEPRECATED("This method is deprecated, use add_feature_flags() instead", "2025.11.0") + void set_supports_action(bool supports_action) { + if (supports_action) { + this->add_feature_flags(CLIMATE_SUPPORTS_ACTION); + } else { + this->clear_feature_flags(CLIMATE_SUPPORTS_ACTION); + } + } + + void set_supported_modes(ClimateModeMask modes) { this->supported_modes_ = modes; } void add_supported_mode(ClimateMode mode) { this->supported_modes_.insert(mode); } - ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20") - void set_supports_auto_mode(bool supports_auto_mode) { set_mode_support_(CLIMATE_MODE_AUTO, supports_auto_mode); } - ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20") - void set_supports_cool_mode(bool supports_cool_mode) { set_mode_support_(CLIMATE_MODE_COOL, supports_cool_mode); } - ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20") - void set_supports_heat_mode(bool supports_heat_mode) { set_mode_support_(CLIMATE_MODE_HEAT, supports_heat_mode); } - ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20") - void set_supports_heat_cool_mode(bool supported) { set_mode_support_(CLIMATE_MODE_HEAT_COOL, supported); } - ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20") - void set_supports_fan_only_mode(bool supports_fan_only_mode) { - set_mode_support_(CLIMATE_MODE_FAN_ONLY, supports_fan_only_mode); - } - ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20") - void set_supports_dry_mode(bool supports_dry_mode) { set_mode_support_(CLIMATE_MODE_DRY, supports_dry_mode); } bool supports_mode(ClimateMode mode) const { return this->supported_modes_.count(mode); } - const std::set &get_supported_modes() const { return this->supported_modes_; } + const ClimateModeMask &get_supported_modes() const { return this->supported_modes_; } - void set_supports_action(bool supports_action) { this->supports_action_ = supports_action; } - bool get_supports_action() const { return this->supports_action_; } - - void set_supported_fan_modes(std::set modes) { this->supported_fan_modes_ = std::move(modes); } + void set_supported_fan_modes(ClimateFanModeMask modes) { this->supported_fan_modes_ = modes; } void add_supported_fan_mode(ClimateFanMode mode) { this->supported_fan_modes_.insert(mode); } - void add_supported_custom_fan_mode(const std::string &mode) { this->supported_custom_fan_modes_.insert(mode); } - ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20") - void set_supports_fan_mode_on(bool supported) { set_fan_mode_support_(CLIMATE_FAN_ON, supported); } - ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20") - void set_supports_fan_mode_off(bool supported) { set_fan_mode_support_(CLIMATE_FAN_OFF, supported); } - ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20") - void set_supports_fan_mode_auto(bool supported) { set_fan_mode_support_(CLIMATE_FAN_AUTO, supported); } - ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20") - void set_supports_fan_mode_low(bool supported) { set_fan_mode_support_(CLIMATE_FAN_LOW, supported); } - ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20") - void set_supports_fan_mode_medium(bool supported) { set_fan_mode_support_(CLIMATE_FAN_MEDIUM, supported); } - ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20") - void set_supports_fan_mode_high(bool supported) { set_fan_mode_support_(CLIMATE_FAN_HIGH, supported); } - ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20") - void set_supports_fan_mode_middle(bool supported) { set_fan_mode_support_(CLIMATE_FAN_MIDDLE, supported); } - ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20") - void set_supports_fan_mode_focus(bool supported) { set_fan_mode_support_(CLIMATE_FAN_FOCUS, supported); } - ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20") - void set_supports_fan_mode_diffuse(bool supported) { set_fan_mode_support_(CLIMATE_FAN_DIFFUSE, supported); } bool supports_fan_mode(ClimateFanMode fan_mode) const { return this->supported_fan_modes_.count(fan_mode); } bool get_supports_fan_modes() const { return !this->supported_fan_modes_.empty() || !this->supported_custom_fan_modes_.empty(); } - const std::set &get_supported_fan_modes() const { return this->supported_fan_modes_; } + const ClimateFanModeMask &get_supported_fan_modes() const { return this->supported_fan_modes_; } - void set_supported_custom_fan_modes(std::set supported_custom_fan_modes) { - this->supported_custom_fan_modes_ = std::move(supported_custom_fan_modes); + void set_supported_custom_fan_modes(std::initializer_list modes) { + this->supported_custom_fan_modes_ = modes; + } + void set_supported_custom_fan_modes(const std::vector &modes) { + this->supported_custom_fan_modes_ = modes; + } + template void set_supported_custom_fan_modes(const char *const (&modes)[N]) { + this->supported_custom_fan_modes_.assign(modes, modes + N); + } + + // Deleted overloads to catch incorrect std::string usage at compile time with clear error messages + void set_supported_custom_fan_modes(const std::vector &modes) = delete; + void set_supported_custom_fan_modes(std::initializer_list modes) = delete; + + const std::vector &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; } + bool supports_custom_fan_mode(const char *custom_fan_mode) const { + return vector_contains(this->supported_custom_fan_modes_, custom_fan_mode); } - const std::set &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; } bool supports_custom_fan_mode(const std::string &custom_fan_mode) const { - return this->supported_custom_fan_modes_.count(custom_fan_mode); + return this->supports_custom_fan_mode(custom_fan_mode.c_str()); } - void set_supported_presets(std::set presets) { this->supported_presets_ = std::move(presets); } + void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; } void add_supported_preset(ClimatePreset preset) { this->supported_presets_.insert(preset); } - void add_supported_custom_preset(const std::string &preset) { this->supported_custom_presets_.insert(preset); } bool supports_preset(ClimatePreset preset) const { return this->supported_presets_.count(preset); } bool get_supports_presets() const { return !this->supported_presets_.empty(); } - const std::set &get_supported_presets() const { return this->supported_presets_; } + const ClimatePresetMask &get_supported_presets() const { return this->supported_presets_; } - void set_supported_custom_presets(std::set supported_custom_presets) { - this->supported_custom_presets_ = std::move(supported_custom_presets); + void set_supported_custom_presets(std::initializer_list presets) { + this->supported_custom_presets_ = presets; + } + void set_supported_custom_presets(const std::vector &presets) { + this->supported_custom_presets_ = presets; + } + template void set_supported_custom_presets(const char *const (&presets)[N]) { + this->supported_custom_presets_.assign(presets, presets + N); + } + + // Deleted overloads to catch incorrect std::string usage at compile time with clear error messages + void set_supported_custom_presets(const std::vector &presets) = delete; + void set_supported_custom_presets(std::initializer_list presets) = delete; + + const std::vector &get_supported_custom_presets() const { return this->supported_custom_presets_; } + bool supports_custom_preset(const char *custom_preset) const { + return vector_contains(this->supported_custom_presets_, custom_preset); } - const std::set &get_supported_custom_presets() const { return this->supported_custom_presets_; } bool supports_custom_preset(const std::string &custom_preset) const { - return this->supported_custom_presets_.count(custom_preset); + return this->supports_custom_preset(custom_preset.c_str()); } - void set_supported_swing_modes(std::set modes) { this->supported_swing_modes_ = std::move(modes); } + void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; } void add_supported_swing_mode(ClimateSwingMode mode) { this->supported_swing_modes_.insert(mode); } - ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead", "v1.20") - void set_supports_swing_mode_off(bool supported) { set_swing_mode_support_(CLIMATE_SWING_OFF, supported); } - ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead", "v1.20") - void set_supports_swing_mode_both(bool supported) { set_swing_mode_support_(CLIMATE_SWING_BOTH, supported); } - ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead", "v1.20") - void set_supports_swing_mode_vertical(bool supported) { set_swing_mode_support_(CLIMATE_SWING_VERTICAL, supported); } - ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead", "v1.20") - void set_supports_swing_mode_horizontal(bool supported) { - set_swing_mode_support_(CLIMATE_SWING_HORIZONTAL, supported); - } bool supports_swing_mode(ClimateSwingMode swing_mode) const { return this->supported_swing_modes_.count(swing_mode); } bool get_supports_swing_modes() const { return !this->supported_swing_modes_.empty(); } - const std::set &get_supported_swing_modes() const { return this->supported_swing_modes_; } + const ClimateSwingModeMask &get_supported_swing_modes() const { return this->supported_swing_modes_; } float get_visual_min_temperature() const { return this->visual_min_temperature_; } void set_visual_min_temperature(float visual_min_temperature) { @@ -180,23 +233,6 @@ class ClimateTraits { void set_visual_max_humidity(float visual_max_humidity) { this->visual_max_humidity_ = visual_max_humidity; } protected: -#ifdef USE_API - // The API connection is a friend class to access internal methods - friend class api::APIConnection; - // These methods return references to internal data structures. - // They are used by the API to avoid copying data when encoding messages. - // Warning: Do not use these methods outside of the API connection code. - // They return references to internal data that can be invalidated. - const std::set &get_supported_modes_for_api_() const { return this->supported_modes_; } - const std::set &get_supported_fan_modes_for_api_() const { return this->supported_fan_modes_; } - const std::set &get_supported_custom_fan_modes_for_api_() const { - return this->supported_custom_fan_modes_; - } - const std::set &get_supported_presets_for_api_() const { return this->supported_presets_; } - const std::set &get_supported_custom_presets_for_api_() const { return this->supported_custom_presets_; } - const std::set &get_supported_swing_modes_for_api_() const { return this->supported_swing_modes_; } -#endif - void set_mode_support_(climate::ClimateMode mode, bool supported) { if (supported) { this->supported_modes_.insert(mode); @@ -219,24 +255,41 @@ class ClimateTraits { } } - bool supports_current_temperature_{false}; - bool supports_current_humidity_{false}; - bool supports_two_point_target_temperature_{false}; - bool supports_target_humidity_{false}; - std::set supported_modes_ = {climate::CLIMATE_MODE_OFF}; - bool supports_action_{false}; - std::set supported_fan_modes_; - std::set supported_swing_modes_; - std::set supported_presets_; - std::set supported_custom_fan_modes_; - std::set supported_custom_presets_; + /// Find and return the matching custom fan mode pointer from supported modes, or nullptr if not found + /// This is protected as it's an implementation detail - use Climate::find_custom_fan_mode_() instead + const char *find_custom_fan_mode_(const char *custom_fan_mode) const { + return vector_find(this->supported_custom_fan_modes_, custom_fan_mode); + } + /// Find and return the matching custom preset pointer from supported presets, or nullptr if not found + /// This is protected as it's an implementation detail - use Climate::find_custom_preset_() instead + const char *find_custom_preset_(const char *custom_preset) const { + return vector_find(this->supported_custom_presets_, custom_preset); + } + + uint32_t feature_flags_{0}; float visual_min_temperature_{10}; float visual_max_temperature_{30}; float visual_target_temperature_step_{0.1}; float visual_current_temperature_step_{0.1}; float visual_min_humidity_{30}; float visual_max_humidity_{99}; + + climate::ClimateModeMask supported_modes_{climate::CLIMATE_MODE_OFF}; + climate::ClimateFanModeMask supported_fan_modes_; + climate::ClimateSwingModeMask supported_swing_modes_; + climate::ClimatePresetMask supported_presets_; + + /** Custom mode storage using const char* pointers to eliminate std::string overhead. + * + * Pointers must remain valid for the ClimateTraits lifetime. Safe patterns: + * - String literals: set_supported_custom_fan_modes({"Turbo", "Silent"}) + * - Static const data: static const char* MODE = "Eco"; + * + * Climate class setters validate pointers are from these vectors before storing. + */ + std::vector supported_custom_fan_modes_; + std::vector supported_custom_presets_; }; } // namespace climate diff --git a/esphome/components/climate_ir/__init__.py b/esphome/components/climate_ir/__init__.py index 312b2ad900..5315be3db6 100644 --- a/esphome/components/climate_ir/__init__.py +++ b/esphome/components/climate_ir/__init__.py @@ -1,10 +1,14 @@ import logging -from esphome import core import esphome.codegen as cg from esphome.components import climate, remote_base, sensor import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT +from esphome.const import ( + CONF_HUMIDITY_SENSOR, + CONF_SENSOR, + CONF_SUPPORTS_COOL, + CONF_SUPPORTS_HEAT, +) from esphome.cpp_generator import MockObjClass _LOGGER = logging.getLogger(__name__) @@ -33,6 +37,7 @@ def climate_ir_schema( cv.Optional(CONF_SUPPORTS_COOL, default=True): cv.boolean, cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean, cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor), + cv.Optional(CONF_HUMIDITY_SENSOR): cv.use_id(sensor.Sensor), } ) .extend(cv.COMPONENT_SCHEMA) @@ -52,26 +57,6 @@ def climate_ir_with_receiver_schema( ) -# Remove before 2025.11.0 -def deprecated_schema_constant(config): - type: str = "unknown" - if (id := config.get(CONF_ID)) is not None and isinstance(id, core.ID): - type = str(id.type).split("::", maxsplit=1)[0] - _LOGGER.warning( - "Using `climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA` is deprecated and will be removed in ESPHome 2025.11.0. " - "Please use `climate_ir.climate_ir_with_receiver_schema(...)` instead. " - "If you are seeing this, report an issue to the external_component author and ask them to update it. " - "https://developers.esphome.io/blog/2025/05/14/_schema-deprecations/. " - "Component using this schema: %s", - type, - ) - return config - - -CLIMATE_IR_WITH_RECEIVER_SCHEMA = climate_ir_with_receiver_schema(ClimateIR) -CLIMATE_IR_WITH_RECEIVER_SCHEMA.add_extra(deprecated_schema_constant) - - async def register_climate_ir(var, config): await cg.register_component(var, config) await remote_base.register_transmittable(var, config) @@ -82,6 +67,9 @@ async def register_climate_ir(var, config): if sensor_id := config.get(CONF_SENSOR): sens = await cg.get_variable(sensor_id) cg.add(var.set_sensor(sens)) + if sensor_id := config.get(CONF_HUMIDITY_SENSOR): + sens = await cg.get_variable(sensor_id) + cg.add(var.set_humidity_sensor(sens)) async def new_climate_ir(config, *args): diff --git a/esphome/components/climate_ir/climate_ir.cpp b/esphome/components/climate_ir/climate_ir.cpp index dc8117f6ae..50c8d459b0 100644 --- a/esphome/components/climate_ir/climate_ir.cpp +++ b/esphome/components/climate_ir/climate_ir.cpp @@ -8,7 +8,12 @@ static const char *const TAG = "climate_ir"; climate::ClimateTraits ClimateIR::traits() { auto traits = climate::ClimateTraits(); - traits.set_supports_current_temperature(this->sensor_ != nullptr); + if (this->sensor_ != nullptr) { + traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE); + } + if (this->humidity_sensor_ != nullptr) { + traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY); + } traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_HEAT_COOL}); if (this->supports_cool_) traits.add_supported_mode(climate::CLIMATE_MODE_COOL); @@ -19,7 +24,6 @@ climate::ClimateTraits ClimateIR::traits() { if (this->supports_fan_only_) traits.add_supported_mode(climate::CLIMATE_MODE_FAN_ONLY); - traits.set_supports_two_point_target_temperature(false); traits.set_visual_min_temperature(this->minimum_temperature_); traits.set_visual_max_temperature(this->maximum_temperature_); traits.set_visual_temperature_step(this->temperature_step_); @@ -37,9 +41,16 @@ void ClimateIR::setup() { this->publish_state(); }); this->current_temperature = this->sensor_->state; - } else { - this->current_temperature = NAN; } + if (this->humidity_sensor_ != nullptr) { + this->humidity_sensor_->add_on_state_callback([this](float state) { + this->current_humidity = state; + // current humidity changed, publish state + this->publish_state(); + }); + this->current_humidity = this->humidity_sensor_->state; + } + // restore set points auto restore = this->restore_state_(); if (restore.has_value()) { diff --git a/esphome/components/climate_ir/climate_ir.h b/esphome/components/climate_ir/climate_ir.h index ea0656121f..ac76d33853 100644 --- a/esphome/components/climate_ir/climate_ir.h +++ b/esphome/components/climate_ir/climate_ir.h @@ -24,16 +24,18 @@ class ClimateIR : public Component, public remote_base::RemoteTransmittable { public: ClimateIR(float minimum_temperature, float maximum_temperature, float temperature_step = 1.0f, - bool supports_dry = false, bool supports_fan_only = false, std::set fan_modes = {}, - std::set swing_modes = {}, std::set presets = {}) { + bool supports_dry = false, bool supports_fan_only = false, + climate::ClimateFanModeMask fan_modes = climate::ClimateFanModeMask(), + climate::ClimateSwingModeMask swing_modes = climate::ClimateSwingModeMask(), + climate::ClimatePresetMask presets = climate::ClimatePresetMask()) { this->minimum_temperature_ = minimum_temperature; this->maximum_temperature_ = maximum_temperature; this->temperature_step_ = temperature_step; this->supports_dry_ = supports_dry; this->supports_fan_only_ = supports_fan_only; - this->fan_modes_ = std::move(fan_modes); - this->swing_modes_ = std::move(swing_modes); - this->presets_ = std::move(presets); + this->fan_modes_ = fan_modes; + this->swing_modes_ = swing_modes; + this->presets_ = presets; } void setup() override; @@ -41,6 +43,7 @@ class ClimateIR : public Component, void set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } void set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; } void set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } + void set_humidity_sensor(sensor::Sensor *sensor) { this->humidity_sensor_ = sensor; } protected: float minimum_temperature_, maximum_temperature_, temperature_step_; @@ -60,11 +63,12 @@ class ClimateIR : public Component, bool supports_heat_{true}; bool supports_dry_{false}; bool supports_fan_only_{false}; - std::set fan_modes_ = {}; - std::set swing_modes_ = {}; - std::set presets_ = {}; + climate::ClimateFanModeMask fan_modes_{}; + climate::ClimateSwingModeMask swing_modes_{}; + climate::ClimatePresetMask presets_{}; sensor::Sensor *sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; }; } // namespace climate_ir diff --git a/esphome/components/cm1106/cm1106.cpp b/esphome/components/cm1106/cm1106.cpp index 339a1659ac..d88ea2e1da 100644 --- a/esphome/components/cm1106/cm1106.cpp +++ b/esphome/components/cm1106/cm1106.cpp @@ -13,7 +13,7 @@ static const uint8_t C_M1106_CMD_SET_CO2_CALIB_RESPONSE[4] = {0x16, 0x01, 0x03, uint8_t cm1106_checksum(const uint8_t *response, size_t len) { uint8_t crc = 0; - for (int i = 0; i < len - 1; i++) { + for (size_t i = 0; i < len - 1; i++) { crc -= response[i]; } return crc; diff --git a/esphome/components/cm1106/cm1106.h b/esphome/components/cm1106/cm1106.h index 3b78e17cf4..ad089bbe7d 100644 --- a/esphome/components/cm1106/cm1106.h +++ b/esphome/components/cm1106/cm1106.h @@ -30,7 +30,7 @@ template class CM1106CalibrateZeroAction : public Action public: CM1106CalibrateZeroAction(CM1106Component *cm1106) : cm1106_(cm1106) {} - void play(Ts... x) override { this->cm1106_->calibrate_zero(400); } + void play(const Ts &...x) override { this->cm1106_->calibrate_zero(400); } protected: CM1106Component *cm1106_; diff --git a/esphome/components/const/__init__.py b/esphome/components/const/__init__.py index 19924f0da7..12a69551f5 100644 --- a/esphome/components/const/__init__.py +++ b/esphome/components/const/__init__.py @@ -8,7 +8,10 @@ BYTE_ORDER_BIG = "big_endian" CONF_COLOR_DEPTH = "color_depth" CONF_DRAW_ROUNDING = "draw_rounding" +CONF_ENABLED = "enabled" +CONF_IGNORE_NOT_FOUND = "ignore_not_found" CONF_ON_RECEIVE = "on_receive" CONF_ON_STATE_CHANGE = "on_state_change" CONF_REQUEST_HEADERS = "request_headers" +CONF_ROWS = "rows" CONF_USE_PSRAM = "use_psram" diff --git a/esphome/components/copy/fan/copy_fan.cpp b/esphome/components/copy/fan/copy_fan.cpp index 15a7f5e025..d35ece950b 100644 --- a/esphome/components/copy/fan/copy_fan.cpp +++ b/esphome/components/copy/fan/copy_fan.cpp @@ -12,7 +12,7 @@ void CopyFan::setup() { this->oscillating = source_->oscillating; this->speed = source_->speed; this->direction = source_->direction; - this->preset_mode = source_->preset_mode; + this->set_preset_mode_(source_->get_preset_mode()); this->publish_state(); }); @@ -20,7 +20,7 @@ void CopyFan::setup() { this->oscillating = source_->oscillating; this->speed = source_->speed; this->direction = source_->direction; - this->preset_mode = source_->preset_mode; + this->set_preset_mode_(source_->get_preset_mode()); this->publish_state(); } @@ -49,7 +49,7 @@ void CopyFan::control(const fan::FanCall &call) { call2.set_speed(*call.get_speed()); if (call.get_direction().has_value()) call2.set_direction(*call.get_direction()); - if (!call.get_preset_mode().empty()) + if (call.has_preset_mode()) call2.set_preset_mode(call.get_preset_mode()); call2.perform(); } diff --git a/esphome/components/copy/lock/copy_lock.cpp b/esphome/components/copy/lock/copy_lock.cpp index 67a8acffec..25bd8c33ef 100644 --- a/esphome/components/copy/lock/copy_lock.cpp +++ b/esphome/components/copy/lock/copy_lock.cpp @@ -11,7 +11,7 @@ void CopyLock::setup() { traits.set_assumed_state(source_->traits.get_assumed_state()); traits.set_requires_code(source_->traits.get_requires_code()); - traits.set_supported_states(source_->traits.get_supported_states()); + traits.set_supported_states_mask(source_->traits.get_supported_states_mask()); traits.set_supports_open(source_->traits.get_supports_open()); this->publish_state(source_->state); diff --git a/esphome/components/copy/select/copy_select.cpp b/esphome/components/copy/select/copy_select.cpp index bdcbd0b42c..e45338e785 100644 --- a/esphome/components/copy/select/copy_select.cpp +++ b/esphome/components/copy/select/copy_select.cpp @@ -7,19 +7,19 @@ namespace copy { static const char *const TAG = "copy.select"; void CopySelect::setup() { - source_->add_on_state_callback([this](const std::string &value, size_t index) { this->publish_state(value); }); + source_->add_on_state_callback([this](const std::string &value, size_t index) { this->publish_state(index); }); traits.set_options(source_->traits.get_options()); if (source_->has_state()) - this->publish_state(source_->state); + this->publish_state(source_->active_index().value()); } void CopySelect::dump_config() { LOG_SELECT("", "Copy Select", this); } -void CopySelect::control(const std::string &value) { +void CopySelect::control(size_t index) { auto call = source_->make_call(); - call.set_option(value); + call.set_index(index); call.perform(); } diff --git a/esphome/components/copy/select/copy_select.h b/esphome/components/copy/select/copy_select.h index fb0aee86f6..bd74a93e82 100644 --- a/esphome/components/copy/select/copy_select.h +++ b/esphome/components/copy/select/copy_select.h @@ -13,7 +13,7 @@ class CopySelect : public select::Select, public Component { void dump_config() override; protected: - void control(const std::string &value) override; + void control(size_t index) override; select::Select *source_; }; diff --git a/esphome/components/cover/__init__.py b/esphome/components/cover/__init__.py index 0e01eb336f..383daee083 100644 --- a/esphome/components/cover/__init__.py +++ b/esphome/components/cover/__init__.py @@ -32,7 +32,7 @@ from esphome.const import ( DEVICE_CLASS_SHUTTER, DEVICE_CLASS_WINDOW, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass @@ -151,11 +151,6 @@ def cover_schema( return _COVER_SCHEMA.extend(schema) -# Remove before 2025.11.0 -COVER_SCHEMA = cover_schema(Cover) -COVER_SCHEMA.add_extra(cv.deprecated_schema_constant("cover")) - - async def setup_cover_core_(var, config): await setup_entity(var, config, "cover") @@ -228,9 +223,9 @@ async def cover_stop_to_code(config, action_id, template_arg, args): @automation.register_action("cover.toggle", ToggleAction, COVER_ACTION_SCHEMA) -def cover_toggle_to_code(config, action_id, template_arg, args): - paren = yield cg.get_variable(config[CONF_ID]) - yield cg.new_Pvariable(action_id, template_arg, paren) +async def cover_toggle_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) COVER_CONTROL_ACTION_SCHEMA = cv.Schema( @@ -263,6 +258,6 @@ async def cover_control_to_code(config, action_id, template_arg, args): return var -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(cover_ns.using) diff --git a/esphome/components/cover/automation.h b/esphome/components/cover/automation.h index 6406ba52cb..c0345a7cc6 100644 --- a/esphome/components/cover/automation.h +++ b/esphome/components/cover/automation.h @@ -4,14 +4,13 @@ #include "esphome/core/automation.h" #include "cover.h" -namespace esphome { -namespace cover { +namespace esphome::cover { template class OpenAction : public Action { public: explicit OpenAction(Cover *cover) : cover_(cover) {} - void play(Ts... x) override { this->cover_->make_call().set_command_open().perform(); } + void play(const Ts &...x) override { this->cover_->make_call().set_command_open().perform(); } protected: Cover *cover_; @@ -21,7 +20,7 @@ template class CloseAction : public Action { public: explicit CloseAction(Cover *cover) : cover_(cover) {} - void play(Ts... x) override { this->cover_->make_call().set_command_close().perform(); } + void play(const Ts &...x) override { this->cover_->make_call().set_command_close().perform(); } protected: Cover *cover_; @@ -31,7 +30,7 @@ template class StopAction : public Action { public: explicit StopAction(Cover *cover) : cover_(cover) {} - void play(Ts... x) override { this->cover_->make_call().set_command_stop().perform(); } + void play(const Ts &...x) override { this->cover_->make_call().set_command_stop().perform(); } protected: Cover *cover_; @@ -41,7 +40,7 @@ template class ToggleAction : public Action { public: explicit ToggleAction(Cover *cover) : cover_(cover) {} - void play(Ts... x) override { this->cover_->make_call().set_command_toggle().perform(); } + void play(const Ts &...x) override { this->cover_->make_call().set_command_toggle().perform(); } protected: Cover *cover_; @@ -55,7 +54,7 @@ template class ControlAction : public Action { TEMPLATABLE_VALUE(float, position) TEMPLATABLE_VALUE(float, tilt) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->cover_->make_call(); if (this->stop_.has_value()) call.set_stop(this->stop_.value(x...)); @@ -77,7 +76,7 @@ template class CoverPublishAction : public Action { TEMPLATABLE_VALUE(float, tilt) TEMPLATABLE_VALUE(CoverOperation, current_operation) - void play(Ts... x) override { + void play(const Ts &...x) override { if (this->position_.has_value()) this->cover_->position = this->position_.value(x...); if (this->tilt_.has_value()) @@ -94,7 +93,7 @@ template class CoverPublishAction : public Action { template class CoverIsOpenCondition : public Condition { public: CoverIsOpenCondition(Cover *cover) : cover_(cover) {} - bool check(Ts... x) override { return this->cover_->is_fully_open(); } + bool check(const Ts &...x) override { return this->cover_->is_fully_open(); } protected: Cover *cover_; @@ -103,7 +102,7 @@ template class CoverIsOpenCondition : public Condition { template class CoverIsClosedCondition : public Condition { public: CoverIsClosedCondition(Cover *cover) : cover_(cover) {} - bool check(Ts... x) override { return this->cover_->is_fully_closed(); } + bool check(const Ts &...x) override { return this->cover_->is_fully_closed(); } protected: Cover *cover_; @@ -131,5 +130,4 @@ class CoverClosedTrigger : public Trigger<> { } }; -} // namespace cover -} // namespace esphome +} // namespace esphome::cover diff --git a/esphome/components/cover/cover.cpp b/esphome/components/cover/cover.cpp index d139bab8ee..8f735982f1 100644 --- a/esphome/components/cover/cover.cpp +++ b/esphome/components/cover/cover.cpp @@ -1,8 +1,12 @@ #include "cover.h" +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" + +#include + #include "esphome/core/log.h" -namespace esphome { -namespace cover { +namespace esphome::cover { static const char *const TAG = "cover"; @@ -99,43 +103,39 @@ const optional &CoverCall::get_tilt() const { return this->tilt_; } const optional &CoverCall::get_toggle() const { return this->toggle_; } void CoverCall::validate_() { auto traits = this->parent_->get_traits(); + const char *name = this->parent_->get_name().c_str(); + if (this->position_.has_value()) { auto pos = *this->position_; if (!traits.get_supports_position() && pos != COVER_OPEN && pos != COVER_CLOSED) { - ESP_LOGW(TAG, "'%s' - This cover device does not support setting position!", this->parent_->get_name().c_str()); + ESP_LOGW(TAG, "'%s': position unsupported", name); this->position_.reset(); } else if (pos < 0.0f || pos > 1.0f) { - ESP_LOGW(TAG, "'%s' - Position %.2f is out of range [0.0 - 1.0]", this->parent_->get_name().c_str(), pos); + ESP_LOGW(TAG, "'%s': position %.2f out of range", name, pos); this->position_ = clamp(pos, 0.0f, 1.0f); } } if (this->tilt_.has_value()) { auto tilt = *this->tilt_; if (!traits.get_supports_tilt()) { - ESP_LOGW(TAG, "'%s' - This cover device does not support tilt!", this->parent_->get_name().c_str()); + ESP_LOGW(TAG, "'%s': tilt unsupported", name); this->tilt_.reset(); } else if (tilt < 0.0f || tilt > 1.0f) { - ESP_LOGW(TAG, "'%s' - Tilt %.2f is out of range [0.0 - 1.0]", this->parent_->get_name().c_str(), tilt); + ESP_LOGW(TAG, "'%s': tilt %.2f out of range", name, tilt); this->tilt_ = clamp(tilt, 0.0f, 1.0f); } } if (this->toggle_.has_value()) { if (!traits.get_supports_toggle()) { - ESP_LOGW(TAG, "'%s' - This cover device does not support toggle!", this->parent_->get_name().c_str()); + ESP_LOGW(TAG, "'%s': toggle unsupported", name); this->toggle_.reset(); } } if (this->stop_) { - if (this->position_.has_value()) { - ESP_LOGW(TAG, "Cannot set position when stopping a cover!"); + if (this->position_.has_value() || this->tilt_.has_value() || this->toggle_.has_value()) { + ESP_LOGW(TAG, "'%s': cannot position/tilt/toggle when stopping", name); this->position_.reset(); - } - if (this->tilt_.has_value()) { - ESP_LOGW(TAG, "Cannot set tilt when stopping a cover!"); this->tilt_.reset(); - } - if (this->toggle_.has_value()) { - ESP_LOGW(TAG, "Cannot set toggle when stopping a cover!"); this->toggle_.reset(); } } @@ -147,21 +147,7 @@ CoverCall &CoverCall::set_stop(bool stop) { bool CoverCall::get_stop() const { return this->stop_; } CoverCall Cover::make_call() { return {this}; } -void Cover::open() { - auto call = this->make_call(); - call.set_command_open(); - call.perform(); -} -void Cover::close() { - auto call = this->make_call(); - call.set_command_close(); - call.perform(); -} -void Cover::stop() { - auto call = this->make_call(); - call.set_command_stop(); - call.perform(); -} + void Cover::add_on_state_callback(std::function &&f) { this->state_callback_.add(std::move(f)); } void Cover::publish_state(bool save) { this->position = clamp(this->position, 0.0f, 1.0f); @@ -186,6 +172,9 @@ void Cover::publish_state(bool save) { ESP_LOGD(TAG, " Current Operation: %s", cover_operation_to_str(this->current_operation)); this->state_callback_.call(); +#if defined(USE_COVER) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_cover_update(this); +#endif if (save) { CoverRestoreState restore{}; @@ -198,7 +187,7 @@ void Cover::publish_state(bool save) { } } optional Cover::restore_state_() { - this->rtc_ = global_preferences->make_preference(this->get_object_id_hash()); + this->rtc_ = global_preferences->make_preference(this->get_preference_hash()); CoverRestoreState recovered{}; if (!this->rtc_.load(&recovered)) return {}; @@ -222,5 +211,4 @@ void CoverRestoreState::apply(Cover *cover) { cover->publish_state(); } -} // namespace cover -} // namespace esphome +} // namespace esphome::cover diff --git a/esphome/components/cover/cover.h b/esphome/components/cover/cover.h index 8b6f5b8a72..6c69c05e71 100644 --- a/esphome/components/cover/cover.h +++ b/esphome/components/cover/cover.h @@ -4,10 +4,10 @@ #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" #include "esphome/core/preferences.h" + #include "cover_traits.h" -namespace esphome { -namespace cover { +namespace esphome::cover { const extern float COVER_OPEN; const extern float COVER_CLOSED; @@ -19,8 +19,8 @@ const extern float COVER_CLOSED; if (traits_.get_is_assumed_state()) { \ ESP_LOGCONFIG(TAG, "%s Assumed State: YES", prefix); \ } \ - if (!(obj)->get_device_class().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class().c_str()); \ + if (!(obj)->get_device_class_ref().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class_ref().c_str()); \ } \ } @@ -125,25 +125,6 @@ class Cover : public EntityBase, public EntityBase_DeviceClass { /// Construct a new cover call used to control the cover. CoverCall make_call(); - /** Open the cover. - * - * This is a legacy method and may be removed later, please use `.make_call()` instead. - */ - ESPDEPRECATED("open() is deprecated, use make_call().set_command_open().perform() instead.", "2021.9") - void open(); - /** Close the cover. - * - * This is a legacy method and may be removed later, please use `.make_call()` instead. - */ - ESPDEPRECATED("close() is deprecated, use make_call().set_command_close().perform() instead.", "2021.9") - void close(); - /** Stop the cover. - * - * This is a legacy method and may be removed later, please use `.make_call()` instead. - * As per solution from issue #2885 the call should include perform() - */ - ESPDEPRECATED("stop() is deprecated, use make_call().set_command_stop().perform() instead.", "2021.9") - void stop(); void add_on_state_callback(std::function &&f); @@ -175,5 +156,4 @@ class Cover : public EntityBase, public EntityBase_DeviceClass { ESPPreferenceObject rtc_; }; -} // namespace cover -} // namespace esphome +} // namespace esphome::cover diff --git a/esphome/components/cover/cover_traits.h b/esphome/components/cover/cover_traits.h index 79001c3b03..723516318b 100644 --- a/esphome/components/cover/cover_traits.h +++ b/esphome/components/cover/cover_traits.h @@ -1,7 +1,6 @@ #pragma once -namespace esphome { -namespace cover { +namespace esphome::cover { class CoverTraits { public: @@ -26,5 +25,4 @@ class CoverTraits { bool supports_stop_{false}; }; -} // namespace cover -} // namespace esphome +} // namespace esphome::cover diff --git a/esphome/components/cs5460a/cs5460a.h b/esphome/components/cs5460a/cs5460a.h index 15ae04f3c6..11b13f5851 100644 --- a/esphome/components/cs5460a/cs5460a.h +++ b/esphome/components/cs5460a/cs5460a.h @@ -114,7 +114,7 @@ template class CS5460ARestartAction : public Action { public: CS5460ARestartAction(CS5460AComponent *cs5460a) : cs5460a_(cs5460a) {} - void play(Ts... x) override { cs5460a_->restart(); } + void play(const Ts &...x) override { cs5460a_->restart(); } protected: CS5460AComponent *cs5460a_; diff --git a/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp b/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp index 0ba2d9df94..f6280a75a1 100644 --- a/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp +++ b/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp @@ -19,13 +19,14 @@ void CST816Touchscreen::continue_setup_() { case CST816T_CHIP_ID: break; default: + ESP_LOGE(TAG, "Unknown chip ID: 0x%02X", this->chip_id_); + this->status_set_error(LOG_STR("Unknown chip ID")); this->mark_failed(); - this->status_set_error(str_sprintf("Unknown chip ID 0x%02X", this->chip_id_).c_str()); return; } this->write_byte(REG_IRQ_CTL, IRQ_EN_MOTION); } else if (!this->skip_probe_) { - this->status_set_error("Failed to read chip id"); + this->status_set_error(LOG_STR("Failed to read chip id")); this->mark_failed(); return; } diff --git a/esphome/components/daikin_arc/daikin_arc.cpp b/esphome/components/daikin_arc/daikin_arc.cpp index f806463d00..f05342f482 100644 --- a/esphome/components/daikin_arc/daikin_arc.cpp +++ b/esphome/components/daikin_arc/daikin_arc.cpp @@ -26,7 +26,7 @@ void DaikinArcClimate::transmit_query_() { uint8_t remote_header[8] = {0x11, 0xDA, 0x27, 0x00, 0x84, 0x87, 0x20, 0x00}; // Calculate checksum - for (int i = 0; i < sizeof(remote_header) - 1; i++) { + for (size_t i = 0; i < sizeof(remote_header) - 1; i++) { remote_header[sizeof(remote_header) - 1] += remote_header[i]; } @@ -102,7 +102,7 @@ void DaikinArcClimate::transmit_state() { remote_state[9] = fan_speed & 0xff; // Calculate checksum - for (int i = 0; i < sizeof(remote_header) - 1; i++) { + for (size_t i = 0; i < sizeof(remote_header) - 1; i++) { remote_header[sizeof(remote_header) - 1] += remote_header[i]; } @@ -241,9 +241,7 @@ uint8_t DaikinArcClimate::humidity_() { climate::ClimateTraits DaikinArcClimate::traits() { climate::ClimateTraits traits = climate_ir::ClimateIR::traits(); - traits.set_supports_current_temperature(true); - traits.set_supports_current_humidity(false); - traits.set_supports_target_humidity(true); + traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE | climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY); traits.set_visual_min_humidity(38); traits.set_visual_max_humidity(52); return traits; @@ -350,7 +348,7 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) { bool valid_daikin_frame = false; if (data.expect_item(DAIKIN_HEADER_MARK, DAIKIN_HEADER_SPACE)) { valid_daikin_frame = true; - int bytes_count = data.size() / 2 / 8; + size_t bytes_count = data.size() / 2 / 8; std::unique_ptr buf(new char[bytes_count * 3 + 1]); buf[0] = '\0'; for (size_t i = 0; i < bytes_count; i++) { @@ -370,7 +368,7 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) { if (!valid_daikin_frame) { char sbuf[16 * 10 + 1]; sbuf[0] = '\0'; - for (size_t j = 0; j < data.size(); j++) { + for (size_t j = 0; j < static_cast(data.size()); j++) { if ((j - 2) % 16 == 0) { if (j > 0) { ESP_LOGD(TAG, "DATA %04x: %s", (j - 16 > 0xffff ? 0 : j - 16), sbuf); @@ -380,19 +378,26 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) { char type_ch = ' '; // debug_tolerance = 25% - if (DAIKIN_DBG_LOWER(DAIKIN_ARC_PRE_MARK) <= data[j] && data[j] <= DAIKIN_DBG_UPPER(DAIKIN_ARC_PRE_MARK)) + if (static_cast(DAIKIN_DBG_LOWER(DAIKIN_ARC_PRE_MARK)) <= data[j] && + data[j] <= static_cast(DAIKIN_DBG_UPPER(DAIKIN_ARC_PRE_MARK))) type_ch = 'P'; - if (DAIKIN_DBG_LOWER(DAIKIN_ARC_PRE_SPACE) <= -data[j] && -data[j] <= DAIKIN_DBG_UPPER(DAIKIN_ARC_PRE_SPACE)) + if (static_cast(DAIKIN_DBG_LOWER(DAIKIN_ARC_PRE_SPACE)) <= -data[j] && + -data[j] <= static_cast(DAIKIN_DBG_UPPER(DAIKIN_ARC_PRE_SPACE))) type_ch = 'a'; - if (DAIKIN_DBG_LOWER(DAIKIN_HEADER_MARK) <= data[j] && data[j] <= DAIKIN_DBG_UPPER(DAIKIN_HEADER_MARK)) + if (static_cast(DAIKIN_DBG_LOWER(DAIKIN_HEADER_MARK)) <= data[j] && + data[j] <= static_cast(DAIKIN_DBG_UPPER(DAIKIN_HEADER_MARK))) type_ch = 'H'; - if (DAIKIN_DBG_LOWER(DAIKIN_HEADER_SPACE) <= -data[j] && -data[j] <= DAIKIN_DBG_UPPER(DAIKIN_HEADER_SPACE)) + if (static_cast(DAIKIN_DBG_LOWER(DAIKIN_HEADER_SPACE)) <= -data[j] && + -data[j] <= static_cast(DAIKIN_DBG_UPPER(DAIKIN_HEADER_SPACE))) type_ch = 'h'; - if (DAIKIN_DBG_LOWER(DAIKIN_BIT_MARK) <= data[j] && data[j] <= DAIKIN_DBG_UPPER(DAIKIN_BIT_MARK)) + if (static_cast(DAIKIN_DBG_LOWER(DAIKIN_BIT_MARK)) <= data[j] && + data[j] <= static_cast(DAIKIN_DBG_UPPER(DAIKIN_BIT_MARK))) type_ch = 'B'; - if (DAIKIN_DBG_LOWER(DAIKIN_ONE_SPACE) <= -data[j] && -data[j] <= DAIKIN_DBG_UPPER(DAIKIN_ONE_SPACE)) + if (static_cast(DAIKIN_DBG_LOWER(DAIKIN_ONE_SPACE)) <= -data[j] && + -data[j] <= static_cast(DAIKIN_DBG_UPPER(DAIKIN_ONE_SPACE))) type_ch = '1'; - if (DAIKIN_DBG_LOWER(DAIKIN_ZERO_SPACE) <= -data[j] && -data[j] <= DAIKIN_DBG_UPPER(DAIKIN_ZERO_SPACE)) + if (static_cast(DAIKIN_DBG_LOWER(DAIKIN_ZERO_SPACE)) <= -data[j] && + -data[j] <= static_cast(DAIKIN_DBG_UPPER(DAIKIN_ZERO_SPACE))) type_ch = '0'; if (abs(data[j]) > 100000) { @@ -400,7 +405,7 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) { } else { sprintf(sbuf, "%s%-5d[%c] ", sbuf, (int) (round(data[j] / 10.) * 10), type_ch); } - if (j == data.size() - 1) { + if (j + 1 == static_cast(data.size())) { ESP_LOGD(TAG, "DATA %04x: %s", (j - 8 > 0xffff ? 0 : j - 8), sbuf); } } diff --git a/esphome/components/dallas_temp/dallas_temp.cpp b/esphome/components/dallas_temp/dallas_temp.cpp index 5cd6063893..a3969e081e 100644 --- a/esphome/components/dallas_temp/dallas_temp.cpp +++ b/esphome/components/dallas_temp/dallas_temp.cpp @@ -64,13 +64,13 @@ bool DallasTemperatureSensor::read_scratch_pad_() { } } else { ESP_LOGW(TAG, "'%s' - reading scratch pad failed bus reset", this->get_name().c_str()); - this->status_set_warning("bus reset failed"); + this->status_set_warning(LOG_STR("bus reset failed")); } return success; } void DallasTemperatureSensor::setup() { - if (!this->check_address_()) + if (!this->check_address_or_index_()) return; if (!this->read_scratch_pad_()) return; @@ -124,7 +124,7 @@ bool DallasTemperatureSensor::check_scratch_pad_() { crc8(this->scratch_pad_, 8)); #endif if (!chksum_validity) { - this->status_set_warning("scratch pad checksum invalid"); + this->status_set_warning(LOG_STR("scratch pad checksum invalid")); ESP_LOGD(TAG, "Scratch pad: %02X.%02X.%02X.%02X.%02X.%02X.%02X.%02X.%02X (%02X)", this->scratch_pad_[0], this->scratch_pad_[1], this->scratch_pad_[2], this->scratch_pad_[3], this->scratch_pad_[4], this->scratch_pad_[5], this->scratch_pad_[6], this->scratch_pad_[7], this->scratch_pad_[8], diff --git a/esphome/components/dashboard_import/dashboard_import.cpp b/esphome/components/dashboard_import/dashboard_import.cpp index 6875fd61a5..d4a95b81f6 100644 --- a/esphome/components/dashboard_import/dashboard_import.cpp +++ b/esphome/components/dashboard_import/dashboard_import.cpp @@ -3,10 +3,10 @@ namespace esphome { namespace dashboard_import { -static std::string g_package_import_url; // NOLINT +static const char *g_package_import_url = ""; // NOLINT -std::string get_package_import_url() { return g_package_import_url; } -void set_package_import_url(std::string url) { g_package_import_url = std::move(url); } +const char *get_package_import_url() { return g_package_import_url; } +void set_package_import_url(const char *url) { g_package_import_url = url; } } // namespace dashboard_import } // namespace esphome diff --git a/esphome/components/dashboard_import/dashboard_import.h b/esphome/components/dashboard_import/dashboard_import.h index 0ca2994aab..488bf80a2e 100644 --- a/esphome/components/dashboard_import/dashboard_import.h +++ b/esphome/components/dashboard_import/dashboard_import.h @@ -1,12 +1,10 @@ #pragma once -#include - namespace esphome { namespace dashboard_import { -std::string get_package_import_url(); -void set_package_import_url(std::string url); +const char *get_package_import_url(); +void set_package_import_url(const char *url); } // namespace dashboard_import } // namespace esphome diff --git a/esphome/components/datetime/__init__.py b/esphome/components/datetime/__init__.py index 1d84b75f26..602db3827a 100644 --- a/esphome/components/datetime/__init__.py +++ b/esphome/components/datetime/__init__.py @@ -21,7 +21,7 @@ from esphome.const import ( CONF_WEB_SERVER, CONF_YEAR, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass @@ -172,7 +172,7 @@ async def new_datetime(config, *args): return var -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(datetime_ns.using) diff --git a/esphome/components/datetime/date_entity.cpp b/esphome/components/datetime/date_entity.cpp index c164a98b2e..2c2775ecf4 100644 --- a/esphome/components/datetime/date_entity.cpp +++ b/esphome/components/datetime/date_entity.cpp @@ -1,5 +1,6 @@ #include "date_entity.h" - +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #ifdef USE_DATETIME_DATE #include "esphome/core/log.h" @@ -32,6 +33,9 @@ void DateEntity::publish_state() { 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_); this->state_callback_.call(); +#if defined(USE_DATETIME_DATE) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_date_update(this); +#endif } DateCall DateEntity::make_call() { return DateCall(this); } diff --git a/esphome/components/datetime/date_entity.h b/esphome/components/datetime/date_entity.h index ce43c5639d..ba2edb127a 100644 --- a/esphome/components/datetime/date_entity.h +++ b/esphome/components/datetime/date_entity.h @@ -16,8 +16,8 @@ namespace datetime { #define LOG_DATETIME_DATE(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - if (!(obj)->get_icon().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \ + if (!(obj)->get_icon_ref().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon_ref().c_str()); \ } \ } @@ -101,7 +101,7 @@ template class DateSetAction : public Action, public Pare public: TEMPLATABLE_VALUE(ESPTime, date) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->parent_->make_call(); if (this->date_.has_value()) { diff --git a/esphome/components/datetime/datetime_base.h b/esphome/components/datetime/datetime_base.h index b7645f5539..b5f54ac96f 100644 --- a/esphome/components/datetime/datetime_base.h +++ b/esphome/components/datetime/datetime_base.h @@ -30,14 +30,12 @@ class DateTimeBase : public EntityBase { #endif }; -#ifdef USE_TIME class DateTimeStateTrigger : public Trigger { public: explicit DateTimeStateTrigger(DateTimeBase *parent) { parent->add_on_state_callback([this, parent]() { this->trigger(parent->state_as_esptime()); }); } }; -#endif } // namespace datetime } // namespace esphome diff --git a/esphome/components/datetime/datetime_entity.cpp b/esphome/components/datetime/datetime_entity.cpp index 4e3b051eb3..8606a47fa7 100644 --- a/esphome/components/datetime/datetime_entity.cpp +++ b/esphome/components/datetime/datetime_entity.cpp @@ -1,5 +1,6 @@ #include "datetime_entity.h" - +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #ifdef USE_DATETIME_DATETIME #include "esphome/core/log.h" @@ -48,6 +49,9 @@ void DateTimeEntity::publish_state() { 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_); this->state_callback_.call(); +#if defined(USE_DATETIME_DATETIME) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_datetime_update(this); +#endif } DateTimeCall DateTimeEntity::make_call() { return DateTimeCall(this); } diff --git a/esphome/components/datetime/datetime_entity.h b/esphome/components/datetime/datetime_entity.h index 27db84cf7e..43bff5a181 100644 --- a/esphome/components/datetime/datetime_entity.h +++ b/esphome/components/datetime/datetime_entity.h @@ -16,8 +16,8 @@ namespace datetime { #define LOG_DATETIME_DATETIME(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - if (!(obj)->get_icon().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \ + if (!(obj)->get_icon_ref().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon_ref().c_str()); \ } \ } @@ -124,7 +124,7 @@ template class DateTimeSetAction : public Action, public public: TEMPLATABLE_VALUE(ESPTime, datetime) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->parent_->make_call(); if (this->datetime_.has_value()) { diff --git a/esphome/components/datetime/time_entity.cpp b/esphome/components/datetime/time_entity.cpp index 9b05c2124f..469be077ea 100644 --- a/esphome/components/datetime/time_entity.cpp +++ b/esphome/components/datetime/time_entity.cpp @@ -1,5 +1,6 @@ #include "time_entity.h" - +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #ifdef USE_DATETIME_TIME #include "esphome/core/log.h" @@ -29,6 +30,9 @@ void TimeEntity::publish_state() { ESP_LOGD(TAG, "'%s': Sending time %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); +#endif } TimeCall TimeEntity::make_call() { return TimeCall(this); } diff --git a/esphome/components/datetime/time_entity.h b/esphome/components/datetime/time_entity.h index f7e0a7ddd9..c5cbeb52da 100644 --- a/esphome/components/datetime/time_entity.h +++ b/esphome/components/datetime/time_entity.h @@ -16,8 +16,8 @@ namespace datetime { #define LOG_DATETIME_TIME(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - if (!(obj)->get_icon().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \ + if (!(obj)->get_icon_ref().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon_ref().c_str()); \ } \ } @@ -103,7 +103,7 @@ template class TimeSetAction : public Action, public Pare public: TEMPLATABLE_VALUE(ESPTime, time) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->parent_->make_call(); if (this->time_.has_value()) { diff --git a/esphome/components/debug/__init__.py b/esphome/components/debug/__init__.py index b8dabc3374..dc032f442e 100644 --- a/esphome/components/debug/__init__.py +++ b/esphome/components/debug/__init__.py @@ -13,7 +13,7 @@ from esphome.const import ( ) from esphome.core import CORE -CODEOWNERS = ["@OttoWinter"] +CODEOWNERS = ["@esphome/core"] DEPENDENCIES = ["logger"] CONF_DEBUG_ID = "debug_id" @@ -48,8 +48,18 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): if CORE.using_zephyr: zephyr_add_prj_conf("HWINFO", True) + # gdb thread support + zephyr_add_prj_conf("DEBUG_THREAD_INFO", True) + # RTT + zephyr_add_prj_conf("USE_SEGGER_RTT", True) + zephyr_add_prj_conf("RTT_CONSOLE", True) + zephyr_add_prj_conf("LOG", True) + zephyr_add_prj_conf("LOG_BLOCK_IN_THREAD", True) + zephyr_add_prj_conf("LOG_BUFFER_SIZE", 4096) + zephyr_add_prj_conf("SEGGER_RTT_MODE_BLOCK_IF_FIFO_FULL", True) var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) + cg.add_define("USE_DEBUG") FILTER_SOURCE_FILES = filter_source_files_from_platform( diff --git a/esphome/components/debug/debug_component.cpp b/esphome/components/debug/debug_component.cpp index ade0968e08..f54bf82eae 100644 --- a/esphome/components/debug/debug_component.cpp +++ b/esphome/components/debug/debug_component.cpp @@ -49,9 +49,9 @@ void DebugComponent::dump_config() { } #endif // USE_TEXT_SENSOR -#ifdef USE_ESP32 - this->log_partition_info_(); // Log partition information for ESP32 -#endif // USE_ESP32 +#if defined(USE_ESP32) || defined(USE_ZEPHYR) + this->log_partition_info_(); // Log partition information +#endif } void DebugComponent::loop() { diff --git a/esphome/components/debug/debug_component.h b/esphome/components/debug/debug_component.h index efd0dafab0..96306f7cdf 100644 --- a/esphome/components/debug/debug_component.h +++ b/esphome/components/debug/debug_component.h @@ -62,19 +62,19 @@ class DebugComponent : public PollingComponent { sensor::Sensor *cpu_frequency_sensor_{nullptr}; #endif // USE_SENSOR -#ifdef USE_ESP32 +#if defined(USE_ESP32) || defined(USE_ZEPHYR) /** * @brief Logs information about the device's partition table. * - * This function iterates through the ESP32's partition table and logs details + * This function iterates through the partition table and logs details * about each partition, including its name, type, subtype, starting address, * and size. The information is useful for diagnosing issues related to flash * memory or verifying the partition configuration dynamically at runtime. * - * Only available when compiled for ESP32 platforms. + * Only available when compiled for ESP32 and ZEPHYR platforms. */ void log_partition_info_(); -#endif // USE_ESP32 +#endif #ifdef USE_TEXT_SENSOR text_sensor::TextSensor *device_info_{nullptr}; diff --git a/esphome/components/debug/debug_esp32.cpp b/esphome/components/debug/debug_esp32.cpp index 37990aeec5..1c3dc3699b 100644 --- a/esphome/components/debug/debug_esp32.cpp +++ b/esphome/components/debug/debug_esp32.cpp @@ -11,8 +11,6 @@ #include #include -#include - #ifdef USE_ARDUINO #include #endif @@ -52,7 +50,7 @@ void DebugComponent::on_shutdown() { char buffer[REBOOT_MAX_LEN]{}; auto pref = global_preferences->make_preference(REBOOT_MAX_LEN, fnv1_hash(REBOOT_KEY + App.get_name())); if (component != nullptr) { - strncpy(buffer, component->get_component_source(), REBOOT_MAX_LEN - 1); + strncpy(buffer, LOG_STR_ARG(component->get_component_log_str()), REBOOT_MAX_LEN - 1); buffer[REBOOT_MAX_LEN - 1] = '\0'; } ESP_LOGD(TAG, "Storing reboot source: %s", buffer); @@ -125,7 +123,12 @@ void DebugComponent::log_partition_info_() { uint32_t DebugComponent::get_free_heap_() { return heap_caps_get_free_size(MALLOC_CAP_INTERNAL); } -static const std::map CHIP_FEATURES = { +struct ChipFeature { + int bit; + const char *name; +}; + +static constexpr ChipFeature CHIP_FEATURES[] = { {CHIP_FEATURE_BLE, "BLE"}, {CHIP_FEATURE_BT, "BT"}, {CHIP_FEATURE_EMB_FLASH, "EMB Flash"}, @@ -170,11 +173,13 @@ void DebugComponent::get_device_info_(std::string &device_info) { esp_chip_info(&info); const char *model = ESPHOME_VARIANT; std::string features; - for (auto feature : CHIP_FEATURES) { - if (info.features & feature.first) { - features += feature.second; + + // Check each known feature bit + for (const auto &feature : CHIP_FEATURES) { + if (info.features & feature.bit) { + features += feature.name; features += ", "; - info.features &= ~feature.first; + info.features &= ~feature.bit; } } if (info.features != 0) diff --git a/esphome/components/debug/debug_zephyr.cpp b/esphome/components/debug/debug_zephyr.cpp index 9a361b158f..c888c41a78 100644 --- a/esphome/components/debug/debug_zephyr.cpp +++ b/esphome/components/debug/debug_zephyr.cpp @@ -5,11 +5,11 @@ #include #include #include +#include #define BOOTLOADER_VERSION_REGISTER NRF_TIMER2->CC[0] -namespace esphome { -namespace debug { +namespace esphome::debug { static const char *const TAG = "debug"; constexpr std::uintptr_t MBR_PARAM_PAGE_ADDR = 0xFFC; @@ -25,10 +25,37 @@ static void show_reset_reason(std::string &reset_reason, bool set, const char *r reset_reason += reason; } -inline uint32_t read_mem_u32(uintptr_t addr) { +static inline uint32_t read_mem_u32(uintptr_t addr) { return *reinterpret_cast(addr); // NOLINT(performance-no-int-to-ptr) } +static inline uint8_t read_mem_u8(uintptr_t addr) { + return *reinterpret_cast(addr); // NOLINT(performance-no-int-to-ptr) +} + +// defines from https://github.com/adafruit/Adafruit_nRF52_Bootloader which prints those information +constexpr uint32_t SD_MAGIC_NUMBER = 0x51B1E5DB; +constexpr uintptr_t MBR_SIZE = 0x1000; +constexpr uintptr_t SOFTDEVICE_INFO_STRUCT_OFFSET = 0x2000; +constexpr uintptr_t SD_ID_OFFSET = SOFTDEVICE_INFO_STRUCT_OFFSET + 0x10; +constexpr uintptr_t SD_VERSION_OFFSET = SOFTDEVICE_INFO_STRUCT_OFFSET + 0x14; + +static inline bool is_sd_present() { + return read_mem_u32(SOFTDEVICE_INFO_STRUCT_OFFSET + MBR_SIZE + 4) == SD_MAGIC_NUMBER; +} +static inline uint32_t sd_id_get() { + if (read_mem_u8(MBR_SIZE + SOFTDEVICE_INFO_STRUCT_OFFSET) > (SD_ID_OFFSET - SOFTDEVICE_INFO_STRUCT_OFFSET)) { + return read_mem_u32(MBR_SIZE + SD_ID_OFFSET); + } + return 0; +} +static inline uint32_t sd_version_get() { + if (read_mem_u8(MBR_SIZE + SOFTDEVICE_INFO_STRUCT_OFFSET) > (SD_VERSION_OFFSET - SOFTDEVICE_INFO_STRUCT_OFFSET)) { + return read_mem_u32(MBR_SIZE + SD_VERSION_OFFSET); + } + return 0; +} + std::string DebugComponent::get_reset_reason_() { uint32_t cause; auto ret = hwinfo_get_reset_cause(&cause); @@ -60,6 +87,37 @@ std::string DebugComponent::get_reset_reason_() { uint32_t DebugComponent::get_free_heap_() { return INT_MAX; } +static void fa_cb(const struct flash_area *fa, void *user_data) { +#if CONFIG_FLASH_MAP_LABELS + const char *fa_label = flash_area_label(fa); + + if (fa_label == nullptr) { + fa_label = "-"; + } + ESP_LOGCONFIG(TAG, "%2d 0x%0*" PRIxPTR " %-26s %-24.24s 0x%-10x 0x%-12x", (int) fa->fa_id, + sizeof(uintptr_t) * 2, (uintptr_t) fa->fa_dev, fa->fa_dev->name, fa_label, (uint32_t) fa->fa_off, + fa->fa_size); +#else + ESP_LOGCONFIG(TAG, "%2d 0x%0*" PRIxPTR " %-26s 0x%-10x 0x%-12x", (int) fa->fa_id, sizeof(uintptr_t) * 2, + (uintptr_t) fa->fa_dev, fa->fa_dev->name, (uint32_t) fa->fa_off, fa->fa_size); +#endif +} + +void DebugComponent::log_partition_info_() { +#if CONFIG_FLASH_MAP_LABELS + ESP_LOGCONFIG(TAG, "ID | Device | Device Name " + "| Label | Offset | Size"); + ESP_LOGCONFIG(TAG, "--------------------------------------------" + "-----------------------------------------------"); +#else + ESP_LOGCONFIG(TAG, "ID | Device | Device Name " + "| Offset | Size"); + ESP_LOGCONFIG(TAG, "-----------------------------------------" + "------------------------------"); +#endif + flash_area_foreach(fa_cb, nullptr); +} + void DebugComponent::get_device_info_(std::string &device_info) { std::string supply = "Main supply status: "; if (nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_NORMAL) { @@ -254,14 +312,18 @@ void DebugComponent::get_device_info_(std::string &device_info) { NRF_FICR->INFO.VARIANT & 0xFF, package(NRF_FICR->INFO.PACKAGE)); ESP_LOGD(TAG, "RAM: %ukB, Flash: %ukB, production test: %sdone", NRF_FICR->INFO.RAM, NRF_FICR->INFO.FLASH, (NRF_FICR->PRODTEST[0] == 0xBB42319F ? "" : "not ")); + bool n_reset_enabled = NRF_UICR->PSELRESET[0] == NRF_UICR->PSELRESET[1] && + (NRF_UICR->PSELRESET[0] & UICR_PSELRESET_CONNECT_Msk) == UICR_PSELRESET_CONNECT_Connected + << UICR_PSELRESET_CONNECT_Pos; ESP_LOGD( TAG, "GPIO as NFC pins: %s, GPIO as nRESET pin: %s", YESNO((NRF_UICR->NFCPINS & UICR_NFCPINS_PROTECT_Msk) == (UICR_NFCPINS_PROTECT_NFC << UICR_NFCPINS_PROTECT_Pos)), - YESNO(((NRF_UICR->PSELRESET[0] & UICR_PSELRESET_CONNECT_Msk) != - (UICR_PSELRESET_CONNECT_Connected << UICR_PSELRESET_CONNECT_Pos)) || - ((NRF_UICR->PSELRESET[1] & UICR_PSELRESET_CONNECT_Msk) != - (UICR_PSELRESET_CONNECT_Connected << UICR_PSELRESET_CONNECT_Pos)))); - + YESNO(n_reset_enabled)); + if (n_reset_enabled) { + uint8_t port = (NRF_UICR->PSELRESET[0] & UICR_PSELRESET_PORT_Msk) >> UICR_PSELRESET_PORT_Pos; + uint8_t pin = (NRF_UICR->PSELRESET[0] & UICR_PSELRESET_PIN_Msk) >> UICR_PSELRESET_PIN_Pos; + ESP_LOGD(TAG, "nRESET port P%u.%02u", port, pin); + } #ifdef USE_BOOTLOADER_MCUBOOT ESP_LOGD(TAG, "bootloader: mcuboot"); #else @@ -271,11 +333,46 @@ void DebugComponent::get_device_info_(std::string &device_info) { NRF_UICR->NRFFW[0]); ESP_LOGD(TAG, "MBR param page addr 0x%08x, UICR param page addr 0x%08x", read_mem_u32(MBR_PARAM_PAGE_ADDR), NRF_UICR->NRFFW[1]); + if (is_sd_present()) { + uint32_t const sd_id = sd_id_get(); + uint32_t const sd_version = sd_version_get(); + + uint32_t ver[3]; + ver[0] = sd_version / 1000000; + ver[1] = (sd_version - ver[0] * 1000000) / 1000; + ver[2] = (sd_version - ver[0] * 1000000 - ver[1] * 1000); + + ESP_LOGD(TAG, "SoftDevice: S%u %u.%u.%u", sd_id, ver[0], ver[1], ver[2]); +#ifdef USE_SOFTDEVICE_ID +#ifdef USE_SOFTDEVICE_VERSION + if (USE_SOFTDEVICE_ID != sd_id || USE_SOFTDEVICE_VERSION != ver[0]) { + ESP_LOGE(TAG, "Built for SoftDevice S%u %u.x.y. It may crash due to mismatch of bootloader version.", + USE_SOFTDEVICE_ID, USE_SOFTDEVICE_VERSION); + } +#else + if (USE_SOFTDEVICE_ID != sd_id) { + ESP_LOGE(TAG, "Built for SoftDevice S%u. It may crash due to mismatch of bootloader version.", USE_SOFTDEVICE_ID); + } #endif +#endif + } +#endif + auto uicr = [](volatile uint32_t *data, uint8_t size) { + std::string res; + char buf[sizeof(uint32_t) * 2 + 1]; + for (size_t i = 0; i < size; i++) { + if (i > 0) { + res += ' '; + } + res += format_hex_pretty(data[i], '\0', false); + } + return res; + }; + ESP_LOGD(TAG, "NRFFW %s", uicr(NRF_UICR->NRFFW, 13).c_str()); + ESP_LOGD(TAG, "NRFHW %s", uicr(NRF_UICR->NRFHW, 12).c_str()); } void DebugComponent::update_platform_() {} -} // namespace debug -} // namespace esphome +} // namespace esphome::debug #endif diff --git a/esphome/components/deep_sleep/__init__.py b/esphome/components/deep_sleep/__init__.py index 05ae60239d..19fb726016 100644 --- a/esphome/components/deep_sleep/__init__.py +++ b/esphome/components/deep_sleep/__init__.py @@ -197,7 +197,8 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_ESP32_EXT1_WAKEUP): cv.All( cv.only_on_esp32, esp32.only_on_variant( - unsupported=[VARIANT_ESP32C3], msg_prefix="Wakeup from ext1" + unsupported=[VARIANT_ESP32C2, VARIANT_ESP32C3], + msg_prefix="Wakeup from ext1", ), cv.Schema( { @@ -214,7 +215,13 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_TOUCH_WAKEUP): cv.All( cv.only_on_esp32, esp32.only_on_variant( - unsupported=[VARIANT_ESP32C3], msg_prefix="Wakeup from touch" + unsupported=[ + VARIANT_ESP32C2, + VARIANT_ESP32C3, + VARIANT_ESP32C6, + VARIANT_ESP32H2, + ], + msg_prefix="Wakeup from touch", ), cv.boolean, ), diff --git a/esphome/components/deep_sleep/deep_sleep_component.h b/esphome/components/deep_sleep/deep_sleep_component.h index 7a640b9ea5..80381e767c 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.h +++ b/esphome/components/deep_sleep/deep_sleep_component.h @@ -34,7 +34,7 @@ enum WakeupPinMode { WAKEUP_PIN_MODE_INVERT_WAKEUP, }; -#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) +#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) struct Ext1Wakeup { uint64_t mask; esp_sleep_ext1_wakeup_mode_t wakeup_mode; @@ -50,7 +50,7 @@ struct WakeupCauseToRunDuration { uint32_t gpio_cause; }; -#endif +#endif // USE_ESP32 template class EnterDeepSleepAction; @@ -73,20 +73,22 @@ class DeepSleepComponent : public Component { void set_wakeup_pin(InternalGPIOPin *pin) { this->wakeup_pin_ = pin; } void set_wakeup_pin_mode(WakeupPinMode wakeup_pin_mode); -#endif +#endif // USE_ESP32 #if defined(USE_ESP32) -#if !defined(USE_ESP32_VARIANT_ESP32C3) - +#if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) void set_ext1_wakeup(Ext1Wakeup ext1_wakeup); - - void set_touch_wakeup(bool touch_wakeup); - #endif + +#if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) && \ + !defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32H2) + void set_touch_wakeup(bool touch_wakeup); +#endif + // Set the duration in ms for how long the code should run before entering // deep sleep mode, according to the cause the ESP32 has woken. void set_run_duration(WakeupCauseToRunDuration wakeup_cause_to_run_duration); -#endif +#endif // USE_ESP32 /// Set a duration in ms for how long the code should run before entering deep sleep mode. void set_run_duration(uint32_t time_ms); @@ -117,13 +119,13 @@ class DeepSleepComponent : public Component { InternalGPIOPin *wakeup_pin_; WakeupPinMode wakeup_pin_mode_{WAKEUP_PIN_MODE_IGNORE}; -#if !defined(USE_ESP32_VARIANT_ESP32C3) +#if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) optional ext1_wakeup_; #endif optional touch_wakeup_; optional wakeup_cause_to_run_duration_; -#endif +#endif // USE_ESP32 optional run_duration_; bool next_enter_deep_sleep_{false}; bool prevent_{false}; @@ -146,7 +148,7 @@ template class EnterDeepSleepAction : public Action { void set_time(time::RealTimeClock *time) { this->time_ = time; } #endif - void play(Ts... x) override { + void play(const Ts &...x) override { if (this->sleep_duration_.has_value()) { this->deep_sleep_->set_sleep_duration(this->sleep_duration_.value(x...)); } @@ -205,12 +207,12 @@ template class EnterDeepSleepAction : public Action { template class PreventDeepSleepAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->prevent_deep_sleep(); } + void play(const Ts &...x) override { this->parent_->prevent_deep_sleep(); } }; template class AllowDeepSleepAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->allow_deep_sleep(); } + void play(const Ts &...x) override { this->parent_->allow_deep_sleep(); } }; } // namespace deep_sleep diff --git a/esphome/components/deep_sleep/deep_sleep_esp32.cpp b/esphome/components/deep_sleep/deep_sleep_esp32.cpp index 7965ab738a..b93d9ce601 100644 --- a/esphome/components/deep_sleep/deep_sleep_esp32.cpp +++ b/esphome/components/deep_sleep/deep_sleep_esp32.cpp @@ -1,10 +1,32 @@ #ifdef USE_ESP32 +#include "soc/soc_caps.h" +#include "driver/gpio.h" #include "deep_sleep_component.h" #include "esphome/core/log.h" namespace esphome { namespace deep_sleep { +// Deep Sleep feature support matrix for ESP32 variants: +// +// | Variant | ext0 | ext1 | Touch | GPIO wakeup | +// |-----------|------|------|-------|-------------| +// | ESP32 | ✓ | ✓ | ✓ | | +// | ESP32-S2 | ✓ | ✓ | ✓ | | +// | ESP32-S3 | ✓ | ✓ | ✓ | | +// | ESP32-C2 | | | | ✓ | +// | ESP32-C3 | | | | ✓ | +// | ESP32-C5 | | (✓) | | (✓) | +// | ESP32-C6 | | ✓ | | ✓ | +// | ESP32-H2 | | ✓ | | | +// +// Notes: +// - (✓) = Supported by hardware but not yet implemented in ESPHome +// - ext0: Single pin wakeup using RTC GPIO (esp_sleep_enable_ext0_wakeup) +// - ext1: Multiple pin wakeup (esp_sleep_enable_ext1_wakeup) +// - Touch: Touch pad wakeup (esp_sleep_enable_touchpad_wakeup) +// - GPIO wakeup: GPIO wakeup for non-RTC pins (esp_deep_sleep_enable_gpio_wakeup) + static const char *const TAG = "deep_sleep"; optional DeepSleepComponent::get_run_duration_() const { @@ -28,13 +50,13 @@ void DeepSleepComponent::set_wakeup_pin_mode(WakeupPinMode wakeup_pin_mode) { this->wakeup_pin_mode_ = wakeup_pin_mode; } -#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32C6) +#if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) void DeepSleepComponent::set_ext1_wakeup(Ext1Wakeup ext1_wakeup) { this->ext1_wakeup_ = ext1_wakeup; } - -#if !defined(USE_ESP32_VARIANT_ESP32H2) -void DeepSleepComponent::set_touch_wakeup(bool touch_wakeup) { this->touch_wakeup_ = touch_wakeup; } #endif +#if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) && \ + !defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32H2) +void DeepSleepComponent::set_touch_wakeup(bool touch_wakeup) { this->touch_wakeup_ = touch_wakeup; } #endif void DeepSleepComponent::set_run_duration(WakeupCauseToRunDuration wakeup_cause_to_run_duration) { @@ -70,38 +92,51 @@ bool DeepSleepComponent::prepare_to_sleep_() { } void DeepSleepComponent::deep_sleep_() { -#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32H2) + // Timer wakeup - all variants support this if (this->sleep_duration_.has_value()) esp_sleep_enable_timer_wakeup(*this->sleep_duration_); + + // Single pin wakeup (ext0) - ESP32, S2, S3 only +#if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) && \ + !defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32H2) if (this->wakeup_pin_ != nullptr) { + const auto gpio_pin = gpio_num_t(this->wakeup_pin_->get_pin()); + if (this->wakeup_pin_->get_flags() & gpio::FLAG_PULLUP) { + gpio_sleep_set_pull_mode(gpio_pin, GPIO_PULLUP_ONLY); + } else if (this->wakeup_pin_->get_flags() & gpio::FLAG_PULLDOWN) { + gpio_sleep_set_pull_mode(gpio_pin, GPIO_PULLDOWN_ONLY); + } + gpio_sleep_set_direction(gpio_pin, GPIO_MODE_INPUT); + gpio_hold_en(gpio_pin); +#if !SOC_GPIO_SUPPORT_HOLD_SINGLE_IO_IN_DSLP + // Some ESP32 variants support holding a single GPIO during deep sleep without this function + // For those variants, gpio_hold_en() is sufficient to hold the pin state during deep sleep + gpio_deep_sleep_hold_en(); +#endif bool level = !this->wakeup_pin_->is_inverted(); if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && this->wakeup_pin_->digital_read()) { level = !level; } - esp_sleep_enable_ext0_wakeup(gpio_num_t(this->wakeup_pin_->get_pin()), level); - } - if (this->ext1_wakeup_.has_value()) { - esp_sleep_enable_ext1_wakeup(this->ext1_wakeup_->mask, this->ext1_wakeup_->wakeup_mode); - } - - if (this->touch_wakeup_.has_value() && *(this->touch_wakeup_)) { - esp_sleep_enable_touchpad_wakeup(); - esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); + esp_sleep_enable_ext0_wakeup(gpio_pin, level); } #endif -#if defined(USE_ESP32_VARIANT_ESP32H2) - if (this->sleep_duration_.has_value()) - esp_sleep_enable_timer_wakeup(*this->sleep_duration_); - if (this->ext1_wakeup_.has_value()) { - esp_sleep_enable_ext1_wakeup(this->ext1_wakeup_->mask, this->ext1_wakeup_->wakeup_mode); - } -#endif - -#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) - if (this->sleep_duration_.has_value()) - esp_sleep_enable_timer_wakeup(*this->sleep_duration_); + // GPIO wakeup - C2, C3, C6 only +#if defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) if (this->wakeup_pin_ != nullptr) { + const auto gpio_pin = gpio_num_t(this->wakeup_pin_->get_pin()); + if (this->wakeup_pin_->get_flags() & gpio::FLAG_PULLUP) { + gpio_sleep_set_pull_mode(gpio_pin, GPIO_PULLUP_ONLY); + } else if (this->wakeup_pin_->get_flags() & gpio::FLAG_PULLDOWN) { + gpio_sleep_set_pull_mode(gpio_pin, GPIO_PULLDOWN_ONLY); + } + gpio_sleep_set_direction(gpio_pin, GPIO_MODE_INPUT); + gpio_hold_en(gpio_pin); +#if !SOC_GPIO_SUPPORT_HOLD_SINGLE_IO_IN_DSLP + // Some ESP32 variants support holding a single GPIO during deep sleep without this function + // For those variants, gpio_hold_en() is sufficient to hold the pin state during deep sleep + gpio_deep_sleep_hold_en(); +#endif bool level = !this->wakeup_pin_->is_inverted(); if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && this->wakeup_pin_->digital_read()) { level = !level; @@ -110,9 +145,26 @@ void DeepSleepComponent::deep_sleep_() { static_cast(level)); } #endif + + // Multiple pin wakeup (ext1) - All except C2, C3 +#if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) + if (this->ext1_wakeup_.has_value()) { + esp_sleep_enable_ext1_wakeup(this->ext1_wakeup_->mask, this->ext1_wakeup_->wakeup_mode); + } +#endif + + // Touch wakeup - ESP32, S2, S3 only +#if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) && \ + !defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32H2) + if (this->touch_wakeup_.has_value() && *(this->touch_wakeup_)) { + esp_sleep_enable_touchpad_wakeup(); + esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); + } +#endif + esp_deep_sleep_start(); } } // namespace deep_sleep } // namespace esphome -#endif +#endif // USE_ESP32 diff --git a/esphome/components/demo/demo_climate.h b/esphome/components/demo/demo_climate.h index 1ba80aabf5..e2dfb0142b 100644 --- a/esphome/components/demo/demo_climate.h +++ b/esphome/components/demo/demo_climate.h @@ -28,16 +28,16 @@ class DemoClimate : public climate::Climate, public Component { this->mode = climate::CLIMATE_MODE_AUTO; this->action = climate::CLIMATE_ACTION_COOLING; this->fan_mode = climate::CLIMATE_FAN_HIGH; - this->custom_preset = {"My Preset"}; + this->set_custom_preset_("My Preset"); break; case DemoClimateType::TYPE_3: this->current_temperature = 21.5; this->target_temperature_low = 21.0; this->target_temperature_high = 22.5; this->mode = climate::CLIMATE_MODE_HEAT_COOL; - this->custom_fan_mode = {"Auto Low"}; + this->set_custom_fan_mode_("Auto Low"); this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL; - this->preset = climate::CLIMATE_PRESET_AWAY; + this->set_preset_(climate::CLIMATE_PRESET_AWAY); break; } this->publish_state(); @@ -58,23 +58,19 @@ class DemoClimate : public climate::Climate, public Component { this->target_temperature_high = *call.get_target_temperature_high(); } if (call.get_fan_mode().has_value()) { - this->fan_mode = *call.get_fan_mode(); - this->custom_fan_mode.reset(); + this->set_fan_mode_(*call.get_fan_mode()); } if (call.get_swing_mode().has_value()) { this->swing_mode = *call.get_swing_mode(); } - if (call.get_custom_fan_mode().has_value()) { - this->custom_fan_mode = *call.get_custom_fan_mode(); - this->fan_mode.reset(); + if (call.has_custom_fan_mode()) { + this->set_custom_fan_mode_(call.get_custom_fan_mode()); } if (call.get_preset().has_value()) { - this->preset = *call.get_preset(); - this->custom_preset.reset(); + this->set_preset_(*call.get_preset()); } - if (call.get_custom_preset().has_value()) { - this->custom_preset = *call.get_custom_preset(); - this->preset.reset(); + if (call.has_custom_preset()) { + this->set_custom_preset_(call.get_custom_preset()); } this->publish_state(); } @@ -82,16 +78,14 @@ class DemoClimate : public climate::Climate, public Component { climate::ClimateTraits traits{}; switch (type_) { case DemoClimateType::TYPE_1: - traits.set_supports_current_temperature(true); + traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE | climate::CLIMATE_SUPPORTS_ACTION); traits.set_supported_modes({ climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_HEAT, }); - traits.set_supports_action(true); traits.set_visual_temperature_step(0.5); break; case DemoClimateType::TYPE_2: - traits.set_supports_current_temperature(false); traits.set_supported_modes({ climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_HEAT, @@ -100,7 +94,7 @@ class DemoClimate : public climate::Climate, public Component { climate::CLIMATE_MODE_DRY, climate::CLIMATE_MODE_FAN_ONLY, }); - traits.set_supports_action(true); + traits.add_feature_flags(climate::CLIMATE_SUPPORTS_ACTION); traits.set_supported_fan_modes({ climate::CLIMATE_FAN_ON, climate::CLIMATE_FAN_OFF, @@ -123,8 +117,8 @@ class DemoClimate : public climate::Climate, public Component { traits.set_supported_custom_presets({"My Preset"}); break; case DemoClimateType::TYPE_3: - traits.set_supports_current_temperature(true); - traits.set_supports_two_point_target_temperature(true); + traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE | + climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE); traits.set_supported_modes({ climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_COOL, diff --git a/esphome/components/demo/demo_select.h b/esphome/components/demo/demo_select.h index 1951a684a2..1a5df13eda 100644 --- a/esphome/components/demo/demo_select.h +++ b/esphome/components/demo/demo_select.h @@ -8,7 +8,7 @@ namespace demo { class DemoSelect : public select::Select, public Component { protected: - void control(const std::string &value) override { this->publish_state(value); } + void control(size_t index) override { this->publish_state(index); } }; } // namespace demo diff --git a/esphome/components/dfplayer/dfplayer.h b/esphome/components/dfplayer/dfplayer.h index d2ec0a2310..03d2230ca6 100644 --- a/esphome/components/dfplayer/dfplayer.h +++ b/esphome/components/dfplayer/dfplayer.h @@ -77,7 +77,7 @@ class DFPlayer : public uart::UARTDevice, public Component { class ACTION_CLASS : /* NOLINT */ \ public Action, \ public Parented { \ - void play(Ts... x) override { this->parent_->ACTION_METHOD(); } \ + void play(const Ts &...x) override { this->parent_->ACTION_METHOD(); } \ }; DFPLAYER_SIMPLE_ACTION(NextAction, next) @@ -87,7 +87,7 @@ template class PlayMp3Action : public Action, public Pare public: TEMPLATABLE_VALUE(uint16_t, file) - void play(Ts... x) override { + void play(const Ts &...x) override { auto file = this->file_.value(x...); this->parent_->play_mp3(file); } @@ -98,7 +98,7 @@ template class PlayFileAction : public Action, public Par TEMPLATABLE_VALUE(uint16_t, file) TEMPLATABLE_VALUE(bool, loop) - void play(Ts... x) override { + void play(const Ts &...x) override { auto file = this->file_.value(x...); auto loop = this->loop_.value(x...); if (loop) { @@ -115,7 +115,7 @@ template class PlayFolderAction : public Action, public P TEMPLATABLE_VALUE(uint16_t, file) TEMPLATABLE_VALUE(bool, loop) - void play(Ts... x) override { + void play(const Ts &...x) override { auto folder = this->folder_.value(x...); auto file = this->file_.value(x...); auto loop = this->loop_.value(x...); @@ -131,7 +131,7 @@ template class SetDeviceAction : public Action, public Pa public: TEMPLATABLE_VALUE(Device, device) - void play(Ts... x) override { + void play(const Ts &...x) override { auto device = this->device_.value(x...); this->parent_->set_device(device); } @@ -141,7 +141,7 @@ template class SetVolumeAction : public Action, public Pa public: TEMPLATABLE_VALUE(uint8_t, volume) - void play(Ts... x) override { + void play(const Ts &...x) override { auto volume = this->volume_.value(x...); this->parent_->set_volume(volume); } @@ -151,7 +151,7 @@ template class SetEqAction : public Action, public Parent public: TEMPLATABLE_VALUE(EqPreset, eq) - void play(Ts... x) override { + void play(const Ts &...x) override { auto eq = this->eq_.value(x...); this->parent_->set_eq(eq); } @@ -168,7 +168,7 @@ DFPLAYER_SIMPLE_ACTION(VolumeDownAction, volume_down) template class DFPlayerIsPlayingCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->is_playing(); } + bool check(const Ts &...x) override { return this->parent_->is_playing(); } }; class DFPlayerFinishedPlaybackTrigger : public Trigger<> { diff --git a/esphome/components/dfrobot_sen0395/automation.h b/esphome/components/dfrobot_sen0395/automation.h index 3f69e482b7..422555d6eb 100644 --- a/esphome/components/dfrobot_sen0395/automation.h +++ b/esphome/components/dfrobot_sen0395/automation.h @@ -11,7 +11,7 @@ namespace dfrobot_sen0395 { template class DfrobotSen0395ResetAction : public Action, public Parented { public: - void play(Ts... x) { this->parent_->enqueue(make_unique()); } + void play(const Ts &...x) { this->parent_->enqueue(make_unique()); } }; template @@ -33,7 +33,7 @@ class DfrobotSen0395SettingsAction : public Action, public Parentedparent_->enqueue(make_unique(0)); if (this->factory_reset_.has_value() && this->factory_reset_.value(x...) == true) { this->parent_->enqueue(make_unique()); diff --git a/esphome/components/display/__init__.py b/esphome/components/display/__init__.py index 81f2536e96..ccbeedcd2f 100644 --- a/esphome/components/display/__init__.py +++ b/esphome/components/display/__init__.py @@ -12,8 +12,10 @@ from esphome.const import ( CONF_ROTATION, CONF_TO, CONF_TRIGGER_ID, + CONF_UPDATE_INTERVAL, + SCHEDULER_DONT_RUN, ) -from esphome.core import coroutine_with_priority +from esphome.core import CoroPriority, coroutine_with_priority IS_PLATFORM_COMPONENT = True @@ -67,6 +69,18 @@ BASIC_DISPLAY_SCHEMA = cv.Schema( } ).extend(cv.polling_component_schema("1s")) + +def _validate_test_card(config): + if ( + config.get(CONF_SHOW_TEST_CARD, False) + and config.get(CONF_UPDATE_INTERVAL, False) == SCHEDULER_DONT_RUN + ): + raise cv.Invalid( + f"`{CONF_SHOW_TEST_CARD}: True` cannot be used with `{CONF_UPDATE_INTERVAL}: never` because this combination will not show a test_card." + ) + return config + + FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend( { cv.Optional(CONF_ROTATION): validate_rotation, @@ -94,6 +108,7 @@ FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend( cv.Optional(CONF_SHOW_TEST_CARD): cv.boolean, } ) +FULL_DISPLAY_SCHEMA.add_extra(_validate_test_card) async def setup_display_core_(var, config): @@ -161,7 +176,7 @@ async def display_page_show_to_code(config, action_id, template_arg, args): DisplayPageShowNextAction, maybe_simple_id( { - cv.Required(CONF_ID): cv.templatable(cv.use_id(Display)), + cv.GenerateID(CONF_ID): cv.templatable(cv.use_id(Display)), } ), ) @@ -175,7 +190,7 @@ async def display_page_show_next_to_code(config, action_id, template_arg, args): DisplayPageShowPrevAction, maybe_simple_id( { - cv.Required(CONF_ID): cv.templatable(cv.use_id(Display)), + cv.GenerateID(CONF_ID): cv.templatable(cv.use_id(Display)), } ), ) @@ -200,11 +215,10 @@ async def display_is_displaying_page_to_code(config, condition_id, template_arg, page = await cg.get_variable(config[CONF_PAGE_ID]) var = cg.new_Pvariable(condition_id, template_arg, paren) cg.add(var.set_page(page)) - return var -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(display_ns.using) cg.add_define("USE_DISPLAY") diff --git a/esphome/components/display/display.cpp b/esphome/components/display/display.cpp index c666eee298..ebc3c0a9f6 100644 --- a/esphome/components/display/display.cpp +++ b/esphome/components/display/display.cpp @@ -7,7 +7,6 @@ namespace esphome { namespace display { - static const char *const TAG = "display"; const Color COLOR_OFF(0, 0, 0, 0); @@ -16,6 +15,7 @@ const Color COLOR_ON(255, 255, 255, 255); void Display::fill(Color color) { this->filled_rectangle(0, 0, this->get_width(), this->get_height(), color); } void Display::clear() { this->fill(COLOR_OFF); } void Display::set_rotation(DisplayRotation rotation) { this->rotation_ = rotation; } + void HOT Display::line(int x1, int y1, int x2, int y2, Color color) { const int32_t dx = abs(x2 - x1), sx = x1 < x2 ? 1 : -1; const int32_t dy = -abs(y2 - y1), sy = y1 < y2 ? 1 : -1; @@ -91,23 +91,27 @@ void HOT Display::horizontal_line(int x, int y, int width, Color color) { for (int i = x; i < x + width; i++) this->draw_pixel_at(i, y, color); } + void HOT Display::vertical_line(int x, int y, int height, Color color) { // Future: Could be made more efficient by manipulating buffer directly in certain rotations. for (int i = y; i < y + height; i++) this->draw_pixel_at(x, i, color); } + void Display::rectangle(int x1, int y1, int width, int height, Color color) { this->horizontal_line(x1, y1, width, color); this->horizontal_line(x1, y1 + height - 1, width, color); this->vertical_line(x1, y1, height, color); this->vertical_line(x1 + width - 1, y1, height, color); } + void Display::filled_rectangle(int x1, int y1, int width, int height, Color color) { // Future: Use vertical_line and horizontal_line methods depending on rotation to reduce memory accesses. for (int i = y1; i < y1 + height; i++) { this->horizontal_line(x1, i, width, color); } } + void HOT Display::circle(int center_x, int center_xy, int radius, Color color) { int dx = -radius; int dy = 0; @@ -131,6 +135,7 @@ void HOT Display::circle(int center_x, int center_xy, int radius, Color color) { } } while (dx <= 0); } + void Display::filled_circle(int center_x, int center_y, int radius, Color color) { int dx = -int32_t(radius); int dy = 0; @@ -157,6 +162,7 @@ void Display::filled_circle(int center_x, int center_y, int radius, Color color) } } while (dx <= 0); } + void Display::filled_ring(int center_x, int center_y, int radius1, int radius2, Color color) { int rmax = radius1 > radius2 ? radius1 : radius2; int rmin = radius1 < radius2 ? radius1 : radius2; @@ -213,6 +219,7 @@ void Display::filled_ring(int center_x, int center_y, int radius1, int radius2, } } while (dxmax <= 0); } + void Display::filled_gauge(int center_x, int center_y, int radius1, int radius2, int progress, Color color) { int rmax = radius1 > radius2 ? radius1 : radius2; int rmin = radius1 < radius2 ? radius1 : radius2; @@ -228,7 +235,8 @@ void Display::filled_gauge(int center_x, int center_y, int radius1, int radius2, // outer dots this->draw_pixel_at(center_x + dxmax, center_y - dymax, color); this->draw_pixel_at(center_x - dxmax, center_y - dymax, color); - if (dymin < rmin) { // side parts + if (dymin < rmin) { + // side parts int lhline_width = -(dxmax - dxmin) + 1; if (progress >= 50) { if (float(dymax) < float(-dxmax) * tan_a) { @@ -239,7 +247,8 @@ void Display::filled_gauge(int center_x, int center_y, int radius1, int radius2, this->horizontal_line(center_x + dxmax, center_y - dymax, lhline_width, color); // left if (!dymax) this->horizontal_line(center_x - dxmin, center_y, lhline_width, color); // right horizontal border - if (upd_dxmax > -dxmin) { // right + if (upd_dxmax > -dxmin) { + // right int rhline_width = (upd_dxmax + dxmin) + 1; this->horizontal_line(center_x - dxmin, center_y - dymax, rhline_width > lhline_width ? lhline_width : rhline_width, color); @@ -256,7 +265,8 @@ void Display::filled_gauge(int center_x, int center_y, int radius1, int radius2, if (lhline_width > 0) this->horizontal_line(center_x + dxmax, center_y - dymax, lhline_width, color); } - } else { // top part + } else { + // top part int hline_width = 2 * (-dxmax) + 1; if (progress >= 50) { if (dymax < float(-dxmax) * tan_a) { @@ -300,11 +310,13 @@ void Display::filled_gauge(int center_x, int center_y, int radius1, int radius2, } } while (dxmax <= 0); } + void HOT Display::triangle(int x1, int y1, int x2, int y2, int x3, int y3, Color color) { this->line(x1, y1, x2, y2, color); this->line(x1, y1, x3, y3, color); this->line(x2, y2, x3, y3, color); } + void Display::sort_triangle_points_by_y_(int *x1, int *y1, int *x2, int *y2, int *x3, int *y3) { if (*y1 > *y2) { int x_temp = *x1, y_temp = *y1; @@ -322,6 +334,7 @@ void Display::sort_triangle_points_by_y_(int *x1, int *y1, int *x2, int *y2, int *x3 = x_temp, *y3 = y_temp; } } + void Display::filled_flat_side_triangle_(int x1, int y1, int x2, int y2, int x3, int y3, Color color) { // y2 must be equal to y3 (same horizontal line) @@ -333,7 +346,8 @@ void Display::filled_flat_side_triangle_(int x1, int y1, int x2, int y2, int x3, int s1_dy = abs(y2 - y1); int s1_sign_x = ((x2 - x1) >= 0) ? 1 : -1; int s1_sign_y = ((y2 - y1) >= 0) ? 1 : -1; - if (s1_dy > s1_dx) { // swap values + if (s1_dy > s1_dx) { + // swap values int tmp = s1_dx; s1_dx = s1_dy; s1_dy = tmp; @@ -349,7 +363,8 @@ void Display::filled_flat_side_triangle_(int x1, int y1, int x2, int y2, int x3, int s2_dy = abs(y3 - y1); int s2_sign_x = ((x3 - x1) >= 0) ? 1 : -1; int s2_sign_y = ((y3 - y1) >= 0) ? 1 : -1; - if (s2_dy > s2_dx) { // swap values + if (s2_dy > s2_dx) { + // swap values int tmp = s2_dx; s2_dx = s2_dy; s2_dy = tmp; @@ -402,20 +417,25 @@ void Display::filled_flat_side_triangle_(int x1, int y1, int x2, int y2, int x3, } } } + void Display::filled_triangle(int x1, int y1, int x2, int y2, int x3, int y3, Color color) { // Sort the three points by y-coordinate ascending, so [x1,y1] is the topmost point this->sort_triangle_points_by_y_(&x1, &y1, &x2, &y2, &x3, &y3); - if (y2 == y3) { // Check for special case of a bottom-flat triangle + if (y2 == y3) { + // Check for special case of a bottom-flat triangle this->filled_flat_side_triangle_(x1, y1, x2, y2, x3, y3, color); - } else if (y1 == y2) { // Check for special case of a top-flat triangle + } else if (y1 == y2) { + // Check for special case of a top-flat triangle this->filled_flat_side_triangle_(x3, y3, x1, y1, x2, y2, color); - } else { // General case: split the no-flat-side triangle in a top-flat triangle and bottom-flat triangle + } else { + // General case: split the no-flat-side triangle in a top-flat triangle and bottom-flat triangle int x_temp = (int) (x1 + ((float) (y2 - y1) / (float) (y3 - y1)) * (x3 - x1)), y_temp = y2; this->filled_flat_side_triangle_(x1, y1, x2, y2, x_temp, y_temp, color); this->filled_flat_side_triangle_(x3, y3, x2, y2, x_temp, y_temp, color); } } + void HOT Display::get_regular_polygon_vertex(int vertex_id, int *vertex_x, int *vertex_y, int center_x, int center_y, int radius, int edges, RegularPolygonVariation variation, float rotation_degrees) { @@ -447,7 +467,8 @@ void HOT Display::regular_polygon(int x, int y, int radius, int edges, RegularPo int current_vertex_x, current_vertex_y; get_regular_polygon_vertex(current_vertex_id, ¤t_vertex_x, ¤t_vertex_y, x, y, radius, edges, variation, rotation_degrees); - if (current_vertex_id > 0) { // Start drawing after the 2nd vertex coordinates has been calculated + if (current_vertex_id > 0) { + // Start drawing after the 2nd vertex coordinates has been calculated if (drawing == DRAWING_FILLED) { this->filled_triangle(x, y, previous_vertex_x, previous_vertex_y, current_vertex_x, current_vertex_y, color); } else if (drawing == DRAWING_OUTLINE) { @@ -459,21 +480,26 @@ void HOT Display::regular_polygon(int x, int y, int radius, int edges, RegularPo } } } + void HOT Display::regular_polygon(int x, int y, int radius, int edges, RegularPolygonVariation variation, Color color, RegularPolygonDrawing drawing) { regular_polygon(x, y, radius, edges, variation, ROTATION_0_DEGREES, color, drawing); } + void HOT Display::regular_polygon(int x, int y, int radius, int edges, Color color, RegularPolygonDrawing drawing) { regular_polygon(x, y, radius, edges, VARIATION_POINTY_TOP, ROTATION_0_DEGREES, color, drawing); } + void Display::filled_regular_polygon(int x, int y, int radius, int edges, RegularPolygonVariation variation, float rotation_degrees, Color color) { regular_polygon(x, y, radius, edges, variation, rotation_degrees, color, DRAWING_FILLED); } + void Display::filled_regular_polygon(int x, int y, int radius, int edges, RegularPolygonVariation variation, Color color) { regular_polygon(x, y, radius, edges, variation, ROTATION_0_DEGREES, color, DRAWING_FILLED); } + void Display::filled_regular_polygon(int x, int y, int radius, int edges, Color color) { regular_polygon(x, y, radius, edges, VARIATION_POINTY_TOP, ROTATION_0_DEGREES, color, DRAWING_FILLED); } @@ -584,15 +610,19 @@ void Display::get_text_bounds(int x, int y, const char *text, BaseFont *font, Te break; } } + void Display::print(int x, int y, BaseFont *font, Color color, const char *text, Color background) { this->print(x, y, font, color, TextAlign::TOP_LEFT, text, background); } + void Display::print(int x, int y, BaseFont *font, TextAlign align, const char *text) { this->print(x, y, font, COLOR_ON, align, text); } + void Display::print(int x, int y, BaseFont *font, const char *text) { this->print(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, text); } + void Display::printf(int x, int y, BaseFont *font, Color color, Color background, TextAlign align, const char *format, ...) { va_list arg; @@ -600,31 +630,37 @@ void Display::printf(int x, int y, BaseFont *font, Color color, Color background this->vprintf_(x, y, font, color, background, align, format, arg); va_end(arg); } + void Display::printf(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, ...) { va_list arg; va_start(arg, format); this->vprintf_(x, y, font, color, COLOR_OFF, align, format, arg); va_end(arg); } + void Display::printf(int x, int y, BaseFont *font, Color color, const char *format, ...) { va_list arg; va_start(arg, format); this->vprintf_(x, y, font, color, COLOR_OFF, TextAlign::TOP_LEFT, format, arg); va_end(arg); } + void Display::printf(int x, int y, BaseFont *font, TextAlign align, const char *format, ...) { va_list arg; va_start(arg, format); this->vprintf_(x, y, font, COLOR_ON, COLOR_OFF, align, format, arg); va_end(arg); } + void Display::printf(int x, int y, BaseFont *font, const char *format, ...) { va_list arg; va_start(arg, format); this->vprintf_(x, y, font, COLOR_ON, COLOR_OFF, TextAlign::TOP_LEFT, format, arg); va_end(arg); } + void Display::set_writer(display_writer_t &&writer) { this->writer_ = writer; } + void Display::set_pages(std::vector pages) { for (auto *page : pages) page->set_parent(this); @@ -637,6 +673,7 @@ void Display::set_pages(std::vector pages) { pages[pages.size() - 1]->set_next(pages[0]); this->show_page(pages[0]); } + void Display::show_page(DisplayPage *page) { this->previous_page_ = this->page_; this->page_ = page; @@ -645,8 +682,10 @@ void Display::show_page(DisplayPage *page) { t->process(this->previous_page_, this->page_); } } + void Display::show_next_page() { this->page_->show_next(); } void Display::show_prev_page() { this->page_->show_prev(); } + void Display::do_update_() { if (this->auto_clear_enabled_) { this->clear(); @@ -660,10 +699,12 @@ void Display::do_update_() { } this->clear_clipping_(); } + void DisplayOnPageChangeTrigger::process(DisplayPage *from, DisplayPage *to) { if ((this->from_ == nullptr || this->from_ == from) && (this->to_ == nullptr || this->to_ == to)) this->trigger(from, to); } + void Display::strftime(int x, int y, BaseFont *font, Color color, Color background, TextAlign align, const char *format, ESPTime time) { char buffer[64]; @@ -671,15 +712,19 @@ void Display::strftime(int x, int y, BaseFont *font, Color color, Color backgrou if (ret > 0) this->print(x, y, font, color, align, buffer, background); } + void Display::strftime(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, ESPTime time) { this->strftime(x, y, font, color, COLOR_OFF, align, format, time); } + void Display::strftime(int x, int y, BaseFont *font, Color color, const char *format, ESPTime time) { this->strftime(x, y, font, color, COLOR_OFF, TextAlign::TOP_LEFT, format, time); } + void Display::strftime(int x, int y, BaseFont *font, TextAlign align, const char *format, ESPTime time) { this->strftime(x, y, font, COLOR_ON, COLOR_OFF, align, format, time); } + void Display::strftime(int x, int y, BaseFont *font, const char *format, ESPTime time) { this->strftime(x, y, font, COLOR_ON, COLOR_OFF, TextAlign::TOP_LEFT, format, time); } @@ -691,6 +736,7 @@ void Display::start_clipping(Rect rect) { } this->clipping_rectangle_.push_back(rect); } + void Display::end_clipping() { if (this->clipping_rectangle_.empty()) { ESP_LOGE(TAG, "clear: Clipping is not set."); @@ -698,6 +744,7 @@ void Display::end_clipping() { this->clipping_rectangle_.pop_back(); } } + void Display::extend_clipping(Rect add_rect) { if (this->clipping_rectangle_.empty()) { ESP_LOGE(TAG, "add: Clipping is not set."); @@ -705,6 +752,7 @@ void Display::extend_clipping(Rect add_rect) { this->clipping_rectangle_.back().extend(add_rect); } } + void Display::shrink_clipping(Rect add_rect) { if (this->clipping_rectangle_.empty()) { ESP_LOGE(TAG, "add: Clipping is not set."); @@ -712,6 +760,7 @@ void Display::shrink_clipping(Rect add_rect) { this->clipping_rectangle_.back().shrink(add_rect); } } + Rect Display::get_clipping() const { if (this->clipping_rectangle_.empty()) { return Rect(); @@ -719,7 +768,9 @@ Rect Display::get_clipping() const { return this->clipping_rectangle_.back(); } } + void Display::clear_clipping_() { this->clipping_rectangle_.clear(); } + bool Display::clip(int x, int y) { if (x < 0 || x >= this->get_width() || y < 0 || y >= this->get_height()) return false; @@ -727,6 +778,7 @@ bool Display::clip(int x, int y) { return false; return true; } + bool Display::clamp_x_(int x, int w, int &min_x, int &max_x) { min_x = std::max(x, 0); max_x = std::min(x + w, this->get_width()); @@ -742,6 +794,7 @@ bool Display::clamp_x_(int x, int w, int &min_x, int &max_x) { return min_x < max_x; } + bool Display::clamp_y_(int y, int h, int &min_y, int &max_y) { min_y = std::max(y, 0); max_y = std::min(y + h, this->get_height()); @@ -766,16 +819,16 @@ void Display::test_card() { int w = get_width(), h = get_height(), image_w, image_h; this->clear(); this->show_test_card_ = false; + image_w = std::min(w - 20, 310); + image_h = std::min(h - 20, 255); + int shift_x = (w - image_w) / 2; + int shift_y = (h - image_h) / 2; + int line_w = (image_w - 6) / 6; + int image_c = image_w / 2; if (this->get_display_type() == DISPLAY_TYPE_COLOR) { Color r(255, 0, 0), g(0, 255, 0), b(0, 0, 255); - image_w = std::min(w - 20, 310); - image_h = std::min(h - 20, 255); - int shift_x = (w - image_w) / 2; - int shift_y = (h - image_h) / 2; - int line_w = (image_w - 6) / 6; - int image_c = image_w / 2; - for (auto i = 0; i <= image_h; i++) { + for (auto i = 0; i != image_h; i++) { int c = esp_scale(i, image_h); this->horizontal_line(shift_x + 0, shift_y + i, line_w, r.fade_to_white(c)); this->horizontal_line(shift_x + line_w, shift_y + i, line_w, r.fade_to_black(c)); // @@ -786,36 +839,41 @@ void Display::test_card() { this->horizontal_line(shift_x + image_w - (line_w * 2), shift_y + i, line_w, b.fade_to_white(c)); this->horizontal_line(shift_x + image_w - line_w, shift_y + i, line_w, b.fade_to_black(c)); } - this->rectangle(shift_x, shift_y, image_w, image_h, Color(127, 127, 0)); + } + this->rectangle(shift_x, shift_y, image_w, image_h, Color(127, 127, 0)); - uint16_t shift_r = shift_x + line_w - (8 * 3); - uint16_t shift_g = shift_x + image_c - (8 * 3); - uint16_t shift_b = shift_x + image_w - line_w - (8 * 3); - shift_y = h / 2 - (8 * 3); - for (auto i = 0; i < 8; i++) { - uint8_t ftr = progmem_read_byte(&TESTCARD_FONT[0][i]); - uint8_t ftg = progmem_read_byte(&TESTCARD_FONT[1][i]); - uint8_t ftb = progmem_read_byte(&TESTCARD_FONT[2][i]); - for (auto k = 0; k < 8; k++) { - if ((ftr & (1 << k)) != 0) { - this->filled_rectangle(shift_r + (i * 6), shift_y + (k * 6), 6, 6, COLOR_OFF); - } - if ((ftg & (1 << k)) != 0) { - this->filled_rectangle(shift_g + (i * 6), shift_y + (k * 6), 6, 6, COLOR_OFF); - } - if ((ftb & (1 << k)) != 0) { - this->filled_rectangle(shift_b + (i * 6), shift_y + (k * 6), 6, 6, COLOR_OFF); - } + uint16_t shift_r = shift_x + line_w - (8 * 3); + uint16_t shift_g = shift_x + image_c - (8 * 3); + uint16_t shift_b = shift_x + image_w - line_w - (8 * 3); + shift_y = h / 2 - (8 * 3); + for (auto i = 0; i < 8; i++) { + uint8_t ftr = progmem_read_byte(&TESTCARD_FONT[0][i]); + uint8_t ftg = progmem_read_byte(&TESTCARD_FONT[1][i]); + uint8_t ftb = progmem_read_byte(&TESTCARD_FONT[2][i]); + for (auto k = 0; k < 8; k++) { + if ((ftr & (1 << k)) != 0) { + this->filled_rectangle(shift_r + (i * 6), shift_y + (k * 6), 6, 6, COLOR_OFF); + } + if ((ftg & (1 << k)) != 0) { + this->filled_rectangle(shift_g + (i * 6), shift_y + (k * 6), 6, 6, COLOR_OFF); + } + if ((ftb & (1 << k)) != 0) { + this->filled_rectangle(shift_b + (i * 6), shift_y + (k * 6), 6, 6, COLOR_OFF); } } } - this->rectangle(0, 0, w, h, Color(127, 0, 127)); this->filled_rectangle(0, 0, 10, 10, Color(255, 0, 255)); + this->filled_rectangle(w - 10, 0, 10, 10, Color(255, 0, 255)); + this->filled_rectangle(0, h - 10, 10, 10, Color(255, 0, 255)); + this->filled_rectangle(w - 10, h - 10, 10, 10, Color(255, 0, 255)); + this->rectangle(0, 0, w, h, Color(255, 255, 255)); this->stop_poller(); } DisplayPage::DisplayPage(display_writer_t writer) : writer_(std::move(writer)) {} + void DisplayPage::show() { this->parent_->show_page(this); } + void DisplayPage::show_next() { if (this->next_ == nullptr) { ESP_LOGE(TAG, "no next page"); @@ -823,6 +881,7 @@ void DisplayPage::show_next() { } this->next_->show(); } + void DisplayPage::show_prev() { if (this->prev_ == nullptr) { ESP_LOGE(TAG, "no previous page"); @@ -830,6 +889,7 @@ void DisplayPage::show_prev() { } this->prev_->show(); } + void DisplayPage::set_parent(Display *parent) { this->parent_ = parent; } void DisplayPage::set_prev(DisplayPage *prev) { this->prev_ = prev; } void DisplayPage::set_next(DisplayPage *next) { this->next_ = next; } @@ -865,6 +925,5 @@ const LogString *text_align_to_string(TextAlign textalign) { return LOG_STR("UNKNOWN"); } } - } // namespace display } // namespace esphome diff --git a/esphome/components/display/display.h b/esphome/components/display/display.h index f2d79d12d9..47d40915aa 100644 --- a/esphome/components/display/display.h +++ b/esphome/components/display/display.h @@ -176,7 +176,117 @@ class Display; class DisplayPage; class DisplayOnPageChangeTrigger; -using display_writer_t = std::function; +/** Optimized display writer that uses function pointers for stateless lambdas. + * + * Similar to TemplatableValue but specialized for display writer callbacks. + * Saves ~8 bytes per stateless lambda on 32-bit platforms (16 bytes std::function → ~8 bytes discriminator+pointer). + * + * Supports both: + * - Stateless lambdas (from YAML) → function pointer (4 bytes) + * - Stateful lambdas/std::function (from C++ code) → std::function* (heap allocated) + * + * @tparam T The display type (e.g., Display, Nextion, GPIOLCDDisplay) + */ +template class DisplayWriter { + public: + DisplayWriter() : type_(NONE) {} + + // For stateless lambdas (convertible to function pointer): use function pointer (4 bytes) + template + DisplayWriter(F f) requires std::invocable && std::convertible_to + : type_(STATELESS_LAMBDA) { + this->stateless_f_ = f; // Implicit conversion to function pointer + } + + // For stateful lambdas and std::function (not convertible to function pointer): use std::function* (heap allocated) + // This handles backwards compatibility with external components + template + DisplayWriter(F f) requires std::invocable &&(!std::convertible_to) : type_(LAMBDA) { + this->f_ = new std::function(std::move(f)); + } + + // Copy constructor + DisplayWriter(const DisplayWriter &other) : type_(other.type_) { + if (type_ == LAMBDA) { + this->f_ = new std::function(*other.f_); + } else if (type_ == STATELESS_LAMBDA) { + this->stateless_f_ = other.stateless_f_; + } + } + + // Move constructor + DisplayWriter(DisplayWriter &&other) noexcept : type_(other.type_) { + if (type_ == LAMBDA) { + this->f_ = other.f_; + other.f_ = nullptr; + } else if (type_ == STATELESS_LAMBDA) { + this->stateless_f_ = other.stateless_f_; + } + other.type_ = NONE; + } + + // Assignment operators + DisplayWriter &operator=(const DisplayWriter &other) { + if (this != &other) { + this->~DisplayWriter(); + new (this) DisplayWriter(other); + } + return *this; + } + + DisplayWriter &operator=(DisplayWriter &&other) noexcept { + if (this != &other) { + this->~DisplayWriter(); + new (this) DisplayWriter(std::move(other)); + } + return *this; + } + + ~DisplayWriter() { + if (type_ == LAMBDA) { + delete this->f_; + } + // STATELESS_LAMBDA/NONE: no cleanup needed (function pointer or empty) + } + + bool has_value() const { return this->type_ != NONE; } + + void call(T &display) const { + switch (this->type_) { + case STATELESS_LAMBDA: + this->stateless_f_(display); // Direct function pointer call + break; + case LAMBDA: + (*this->f_)(display); // std::function call + break; + case NONE: + default: + break; + } + } + + // Operator() for convenience + void operator()(T &display) const { this->call(display); } + + // Operator* for backwards compatibility with (*writer_)(*this) pattern + DisplayWriter &operator*() { return *this; } + const DisplayWriter &operator*() const { return *this; } + + protected: + enum : uint8_t { + NONE, + LAMBDA, + STATELESS_LAMBDA, + } type_; + + union { + std::function *f_; + void (*stateless_f_)(T &); + }; +}; + +// Type alias for Display writer - uses optimized DisplayWriter instead of std::function +using display_writer_t = DisplayWriter; #define LOG_DISPLAY(prefix, type, obj) \ if ((obj) != nullptr) { \ @@ -210,7 +320,7 @@ class Display : public PollingComponent { /// Fill the entire screen with the given color. virtual void fill(Color color); /// Clear the entire screen by filling it with OFF pixels. - void clear(); + virtual void clear(); /// Get the calculated width of the display in pixels with rotation applied. virtual int get_width() { return this->get_width_internal(); } @@ -678,7 +788,7 @@ class Display : public PollingComponent { void sort_triangle_points_by_y_(int *x1, int *y1, int *x2, int *y2, int *x3, int *y3); DisplayRotation rotation_{DISPLAY_ROTATION_0_DEGREES}; - optional writer_{}; + display_writer_t writer_{}; DisplayPage *page_{nullptr}; DisplayPage *previous_page_{nullptr}; std::vector on_page_change_triggers_; @@ -709,7 +819,7 @@ template class DisplayPageShowAction : public Action { public: TEMPLATABLE_VALUE(DisplayPage *, page) - void play(Ts... x) override { + void play(const Ts &...x) override { auto *page = this->page_.value(x...); if (page != nullptr) { page->show(); @@ -721,7 +831,7 @@ template class DisplayPageShowNextAction : public Action public: DisplayPageShowNextAction(Display *buffer) : buffer_(buffer) {} - void play(Ts... x) override { this->buffer_->show_next_page(); } + void play(const Ts &...x) override { this->buffer_->show_next_page(); } Display *buffer_; }; @@ -730,7 +840,7 @@ template class DisplayPageShowPrevAction : public Action public: DisplayPageShowPrevAction(Display *buffer) : buffer_(buffer) {} - void play(Ts... x) override { this->buffer_->show_prev_page(); } + void play(const Ts &...x) override { this->buffer_->show_prev_page(); } Display *buffer_; }; @@ -740,7 +850,7 @@ template class DisplayIsDisplayingPageCondition : public Conditi DisplayIsDisplayingPageCondition(Display *parent) : parent_(parent) {} void set_page(DisplayPage *page) { this->page_ = page; } - bool check(Ts... x) override { return this->parent_->get_active_page() == this->page_; } + bool check(const Ts &...x) override { return this->parent_->get_active_page() == this->page_; } protected: Display *parent_; diff --git a/esphome/components/display_menu_base/automation.h b/esphome/components/display_menu_base/automation.h index d5394a1e0c..9c64794cef 100644 --- a/esphome/components/display_menu_base/automation.h +++ b/esphome/components/display_menu_base/automation.h @@ -10,7 +10,7 @@ template class UpAction : public Action { public: explicit UpAction(DisplayMenuComponent *menu) : menu_(menu) {} - void play(Ts... x) override { this->menu_->up(); } + void play(const Ts &...x) override { this->menu_->up(); } protected: DisplayMenuComponent *menu_; @@ -20,7 +20,7 @@ template class DownAction : public Action { public: explicit DownAction(DisplayMenuComponent *menu) : menu_(menu) {} - void play(Ts... x) override { this->menu_->down(); } + void play(const Ts &...x) override { this->menu_->down(); } protected: DisplayMenuComponent *menu_; @@ -30,7 +30,7 @@ template class LeftAction : public Action { public: explicit LeftAction(DisplayMenuComponent *menu) : menu_(menu) {} - void play(Ts... x) override { this->menu_->left(); } + void play(const Ts &...x) override { this->menu_->left(); } protected: DisplayMenuComponent *menu_; @@ -40,7 +40,7 @@ template class RightAction : public Action { public: explicit RightAction(DisplayMenuComponent *menu) : menu_(menu) {} - void play(Ts... x) override { this->menu_->right(); } + void play(const Ts &...x) override { this->menu_->right(); } protected: DisplayMenuComponent *menu_; @@ -50,7 +50,7 @@ template class EnterAction : public Action { public: explicit EnterAction(DisplayMenuComponent *menu) : menu_(menu) {} - void play(Ts... x) override { this->menu_->enter(); } + void play(const Ts &...x) override { this->menu_->enter(); } protected: DisplayMenuComponent *menu_; @@ -60,7 +60,7 @@ template class ShowAction : public Action { public: explicit ShowAction(DisplayMenuComponent *menu) : menu_(menu) {} - void play(Ts... x) override { this->menu_->show(); } + void play(const Ts &...x) override { this->menu_->show(); } protected: DisplayMenuComponent *menu_; @@ -70,7 +70,7 @@ template class HideAction : public Action { public: explicit HideAction(DisplayMenuComponent *menu) : menu_(menu) {} - void play(Ts... x) override { this->menu_->hide(); } + void play(const Ts &...x) override { this->menu_->hide(); } protected: DisplayMenuComponent *menu_; @@ -80,7 +80,7 @@ template class ShowMainAction : public Action { public: explicit ShowMainAction(DisplayMenuComponent *menu) : menu_(menu) {} - void play(Ts... x) override { this->menu_->show_main(); } + void play(const Ts &...x) override { this->menu_->show_main(); } protected: DisplayMenuComponent *menu_; @@ -88,7 +88,7 @@ template class ShowMainAction : public Action { template class IsActiveCondition : public Condition { public: explicit IsActiveCondition(DisplayMenuComponent *menu) : menu_(menu) {} - bool check(Ts... x) override { return this->menu_->is_active(); } + bool check(const Ts &...x) override { return this->menu_->is_active(); } protected: DisplayMenuComponent *menu_; diff --git a/esphome/components/display_menu_base/menu_item.cpp b/esphome/components/display_menu_base/menu_item.cpp index 2c7f34c493..8224adf3fe 100644 --- a/esphome/components/display_menu_base/menu_item.cpp +++ b/esphome/components/display_menu_base/menu_item.cpp @@ -42,7 +42,7 @@ std::string MenuItemSelect::get_value_text() const { result = this->value_getter_.value()(this); } else { if (this->select_var_ != nullptr) { - result = this->select_var_->state; + result = this->select_var_->current_option(); } } diff --git a/esphome/components/ds1307/ds1307.cpp b/esphome/components/ds1307/ds1307.cpp index 077db497b1..adbd7b5487 100644 --- a/esphome/components/ds1307/ds1307.cpp +++ b/esphome/components/ds1307/ds1307.cpp @@ -23,7 +23,7 @@ void DS1307Component::dump_config() { if (this->is_failed()) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); } - ESP_LOGCONFIG(TAG, " Timezone: '%s'", this->timezone_.c_str()); + RealTimeClock::dump_config(); } float DS1307Component::get_setup_priority() const { return setup_priority::DATA; } diff --git a/esphome/components/ds1307/ds1307.h b/esphome/components/ds1307/ds1307.h index 2e9ac2275c..f7f06253b7 100644 --- a/esphome/components/ds1307/ds1307.h +++ b/esphome/components/ds1307/ds1307.h @@ -59,12 +59,12 @@ class DS1307Component : public time::RealTimeClock, public i2c::I2CDevice { template class WriteAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->write_time(); } + void play(const Ts &...x) override { this->parent_->write_time(); } }; template class ReadAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->read_time(); } + void play(const Ts &...x) override { this->parent_->read_time(); } }; } // namespace ds1307 } // namespace esphome diff --git a/esphome/components/duty_time/duty_time_sensor.cpp b/esphome/components/duty_time/duty_time_sensor.cpp index c7319f7c33..f77f1fcf53 100644 --- a/esphome/components/duty_time/duty_time_sensor.cpp +++ b/esphome/components/duty_time/duty_time_sensor.cpp @@ -41,7 +41,7 @@ void DutyTimeSensor::setup() { uint32_t seconds = 0; if (this->restore_) { - this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); + this->pref_ = global_preferences->make_preference(this->get_preference_hash()); this->pref_.load(&seconds); } diff --git a/esphome/components/duty_time/duty_time_sensor.h b/esphome/components/duty_time/duty_time_sensor.h index 18280f8e21..d9fb2a6d60 100644 --- a/esphome/components/duty_time/duty_time_sensor.h +++ b/esphome/components/duty_time/duty_time_sensor.h @@ -51,15 +51,15 @@ class DutyTimeSensor : public sensor::Sensor, public PollingComponent { template class BaseAction : public Action, public Parented {}; template class StartAction : public BaseAction { - void play(Ts... x) override { this->parent_->start(); } + void play(const Ts &...x) override { this->parent_->start(); } }; template class StopAction : public BaseAction { - void play(Ts... x) override { this->parent_->stop(); } + void play(const Ts &...x) override { this->parent_->stop(); } }; template class ResetAction : public BaseAction { - void play(Ts... x) override { this->parent_->reset(); } + void play(const Ts &...x) override { this->parent_->reset(); } }; template class RunningCondition : public Condition, public Parented { @@ -67,7 +67,7 @@ template class RunningCondition : public Condition, publi explicit RunningCondition(DutyTimeSensor *parent, bool state) : Parented(parent), state_(state) {} protected: - bool check(Ts... x) override { return this->parent_->is_running() == this->state_; } + bool check(const Ts &...x) override { return this->parent_->is_running() == this->state_; } bool state_; }; diff --git a/esphome/components/e131/e131.cpp b/esphome/components/e131/e131.cpp index a74fc9be4a..c10c88faf2 100644 --- a/esphome/components/e131/e131.cpp +++ b/esphome/components/e131/e131.cpp @@ -3,6 +3,8 @@ #include "e131_addressable_light_effect.h" #include "esphome/core/log.h" +#include + namespace esphome { namespace e131 { @@ -76,14 +78,14 @@ void E131Component::loop() { } void E131Component::add_effect(E131AddressableLightEffect *light_effect) { - if (light_effects_.count(light_effect)) { + if (std::find(light_effects_.begin(), light_effects_.end(), light_effect) != light_effects_.end()) { return; } - ESP_LOGD(TAG, "Registering '%s' for universes %d-%d.", light_effect->get_name().c_str(), - light_effect->get_first_universe(), light_effect->get_last_universe()); + ESP_LOGD(TAG, "Registering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(), + light_effect->get_last_universe()); - light_effects_.insert(light_effect); + light_effects_.push_back(light_effect); for (auto universe = light_effect->get_first_universe(); universe <= light_effect->get_last_universe(); ++universe) { join_(universe); @@ -91,14 +93,17 @@ void E131Component::add_effect(E131AddressableLightEffect *light_effect) { } void E131Component::remove_effect(E131AddressableLightEffect *light_effect) { - if (!light_effects_.count(light_effect)) { + auto it = std::find(light_effects_.begin(), light_effects_.end(), light_effect); + if (it == light_effects_.end()) { return; } - ESP_LOGD(TAG, "Unregistering '%s' for universes %d-%d.", light_effect->get_name().c_str(), - light_effect->get_first_universe(), light_effect->get_last_universe()); + ESP_LOGD(TAG, "Unregistering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(), + light_effect->get_last_universe()); - light_effects_.erase(light_effect); + // Swap with last element and pop for O(1) removal (order doesn't matter) + *it = light_effects_.back(); + light_effects_.pop_back(); for (auto universe = light_effect->get_first_universe(); universe <= light_effect->get_last_universe(); ++universe) { leave_(universe); diff --git a/esphome/components/e131/e131.h b/esphome/components/e131/e131.h index d0e38fa98c..831138a545 100644 --- a/esphome/components/e131/e131.h +++ b/esphome/components/e131/e131.h @@ -7,7 +7,6 @@ #include #include #include -#include #include namespace esphome { @@ -47,9 +46,8 @@ class E131Component : public esphome::Component { E131ListenMethod listen_method_{E131_MULTICAST}; std::unique_ptr socket_; - std::set light_effects_; + std::vector light_effects_; std::map universe_consumers_; - std::map universe_packets_; }; } // namespace e131 diff --git a/esphome/components/e131/e131_addressable_light_effect.cpp b/esphome/components/e131/e131_addressable_light_effect.cpp index 4d1f98ab6c..780e181f04 100644 --- a/esphome/components/e131/e131_addressable_light_effect.cpp +++ b/esphome/components/e131/e131_addressable_light_effect.cpp @@ -9,7 +9,7 @@ namespace e131 { static const char *const TAG = "e131_addressable_light_effect"; static const int MAX_DATA_SIZE = (sizeof(E131Packet::values) - 1); -E131AddressableLightEffect::E131AddressableLightEffect(const std::string &name) : AddressableLightEffect(name) {} +E131AddressableLightEffect::E131AddressableLightEffect(const char *name) : AddressableLightEffect(name) {} int E131AddressableLightEffect::get_data_per_universe() const { return get_lights_per_universe() * channels_; } @@ -58,8 +58,8 @@ bool E131AddressableLightEffect::process_(int universe, const E131Packet &packet std::min(it->size(), std::min(output_offset + get_lights_per_universe(), output_offset + packet.count - 1)); auto *input_data = packet.values + 1; - ESP_LOGV(TAG, "Applying data for '%s' on %d universe, for %" PRId32 "-%d.", get_name().c_str(), universe, - output_offset, output_end); + ESP_LOGV(TAG, "Applying data for '%s' on %d universe, for %" PRId32 "-%d.", get_name(), universe, output_offset, + output_end); switch (channels_) { case E131_MONO: diff --git a/esphome/components/e131/e131_addressable_light_effect.h b/esphome/components/e131/e131_addressable_light_effect.h index 17d7bd2829..381e08163b 100644 --- a/esphome/components/e131/e131_addressable_light_effect.h +++ b/esphome/components/e131/e131_addressable_light_effect.h @@ -13,7 +13,7 @@ enum E131LightChannels { E131_MONO = 1, E131_RGB = 3, E131_RGBW = 4 }; class E131AddressableLightEffect : public light::AddressableLightEffect { public: - E131AddressableLightEffect(const std::string &name); + E131AddressableLightEffect(const char *name); void start() override; void stop() override; diff --git a/esphome/components/ee895/ee895.cpp b/esphome/components/ee895/ee895.cpp index 3a8a9b3725..c6eaf4e728 100644 --- a/esphome/components/ee895/ee895.cpp +++ b/esphome/components/ee895/ee895.cpp @@ -83,7 +83,7 @@ void EE895Component::write_command_(uint16_t addr, uint16_t reg_cnt) { crc16 = calc_crc16_(address, 6); address[5] = crc16 & 0xFF; address[6] = (crc16 >> 8) & 0xFF; - this->write(address, 7, true); + this->write(address, 7); } float EE895Component::read_float_() { diff --git a/esphome/components/ektf2232/touchscreen/__init__.py b/esphome/components/ektf2232/touchscreen/__init__.py index 7d946fdcb9..123f03ca08 100644 --- a/esphome/components/ektf2232/touchscreen/__init__.py +++ b/esphome/components/ektf2232/touchscreen/__init__.py @@ -2,7 +2,7 @@ from esphome import pins import esphome.codegen as cg from esphome.components import i2c, touchscreen import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_INTERRUPT_PIN +from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_RESET_PIN CODEOWNERS = ["@jesserockz"] DEPENDENCIES = ["i2c"] @@ -15,7 +15,7 @@ EKTF2232Touchscreen = ektf2232_ns.class_( ) CONF_EKTF2232_ID = "ektf2232_id" -CONF_RTS_PIN = "rts_pin" +CONF_RTS_PIN = "rts_pin" # To be removed before 2026.4.0 CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( cv.Schema( @@ -24,7 +24,10 @@ CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( cv.Required(CONF_INTERRUPT_PIN): cv.All( pins.internal_gpio_input_pin_schema ), - cv.Required(CONF_RTS_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_RESET_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_RTS_PIN): cv.invalid( + f"{CONF_RTS_PIN} has been renamed to {CONF_RESET_PIN}" + ), } ).extend(i2c.i2c_device_schema(0x15)) ) @@ -37,5 +40,5 @@ async def to_code(config): interrupt_pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN]) cg.add(var.set_interrupt_pin(interrupt_pin)) - rts_pin = await cg.gpio_pin_expression(config[CONF_RTS_PIN]) - cg.add(var.set_rts_pin(rts_pin)) + reset_pin = await cg.gpio_pin_expression(config[CONF_RESET_PIN]) + cg.add(var.set_reset_pin(reset_pin)) diff --git a/esphome/components/ektf2232/touchscreen/ektf2232.cpp b/esphome/components/ektf2232/touchscreen/ektf2232.cpp index 1dacee6a57..63ebb2166b 100644 --- a/esphome/components/ektf2232/touchscreen/ektf2232.cpp +++ b/esphome/components/ektf2232/touchscreen/ektf2232.cpp @@ -21,7 +21,7 @@ void EKTF2232Touchscreen::setup() { this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE); - this->rts_pin_->setup(); + this->reset_pin_->setup(); this->hard_reset_(); if (!this->soft_reset_()) { @@ -98,9 +98,9 @@ bool EKTF2232Touchscreen::get_power_state() { } void EKTF2232Touchscreen::hard_reset_() { - this->rts_pin_->digital_write(false); + this->reset_pin_->digital_write(false); delay(15); - this->rts_pin_->digital_write(true); + this->reset_pin_->digital_write(true); delay(15); } @@ -127,7 +127,7 @@ void EKTF2232Touchscreen::dump_config() { ESP_LOGCONFIG(TAG, "EKT2232 Touchscreen:"); LOG_I2C_DEVICE(this); LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); - LOG_PIN(" RTS Pin: ", this->rts_pin_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); } } // namespace ektf2232 diff --git a/esphome/components/ektf2232/touchscreen/ektf2232.h b/esphome/components/ektf2232/touchscreen/ektf2232.h index e9288d0a27..2ddc60851f 100644 --- a/esphome/components/ektf2232/touchscreen/ektf2232.h +++ b/esphome/components/ektf2232/touchscreen/ektf2232.h @@ -17,7 +17,7 @@ class EKTF2232Touchscreen : public Touchscreen, public i2c::I2CDevice { void dump_config() override; void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } - void set_rts_pin(GPIOPin *pin) { this->rts_pin_ = pin; } + void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; } void set_power_state(bool enable); bool get_power_state(); @@ -28,7 +28,7 @@ class EKTF2232Touchscreen : public Touchscreen, public i2c::I2CDevice { void update_touches() override; InternalGPIOPin *interrupt_pin_; - GPIOPin *rts_pin_; + GPIOPin *reset_pin_; }; } // namespace ektf2232 diff --git a/esphome/components/epaper_spi/__init__.py b/esphome/components/epaper_spi/__init__.py new file mode 100644 index 0000000000..f70ffa9520 --- /dev/null +++ b/esphome/components/epaper_spi/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@esphome/core"] diff --git a/esphome/components/epaper_spi/display.py b/esphome/components/epaper_spi/display.py new file mode 100644 index 0000000000..ff5693c206 --- /dev/null +++ b/esphome/components/epaper_spi/display.py @@ -0,0 +1,230 @@ +import importlib +import pkgutil + +from esphome import core, pins +import esphome.codegen as cg +from esphome.components import display, spi +from esphome.components.display import CONF_SHOW_TEST_CARD, validate_rotation +from esphome.components.mipi import flatten_sequence, map_sequence +import esphome.config_validation as cv +from esphome.config_validation import update_interval +from esphome.const import ( + CONF_BUSY_PIN, + CONF_CS_PIN, + CONF_DATA_RATE, + CONF_DC_PIN, + CONF_DIMENSIONS, + CONF_ENABLE_PIN, + CONF_FULL_UPDATE_EVERY, + CONF_HEIGHT, + CONF_ID, + CONF_INIT_SEQUENCE, + CONF_LAMBDA, + CONF_MIRROR_X, + CONF_MIRROR_Y, + CONF_MODEL, + CONF_PAGES, + CONF_RESET_DURATION, + CONF_RESET_PIN, + CONF_ROTATION, + CONF_SWAP_XY, + CONF_TRANSFORM, + CONF_UPDATE_INTERVAL, + CONF_WIDTH, +) +from esphome.cpp_generator import RawExpression +from esphome.final_validate import full_config + +from . import models + +AUTO_LOAD = ["split_buffer"] +DEPENDENCIES = ["spi"] + +CONF_INIT_SEQUENCE_ID = "init_sequence_id" + +epaper_spi_ns = cg.esphome_ns.namespace("epaper_spi") +EPaperBase = epaper_spi_ns.class_( + "EPaperBase", cg.PollingComponent, spi.SPIDevice, display.Display +) +Transform = epaper_spi_ns.enum("Transform") + +EPaperSpectraE6 = epaper_spi_ns.class_("EPaperSpectraE6", EPaperBase) +EPaper7p3InSpectraE6 = epaper_spi_ns.class_("EPaper7p3InSpectraE6", EPaperSpectraE6) + + +# Import all models dynamically from the models package +for module_info in pkgutil.iter_modules(models.__path__): + importlib.import_module(f".models.{module_info.name}", package=__package__) + +MODELS = models.EpaperModel.models + +DIMENSION_SCHEMA = cv.Schema( + { + cv.Required(CONF_WIDTH): cv.int_, + cv.Required(CONF_HEIGHT): cv.int_, + } +) + +TRANSFORM_OPTIONS = {CONF_MIRROR_X, CONF_MIRROR_Y, CONF_SWAP_XY} + + +def model_schema(config): + model = MODELS[config[CONF_MODEL]] + class_name = epaper_spi_ns.class_(model.class_name, EPaperBase) + cv_dimensions = cv.Optional if model.get_default(CONF_WIDTH) else cv.Required + return ( + display.FULL_DISPLAY_SCHEMA.extend( + spi.spi_device_schema( + cs_pin_required=False, + default_mode="MODE0", + default_data_rate=model.get_default(CONF_DATA_RATE, 10_000_000), + ) + ) + .extend( + { + model.option(pin): pins.gpio_output_pin_schema + for pin in (CONF_RESET_PIN, CONF_CS_PIN, CONF_BUSY_PIN) + } + ) + .extend( + { + cv.Optional(CONF_ROTATION, default=0): validate_rotation, + cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True), + cv.Optional( + CONF_UPDATE_INTERVAL, default=cv.UNDEFINED + ): update_interval, + cv.Optional(CONF_TRANSFORM): cv.Schema( + { + cv.Required(CONF_MIRROR_X): cv.boolean, + cv.Required(CONF_MIRROR_Y): cv.boolean, + } + ), + cv.Optional(CONF_FULL_UPDATE_EVERY, default=1): cv.int_range(1, 255), + model.option(CONF_DC_PIN, fallback=None): pins.gpio_output_pin_schema, + cv.GenerateID(): cv.declare_id(class_name), + cv.GenerateID(CONF_INIT_SEQUENCE_ID): cv.declare_id(cg.uint8), + cv_dimensions(CONF_DIMENSIONS): DIMENSION_SCHEMA, + model.option(CONF_ENABLE_PIN): cv.ensure_list( + pins.gpio_output_pin_schema + ), + model.option(CONF_INIT_SEQUENCE, cv.UNDEFINED): cv.ensure_list( + map_sequence + ), + model.option(CONF_RESET_DURATION, cv.UNDEFINED): cv.All( + cv.positive_time_period_milliseconds, + cv.Range(max=core.TimePeriod(milliseconds=500)), + ), + } + ) + ) + + +def customise_schema(config): + """ + Create a customised config schema for a specific model and validate the configuration. + :param config: The configuration dictionary to validate + :return: The validated configuration dictionary + :raises cv.Invalid: If the configuration is invalid + """ + config = cv.Schema( + { + cv.Required(CONF_MODEL): cv.one_of(*MODELS, upper=True, space="-"), + }, + extra=cv.ALLOW_EXTRA, + )(config) + return model_schema(config)(config) + + +CONFIG_SCHEMA = customise_schema + + +def _final_validate(config): + spi.final_validate_device_schema( + "epaper_spi", require_miso=False, require_mosi=True + )(config) + + global_config = full_config.get() + from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN + + if CONF_LAMBDA not in config and CONF_PAGES not in config: + if LVGL_DOMAIN in global_config: + if CONF_UPDATE_INTERVAL not in config: + config[CONF_UPDATE_INTERVAL] = update_interval("never") + else: + # If no drawing methods are configured, and LVGL is not enabled, show a test card + config[CONF_SHOW_TEST_CARD] = True + config[CONF_UPDATE_INTERVAL] = core.TimePeriod( + seconds=60 + ).total_milliseconds + return config + + +FINAL_VALIDATE_SCHEMA = _final_validate + + +async def to_code(config): + model = MODELS[config[CONF_MODEL]] + + init_sequence = config.get(CONF_INIT_SEQUENCE) + if init_sequence is None: + init_sequence = model.get_init_sequence(config) + init_sequence = flatten_sequence(init_sequence) + init_sequence_length = len(init_sequence) + init_sequence_id = cg.static_const_array( + config[CONF_INIT_SEQUENCE_ID], init_sequence + ) + width, height = model.get_dimensions(config) + var = cg.new_Pvariable( + config[CONF_ID], + model.name, + width, + height, + init_sequence_id, + init_sequence_length, + ) + + # Rotation is handled by setting the transform + display_config = {k: v for k, v in config.items() if k != CONF_ROTATION} + await display.register_display(var, display_config) + await spi.register_spi_device(var, config) + + dc = await cg.gpio_pin_expression(config[CONF_DC_PIN]) + cg.add(var.set_dc_pin(dc)) + + if CONF_LAMBDA in config: + lambda_ = await cg.process_lambda( + config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void + ) + cg.add(var.set_writer(lambda_)) + if reset_pin := config.get(CONF_RESET_PIN): + reset = await cg.gpio_pin_expression(reset_pin) + cg.add(var.set_reset_pin(reset)) + if busy_pin := config.get(CONF_BUSY_PIN): + busy = await cg.gpio_pin_expression(busy_pin) + cg.add(var.set_busy_pin(busy)) + cg.add(var.set_full_update_every(config[CONF_FULL_UPDATE_EVERY])) + if CONF_RESET_DURATION in config: + cg.add(var.set_reset_duration(config[CONF_RESET_DURATION])) + if transform := config.get(CONF_TRANSFORM): + transform[CONF_SWAP_XY] = False + else: + transform = {x: model.get_default(x, False) for x in TRANSFORM_OPTIONS} + rotation = config[CONF_ROTATION] + if rotation == 180: + transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X] + transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y] + elif rotation == 90: + transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY] + transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X] + elif rotation == 270: + transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY] + transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y] + transform_str = "|".join( + { + str(getattr(Transform, x.upper())) + for x in TRANSFORM_OPTIONS + if transform.get(x) + } + ) + if transform_str: + cg.add(var.set_transform(RawExpression(transform_str))) diff --git a/esphome/components/epaper_spi/epaper_spi.cpp b/esphome/components/epaper_spi/epaper_spi.cpp new file mode 100644 index 0000000000..f6313d33ef --- /dev/null +++ b/esphome/components/epaper_spi/epaper_spi.cpp @@ -0,0 +1,346 @@ +#include "epaper_spi.h" +#include +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome::epaper_spi { + +static const char *const TAG = "epaper_spi"; + +static constexpr const char *const EPAPER_STATE_STRINGS[] = { + "IDLE", "UPDATE", "RESET", "RESET_END", "SHOULD_WAIT", "INITIALISE", + "TRANSFER_DATA", "POWER_ON", "REFRESH_SCREEN", "POWER_OFF", "DEEP_SLEEP", +}; + +const char *EPaperBase::epaper_state_to_string_() { + if (auto idx = static_cast(this->state_); idx < std::size(EPAPER_STATE_STRINGS)) + return EPAPER_STATE_STRINGS[idx]; + return "Unknown"; +} + +void EPaperBase::setup() { + if (!this->init_buffer_(this->buffer_length_)) { + this->mark_failed(LOG_STR("Failed to initialise buffer")); + return; + } + this->setup_pins_(); + this->spi_setup(); +} + +bool EPaperBase::init_buffer_(size_t buffer_length) { + if (!this->buffer_.init(buffer_length)) { + return false; + } + this->clear(); + return true; +} + +void EPaperBase::setup_pins_() const { + this->dc_pin_->setup(); // OUTPUT + this->dc_pin_->digital_write(false); + + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); // OUTPUT + this->reset_pin_->digital_write(true); + } + + if (this->busy_pin_ != nullptr) { + this->busy_pin_->setup(); // INPUT + } +} + +float EPaperBase::get_setup_priority() const { return setup_priority::PROCESSOR; } + +void EPaperBase::command(uint8_t value) { + this->start_command_(); + this->write_byte(value); + this->end_command_(); +} + +void EPaperBase::data(uint8_t value) { + this->start_data_(); + this->write_byte(value); + this->end_data_(); +} + +// write a command followed by zero or more bytes of data. +// The command is the first byte, length is the length of data only in the second byte, followed by the data. +// [COMMAND, LENGTH, DATA...] +void EPaperBase::cmd_data(uint8_t command, const uint8_t *ptr, size_t length) { + ESP_LOGV(TAG, "Command: 0x%02X, Length: %d, Data: %s", command, length, + format_hex_pretty(ptr, length, '.', false).c_str()); + + this->dc_pin_->digital_write(false); + this->enable(); + this->write_byte(command); + if (length > 0) { + this->dc_pin_->digital_write(true); + this->write_array(ptr, length); + } + this->disable(); +} + +bool EPaperBase::is_idle_() const { + if (this->busy_pin_ == nullptr) { + return true; + } + return !this->busy_pin_->digital_read(); +} + +bool EPaperBase::reset() { + if (this->reset_pin_ != nullptr) { + if (this->state_ == EPaperState::RESET) { + this->reset_pin_->digital_write(false); + return false; + } + this->reset_pin_->digital_write(true); + } + return true; +} + +void EPaperBase::update() { + if (this->state_ != EPaperState::IDLE) { + ESP_LOGE(TAG, "Display already in state %s", epaper_state_to_string_()); + return; + } + this->set_state_(EPaperState::UPDATE); + this->enable_loop(); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG + this->update_start_time_ = millis(); +#endif +} + +void EPaperBase::wait_for_idle_(bool should_wait) { +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + this->waiting_for_idle_start_ = millis(); +#endif + this->waiting_for_idle_ = should_wait; +} + +/** + * Called during the loop task. + * First defer for any pending delays, then check if we are waiting for the display to become idle. + * If not waiting for idle, process the state machine. + */ + +void EPaperBase::loop() { + auto now = millis(); + if (this->delay_until_ != 0) { + // using modulus arithmetic to handle wrap-around + int diff = now - this->delay_until_; + if (diff < 0) { + return; + } + this->delay_until_ = 0; + } + if (this->waiting_for_idle_) { + if (this->is_idle_()) { + this->waiting_for_idle_ = false; +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + ESP_LOGV(TAG, "Screen was busy for %u ms", (unsigned) (millis() - this->waiting_for_idle_start_)); +#endif + } else { +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + if (now - this->waiting_for_idle_last_print_ >= 1000) { + ESP_LOGV(TAG, "Waiting for idle in state %s", this->epaper_state_to_string_()); + this->waiting_for_idle_last_print_ = millis(); + } +#endif + return; + } + } + this->process_state_(); +} + +/** + * Process the state machine. + * Typical state sequence: + * IDLE -> RESET -> RESET_END -> UPDATE -> INITIALISE -> TRANSFER_DATA -> POWER_ON -> REFRESH_SCREEN -> POWER_OFF -> + * DEEP_SLEEP -> IDLE + * + * Should a subclassed class need to override this, the method will need to be made virtual. + */ +void EPaperBase::process_state_() { + ESP_LOGV(TAG, "Process state entered in state %s", epaper_state_to_string_()); + switch (this->state_) { + default: + ESP_LOGE(TAG, "Display is in unhandled state %s", epaper_state_to_string_()); + this->set_state_(EPaperState::IDLE); + break; + case EPaperState::IDLE: + this->disable_loop(); + break; + case EPaperState::RESET: + case EPaperState::RESET_END: + if (this->reset()) { + this->set_state_(EPaperState::INITIALISE); + } else { + this->set_state_(EPaperState::RESET_END, this->reset_duration_); + } + break; + case EPaperState::UPDATE: + this->do_update_(); // Calls ESPHome (current page) lambda + if (this->x_high_ < this->x_low_ || this->y_high_ < this->y_low_) { + this->set_state_(EPaperState::IDLE); + return; + } + this->set_state_(EPaperState::RESET); + break; + case EPaperState::INITIALISE: + this->initialise_(); + this->set_state_(EPaperState::TRANSFER_DATA); + break; + case EPaperState::TRANSFER_DATA: + if (!this->transfer_data()) { + return; // Not done yet, come back next loop + } + this->x_low_ = this->width_; + this->x_high_ = 0; + this->y_low_ = this->height_; + this->y_high_ = 0; + this->set_state_(EPaperState::POWER_ON); + break; + case EPaperState::POWER_ON: + this->power_on(); + this->set_state_(EPaperState::REFRESH_SCREEN); + break; + case EPaperState::REFRESH_SCREEN: + this->refresh_screen(this->update_count_ != 0); + this->update_count_ = (this->update_count_ + 1) % this->full_update_every_; + this->set_state_(EPaperState::POWER_OFF); + break; + case EPaperState::POWER_OFF: + this->power_off(); + this->set_state_(EPaperState::DEEP_SLEEP); + break; + case EPaperState::DEEP_SLEEP: + this->deep_sleep(); + this->set_state_(EPaperState::IDLE); + ESP_LOGD(TAG, "Display update took %" PRIu32 " ms", millis() - this->update_start_time_); + break; + } +} + +void EPaperBase::set_state_(EPaperState state, uint16_t delay) { + ESP_LOGV(TAG, "Exit state %s", this->epaper_state_to_string_()); + this->state_ = state; + this->wait_for_idle_(state > EPaperState::SHOULD_WAIT); + if (delay != 0) { + this->delay_until_ = millis() + delay; + } else { + this->delay_until_ = 0; + } + ESP_LOGV(TAG, "Enter state %s, delay %u, wait_for_idle=%s", this->epaper_state_to_string_(), delay, + TRUEFALSE(this->waiting_for_idle_)); + if (state == EPaperState::IDLE) { + this->disable_loop(); + } +} + +void EPaperBase::start_command_() { + this->dc_pin_->digital_write(false); + this->enable(); +} + +void EPaperBase::end_command_() { this->disable(); } + +void EPaperBase::start_data_() { + this->dc_pin_->digital_write(true); + this->enable(); +} +void EPaperBase::end_data_() { this->disable(); } + +void EPaperBase::on_safe_shutdown() { this->deep_sleep(); } + +void EPaperBase::initialise_() { + size_t index = 0; + + auto *sequence = this->init_sequence_; + auto length = this->init_sequence_length_; + while (index != length) { + if (length - index < 2) { + this->mark_failed(LOG_STR("Malformed init sequence")); + return; + } + const uint8_t cmd = sequence[index++]; + if (const uint8_t x = sequence[index++]; x == DELAY_FLAG) { + ESP_LOGV(TAG, "Delay %dms", cmd); + delay(cmd); + } else { + const uint8_t num_args = x & 0x7F; + if (length - index < num_args) { + ESP_LOGE(TAG, "Malformed init sequence, cmd = %X, num_args = %u", cmd, num_args); + this->mark_failed(); + return; + } + this->cmd_data(cmd, sequence + index, num_args); + index += num_args; + } + } +} + +/** + * Check and rotate coordinates based on the transform flags. + * @param x + * @param y + * @return false if the coordinates are out of bounds + */ +bool EPaperBase::rotate_coordinates_(int &x, int &y) const { + if (!this->get_clipping().inside(x, y)) + return false; + if (this->transform_ & SWAP_XY) + std::swap(x, y); + if (this->transform_ & MIRROR_X) + x = this->width_ - x - 1; + if (this->transform_ & MIRROR_Y) + y = this->height_ - y - 1; + if (x >= this->width_ || y >= this->height_ || x < 0 || y < 0) + return false; + return true; +} + +/** + * Default implementation for monochrome displays where 8 pixels are packed to a byte. + * @param x + * @param y + * @param color + */ +void HOT EPaperBase::draw_pixel_at(int x, int y, Color color) { + if (!rotate_coordinates_(x, y)) + return; + const size_t pixel_position = y * this->width_ + x; + const size_t byte_position = pixel_position / 8; + const uint8_t bit_position = pixel_position % 8; + const uint8_t pixel_bit = 0x80 >> bit_position; + const auto original = this->buffer_[byte_position]; + if ((color_to_bit(color) == 0)) { + this->buffer_[byte_position] = original & ~pixel_bit; + } else { + this->buffer_[byte_position] = original | pixel_bit; + } + this->x_low_ = clamp_at_most(this->x_low_, x); + this->x_high_ = clamp_at_least(this->x_high_, x + 1); + this->y_low_ = clamp_at_most(this->y_low_, y); + this->y_high_ = clamp_at_least(this->y_high_, y + 1); +} + +void EPaperBase::dump_config() { + LOG_DISPLAY("", "E-Paper SPI", this); + ESP_LOGCONFIG(TAG, " Model: %s", this->name_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Busy Pin: ", this->busy_pin_); + LOG_PIN(" CS Pin: ", this->cs_); + LOG_UPDATE_INTERVAL(this); + ESP_LOGCONFIG(TAG, + " SPI Data Rate: %uMHz\n" + " Full update every: %d\n" + " Swap X/Y: %s\n" + " Mirror X: %s\n" + " Mirror Y: %s", + (unsigned) (this->data_rate_ / 1000000), this->full_update_every_, YESNO(this->transform_ & SWAP_XY), + YESNO(this->transform_ & MIRROR_X), YESNO(this->transform_ & MIRROR_Y)); +} + +} // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/epaper_spi.h b/esphome/components/epaper_spi/epaper_spi.h new file mode 100644 index 0000000000..544ea3e9ba --- /dev/null +++ b/esphome/components/epaper_spi/epaper_spi.h @@ -0,0 +1,181 @@ +#pragma once + +#include "esphome/components/display/display_buffer.h" +#include "esphome/components/spi/spi.h" +#include "esphome/components/split_buffer/split_buffer.h" +#include "esphome/core/component.h" + +namespace esphome::epaper_spi { +using namespace display; + +enum class EPaperState : uint8_t { + IDLE, // not doing anything + UPDATE, // update the buffer + RESET, // drive reset low (active) + RESET_END, // drive reset high (inactive) + + SHOULD_WAIT, // states higher than this should wait for the display to be not busy + INITIALISE, // send the init sequence + TRANSFER_DATA, // transfer data to the display + POWER_ON, // power on the display + REFRESH_SCREEN, // send refresh command + POWER_OFF, // power off the display + DEEP_SLEEP, // deep sleep the display +}; + +static constexpr uint8_t NONE = 0; +static constexpr uint8_t MIRROR_X = 1; +static constexpr uint8_t MIRROR_Y = 2; +static constexpr uint8_t SWAP_XY = 4; + +static constexpr uint32_t MAX_TRANSFER_TIME = 10; // Transfer in 10ms blocks to allow the loop to run +static constexpr size_t MAX_TRANSFER_SIZE = 128; +static constexpr uint8_t DELAY_FLAG = 0xFF; + +class EPaperBase : public Display, + public spi::SPIDevice { + public: + EPaperBase(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence, + size_t init_sequence_length, DisplayType display_type = DISPLAY_TYPE_BINARY) + : name_(name), + width_(width), + height_(height), + init_sequence_(init_sequence), + init_sequence_length_(init_sequence_length), + display_type_(display_type) {} + void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; } + float get_setup_priority() const override; + void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; } + void set_busy_pin(GPIOPin *busy) { this->busy_pin_ = busy; } + void set_reset_duration(uint32_t reset_duration) { this->reset_duration_ = reset_duration; } + void set_transform(uint8_t transform) { this->transform_ = transform; } + void set_full_update_every(uint8_t full_update_every) { this->full_update_every_ = full_update_every; } + void dump_config() override; + + void command(uint8_t value); + void data(uint8_t value); + void cmd_data(uint8_t command, const uint8_t *ptr, size_t length); + + void update() override; + void loop() override; + + void setup() override; + + void on_safe_shutdown() override; + + DisplayType get_display_type() override { return this->display_type_; }; + + // Default implementations for monochrome displays + static uint8_t color_to_bit(Color color) { + // It's always a shade of gray. Map to BLACK or WHITE. + // We split the luminance at a suitable point + if ((static_cast(color.r) + color.g + color.b) > 512) { + return 1; + } + return 0; + } + void fill(Color color) override { + auto pixel_color = color_to_bit(color) ? 0xFF : 0x00; + + // We store 8 pixels per byte + this->buffer_.fill(pixel_color); + this->x_high_ = this->width_; + this->y_high_ = this->height_; + this->x_low_ = 0; + this->y_low_ = 0; + } + + void clear() override { + // clear buffer to white, just like real paper. + this->fill(COLOR_ON); + } + + protected: + int get_height_internal() override { return this->height_; }; + int get_width_internal() override { return this->width_; }; + int get_width() override { return this->transform_ & SWAP_XY ? this->height_ : this->width_; } + int get_height() override { return this->transform_ & SWAP_XY ? this->width_ : this->height_; } + void draw_pixel_at(int x, int y, Color color) override; + void process_state_(); + + const char *epaper_state_to_string_(); + bool is_idle_() const; + void setup_pins_() const; + virtual bool reset(); + void initialise_(); + void wait_for_idle_(bool should_wait); + bool init_buffer_(size_t buffer_length); + bool rotate_coordinates_(int &x, int &y) const; + + /** + * Methods that must be implemented by concrete classes to control the display + */ + /** + * Send data to the device via SPI + * @return true if done, false if it should be called next loop + */ + virtual bool transfer_data() = 0; + /** + * Refresh the screen after data transfer + */ + virtual void refresh_screen(bool partial) = 0; + + /** + * Power the display on + */ + virtual void power_on() = 0; + /** + * Power the display off + */ + virtual void power_off() = 0; + + /** + * Place the display into deep sleep + */ + virtual void deep_sleep() = 0; + + void set_state_(EPaperState state, uint16_t delay = 0); + + void start_command_(); + void end_command_(); + void start_data_(); + void end_data_(); + + // properties initialised in the constructor + const char *name_; + uint16_t width_; + uint16_t height_; + const uint8_t *init_sequence_; + size_t init_sequence_length_; + DisplayType display_type_; + + size_t buffer_length_{}; + size_t current_data_index_{}; // used by data transfer to track progress + split_buffer::SplitBuffer buffer_{}; + GPIOPin *dc_pin_{}; + GPIOPin *busy_pin_{}; + GPIOPin *reset_pin_{}; + bool waiting_for_idle_{}; + uint32_t delay_until_{}; + uint8_t transform_{}; + uint8_t update_count_{}; + // these values represent the bounds of the updated buffer. Note that x_high and y_high + // point to the pixel past the last one updated, i.e. may range up to width/height. + uint16_t x_low_{}, y_low_{}, x_high_{}, y_high_{}; + +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + uint32_t waiting_for_idle_last_print_{}; + uint32_t waiting_for_idle_start_{}; +#endif +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG + uint32_t update_start_time_{}; +#endif + + // properties with specific initialisers go last + EPaperState state_{EPaperState::IDLE}; + uint32_t reset_duration_{10}; + uint8_t full_update_every_{1}; +}; + +} // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp b/esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp new file mode 100644 index 0000000000..d0e68595d0 --- /dev/null +++ b/esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp @@ -0,0 +1,159 @@ +#include "epaper_spi_spectra_e6.h" + +#include + +#include "esphome/core/log.h" + +namespace esphome::epaper_spi { +static constexpr const char *const TAG = "epaper_spi.6c"; +static constexpr unsigned char GRAY_THRESHOLD = 50; + +enum E6Color { + BLACK, + WHITE, + YELLOW, + RED, + SKIP_1, + BLUE, + GREEN, + CYAN, + SKIP_2, +}; + +static uint8_t color_to_hex(Color color) { + // --- Step 1: Check for Grayscale (Black or White) --- + // We define "grayscale" as a color where the min and max components + // are close to each other. + unsigned char max_rgb = std::max({color.r, color.g, color.b}); + unsigned char min_rgb = std::min({color.r, color.g, color.b}); + + if ((max_rgb - min_rgb) < GRAY_THRESHOLD) { + // It's a shade of gray. Map to BLACK or WHITE. + // We split the luminance at the halfway point (382 = (255*3)/2) + if ((static_cast(color.r) + color.g + color.b) > 382) { + return WHITE; + } + return BLACK; + } + // --- Step 2: Check for Primary/Secondary Colors --- + // If it's not gray, it's a color. We check which components are + // "on" (over 128) vs "off". This divides the RGB cube into 8 corners. + bool r_on = (color.r > 128); + bool g_on = (color.g > 128); + bool b_on = (color.b > 128); + + if (r_on && g_on && !b_on) { + return YELLOW; + } + if (r_on && !g_on && !b_on) { + return RED; + } + if (!r_on && g_on && !b_on) { + return GREEN; + } + if (!r_on && !g_on && b_on) { + return BLUE; + } + // Handle "impure" colors (Cyan, Magenta) + if (!r_on && g_on && b_on) { + // Cyan (G+B) -> Closest is Green or Blue. Pick Green. + return GREEN; + } + if (r_on && !g_on) { + // Magenta (R+B) -> Closest is Red or Blue. Pick Red. + return RED; + } + // Handle the remaining corners (White-ish, Black-ish) + if (r_on) { + // All high (but not gray) -> White + return WHITE; + } + // !r_on && !g_on && !b_on + // All low (but not gray) -> Black + return BLACK; +} + +void EPaperSpectraE6::power_on() { + ESP_LOGV(TAG, "Power on"); + this->command(0x04); +} + +void EPaperSpectraE6::power_off() { + ESP_LOGV(TAG, "Power off"); + this->command(0x02); + this->data(0x00); +} + +void EPaperSpectraE6::refresh_screen(bool partial) { + ESP_LOGV(TAG, "Refresh"); + this->command(0x12); + this->data(0x00); +} + +void EPaperSpectraE6::deep_sleep() { + ESP_LOGV(TAG, "Deep sleep"); + this->command(0x07); + this->data(0xA5); +} + +void EPaperSpectraE6::fill(Color color) { + auto pixel_color = color_to_hex(color); + + // We store 2 pixels per byte + this->buffer_.fill(pixel_color + (pixel_color << 4)); +} + +void EPaperSpectraE6::clear() { + // clear buffer to white, just like real paper. + this->fill(COLOR_ON); +} + +void HOT EPaperSpectraE6::draw_pixel_at(int x, int y, Color color) { + if (!this->rotate_coordinates_(x, y)) + return; + auto pixel_bits = color_to_hex(color); + uint32_t pixel_position = x + y * this->get_width_internal(); + uint32_t byte_position = pixel_position / 2; + auto original = this->buffer_[byte_position]; + if ((pixel_position & 1) != 0) { + this->buffer_[byte_position] = (original & 0xF0) | pixel_bits; + } else { + this->buffer_[byte_position] = (original & 0x0F) | (pixel_bits << 4); + } +} + +bool HOT EPaperSpectraE6::transfer_data() { + const uint32_t start_time = App.get_loop_component_start_time(); + const size_t buffer_length = this->buffer_length_; + if (this->current_data_index_ == 0) { + this->command(0x10); + } + + size_t buf_idx = 0; + uint8_t bytes_to_send[MAX_TRANSFER_SIZE]; + while (this->current_data_index_ != buffer_length) { + bytes_to_send[buf_idx++] = this->buffer_[this->current_data_index_++]; + + if (buf_idx == sizeof bytes_to_send) { + this->start_data_(); + this->write_array(bytes_to_send, buf_idx); + this->end_data_(); + ESP_LOGV(TAG, "Wrote %d bytes at %ums", buf_idx, (unsigned) millis()); + buf_idx = 0; + + if (millis() - start_time > MAX_TRANSFER_TIME) { + // Let the main loop run and come back next loop + return false; + } + } + } + // Finished the entire dataset + if (buf_idx != 0) { + this->start_data_(); + this->write_array(bytes_to_send, buf_idx); + this->end_data_(); + } + this->current_data_index_ = 0; + return true; +} +} // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/epaper_spi_spectra_e6.h b/esphome/components/epaper_spi/epaper_spi_spectra_e6.h new file mode 100644 index 0000000000..b8dbf0b0c5 --- /dev/null +++ b/esphome/components/epaper_spi/epaper_spi_spectra_e6.h @@ -0,0 +1,28 @@ +#pragma once + +#include "epaper_spi.h" + +namespace esphome::epaper_spi { + +class EPaperSpectraE6 : public EPaperBase { + public: + EPaperSpectraE6(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence, + size_t init_sequence_length) + : EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_COLOR) { + this->buffer_length_ = width * height / 2; // 2 pixels per byte + } + + void fill(Color color) override; + void clear() override; + + protected: + void refresh_screen(bool partial) override; + void power_on() override; + void power_off() override; + void deep_sleep() override; + void draw_pixel_at(int x, int y, Color color) override; + + bool transfer_data() override; +}; + +} // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/epaper_spi_ssd1677.cpp b/esphome/components/epaper_spi/epaper_spi_ssd1677.cpp new file mode 100644 index 0000000000..e4f04657ad --- /dev/null +++ b/esphome/components/epaper_spi/epaper_spi_ssd1677.cpp @@ -0,0 +1,86 @@ +#include "epaper_spi_ssd1677.h" + +#include + +#include "esphome/core/log.h" + +namespace esphome::epaper_spi { +static constexpr const char *const TAG = "epaper_spi.ssd1677"; + +void EPaperSSD1677::refresh_screen(bool partial) { + ESP_LOGV(TAG, "Refresh screen"); + this->command(0x22); + this->data(partial ? 0xFF : 0xF7); + this->command(0x20); +} + +void EPaperSSD1677::deep_sleep() { + ESP_LOGV(TAG, "Deep sleep"); + this->command(0x10); +} + +bool EPaperSSD1677::reset() { + if (EPaperBase::reset()) { + this->command(0x12); + return true; + } + return false; +} + +bool HOT EPaperSSD1677::transfer_data() { + auto start_time = millis(); + if (this->current_data_index_ == 0) { + uint8_t data[4]{}; + // round to byte boundaries + this->x_low_ &= ~7; + this->y_low_ &= ~7; + this->x_high_ += 7; + this->x_high_ &= ~7; + this->y_high_ += 7; + this->y_high_ &= ~7; + data[0] = this->x_low_; + data[1] = this->x_low_ / 256; + data[2] = this->x_high_ - 1; + data[3] = (this->x_high_ - 1) / 256; + cmd_data(0x4E, data, 2); + cmd_data(0x44, data, sizeof(data)); + data[0] = this->y_low_; + data[1] = this->y_low_ / 256; + data[2] = this->y_high_ - 1; + data[3] = (this->y_high_ - 1) / 256; + cmd_data(0x4F, data, 2); + this->cmd_data(0x45, data, sizeof(data)); + // for monochrome, we still need to clear the red data buffer at least once to prevent it + // causing dirty pixels after partial refresh. + this->command(this->send_red_ ? 0x26 : 0x24); + this->current_data_index_ = this->y_low_; // actually current line + } + size_t row_length = (this->x_high_ - this->x_low_) / 8; + FixedVector bytes_to_send{}; + bytes_to_send.init(row_length); + ESP_LOGV(TAG, "Writing bytes at line %zu at %ums", this->current_data_index_, (unsigned) millis()); + this->start_data_(); + while (this->current_data_index_ != this->y_high_) { + size_t data_idx = (this->current_data_index_ * this->width_ + this->x_low_) / 8; + for (size_t i = 0; i != row_length; i++) { + bytes_to_send[i] = this->send_red_ ? 0 : this->buffer_[data_idx++]; + } + ++this->current_data_index_; + this->write_array(&bytes_to_send.front(), row_length); // NOLINT + if (millis() - start_time > MAX_TRANSFER_TIME) { + // Let the main loop run and come back next loop + this->end_data_(); + return false; + } + } + + this->end_data_(); + this->current_data_index_ = 0; + if (this->send_red_) { + this->send_red_ = false; + return false; + } + return true; +} + +} // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/epaper_spi_ssd1677.h b/esphome/components/epaper_spi/epaper_spi_ssd1677.h new file mode 100644 index 0000000000..47584d24c0 --- /dev/null +++ b/esphome/components/epaper_spi/epaper_spi_ssd1677.h @@ -0,0 +1,25 @@ +#pragma once + +#include "epaper_spi.h" + +namespace esphome::epaper_spi { + +class EPaperSSD1677 : public EPaperBase { + public: + EPaperSSD1677(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence, + size_t init_sequence_length) + : EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_BINARY) { + this->buffer_length_ = width * height / 8; // 8 pixels per byte + } + + protected: + void refresh_screen(bool partial) override; + void power_on() override {} + void power_off() override{}; + void deep_sleep() override; + bool reset() override; + bool transfer_data() override; + bool send_red_{true}; +}; + +} // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/models/__init__.py b/esphome/components/epaper_spi/models/__init__.py new file mode 100644 index 0000000000..019eb31d18 --- /dev/null +++ b/esphome/components/epaper_spi/models/__init__.py @@ -0,0 +1,65 @@ +from typing import Any, Self + +import esphome.config_validation as cv +from esphome.const import CONF_DIMENSIONS, CONF_HEIGHT, CONF_WIDTH + + +class EpaperModel: + models: dict[str, Self] = {} + + def __init__( + self, + name: str, + class_name: str, + initsequence=None, + **defaults, + ): + name = name.upper() + self.name = name + self.class_name = class_name + self.initsequence = initsequence + self.defaults = defaults + EpaperModel.models[name] = self + + def get_default(self, key, fallback: Any = False) -> Any: + return self.defaults.get(key, fallback) + + def get_init_sequence(self, config: dict): + return self.initsequence + + def option(self, name, fallback=cv.UNDEFINED) -> cv.Optional | cv.Required: + if fallback is None and self.get_default(name, None) is None: + return cv.Required(name) + return cv.Optional(name, default=self.get_default(name, fallback)) + + def get_dimensions(self, config) -> tuple[int, int]: + if CONF_DIMENSIONS in config: + # Explicit dimensions, just use as is + dimensions = config[CONF_DIMENSIONS] + if isinstance(dimensions, dict): + width = dimensions[CONF_WIDTH] + height = dimensions[CONF_HEIGHT] + else: + (width, height) = dimensions + + else: + # Default dimensions, use model defaults + width = self.get_default(CONF_WIDTH) + height = self.get_default(CONF_HEIGHT) + return width, height + + def extend(self, name, **kwargs) -> "EpaperModel": + """ + Extend the current model with additional parameters or a modified init sequence. + Parameters supplied here will override the defaults of the current model. + if the initsequence is not provided, the current model's initsequence will be used. + If add_init_sequence is provided, it will be appended to the current initsequence. + :param name: + :param kwargs: + :return: + """ + initsequence = list(kwargs.pop("initsequence", self.initsequence) or ()) + initsequence.extend(kwargs.pop("add_init_sequence", ())) + defaults = self.defaults.copy() + defaults.update(kwargs) + return self.__class__(name, initsequence=tuple(initsequence), **defaults) diff --git a/esphome/components/epaper_spi/models/spectra_e6.py b/esphome/components/epaper_spi/models/spectra_e6.py new file mode 100644 index 0000000000..42a5a7da72 --- /dev/null +++ b/esphome/components/epaper_spi/models/spectra_e6.py @@ -0,0 +1,55 @@ +from typing import Any + +from . import EpaperModel + + +class SpectraE6(EpaperModel): + def __init__(self, name, class_name="EPaperSpectraE6", **kwargs): + super().__init__(name, class_name, **kwargs) + + # fmt: off + def get_init_sequence(self, config: dict): + width, height = self.get_dimensions(config) + return ( + (0xAA, 0x49, 0x55, 0x20, 0x08, 0x09, 0x18,), + (0x01, 0x3F,), + (0x00, 0x5F, 0x69,), + (0x03, 0x00, 0x54, 0x00, 0x44,), + (0x05, 0x40, 0x1F, 0x1F, 0x2C,), + (0x06, 0x6F, 0x1F, 0x17, 0x49,), + (0x08, 0x6F, 0x1F, 0x1F, 0x22,), + (0x30, 0x03,), + (0x50, 0x3F,), + (0x60, 0x02, 0x00,), + (0x61, width // 256, width % 256, height // 256, height % 256,), + (0x84, 0x01,), + (0xE3, 0x2F,), + ) + + def get_default(self, key, fallback: Any = False) -> Any: + return self.defaults.get(key, fallback) + + +spectra_e6 = SpectraE6("spectra-e6") + +spectra_e6_7p3 = spectra_e6.extend( + "7.3in-Spectra-E6", + width=800, + height=480, + data_rate="20MHz", +) + +spectra_e6_7p3.extend( + "Seeed-reTerminal-E1002", + cs_pin=10, + dc_pin=11, + reset_pin=12, + busy_pin={ + "number": 13, + "inverted": True, + "mode": { + "input": True, + "pullup": True, + }, + }, +) diff --git a/esphome/components/epaper_spi/models/ssd1677.py b/esphome/components/epaper_spi/models/ssd1677.py new file mode 100644 index 0000000000..3eb53d650e --- /dev/null +++ b/esphome/components/epaper_spi/models/ssd1677.py @@ -0,0 +1,42 @@ +from esphome.const import CONF_DATA_RATE + +from . import EpaperModel + + +class SSD1677(EpaperModel): + def __init__(self, name, class_name="EPaperSSD1677", **kwargs): + if CONF_DATA_RATE not in kwargs: + kwargs[CONF_DATA_RATE] = "20MHz" + super().__init__(name, class_name, **kwargs) + + # fmt: off + def get_init_sequence(self, config: dict): + width, _height = self.get_dimensions(config) + return ( + (0x18, 0x80), # Select internal Temp sensor + (0x0C, 0xAE, 0xC7, 0xC3, 0xC0, 0x80), # inrush current level 2 + (0x01, (width - 1) % 256, (width - 1) // 256, 0x02), # Set column gate limit + (0x3C, 0x01), # Set border waveform + (0x11, 3), # Set transform + ) + + +ssd1677 = SSD1677("ssd1677") + +ssd1677.extend( + "seeed-ee04-mono-4.26", + width=800, + height=480, + mirror_x=True, + cs_pin=44, + dc_pin=10, + reset_pin=38, + busy_pin={ + "number": 4, + "inverted": False, + "mode": { + "input": True, + "pulldown": True, + }, + }, +) diff --git a/esphome/components/es7210/es7210.cpp b/esphome/components/es7210/es7210.cpp index e5729703ed..1358121c1b 100644 --- a/esphome/components/es7210/es7210.cpp +++ b/esphome/components/es7210/es7210.cpp @@ -97,12 +97,12 @@ bool ES7210::set_mic_gain(float mic_gain) { } bool ES7210::configure_sample_rate_() { - int mclk_fre = this->sample_rate_ * MCLK_DIV_FRE; + uint32_t mclk_fre = this->sample_rate_ * MCLK_DIV_FRE; int coeff = -1; - for (int i = 0; i < (sizeof(ES7210_COEFFICIENTS) / sizeof(ES7210_COEFFICIENTS[0])); ++i) { + for (size_t i = 0; i < (sizeof(ES7210_COEFFICIENTS) / sizeof(ES7210_COEFFICIENTS[0])); ++i) { if (ES7210_COEFFICIENTS[i].lrclk == this->sample_rate_ && ES7210_COEFFICIENTS[i].mclk == mclk_fre) - coeff = i; + coeff = static_cast(i); } if (coeff >= 0) { diff --git a/esphome/components/es8388/select/adc_input_mic_select.cpp b/esphome/components/es8388/select/adc_input_mic_select.cpp index 5fab5b8a92..2e47534296 100644 --- a/esphome/components/es8388/select/adc_input_mic_select.cpp +++ b/esphome/components/es8388/select/adc_input_mic_select.cpp @@ -3,9 +3,9 @@ namespace esphome { namespace es8388 { -void ADCInputMicSelect::control(const std::string &value) { - this->publish_state(value); - this->parent_->set_adc_input_mic(static_cast(this->index_of(value).value())); +void ADCInputMicSelect::control(size_t index) { + this->publish_state(index); + this->parent_->set_adc_input_mic(static_cast(index)); } } // namespace es8388 diff --git a/esphome/components/es8388/select/adc_input_mic_select.h b/esphome/components/es8388/select/adc_input_mic_select.h index 8d035525ef..f0fa840d00 100644 --- a/esphome/components/es8388/select/adc_input_mic_select.h +++ b/esphome/components/es8388/select/adc_input_mic_select.h @@ -8,7 +8,7 @@ namespace es8388 { class ADCInputMicSelect : public select::Select, public Parented { protected: - void control(const std::string &value) override; + void control(size_t index) override; }; } // namespace es8388 diff --git a/esphome/components/es8388/select/dac_output_select.cpp b/esphome/components/es8388/select/dac_output_select.cpp index 268b5f290c..9af288a721 100644 --- a/esphome/components/es8388/select/dac_output_select.cpp +++ b/esphome/components/es8388/select/dac_output_select.cpp @@ -3,9 +3,9 @@ namespace esphome { namespace es8388 { -void DacOutputSelect::control(const std::string &value) { - this->publish_state(value); - this->parent_->set_dac_output(static_cast(this->index_of(value).value())); +void DacOutputSelect::control(size_t index) { + this->publish_state(index); + this->parent_->set_dac_output(static_cast(index)); } } // namespace es8388 diff --git a/esphome/components/es8388/select/dac_output_select.h b/esphome/components/es8388/select/dac_output_select.h index fccae9fc19..40d8a66553 100644 --- a/esphome/components/es8388/select/dac_output_select.h +++ b/esphome/components/es8388/select/dac_output_select.h @@ -8,7 +8,7 @@ namespace es8388 { class DacOutputSelect : public select::Select, public Parented { protected: - void control(const std::string &value) override; + void control(size_t index) override; }; } // namespace es8388 diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index c43cafc100..d372af3e6a 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -1,3 +1,4 @@ +import contextlib from dataclasses import dataclass import itertools import logging @@ -15,6 +16,7 @@ from esphome.const import ( CONF_FRAMEWORK, CONF_IGNORE_EFUSE_CUSTOM_MAC, CONF_IGNORE_EFUSE_MAC_CRC, + CONF_LOG_LEVEL, CONF_NAME, CONF_PATH, CONF_PLATFORM_VERSION, @@ -35,10 +37,10 @@ from esphome.const import ( __version__, ) from esphome.core import CORE, HexInt, TimePeriod -from esphome.cpp_generator import RawExpression import esphome.final_validate as fv -from esphome.helpers import copy_file_if_changed, mkdir_p, write_file_if_changed +from esphome.helpers import copy_file_if_changed, write_file_if_changed from esphome.types import ConfigType +from esphome.writer import clean_cmake_cache from .boards import BOARDS, STANDARD_BOARDS from .const import ( # noqa @@ -79,6 +81,15 @@ CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert" CONF_EXECUTE_FROM_PSRAM = "execute_from_psram" CONF_RELEASE = "release" +LOG_LEVELS_IDF = [ + "NONE", + "ERROR", + "WARN", + "INFO", + "DEBUG", + "VERBOSE", +] + ASSERTION_LEVELS = { "DISABLE": "CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_DISABLE", "ENABLE": "CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_ENABLE", @@ -92,6 +103,10 @@ COMPILER_OPTIMIZATIONS = { "SIZE": "CONFIG_COMPILER_OPTIMIZATION_SIZE", } +# Socket limit configuration for ESP-IDF +# ESP-IDF CONFIG_LWIP_MAX_SOCKETS has range 1-253, default 10 +DEFAULT_MAX_SOCKETS = 10 # ESP-IDF default + ARDUINO_ALLOWED_VARIANTS = [ VARIANT_ESP32, VARIANT_ESP32C3, @@ -146,8 +161,6 @@ def set_core_data(config): conf = config[CONF_FRAMEWORK] if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF: CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = "esp-idf" - CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] = {} - CORE.data[KEY_ESP32][KEY_COMPONENTS] = {} elif conf[CONF_TYPE] == FRAMEWORK_ARDUINO: CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = "arduino" if variant not in ARDUINO_ALLOWED_VARIANTS: @@ -155,6 +168,8 @@ def set_core_data(config): f"ESPHome does not support using the Arduino framework for the {variant}. Please use the ESP-IDF framework instead.", path=[CONF_FRAMEWORK, CONF_TYPE], ) + CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] = {} + CORE.data[KEY_ESP32][KEY_COMPONENTS] = {} CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse( config[CONF_FRAMEWORK][CONF_VERSION] ) @@ -225,8 +240,6 @@ SdkconfigValueType = bool | int | HexInt | str | RawSdkconfigValue def add_idf_sdkconfig_option(name: str, value: SdkconfigValueType): """Set an esp-idf sdkconfig value.""" - if not CORE.using_esp_idf: - raise ValueError("Not an esp-idf project") CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS][name] = value @@ -241,8 +254,6 @@ def add_idf_component( submodules: list[str] | None = None, ): """Add an esp-idf component to the project.""" - if not CORE.using_esp_idf: - raise ValueError("Not an esp-idf project") if not repo and not ref and not path: raise ValueError("Requires at least one of repo, ref or path") if refresh or submodules or components: @@ -266,14 +277,14 @@ def add_idf_component( } -def add_extra_script(stage: str, filename: str, path: str): +def add_extra_script(stage: str, filename: str, path: Path): """Add an extra script to the project.""" key = f"{stage}:{filename}" if add_extra_build_file(filename, path): cg.add_platformio_option("extra_scripts", [key]) -def add_extra_build_file(filename: str, path: str) -> bool: +def add_extra_build_file(filename: str, path: Path) -> bool: """Add an extra build file to the project.""" if filename not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]: CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES][filename] = { @@ -290,17 +301,27 @@ def _format_framework_arduino_version(ver: cv.Version) -> str: return f"pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/{str(ver)}/esp32-{str(ver)}.zip" -def _format_framework_espidf_version( - ver: cv.Version, release: str, for_platformio: bool -) -> str: - # format the given arduino (https://github.com/espressif/esp-idf/releases) version to +def _format_framework_espidf_version(ver: cv.Version, release: str) -> str: + # format the given espidf (https://github.com/pioarduino/esp-idf/releases) version to # a PIO platformio/framework-espidf value - # List of package versions: https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf - if for_platformio: - return f"platformio/framework-espidf@~3.{ver.major}{ver.minor:02d}{ver.patch:02d}.0" + if ver == cv.Version(5, 4, 3) or ver >= cv.Version(5, 5, 1): + ext = "tar.xz" + else: + ext = "zip" if release: - return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}.{release}/esp-idf-v{str(ver)}.zip" - return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}/esp-idf-v{str(ver)}.zip" + return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}.{release}/esp-idf-v{str(ver)}.{ext}" + return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}/esp-idf-v{str(ver)}.{ext}" + + +def _is_framework_url(source: str) -> str: + # platformio accepts many URL schemes for framework repositories and archives including http, https, git, file, and symlink + import urllib.parse + + try: + parsed = urllib.parse.urlparse(source) + except ValueError: + return False + return bool(parsed.scheme) # NOTE: Keep this in mind when updating the recommended version: @@ -311,183 +332,136 @@ def _format_framework_espidf_version( # The default/recommended arduino framework version # - https://github.com/espressif/arduino-esp32/releases -RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 2, 1) -# The platform-espressif32 version to use for arduino frameworks -# - https://github.com/pioarduino/platform-espressif32/releases -ARDUINO_PLATFORM_VERSION = cv.Version(54, 3, 21, "2") +ARDUINO_FRAMEWORK_VERSION_LOOKUP = { + "recommended": cv.Version(3, 3, 2), + "latest": cv.Version(3, 3, 4), + "dev": cv.Version(3, 3, 4), +} +ARDUINO_PLATFORM_VERSION_LOOKUP = { + cv.Version(3, 3, 4): cv.Version(55, 3, 31, "2"), + cv.Version(3, 3, 3): cv.Version(55, 3, 31, "2"), + cv.Version(3, 3, 2): cv.Version(55, 3, 31, "2"), + cv.Version(3, 3, 1): cv.Version(55, 3, 31, "2"), + cv.Version(3, 3, 0): cv.Version(55, 3, 30, "2"), + cv.Version(3, 2, 1): cv.Version(54, 3, 21, "2"), + cv.Version(3, 2, 0): cv.Version(54, 3, 20), + cv.Version(3, 1, 3): cv.Version(53, 3, 13), + cv.Version(3, 1, 2): cv.Version(53, 3, 12), + cv.Version(3, 1, 1): cv.Version(53, 3, 11), + cv.Version(3, 1, 0): cv.Version(53, 3, 10), +} # The default/recommended esp-idf framework version # - https://github.com/espressif/esp-idf/releases -# - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf -RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(5, 4, 2) -# The platformio/espressif32 version to use for esp-idf frameworks -# - https://github.com/platformio/platform-espressif32/releases -# - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32 -ESP_IDF_PLATFORM_VERSION = cv.Version(54, 3, 21, "2") +ESP_IDF_FRAMEWORK_VERSION_LOOKUP = { + "recommended": cv.Version(5, 5, 1), + "latest": cv.Version(5, 5, 1), + "dev": cv.Version(5, 5, 1), +} +ESP_IDF_PLATFORM_VERSION_LOOKUP = { + cv.Version(5, 5, 1): cv.Version(55, 3, 31, "2"), + cv.Version(5, 5, 0): cv.Version(55, 3, 31, "2"), + cv.Version(5, 4, 3): cv.Version(55, 3, 32), + cv.Version(5, 4, 2): cv.Version(54, 3, 21, "2"), + cv.Version(5, 4, 1): cv.Version(54, 3, 21, "2"), + cv.Version(5, 4, 0): cv.Version(54, 3, 21, "2"), + cv.Version(5, 3, 2): cv.Version(53, 3, 13), + cv.Version(5, 3, 1): cv.Version(53, 3, 13), + cv.Version(5, 3, 0): cv.Version(53, 3, 13), + cv.Version(5, 1, 6): cv.Version(51, 3, 7), + cv.Version(5, 1, 5): cv.Version(51, 3, 7), +} -# List based on https://registry.platformio.org/tools/platformio/framework-espidf/versions -SUPPORTED_PLATFORMIO_ESP_IDF_5X = [ - cv.Version(5, 3, 1), - cv.Version(5, 3, 0), - cv.Version(5, 2, 2), - cv.Version(5, 2, 1), - cv.Version(5, 1, 2), - cv.Version(5, 1, 1), - cv.Version(5, 1, 0), - cv.Version(5, 0, 2), - cv.Version(5, 0, 1), - cv.Version(5, 0, 0), -] - -# pioarduino versions that don't require a release number -# List based on https://github.com/pioarduino/esp-idf/releases -SUPPORTED_PIOARDUINO_ESP_IDF_5X = [ - cv.Version(5, 5, 0), - cv.Version(5, 4, 2), - cv.Version(5, 4, 1), - cv.Version(5, 4, 0), - cv.Version(5, 3, 3), - cv.Version(5, 3, 2), - cv.Version(5, 3, 1), - cv.Version(5, 3, 0), - cv.Version(5, 1, 5), - cv.Version(5, 1, 6), -] +# The platform-espressif32 version +# - https://github.com/pioarduino/platform-espressif32/releases +PLATFORM_VERSION_LOOKUP = { + "recommended": cv.Version(55, 3, 31, "2"), + "latest": cv.Version(55, 3, 31, "2"), + "dev": cv.Version(55, 3, 31, "2"), +} -def _arduino_check_versions(value): - value = value.copy() - lookups = { - "dev": (cv.Version(3, 2, 1), "https://github.com/espressif/arduino-esp32.git"), - "latest": (cv.Version(3, 2, 1), None), - "recommended": (RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, None), - } +def _check_versions(config): + config = config.copy() + value = config[CONF_FRAMEWORK] - if value[CONF_VERSION] in lookups: - if CONF_SOURCE in value: + if value[CONF_VERSION] in PLATFORM_VERSION_LOOKUP: + if CONF_SOURCE in value or CONF_PLATFORM_VERSION in value: raise cv.Invalid( - "Framework version needs to be explicitly specified when custom source is used." + "Version needs to be explicitly set when a custom source or platform_version is used." ) - version, source = lookups[value[CONF_VERSION]] + platform_lookup = PLATFORM_VERSION_LOOKUP[value[CONF_VERSION]] + value[CONF_PLATFORM_VERSION] = _parse_platform_version(str(platform_lookup)) + + if value[CONF_TYPE] == FRAMEWORK_ARDUINO: + version = ARDUINO_FRAMEWORK_VERSION_LOOKUP[value[CONF_VERSION]] + else: + version = ESP_IDF_FRAMEWORK_VERSION_LOOKUP[value[CONF_VERSION]] else: version = cv.Version.parse(cv.version_number(value[CONF_VERSION])) - source = value.get(CONF_SOURCE, None) value[CONF_VERSION] = str(version) - value[CONF_SOURCE] = source or _format_framework_arduino_version(version) - value[CONF_PLATFORM_VERSION] = value.get( - CONF_PLATFORM_VERSION, _parse_platform_version(str(ARDUINO_PLATFORM_VERSION)) - ) + if value[CONF_TYPE] == FRAMEWORK_ARDUINO: + if version < cv.Version(3, 0, 0): + raise cv.Invalid("Only Arduino 3.0+ is supported.") + recommended_version = ARDUINO_FRAMEWORK_VERSION_LOOKUP["recommended"] + platform_lookup = ARDUINO_PLATFORM_VERSION_LOOKUP.get(version) + value[CONF_SOURCE] = value.get( + CONF_SOURCE, _format_framework_arduino_version(version) + ) + if _is_framework_url(value[CONF_SOURCE]): + value[CONF_SOURCE] = ( + f"pioarduino/framework-arduinoespressif32@{value[CONF_SOURCE]}" + ) + else: + if version < cv.Version(5, 0, 0): + raise cv.Invalid("Only ESP-IDF 5.0+ is supported.") + recommended_version = ESP_IDF_FRAMEWORK_VERSION_LOOKUP["recommended"] + platform_lookup = ESP_IDF_PLATFORM_VERSION_LOOKUP.get(version) + value[CONF_SOURCE] = value.get( + CONF_SOURCE, + _format_framework_espidf_version(version, value.get(CONF_RELEASE, None)), + ) + if _is_framework_url(value[CONF_SOURCE]): + value[CONF_SOURCE] = f"pioarduino/framework-espidf@{value[CONF_SOURCE]}" - if value[CONF_SOURCE].startswith("http"): - # prefix is necessary or platformio will complain with a cryptic error - value[CONF_SOURCE] = f"framework-arduinoespressif32@{value[CONF_SOURCE]}" + if CONF_PLATFORM_VERSION not in value: + if platform_lookup is None: + raise cv.Invalid( + "Framework version not recognized; please specify platform_version" + ) + value[CONF_PLATFORM_VERSION] = _parse_platform_version(str(platform_lookup)) - if version != RECOMMENDED_ARDUINO_FRAMEWORK_VERSION: + if version != recommended_version: _LOGGER.warning( - "The selected Arduino framework version is not the recommended one. " + "The selected framework version is not the recommended one. " "If there are connectivity or build issues please remove the manual version." ) - return value - - -def _esp_idf_check_versions(value): - value = value.copy() - lookups = { - "dev": (cv.Version(5, 4, 2), "https://github.com/espressif/esp-idf.git"), - "latest": (cv.Version(5, 2, 2), None), - "recommended": (RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION, None), - } - - if value[CONF_VERSION] in lookups: - if CONF_SOURCE in value: - raise cv.Invalid( - "Framework version needs to be explicitly specified when custom source is used." - ) - - version, source = lookups[value[CONF_VERSION]] - else: - version = cv.Version.parse(cv.version_number(value[CONF_VERSION])) - source = value.get(CONF_SOURCE, None) - - if version < cv.Version(5, 0, 0): - raise cv.Invalid("Only ESP-IDF 5.0+ is supported.") - - # flag this for later *before* we set value[CONF_PLATFORM_VERSION] below - has_platform_ver = CONF_PLATFORM_VERSION in value - - value[CONF_PLATFORM_VERSION] = value.get( - CONF_PLATFORM_VERSION, _parse_platform_version(str(ESP_IDF_PLATFORM_VERSION)) - ) - - if ( - is_platformio := _platform_is_platformio(value[CONF_PLATFORM_VERSION]) - ) and version not in SUPPORTED_PLATFORMIO_ESP_IDF_5X: - raise cv.Invalid( - f"ESP-IDF {str(version)} not supported by platformio/espressif32" - ) - - if ( - version in SUPPORTED_PLATFORMIO_ESP_IDF_5X - and version not in SUPPORTED_PIOARDUINO_ESP_IDF_5X - ) and not has_platform_ver: - raise cv.Invalid( - f"ESP-IDF {value[CONF_VERSION]} may be supported by platformio/espressif32; please specify '{CONF_PLATFORM_VERSION}'" - ) - - if ( - not is_platformio - and CONF_RELEASE not in value - and version not in SUPPORTED_PIOARDUINO_ESP_IDF_5X + if value[CONF_PLATFORM_VERSION] != _parse_platform_version( + str(PLATFORM_VERSION_LOOKUP["recommended"]) ): - raise cv.Invalid( - f"ESP-IDF {value[CONF_VERSION]} is not available with pioarduino; you may need to specify '{CONF_RELEASE}'" - ) - - value[CONF_VERSION] = str(version) - value[CONF_SOURCE] = source or _format_framework_espidf_version( - version, value.get(CONF_RELEASE, None), is_platformio - ) - - if value[CONF_SOURCE].startswith("http"): - # prefix is necessary or platformio will complain with a cryptic error - value[CONF_SOURCE] = f"framework-espidf@{value[CONF_SOURCE]}" - - if version != RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION: _LOGGER.warning( - "The selected ESP-IDF framework version is not the recommended one. " + "The selected platform version is not the recommended one. " "If there are connectivity or build issues please remove the manual version." ) - return value + return config def _parse_platform_version(value): try: ver = cv.Version.parse(cv.version_number(value)) - if ver.major >= 50: # a pioarduino version - release = f"{ver.major}.{ver.minor:02d}.{ver.patch:02d}" - if ver.extra: - release += f"-{ver.extra}" - return f"https://github.com/pioarduino/platform-espressif32/releases/download/{release}/platform-espressif32.zip" - # if platform version is a valid version constraint, prefix the default package - cv.platformio_version_constraint(value) - return f"platformio/espressif32@{value}" + release = f"{ver.major}.{ver.minor:02d}.{ver.patch:02d}" + if ver.extra: + release += f"-{ver.extra}" + return f"https://github.com/pioarduino/platform-espressif32/releases/download/{release}/platform-espressif32.zip" except cv.Invalid: return value -def _platform_is_platformio(value): - try: - ver = cv.Version.parse(cv.version_number(value)) - return ver.major < 50 - except cv.Invalid: - return "platformio" in value - - def _detect_variant(value): board = value.get(CONF_BOARD) variant = value.get(CONF_VARIANT) @@ -524,6 +498,8 @@ def final_validate(config): from esphome.components.psram import DOMAIN as PSRAM_DOMAIN errs = [] + conf_fw = config[CONF_FRAMEWORK] + advanced = conf_fw[CONF_ADVANCED] full_config = fv.full_config.get() if pio_options := full_config[CONF_ESPHOME].get(CONF_PLATFORMIO_OPTIONS): pio_flash_size_key = "board_upload.flash_size" @@ -540,22 +516,14 @@ def final_validate(config): f"Please specify {CONF_FLASH_SIZE} within esp32 configuration only" ) ) - if ( - config[CONF_VARIANT] != VARIANT_ESP32 - and CONF_ADVANCED in (conf_fw := config[CONF_FRAMEWORK]) - and CONF_IGNORE_EFUSE_MAC_CRC in conf_fw[CONF_ADVANCED] - ): + if config[CONF_VARIANT] != VARIANT_ESP32 and advanced[CONF_IGNORE_EFUSE_MAC_CRC]: errs.append( cv.Invalid( f"'{CONF_IGNORE_EFUSE_MAC_CRC}' is not supported on {config[CONF_VARIANT]}", path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_IGNORE_EFUSE_MAC_CRC], ) ) - if ( - config.get(CONF_FRAMEWORK, {}) - .get(CONF_ADVANCED, {}) - .get(CONF_EXECUTE_FROM_PSRAM) - ): + if advanced[CONF_EXECUTE_FROM_PSRAM]: if config[CONF_VARIANT] != VARIANT_ESP32S3: errs.append( cv.Invalid( @@ -571,36 +539,57 @@ def final_validate(config): ) ) + if ( + config[CONF_FLASH_SIZE] == "32MB" + and "ota" in full_config + and not advanced[CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES] + ): + errs.append( + cv.Invalid( + f"OTA with 32MB flash requires '{CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES}' to be set in the '{CONF_ADVANCED}' section of the esp32 configuration", + path=[CONF_FLASH_SIZE], + ) + ) if errs: raise cv.MultipleInvalid(errs) return config -ARDUINO_FRAMEWORK_SCHEMA = cv.All( - cv.Schema( - { - cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict, - cv.Optional(CONF_SOURCE): cv.string_strict, - cv.Optional(CONF_PLATFORM_VERSION): _parse_platform_version, - cv.Optional(CONF_ADVANCED, default={}): cv.Schema( - { - cv.Optional( - CONF_IGNORE_EFUSE_CUSTOM_MAC, default=False - ): cv.boolean, - } - ), - } - ), - _arduino_check_versions, -) - CONF_SDKCONFIG_OPTIONS = "sdkconfig_options" CONF_ENABLE_LWIP_DHCP_SERVER = "enable_lwip_dhcp_server" CONF_ENABLE_LWIP_MDNS_QUERIES = "enable_lwip_mdns_queries" CONF_ENABLE_LWIP_BRIDGE_INTERFACE = "enable_lwip_bridge_interface" CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING = "enable_lwip_tcpip_core_locking" CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY = "enable_lwip_check_thread_safety" +CONF_DISABLE_LIBC_LOCKS_IN_IRAM = "disable_libc_locks_in_iram" +CONF_DISABLE_VFS_SUPPORT_TERMIOS = "disable_vfs_support_termios" +CONF_DISABLE_VFS_SUPPORT_SELECT = "disable_vfs_support_select" +CONF_DISABLE_VFS_SUPPORT_DIR = "disable_vfs_support_dir" +CONF_LOOP_TASK_STACK_SIZE = "loop_task_stack_size" + +# VFS requirement tracking +# Components that need VFS features can call require_vfs_select() or require_vfs_dir() +KEY_VFS_SELECT_REQUIRED = "vfs_select_required" +KEY_VFS_DIR_REQUIRED = "vfs_dir_required" + + +def require_vfs_select() -> None: + """Mark that VFS select support is required by a component. + + Call this from components that use esp_vfs_eventfd or other VFS select features. + This prevents CONFIG_VFS_SUPPORT_SELECT from being disabled. + """ + CORE.data[KEY_VFS_SELECT_REQUIRED] = True + + +def require_vfs_dir() -> None: + """Mark that VFS directory support is required by a component. + + Call this from components that use directory functions (opendir, readdir, mkdir, etc.). + This prevents CONFIG_VFS_SUPPORT_DIR from being disabled. + """ + CORE.data[KEY_VFS_DIR_REQUIRED] = True def _validate_idf_component(config: ConfigType) -> ConfigType: @@ -613,70 +602,76 @@ def _validate_idf_component(config: ConfigType) -> ConfigType: return config -ESP_IDF_FRAMEWORK_SCHEMA = cv.All( - cv.Schema( - { - cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict, - cv.Optional(CONF_RELEASE): cv.string_strict, - cv.Optional(CONF_SOURCE): cv.string_strict, - cv.Optional(CONF_PLATFORM_VERSION): _parse_platform_version, - cv.Optional(CONF_SDKCONFIG_OPTIONS, default={}): { - cv.string_strict: cv.string_strict - }, - cv.Optional(CONF_ADVANCED, default={}): cv.Schema( - { - cv.Optional(CONF_ASSERTION_LEVEL): cv.one_of( - *ASSERTION_LEVELS, upper=True - ), - cv.Optional(CONF_COMPILER_OPTIMIZATION, default="SIZE"): cv.one_of( - *COMPILER_OPTIMIZATIONS, upper=True - ), - cv.Optional(CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES): cv.boolean, - cv.Optional(CONF_ENABLE_LWIP_ASSERT, default=True): cv.boolean, - cv.Optional( - CONF_IGNORE_EFUSE_CUSTOM_MAC, default=False - ): cv.boolean, - cv.Optional(CONF_IGNORE_EFUSE_MAC_CRC): cv.boolean, - # DHCP server is needed for WiFi AP mode. When WiFi component is used, - # it will handle disabling DHCP server when AP is not configured. - # Default to false (disabled) when WiFi is not used. - cv.OnlyWithout( - CONF_ENABLE_LWIP_DHCP_SERVER, "wifi", default=False - ): cv.boolean, - cv.Optional( - CONF_ENABLE_LWIP_MDNS_QUERIES, default=True - ): cv.boolean, - cv.Optional( - CONF_ENABLE_LWIP_BRIDGE_INTERFACE, default=False - ): cv.boolean, - cv.Optional( - CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING, default=True - ): cv.boolean, - cv.Optional( - CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, default=True - ): cv.boolean, - cv.Optional(CONF_EXECUTE_FROM_PSRAM): cv.boolean, - } - ), - cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list( - cv.All( - cv.Schema( - { - cv.Required(CONF_NAME): cv.string_strict, - cv.Optional(CONF_SOURCE): cv.git_ref, - cv.Optional(CONF_REF): cv.string, - cv.Optional(CONF_PATH): cv.string, - cv.Optional(CONF_REFRESH): cv.All( - cv.string, cv.source_refresh - ), - } - ), - _validate_idf_component, - ) - ), - } - ), - _esp_idf_check_versions, +FRAMEWORK_ESP_IDF = "esp-idf" +FRAMEWORK_ARDUINO = "arduino" +FRAMEWORK_SCHEMA = cv.Schema( + { + cv.Optional(CONF_TYPE): cv.one_of(FRAMEWORK_ESP_IDF, FRAMEWORK_ARDUINO), + cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict, + cv.Optional(CONF_RELEASE): cv.string_strict, + cv.Optional(CONF_SOURCE): cv.string_strict, + cv.Optional(CONF_PLATFORM_VERSION): _parse_platform_version, + cv.Optional(CONF_SDKCONFIG_OPTIONS, default={}): { + cv.string_strict: cv.string_strict + }, + cv.Optional(CONF_LOG_LEVEL, default="ERROR"): cv.one_of( + *LOG_LEVELS_IDF, upper=True + ), + cv.Optional(CONF_ADVANCED, default={}): cv.Schema( + { + cv.Optional(CONF_ASSERTION_LEVEL): cv.one_of( + *ASSERTION_LEVELS, upper=True + ), + cv.Optional(CONF_COMPILER_OPTIMIZATION, default="SIZE"): cv.one_of( + *COMPILER_OPTIMIZATIONS, upper=True + ), + cv.Optional( + CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES, default=False + ): cv.boolean, + cv.Optional(CONF_ENABLE_LWIP_ASSERT, default=True): cv.boolean, + cv.Optional(CONF_IGNORE_EFUSE_CUSTOM_MAC, default=False): cv.boolean, + cv.Optional(CONF_IGNORE_EFUSE_MAC_CRC, default=False): cv.boolean, + # DHCP server is needed for WiFi AP mode. When WiFi component is used, + # it will handle disabling DHCP server when AP is not configured. + # Default to false (disabled) when WiFi is not used. + cv.OnlyWithout( + CONF_ENABLE_LWIP_DHCP_SERVER, "wifi", default=False + ): cv.boolean, + cv.Optional(CONF_ENABLE_LWIP_MDNS_QUERIES, default=True): cv.boolean, + cv.Optional( + CONF_ENABLE_LWIP_BRIDGE_INTERFACE, default=False + ): cv.boolean, + cv.Optional( + CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING, default=True + ): cv.boolean, + cv.Optional( + CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, default=True + ): cv.boolean, + cv.Optional(CONF_DISABLE_LIBC_LOCKS_IN_IRAM, default=True): cv.boolean, + cv.Optional(CONF_DISABLE_VFS_SUPPORT_TERMIOS, default=True): cv.boolean, + cv.Optional(CONF_DISABLE_VFS_SUPPORT_SELECT, default=True): cv.boolean, + cv.Optional(CONF_DISABLE_VFS_SUPPORT_DIR, default=True): cv.boolean, + cv.Optional(CONF_EXECUTE_FROM_PSRAM, default=False): cv.boolean, + cv.Optional(CONF_LOOP_TASK_STACK_SIZE, default=8192): cv.int_range( + min=8192, max=32768 + ), + } + ), + cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list( + cv.All( + cv.Schema( + { + cv.Required(CONF_NAME): cv.string_strict, + cv.Optional(CONF_SOURCE): cv.git_ref, + cv.Optional(CONF_REF): cv.string, + cv.Optional(CONF_PATH): cv.string, + cv.Optional(CONF_REFRESH): cv.All(cv.string, cv.source_refresh), + } + ), + _validate_idf_component, + ) + ), + } ) @@ -706,6 +701,7 @@ def _show_framework_migration_message(name: str, variant: str) -> None: + "Why change? ESP-IDF offers:\n" + color(AnsiFore.GREEN, " ✨ Up to 40% smaller binaries\n") + color(AnsiFore.GREEN, " 🚀 Better performance and optimization\n") + + color(AnsiFore.GREEN, " ⚡ 2-3x faster compile times\n") + color(AnsiFore.GREEN, " 📦 Custom-built firmware for your exact needs\n") + color( AnsiFore.GREEN, @@ -713,7 +709,6 @@ def _show_framework_migration_message(name: str, variant: str) -> None: ) + "\n" + "Trade-offs:\n" - + color(AnsiFore.YELLOW, " ⏱️ Compile times are ~25% longer\n") + color(AnsiFore.YELLOW, " 🔄 Some components need migration\n") + "\n" + "What should I do?\n" @@ -739,36 +734,22 @@ def _show_framework_migration_message(name: str, variant: str) -> None: def _set_default_framework(config): + config = config.copy() if CONF_FRAMEWORK not in config: - config = config.copy() - + config[CONF_FRAMEWORK] = FRAMEWORK_SCHEMA({}) + if CONF_TYPE not in config[CONF_FRAMEWORK]: variant = config[CONF_VARIANT] if variant in ARDUINO_ALLOWED_VARIANTS: - config[CONF_FRAMEWORK] = ARDUINO_FRAMEWORK_SCHEMA({}) config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ARDUINO - # Show the migration message _show_framework_migration_message( config.get(CONF_NAME, "This device"), variant ) else: - config[CONF_FRAMEWORK] = ESP_IDF_FRAMEWORK_SCHEMA({}) config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ESP_IDF return config -FRAMEWORK_ESP_IDF = "esp-idf" -FRAMEWORK_ARDUINO = "arduino" -FRAMEWORK_SCHEMA = cv.typed_schema( - { - FRAMEWORK_ESP_IDF: ESP_IDF_FRAMEWORK_SCHEMA, - FRAMEWORK_ARDUINO: ARDUINO_FRAMEWORK_SCHEMA, - }, - lower=True, - space="-", -) - - FLASH_SIZES = [ "2MB", "4MB", @@ -797,6 +778,7 @@ CONFIG_SCHEMA = cv.All( ), _detect_variant, _set_default_framework, + _check_versions, set_core_data, cv.has_at_least_one_key(CONF_BOARD, CONF_VARIANT), ) @@ -805,14 +787,83 @@ CONFIG_SCHEMA = cv.All( FINAL_VALIDATE_SCHEMA = cv.Schema(final_validate) +def _configure_lwip_max_sockets(conf: dict) -> None: + """Calculate and set CONFIG_LWIP_MAX_SOCKETS based on component needs. + + Socket component tracks consumer needs via consume_sockets() called during config validation. + This function runs in to_code() after all components have registered their socket needs. + User-provided sdkconfig_options take precedence. + """ + from esphome.components.socket import KEY_SOCKET_CONSUMERS + + # Check if user manually specified CONFIG_LWIP_MAX_SOCKETS + user_max_sockets = conf[CONF_SDKCONFIG_OPTIONS].get("CONFIG_LWIP_MAX_SOCKETS") + + socket_consumers: dict[str, int] = CORE.data.get(KEY_SOCKET_CONSUMERS, {}) + total_sockets = sum(socket_consumers.values()) + + # Early return if no sockets registered and no user override + if total_sockets == 0 and user_max_sockets is None: + return + + components_list = ", ".join( + f"{name}={count}" for name, count in sorted(socket_consumers.items()) + ) + + # User specified their own value - respect it but warn if insufficient + if user_max_sockets is not None: + _LOGGER.info( + "Using user-provided CONFIG_LWIP_MAX_SOCKETS: %s", + user_max_sockets, + ) + + # Warn if user's value is less than what components need + if total_sockets > 0: + user_sockets_int = 0 + with contextlib.suppress(ValueError, TypeError): + user_sockets_int = int(user_max_sockets) + + if user_sockets_int < total_sockets: + _LOGGER.warning( + "CONFIG_LWIP_MAX_SOCKETS is set to %d but your configuration " + "needs %d sockets (registered: %s). You may experience socket " + "exhaustion errors. Consider increasing to at least %d.", + user_sockets_int, + total_sockets, + components_list, + total_sockets, + ) + # User's value already added via sdkconfig_options processing + return + + # Auto-calculate based on component needs + # Use at least the ESP-IDF default (10), or the total needed by components + max_sockets = max(DEFAULT_MAX_SOCKETS, total_sockets) + + log_level = logging.INFO if max_sockets > DEFAULT_MAX_SOCKETS else logging.DEBUG + _LOGGER.log( + log_level, + "Setting CONFIG_LWIP_MAX_SOCKETS to %d (registered: %s)", + max_sockets, + components_list, + ) + + add_idf_sdkconfig_option("CONFIG_LWIP_MAX_SOCKETS", max_sockets) + + async def to_code(config): cg.add_platformio_option("board", config[CONF_BOARD]) cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE]) + cg.add_platformio_option( + "board_upload.maximum_size", + int(config[CONF_FLASH_SIZE].removesuffix("MB")) * 1024 * 1024, + ) cg.set_cpp_standard("gnu++20") cg.add_build_flag("-DUSE_ESP32") cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) - cg.add_build_flag(f"-DUSE_ESP32_VARIANT_{config[CONF_VARIANT]}") - cg.add_define("ESPHOME_VARIANT", VARIANT_FRIENDLY[config[CONF_VARIANT]]) + variant = config[CONF_VARIANT] + cg.add_build_flag(f"-DUSE_ESP32_VARIANT_{variant}") + cg.add_define("ESPHOME_VARIANT", VARIANT_FRIENDLY[variant]) cg.add_define(ThreadModel.MULTI_ATOMICS) cg.add_platformio_option("lib_ldf_mode", "off") @@ -822,149 +873,237 @@ async def to_code(config): conf = config[CONF_FRAMEWORK] cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION]) + if CONF_SOURCE in conf: + cg.add_platformio_option("platform_packages", [conf[CONF_SOURCE]]) if conf[CONF_ADVANCED][CONF_IGNORE_EFUSE_CUSTOM_MAC]: cg.add_define("USE_ESP32_IGNORE_EFUSE_CUSTOM_MAC") + for clean_var in ("IDF_PATH", "IDF_TOOLS_PATH"): + os.environ.pop(clean_var, None) + + # Set the location of the IDF component manager cache + os.environ["IDF_COMPONENT_CACHE_PATH"] = str( + CORE.relative_internal_path(".espressif") + ) + + add_extra_script( + "pre", + "pre_build.py", + Path(__file__).parent / "pre_build.py.script", + ) + add_extra_script( "post", "post_build.py", - os.path.join(os.path.dirname(__file__), "post_build.py.script"), + Path(__file__).parent / "post_build.py.script", ) - freq = config[CONF_CPU_FREQUENCY][:-3] + # In testing mode, add IRAM fix script to allow linking grouped component tests + # Similar to ESP8266's approach but for ESP-IDF + if CORE.testing_mode: + cg.add_build_flag("-DESPHOME_TESTING_MODE") + add_extra_script( + "pre", + "iram_fix.py", + Path(__file__).parent / "iram_fix.py.script", + ) + if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF: cg.add_platformio_option("framework", "espidf") cg.add_build_flag("-DUSE_ESP_IDF") cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF") - cg.add_build_flag("-Wno-nonnull-compare") - - cg.add_platformio_option("platform_packages", [conf[CONF_SOURCE]]) - - # platformio/toolchain-esp32ulp does not support linux_aarch64 yet and has not been updated for over 2 years - # This is espressif's own published version which is more up to date. - cg.add_platformio_option( - "platform_packages", ["espressif/toolchain-esp32ulp@2.35.0-20220830"] - ) - add_idf_sdkconfig_option( - f"CONFIG_ESPTOOLPY_FLASHSIZE_{config[CONF_FLASH_SIZE]}", True - ) - add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_SINGLE_APP", False) - add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_CUSTOM", True) - add_idf_sdkconfig_option( - "CONFIG_PARTITION_TABLE_CUSTOM_FILENAME", "partitions.csv" - ) - - # Increase freertos tick speed from 100Hz to 1kHz so that delay() resolution is 1ms - add_idf_sdkconfig_option("CONFIG_FREERTOS_HZ", 1000) - - # Setup watchdog - add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT", True) - add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_PANIC", True) - add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0", False) - add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1", False) - - # Disable dynamic log level control to save memory - add_idf_sdkconfig_option("CONFIG_LOG_DYNAMIC_LEVEL_CONTROL", False) - - # Set default CPU frequency - add_idf_sdkconfig_option(f"CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_{freq}", True) - - # Apply LWIP optimization settings - advanced = conf[CONF_ADVANCED] - # DHCP server: only disable if explicitly set to false - # WiFi component handles its own optimization when AP mode is not used - if ( - CONF_ENABLE_LWIP_DHCP_SERVER in advanced - and not advanced[CONF_ENABLE_LWIP_DHCP_SERVER] - ): - add_idf_sdkconfig_option("CONFIG_LWIP_DHCPS", False) - if not advanced.get(CONF_ENABLE_LWIP_MDNS_QUERIES, True): - add_idf_sdkconfig_option("CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES", False) - if not advanced.get(CONF_ENABLE_LWIP_BRIDGE_INTERFACE, False): - add_idf_sdkconfig_option("CONFIG_LWIP_BRIDGEIF_MAX_PORTS", 0) - if advanced.get(CONF_EXECUTE_FROM_PSRAM, False): - add_idf_sdkconfig_option("CONFIG_SPIRAM_FETCH_INSTRUCTIONS", True) - add_idf_sdkconfig_option("CONFIG_SPIRAM_RODATA", True) - - # Apply LWIP core locking for better socket performance - # This is already enabled by default in Arduino framework, where it provides - # significant performance benefits. Our benchmarks show socket operations are - # 24-200% faster with core locking enabled: - # - select() on 4 sockets: ~190μs (Arduino/core locking) vs ~235μs (ESP-IDF default) - # - Up to 200% slower under load when all operations queue through tcpip_thread - # Enabling this makes ESP-IDF socket performance match Arduino framework. - if advanced.get(CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING, True): - add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_CORE_LOCKING", True) - if advanced.get(CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, True): - add_idf_sdkconfig_option("CONFIG_LWIP_CHECK_THREAD_SAFETY", True) - - cg.add_platformio_option("board_build.partitions", "partitions.csv") - if CONF_PARTITIONS in config: - add_extra_build_file( - "partitions.csv", CORE.relative_config_path(config[CONF_PARTITIONS]) - ) - - if assertion_level := advanced.get(CONF_ASSERTION_LEVEL): - for key, flag in ASSERTION_LEVELS.items(): - add_idf_sdkconfig_option(flag, assertion_level == key) - - add_idf_sdkconfig_option("CONFIG_COMPILER_OPTIMIZATION_DEFAULT", False) - compiler_optimization = advanced.get(CONF_COMPILER_OPTIMIZATION) - for key, flag in COMPILER_OPTIMIZATIONS.items(): - add_idf_sdkconfig_option(flag, compiler_optimization == key) - - add_idf_sdkconfig_option( - "CONFIG_LWIP_ESP_LWIP_ASSERT", - conf[CONF_ADVANCED][CONF_ENABLE_LWIP_ASSERT], - ) - - if advanced.get(CONF_IGNORE_EFUSE_MAC_CRC): - add_idf_sdkconfig_option("CONFIG_ESP_MAC_IGNORE_MAC_CRC_ERROR", True) - add_idf_sdkconfig_option( - "CONFIG_ESP_PHY_CALIBRATION_AND_DATA_STORAGE", False - ) - if advanced.get(CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES): - _LOGGER.warning( - "Using experimental features in ESP-IDF may result in unexpected failures." - ) - add_idf_sdkconfig_option("CONFIG_IDF_EXPERIMENTAL_FEATURES", True) - - cg.add_define( - "USE_ESP_IDF_VERSION_CODE", - cg.RawExpression( - f"VERSION_CODE({framework_ver.major}, {framework_ver.minor}, {framework_ver.patch})" - ), - ) - - for name, value in conf[CONF_SDKCONFIG_OPTIONS].items(): - add_idf_sdkconfig_option(name, RawSdkconfigValue(value)) - - for component in conf[CONF_COMPONENTS]: - add_idf_component( - name=component[CONF_NAME], - repo=component.get(CONF_SOURCE), - ref=component.get(CONF_REF), - path=component.get(CONF_PATH), - ) - elif conf[CONF_TYPE] == FRAMEWORK_ARDUINO: - cg.add_platformio_option("framework", "arduino") + else: + cg.add_platformio_option("framework", "arduino, espidf") cg.add_build_flag("-DUSE_ARDUINO") cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ARDUINO") - cg.add_platformio_option("platform_packages", [conf[CONF_SOURCE]]) - - if CONF_PARTITIONS in config: - cg.add_platformio_option("board_build.partitions", config[CONF_PARTITIONS]) - else: - cg.add_platformio_option("board_build.partitions", "partitions.csv") - + cg.add_platformio_option( + "board_build.embed_txtfiles", + [ + "managed_components/espressif__esp_insights/server_certs/https_server.crt", + "managed_components/espressif__esp_rainmaker/server_certs/rmaker_mqtt_server.crt", + "managed_components/espressif__esp_rainmaker/server_certs/rmaker_claim_service_server.crt", + "managed_components/espressif__esp_rainmaker/server_certs/rmaker_ota_server.crt", + ], + ) cg.add_define( "USE_ARDUINO_VERSION_CODE", cg.RawExpression( f"VERSION_CODE({framework_ver.major}, {framework_ver.minor}, {framework_ver.patch})" ), ) - cg.add(RawExpression(f"setCpuFrequencyMhz({freq})")) + add_idf_sdkconfig_option( + "CONFIG_ARDUINO_LOOP_STACK_SIZE", + conf[CONF_ADVANCED][CONF_LOOP_TASK_STACK_SIZE], + ) + add_idf_sdkconfig_option("CONFIG_AUTOSTART_ARDUINO", True) + add_idf_sdkconfig_option("CONFIG_MBEDTLS_PSK_MODES", True) + add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True) + add_idf_sdkconfig_option("CONFIG_ESP_PHY_REDUCE_TX_POWER", True) + + # ESP32-S2 Arduino: Disable USB Serial on boot to avoid TinyUSB dependency + if get_esp32_variant() == VARIANT_ESP32S2: + cg.add_build_unflag("-DARDUINO_USB_CDC_ON_BOOT=1") + cg.add_build_unflag("-DARDUINO_USB_CDC_ON_BOOT=0") + cg.add_build_flag("-DARDUINO_USB_CDC_ON_BOOT=0") + + cg.add_build_flag("-Wno-nonnull-compare") + + add_idf_sdkconfig_option(f"CONFIG_IDF_TARGET_{variant}", True) + add_idf_sdkconfig_option( + f"CONFIG_ESPTOOLPY_FLASHSIZE_{config[CONF_FLASH_SIZE]}", True + ) + add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_SINGLE_APP", False) + add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_CUSTOM", True) + add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_CUSTOM_FILENAME", "partitions.csv") + + # Increase freertos tick speed from 100Hz to 1kHz so that delay() resolution is 1ms + add_idf_sdkconfig_option("CONFIG_FREERTOS_HZ", 1000) + + # Setup watchdog + add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT", True) + add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_PANIC", True) + add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0", False) + add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1", False) + + # Disable dynamic log level control to save memory + add_idf_sdkconfig_option("CONFIG_LOG_DYNAMIC_LEVEL_CONTROL", False) + + # Reduce PHY TX power in the event of a brownout + add_idf_sdkconfig_option("CONFIG_ESP_PHY_REDUCE_TX_POWER", True) + + # Set default CPU frequency + add_idf_sdkconfig_option( + f"CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_{config[CONF_CPU_FREQUENCY][:-3]}", True + ) + + # Apply LWIP optimization settings + advanced = conf[CONF_ADVANCED] + # DHCP server: only disable if explicitly set to false + # WiFi component handles its own optimization when AP mode is not used + # When using Arduino with Ethernet, DHCP server functions must be available + # for the Network library to compile, even if not actively used + if advanced.get(CONF_ENABLE_LWIP_DHCP_SERVER) is False and not ( + conf[CONF_TYPE] == FRAMEWORK_ARDUINO and "ethernet" in CORE.loaded_integrations + ): + add_idf_sdkconfig_option("CONFIG_LWIP_DHCPS", False) + if not advanced[CONF_ENABLE_LWIP_MDNS_QUERIES]: + add_idf_sdkconfig_option("CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES", False) + if not advanced[CONF_ENABLE_LWIP_BRIDGE_INTERFACE]: + add_idf_sdkconfig_option("CONFIG_LWIP_BRIDGEIF_MAX_PORTS", 0) + + _configure_lwip_max_sockets(conf) + + if advanced[CONF_EXECUTE_FROM_PSRAM]: + add_idf_sdkconfig_option("CONFIG_SPIRAM_FETCH_INSTRUCTIONS", True) + add_idf_sdkconfig_option("CONFIG_SPIRAM_RODATA", True) + + # Apply LWIP core locking for better socket performance + # This is already enabled by default in Arduino framework, where it provides + # significant performance benefits. Our benchmarks show socket operations are + # 24-200% faster with core locking enabled: + # - select() on 4 sockets: ~190μs (Arduino/core locking) vs ~235μs (ESP-IDF default) + # - Up to 200% slower under load when all operations queue through tcpip_thread + # Enabling this makes ESP-IDF socket performance match Arduino framework. + if advanced[CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING]: + add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_CORE_LOCKING", True) + if advanced[CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY]: + add_idf_sdkconfig_option("CONFIG_LWIP_CHECK_THREAD_SAFETY", True) + + # Disable placing libc locks in IRAM to save RAM + # This is safe for ESPHome since no IRAM ISRs (interrupts that run while cache is disabled) + # use libc lock APIs. Saves approximately 1.3KB (1,356 bytes) of IRAM. + if advanced[CONF_DISABLE_LIBC_LOCKS_IN_IRAM]: + add_idf_sdkconfig_option("CONFIG_LIBC_LOCKS_PLACE_IN_IRAM", False) + + # Disable VFS support for termios (terminal I/O functions) + # ESPHome doesn't use termios functions on ESP32 (only used in host UART driver). + # Saves approximately 1.8KB of flash when disabled (default). + add_idf_sdkconfig_option( + "CONFIG_VFS_SUPPORT_TERMIOS", not advanced[CONF_DISABLE_VFS_SUPPORT_TERMIOS] + ) + + # Disable VFS support for select() with file descriptors + # ESPHome only uses select() with sockets via lwip_select(), which still works. + # VFS select is only needed for UART/eventfd file descriptors. + # Components that need it (e.g., openthread) call require_vfs_select(). + # Saves approximately 2.7KB of flash when disabled (default). + if CORE.data.get(KEY_VFS_SELECT_REQUIRED, False): + # Component requires VFS select - force enable regardless of user setting + add_idf_sdkconfig_option("CONFIG_VFS_SUPPORT_SELECT", True) + else: + # No component needs it - allow user to control (default: disabled) + add_idf_sdkconfig_option( + "CONFIG_VFS_SUPPORT_SELECT", not advanced[CONF_DISABLE_VFS_SUPPORT_SELECT] + ) + + # Disable VFS support for directory functions (opendir, readdir, mkdir, etc.) + # ESPHome doesn't use directory functions on ESP32. + # Components that need it (e.g., storage components) call require_vfs_dir(). + # Saves approximately 0.5KB+ of flash when disabled (default). + if CORE.data.get(KEY_VFS_DIR_REQUIRED, False): + # Component requires VFS directory support - force enable regardless of user setting + add_idf_sdkconfig_option("CONFIG_VFS_SUPPORT_DIR", True) + else: + # No component needs it - allow user to control (default: disabled) + add_idf_sdkconfig_option( + "CONFIG_VFS_SUPPORT_DIR", not advanced[CONF_DISABLE_VFS_SUPPORT_DIR] + ) + + cg.add_platformio_option("board_build.partitions", "partitions.csv") + if CONF_PARTITIONS in config: + add_extra_build_file( + "partitions.csv", CORE.relative_config_path(config[CONF_PARTITIONS]) + ) + + if assertion_level := advanced.get(CONF_ASSERTION_LEVEL): + for key, flag in ASSERTION_LEVELS.items(): + add_idf_sdkconfig_option(flag, assertion_level == key) + + add_idf_sdkconfig_option("CONFIG_COMPILER_OPTIMIZATION_DEFAULT", False) + compiler_optimization = advanced[CONF_COMPILER_OPTIMIZATION] + for key, flag in COMPILER_OPTIMIZATIONS.items(): + add_idf_sdkconfig_option(flag, compiler_optimization == key) + + add_idf_sdkconfig_option( + "CONFIG_LWIP_ESP_LWIP_ASSERT", + conf[CONF_ADVANCED][CONF_ENABLE_LWIP_ASSERT], + ) + + if advanced[CONF_IGNORE_EFUSE_MAC_CRC]: + add_idf_sdkconfig_option("CONFIG_ESP_MAC_IGNORE_MAC_CRC_ERROR", True) + add_idf_sdkconfig_option("CONFIG_ESP_PHY_CALIBRATION_AND_DATA_STORAGE", False) + if advanced[CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES]: + _LOGGER.warning( + "Using experimental features in ESP-IDF may result in unexpected failures." + ) + add_idf_sdkconfig_option("CONFIG_IDF_EXPERIMENTAL_FEATURES", True) + if config[CONF_FLASH_SIZE] == "32MB": + add_idf_sdkconfig_option( + "CONFIG_BOOTLOADER_CACHE_32BIT_ADDR_QUAD_FLASH", True + ) + + cg.add_define("ESPHOME_LOOP_TASK_STACK_SIZE", advanced[CONF_LOOP_TASK_STACK_SIZE]) + + cg.add_define( + "USE_ESP_IDF_VERSION_CODE", + cg.RawExpression( + f"VERSION_CODE({framework_ver.major}, {framework_ver.minor}, {framework_ver.patch})" + ), + ) + + add_idf_sdkconfig_option(f"CONFIG_LOG_DEFAULT_LEVEL_{conf[CONF_LOG_LEVEL]}", True) + + for name, value in conf[CONF_SDKCONFIG_OPTIONS].items(): + add_idf_sdkconfig_option(name, RawSdkconfigValue(value)) + + for component in conf[CONF_COMPONENTS]: + add_idf_component( + name=component[CONF_NAME], + repo=component.get(CONF_SOURCE), + ref=component.get(CONF_REF), + path=component.get(CONF_PATH), + ) APP_PARTITION_SIZES = { @@ -1038,13 +1177,14 @@ def _write_sdkconfig(): ) + "\n" ) + if write_file_if_changed(internal_path, contents): # internal changed, update real one write_file_if_changed(sdk_path, contents) def _write_idf_component_yml(): - yml_path = Path(CORE.relative_build_path("src/idf_component.yml")) + yml_path = CORE.relative_build_path("src/idf_component.yml") if CORE.data[KEY_ESP32][KEY_COMPONENTS]: components: dict = CORE.data[KEY_ESP32][KEY_COMPONENTS] dependencies = {} @@ -1060,49 +1200,50 @@ def _write_idf_component_yml(): contents = yaml_util.dump({"dependencies": dependencies}) else: contents = "" - write_file_if_changed(yml_path, contents) + if write_file_if_changed(yml_path, contents): + dependencies_lock = CORE.relative_build_path("dependencies.lock") + if dependencies_lock.is_file(): + dependencies_lock.unlink() + clean_cmake_cache() # Called by writer.py def copy_files(): - if ( - CORE.using_arduino - and "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES] - ): - write_file_if_changed( - CORE.relative_build_path("partitions.csv"), - get_arduino_partition_csv( - CORE.platformio_options.get("board_upload.flash_size") - ), - ) - if CORE.using_esp_idf: - _write_sdkconfig() - _write_idf_component_yml() - if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]: + _write_sdkconfig() + _write_idf_component_yml() + + if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]: + if CORE.using_arduino: + write_file_if_changed( + CORE.relative_build_path("partitions.csv"), + get_arduino_partition_csv( + CORE.platformio_options.get("board_upload.flash_size") + ), + ) + else: write_file_if_changed( CORE.relative_build_path("partitions.csv"), get_idf_partition_csv( CORE.platformio_options.get("board_upload.flash_size") ), ) - # IDF build scripts look for version string to put in the build. - # However, if the build path does not have an initialized git repo, - # and no version.txt file exists, the CMake script fails for some setups. - # Fix by manually pasting a version.txt file, containing the ESPHome version - write_file_if_changed( - CORE.relative_build_path("version.txt"), - __version__, - ) + # IDF build scripts look for version string to put in the build. + # However, if the build path does not have an initialized git repo, + # and no version.txt file exists, the CMake script fails for some setups. + # Fix by manually pasting a version.txt file, containing the ESPHome version + write_file_if_changed( + CORE.relative_build_path("version.txt"), + __version__, + ) for file in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES].values(): - if file[KEY_PATH].startswith("http"): + name: str = file[KEY_NAME] + path: Path = file[KEY_PATH] + if str(path).startswith("http"): import requests - mkdir_p(CORE.relative_build_path(os.path.dirname(file[KEY_NAME]))) - with open(CORE.relative_build_path(file[KEY_NAME]), "wb") as f: - f.write(requests.get(file[KEY_PATH], timeout=30).content) + CORE.relative_build_path(name).parent.mkdir(parents=True, exist_ok=True) + content = requests.get(path, timeout=30).content + CORE.relative_build_path(name).write_bytes(content) else: - copy_file_if_changed( - file[KEY_PATH], - CORE.relative_build_path(file[KEY_NAME]), - ) + copy_file_if_changed(path, CORE.relative_build_path(name)) diff --git a/esphome/components/esp32/boards.py b/esphome/components/esp32/boards.py index cf6cf8cbe5..cbb314650a 100644 --- a/esphome/components/esp32/boards.py +++ b/esphome/components/esp32/boards.py @@ -1504,6 +1504,10 @@ BOARDS = { "name": "BPI-Bit", "variant": VARIANT_ESP32, }, + "bpi-centi-s3": { + "name": "BPI-Centi-S3", + "variant": VARIANT_ESP32S3, + }, "bpi_leaf_s3": { "name": "BPI-Leaf-S3", "variant": VARIANT_ESP32S3, @@ -1560,6 +1564,10 @@ BOARDS = { "name": "DFRobot Beetle ESP32-C3", "variant": VARIANT_ESP32C3, }, + "dfrobot_firebeetle2_esp32c6": { + "name": "DFRobot FireBeetle 2 ESP32-C6", + "variant": VARIANT_ESP32C6, + }, "dfrobot_firebeetle2_esp32e": { "name": "DFRobot Firebeetle 2 ESP32-E", "variant": VARIANT_ESP32, @@ -1600,6 +1608,22 @@ BOARDS = { "name": "Ai-Thinker ESP-C3-M1-I-Kit", "variant": VARIANT_ESP32C3, }, + "esp32-c5-devkitc-1": { + "name": "Espressif ESP32-C5-DevKitC-1 4MB no PSRAM", + "variant": VARIANT_ESP32C5, + }, + "esp32-c5-devkitc1-n16r4": { + "name": "Espressif ESP32-C5-DevKitC-1 N16R4 (16 MB Flash Quad, 4 MB PSRAM Quad)", + "variant": VARIANT_ESP32C5, + }, + "esp32-c5-devkitc1-n4": { + "name": "Espressif ESP32-C5-DevKitC-1 N4 (4MB no PSRAM)", + "variant": VARIANT_ESP32C5, + }, + "esp32-c5-devkitc1-n8r4": { + "name": "Espressif ESP32-C5-DevKitC-1 N8R4 (8 MB Flash Quad, 4 MB PSRAM Quad)", + "variant": VARIANT_ESP32C5, + }, "esp32-c6-devkitc-1": { "name": "Espressif ESP32-C6-DevKitC-1", "variant": VARIANT_ESP32C6, @@ -1664,10 +1688,46 @@ BOARDS = { "name": "Espressif ESP32-S3-DevKitC-1-N8 (8 MB QD, No PSRAM)", "variant": VARIANT_ESP32S3, }, + "esp32-s3-devkitc-1-n32r8v": { + "name": "Espressif ESP32-S3-DevKitC-1-N32R8V (32 MB Flash Octal, 8 MB PSRAM Octal)", + "variant": VARIANT_ESP32S3, + }, + "esp32-s3-devkitc1-n16r16": { + "name": "Espressif ESP32-S3-DevKitC-1-N16R16V (16 MB Flash Quad, 16 MB PSRAM Octal)", + "variant": VARIANT_ESP32S3, + }, + "esp32-s3-devkitc1-n16r2": { + "name": "Espressif ESP32-S3-DevKitC-1-N16R2 (16 MB Flash Quad, 2 MB PSRAM Quad)", + "variant": VARIANT_ESP32S3, + }, + "esp32-s3-devkitc1-n16r8": { + "name": "Espressif ESP32-S3-DevKitC-1-N16R8V (16 MB Flash Quad, 8 MB PSRAM Octal)", + "variant": VARIANT_ESP32S3, + }, + "esp32-s3-devkitc1-n4r2": { + "name": "Espressif ESP32-S3-DevKitC-1-N4R2 (4 MB Flash Quad, 2 MB PSRAM Quad)", + "variant": VARIANT_ESP32S3, + }, + "esp32-s3-devkitc1-n4r8": { + "name": "Espressif ESP32-S3-DevKitC-1-N4R8 (4 MB Flash Quad, 8 MB PSRAM Octal)", + "variant": VARIANT_ESP32S3, + }, + "esp32-s3-devkitc1-n8r2": { + "name": "Espressif ESP32-S3-DevKitC-1-N8R2 (8 MB Flash Quad, 2 MB PSRAM quad)", + "variant": VARIANT_ESP32S3, + }, + "esp32-s3-devkitc1-n8r8": { + "name": "Espressif ESP32-S3-DevKitC-1-N8R8 (8 MB Flash Quad, 8 MB PSRAM Octal)", + "variant": VARIANT_ESP32S3, + }, "esp32-s3-devkitm-1": { "name": "Espressif ESP32-S3-DevKitM-1", "variant": VARIANT_ESP32S3, }, + "esp32-s3-fh4r2": { + "name": "Espressif ESP32-S3-FH4R2 (4 MB QD, 2MB PSRAM)", + "variant": VARIANT_ESP32S3, + }, "esp32-solo1": { "name": "Espressif Generic ESP32-solo1 4M Flash", "variant": VARIANT_ESP32, @@ -1764,6 +1824,10 @@ BOARDS = { "name": "Franzininho WiFi MSC", "variant": VARIANT_ESP32S2, }, + "freenove-esp32-s3-n8r8": { + "name": "Freenove ESP32-S3 WROOM N8R8 (8MB Flash / 8MB PSRAM)", + "variant": VARIANT_ESP32S3, + }, "freenove_esp32_s3_wroom": { "name": "Freenove ESP32-S3 WROOM N8R8 (8MB Flash / 8MB PSRAM)", "variant": VARIANT_ESP32S3, @@ -1964,6 +2028,10 @@ BOARDS = { "name": "M5Stack AtomS3", "variant": VARIANT_ESP32S3, }, + "m5stack-atoms3u": { + "name": "M5Stack AtomS3U", + "variant": VARIANT_ESP32S3, + }, "m5stack-core-esp32": { "name": "M5Stack Core ESP32", "variant": VARIANT_ESP32, @@ -2000,6 +2068,10 @@ BOARDS = { "name": "M5Stack Station", "variant": VARIANT_ESP32, }, + "m5stack-tab5-p4": { + "name": "M5STACK Tab5 esp32-p4 Board", + "variant": VARIANT_ESP32P4, + }, "m5stack-timer-cam": { "name": "M5Stack Timer CAM", "variant": VARIANT_ESP32, @@ -2084,6 +2156,10 @@ BOARDS = { "name": "Ai-Thinker NodeMCU-32S2 (ESP-12K)", "variant": VARIANT_ESP32S2, }, + "nologo_esp32c3_super_mini": { + "name": "Nologo ESP32C3 SuperMini", + "variant": VARIANT_ESP32C3, + }, "nscreen-32": { "name": "YeaCreate NSCREEN-32", "variant": VARIANT_ESP32, @@ -2192,6 +2268,10 @@ BOARDS = { "name": "SparkFun LoRa Gateway 1-Channel", "variant": VARIANT_ESP32, }, + "sparkfun_pro_micro_esp32c3": { + "name": "SparkFun Pro Micro ESP32-C3", + "variant": VARIANT_ESP32C3, + }, "sparkfun_qwiic_pocket_esp32c6": { "name": "SparkFun ESP32-C6 Qwiic Pocket", "variant": VARIANT_ESP32C6, @@ -2256,6 +2336,14 @@ BOARDS = { "name": "Turta IoT Node", "variant": VARIANT_ESP32, }, + "um_bling": { + "name": "Unexpected Maker BLING!", + "variant": VARIANT_ESP32S3, + }, + "um_edges3_d": { + "name": "Unexpected Maker EDGES3[D]", + "variant": VARIANT_ESP32S3, + }, "um_feathers2": { "name": "Unexpected Maker FeatherS2", "variant": VARIANT_ESP32S2, @@ -2268,10 +2356,18 @@ BOARDS = { "name": "Unexpected Maker FeatherS3", "variant": VARIANT_ESP32S3, }, + "um_feathers3_neo": { + "name": "Unexpected Maker FeatherS3 Neo", + "variant": VARIANT_ESP32S3, + }, "um_nanos3": { "name": "Unexpected Maker NanoS3", "variant": VARIANT_ESP32S3, }, + "um_omgs3": { + "name": "Unexpected Maker OMGS3", + "variant": VARIANT_ESP32S3, + }, "um_pros3": { "name": "Unexpected Maker PROS3", "variant": VARIANT_ESP32S3, @@ -2280,6 +2376,14 @@ BOARDS = { "name": "Unexpected Maker RMP", "variant": VARIANT_ESP32S2, }, + "um_squixl": { + "name": "Unexpected Maker SQUiXL", + "variant": VARIANT_ESP32S3, + }, + "um_tinyc6": { + "name": "Unexpected Maker TinyC6", + "variant": VARIANT_ESP32C6, + }, "um_tinys2": { "name": "Unexpected Maker TinyS2", "variant": VARIANT_ESP32S2, @@ -2396,8 +2500,13 @@ BOARDS = { "name": "YelloByte YB-ESP32-S3-AMP (Rev.3)", "variant": VARIANT_ESP32S3, }, + "yb_esp32s3_drv": { + "name": "YelloByte YB-ESP32-S3-DRV", + "variant": VARIANT_ESP32S3, + }, "yb_esp32s3_eth": { "name": "YelloByte YB-ESP32-S3-ETH", "variant": VARIANT_ESP32S3, }, } +# DO NOT ADD ANYTHING BELOW THIS LINE diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index f3bdfea2a0..6215ff862f 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -1,11 +1,13 @@ #ifdef USE_ESP32 +#include "esphome/core/defines.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "preferences.h" #include #include #include +#include #include #include #include @@ -52,6 +54,16 @@ void arch_init() { disableCore1WDT(); #endif #endif + + // If the bootloader was compiled with CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE the current + // partition will get rolled back unless it is marked as valid. + esp_ota_img_states_t state; + const esp_partition_t *running = esp_ota_get_running_partition(); + if (esp_ota_get_state_partition(running, &state) == ESP_OK) { + if (state == ESP_OTA_IMG_PENDING_VERIFY) { + esp_ota_mark_app_valid_cancel_rollback(); + } + } } void IRAM_ATTR HOT arch_feed_wdt() { esp_task_wdt_reset(); } @@ -85,7 +97,11 @@ void loop_task(void *pv_params) { extern "C" void app_main() { esp32::setup_preferences(); - xTaskCreate(loop_task, "loopTask", 8192, nullptr, 1, &loop_task_handle); +#if CONFIG_FREERTOS_UNICORE + xTaskCreate(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1, &loop_task_handle); +#else + xTaskCreatePinnedToCore(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1, &loop_task_handle, 1); +#endif } #endif // USE_ESP_IDF diff --git a/esphome/components/esp32/gpio.cpp b/esphome/components/esp32/gpio.cpp index 27572063ca..a98245b889 100644 --- a/esphome/components/esp32/gpio.cpp +++ b/esphome/components/esp32/gpio.cpp @@ -54,13 +54,13 @@ struct ISRPinArg { ISRInternalGPIOPin ESP32InternalGPIOPin::to_isr() const { auto *arg = new ISRPinArg{}; // NOLINT(cppcoreguidelines-owning-memory) - arg->pin = this->pin_; + arg->pin = this->get_pin_num(); arg->flags = gpio::FLAG_NONE; - arg->inverted = inverted_; + arg->inverted = this->pin_flags_.inverted; #if defined(USE_ESP32_VARIANT_ESP32) - arg->use_rtc = rtc_gpio_is_valid_gpio(this->pin_); + arg->use_rtc = rtc_gpio_is_valid_gpio(this->get_pin_num()); if (arg->use_rtc) - arg->rtc_pin = rtc_io_number_get(this->pin_); + arg->rtc_pin = rtc_io_number_get(this->get_pin_num()); #endif return ISRInternalGPIOPin((void *) arg); } @@ -69,23 +69,23 @@ void ESP32InternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpi gpio_int_type_t idf_type = GPIO_INTR_ANYEDGE; switch (type) { case gpio::INTERRUPT_RISING_EDGE: - idf_type = inverted_ ? GPIO_INTR_NEGEDGE : GPIO_INTR_POSEDGE; + idf_type = this->pin_flags_.inverted ? GPIO_INTR_NEGEDGE : GPIO_INTR_POSEDGE; break; case gpio::INTERRUPT_FALLING_EDGE: - idf_type = inverted_ ? GPIO_INTR_POSEDGE : GPIO_INTR_NEGEDGE; + idf_type = this->pin_flags_.inverted ? GPIO_INTR_POSEDGE : GPIO_INTR_NEGEDGE; break; case gpio::INTERRUPT_ANY_EDGE: idf_type = GPIO_INTR_ANYEDGE; break; case gpio::INTERRUPT_LOW_LEVEL: - idf_type = inverted_ ? GPIO_INTR_HIGH_LEVEL : GPIO_INTR_LOW_LEVEL; + idf_type = this->pin_flags_.inverted ? GPIO_INTR_HIGH_LEVEL : GPIO_INTR_LOW_LEVEL; break; case gpio::INTERRUPT_HIGH_LEVEL: - idf_type = inverted_ ? GPIO_INTR_LOW_LEVEL : GPIO_INTR_HIGH_LEVEL; + idf_type = this->pin_flags_.inverted ? GPIO_INTR_LOW_LEVEL : GPIO_INTR_HIGH_LEVEL; break; } - gpio_set_intr_type(pin_, idf_type); - gpio_intr_enable(pin_); + gpio_set_intr_type(this->get_pin_num(), idf_type); + gpio_intr_enable(this->get_pin_num()); if (!isr_service_installed) { auto res = gpio_install_isr_service(ESP_INTR_FLAG_LEVEL3); if (res != ESP_OK) { @@ -94,31 +94,31 @@ void ESP32InternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpi } isr_service_installed = true; } - gpio_isr_handler_add(pin_, func, arg); + gpio_isr_handler_add(this->get_pin_num(), func, arg); } std::string ESP32InternalGPIOPin::dump_summary() const { char buffer[32]; - snprintf(buffer, sizeof(buffer), "GPIO%" PRIu32, static_cast(pin_)); + snprintf(buffer, sizeof(buffer), "GPIO%" PRIu32, static_cast(this->pin_)); return buffer; } void ESP32InternalGPIOPin::setup() { gpio_config_t conf{}; - conf.pin_bit_mask = 1ULL << static_cast(pin_); - conf.mode = flags_to_mode(flags_); - conf.pull_up_en = flags_ & gpio::FLAG_PULLUP ? GPIO_PULLUP_ENABLE : GPIO_PULLUP_DISABLE; - conf.pull_down_en = flags_ & gpio::FLAG_PULLDOWN ? GPIO_PULLDOWN_ENABLE : GPIO_PULLDOWN_DISABLE; + conf.pin_bit_mask = 1ULL << static_cast(this->pin_); + conf.mode = flags_to_mode(this->flags_); + conf.pull_up_en = this->flags_ & gpio::FLAG_PULLUP ? GPIO_PULLUP_ENABLE : GPIO_PULLUP_DISABLE; + conf.pull_down_en = this->flags_ & gpio::FLAG_PULLDOWN ? GPIO_PULLDOWN_ENABLE : GPIO_PULLDOWN_DISABLE; conf.intr_type = GPIO_INTR_DISABLE; gpio_config(&conf); - if (flags_ & gpio::FLAG_OUTPUT) { - gpio_set_drive_capability(pin_, drive_strength_); + if (this->flags_ & gpio::FLAG_OUTPUT) { + gpio_set_drive_capability(this->get_pin_num(), this->get_drive_strength()); } } void ESP32InternalGPIOPin::pin_mode(gpio::Flags flags) { // can't call gpio_config here because that logs in esp-idf which may cause issues - gpio_set_direction(pin_, flags_to_mode(flags)); + gpio_set_direction(this->get_pin_num(), flags_to_mode(flags)); gpio_pull_mode_t pull_mode = GPIO_FLOATING; if ((flags & gpio::FLAG_PULLUP) && (flags & gpio::FLAG_PULLDOWN)) { pull_mode = GPIO_PULLUP_PULLDOWN; @@ -127,12 +127,16 @@ void ESP32InternalGPIOPin::pin_mode(gpio::Flags flags) { } else if (flags & gpio::FLAG_PULLDOWN) { pull_mode = GPIO_PULLDOWN_ONLY; } - gpio_set_pull_mode(pin_, pull_mode); + gpio_set_pull_mode(this->get_pin_num(), pull_mode); } -bool ESP32InternalGPIOPin::digital_read() { return bool(gpio_get_level(pin_)) != inverted_; } -void ESP32InternalGPIOPin::digital_write(bool value) { gpio_set_level(pin_, value != inverted_ ? 1 : 0); } -void ESP32InternalGPIOPin::detach_interrupt() const { gpio_intr_disable(pin_); } +bool ESP32InternalGPIOPin::digital_read() { + return bool(gpio_get_level(this->get_pin_num())) != this->pin_flags_.inverted; +} +void ESP32InternalGPIOPin::digital_write(bool value) { + gpio_set_level(this->get_pin_num(), value != this->pin_flags_.inverted ? 1 : 0); +} +void ESP32InternalGPIOPin::detach_interrupt() const { gpio_intr_disable(this->get_pin_num()); } } // namespace esp32 diff --git a/esphome/components/esp32/gpio.h b/esphome/components/esp32/gpio.h index 0fefc1c058..d30f4bdcba 100644 --- a/esphome/components/esp32/gpio.h +++ b/esphome/components/esp32/gpio.h @@ -7,12 +7,18 @@ namespace esphome { namespace esp32 { +// Static assertions to ensure our bit-packed fields can hold the enum values +static_assert(GPIO_NUM_MAX <= 256, "gpio_num_t has too many values for uint8_t"); +static_assert(GPIO_DRIVE_CAP_MAX <= 4, "gpio_drive_cap_t has too many values for 2-bit field"); + class ESP32InternalGPIOPin : public InternalGPIOPin { public: - void set_pin(gpio_num_t pin) { pin_ = pin; } - void set_inverted(bool inverted) { inverted_ = inverted; } - void set_drive_strength(gpio_drive_cap_t drive_strength) { drive_strength_ = drive_strength; } - void set_flags(gpio::Flags flags) { flags_ = flags; } + void set_pin(gpio_num_t pin) { this->pin_ = static_cast(pin); } + void set_inverted(bool inverted) { this->pin_flags_.inverted = inverted; } + void set_drive_strength(gpio_drive_cap_t drive_strength) { + this->pin_flags_.drive_strength = static_cast(drive_strength); + } + void set_flags(gpio::Flags flags) { this->flags_ = flags; } void setup() override; void pin_mode(gpio::Flags flags) override; @@ -21,17 +27,26 @@ class ESP32InternalGPIOPin : public InternalGPIOPin { std::string dump_summary() const override; void detach_interrupt() const override; ISRInternalGPIOPin to_isr() const override; - uint8_t get_pin() const override { return (uint8_t) pin_; } - gpio::Flags get_flags() const override { return flags_; } - bool is_inverted() const override { return inverted_; } + uint8_t get_pin() const override { return this->pin_; } + gpio::Flags get_flags() const override { return this->flags_; } + bool is_inverted() const override { return this->pin_flags_.inverted; } + gpio_num_t get_pin_num() const { return static_cast(this->pin_); } + gpio_drive_cap_t get_drive_strength() const { return static_cast(this->pin_flags_.drive_strength); } protected: void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; - gpio_num_t pin_; - gpio_drive_cap_t drive_strength_; - gpio::Flags flags_; - bool inverted_; + // Memory layout: 8 bytes total on 32-bit systems + // - 3 bytes for members below + // - 1 byte padding for alignment + // - 4 bytes for vtable pointer + uint8_t pin_; // GPIO pin number (0-255, actual max ~54 on ESP32) + gpio::Flags flags_{}; // GPIO flags (1 byte) + struct PinFlags { + uint8_t inverted : 1; // Invert pin logic (1 bit) + uint8_t drive_strength : 2; // Drive strength 0-3 (2 bits) + uint8_t reserved : 5; // Reserved for future use (5 bits) + } pin_flags_{}; // Total: 1 byte // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) static bool isr_service_installed; }; diff --git a/esphome/components/esp32/gpio.py b/esphome/components/esp32/gpio.py index 513f463d57..954891ea8d 100644 --- a/esphome/components/esp32/gpio.py +++ b/esphome/components/esp32/gpio.py @@ -223,7 +223,10 @@ async def esp32_pin_to_code(config): var = cg.new_Pvariable(config[CONF_ID]) num = config[CONF_NUMBER] cg.add(var.set_pin(getattr(gpio_num_t, f"GPIO_NUM_{num}"))) - cg.add(var.set_inverted(config[CONF_INVERTED])) + # Only set if true to avoid bloating setup() function + # (inverted bit in pin_flags_ bitfield is zero-initialized to false) + if config[CONF_INVERTED]: + cg.add(var.set_inverted(True)) if CONF_DRIVE_STRENGTH in config: cg.add(var.set_drive_strength(config[CONF_DRIVE_STRENGTH])) cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) diff --git a/esphome/components/esp32/iram_fix.py.script b/esphome/components/esp32/iram_fix.py.script new file mode 100644 index 0000000000..0d23f9a81b --- /dev/null +++ b/esphome/components/esp32/iram_fix.py.script @@ -0,0 +1,71 @@ +import os +import re + +# pylint: disable=E0602 +Import("env") # noqa + +# IRAM size for testing mode (2MB - large enough to accommodate grouped tests) +TESTING_IRAM_SIZE = 0x200000 + + +def patch_idf_linker_script(source, target, env): + """Patch ESP-IDF linker script to increase IRAM size for testing mode.""" + # Check if we're in testing mode by looking for the define + build_flags = env.get("BUILD_FLAGS", []) + testing_mode = any("-DESPHOME_TESTING_MODE" in flag for flag in build_flags) + + if not testing_mode: + return + + # For ESP-IDF, the linker scripts are generated in the build directory + build_dir = env.subst("$BUILD_DIR") + + # The memory.ld file is directly in the build directory + memory_ld = os.path.join(build_dir, "memory.ld") + + if not os.path.exists(memory_ld): + print(f"ESPHome: Warning - could not find linker script at {memory_ld}") + return + + try: + with open(memory_ld, "r") as f: + content = f.read() + except OSError as e: + print(f"ESPHome: Error reading linker script: {e}") + return + + # Check if this file contains iram0_0_seg + if 'iram0_0_seg' not in content: + print(f"ESPHome: Warning - iram0_0_seg not found in {memory_ld}") + return + + # Look for iram0_0_seg definition and increase its length + # ESP-IDF format can be: + # iram0_0_seg (RX) : org = 0x40080000, len = 0x20000 + 0x0 + # or more complex with nested parentheses: + # iram0_0_seg (RX) : org = (0x40370000 + 0x4000), len = (((0x403CB700 - (0x40378000 - 0x3FC88000)) - 0x3FC88000) + 0x8000 - 0x4000) + # We want to change len to TESTING_IRAM_SIZE for testing + + # Use a more robust approach: find the line and manually parse it + lines = content.split('\n') + for i, line in enumerate(lines): + if 'iram0_0_seg' in line and 'len' in line: + # Find the position of "len = " and replace everything after it until the end of the statement + match = re.search(r'(iram0_0_seg\s*\([^)]*\)\s*:\s*org\s*=\s*(?:\([^)]+\)|0x[0-9a-fA-F]+)\s*,\s*len\s*=\s*)(.+?)(\s*)$', line) + if match: + lines[i] = f"{match.group(1)}{TESTING_IRAM_SIZE:#x}{match.group(3)}" + break + + updated = '\n'.join(lines) + + if updated != content: + with open(memory_ld, "w") as f: + f.write(updated) + print(f"ESPHome: Patched IRAM size to {TESTING_IRAM_SIZE:#x} in {memory_ld} for testing mode") + else: + print(f"ESPHome: Warning - could not patch iram0_0_seg in {memory_ld}") + + +# Hook into the build process before linking +# For ESP-IDF, we need to run this after the linker scripts are generated +env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", patch_idf_linker_script) diff --git a/esphome/components/esp32/pre_build.py.script b/esphome/components/esp32/pre_build.py.script new file mode 100644 index 0000000000..af12275a0b --- /dev/null +++ b/esphome/components/esp32/pre_build.py.script @@ -0,0 +1,9 @@ +Import("env") # noqa: F821 + +# Remove custom_sdkconfig from the board config as it causes +# pioarduino to enable some strange hybrid build mode that breaks IDF +board = env.BoardConfig() +if "espidf.custom_sdkconfig" in board: + del board._manifest["espidf"]["custom_sdkconfig"] + if not board._manifest["espidf"]: + del board._manifest["espidf"] diff --git a/esphome/components/esp32/preferences.cpp b/esphome/components/esp32/preferences.cpp index e53cdd90d3..7bdbb265ca 100644 --- a/esphome/components/esp32/preferences.cpp +++ b/esphome/components/esp32/preferences.cpp @@ -8,6 +8,7 @@ #include #include #include +#include namespace esphome { namespace esp32 { @@ -16,7 +17,14 @@ static const char *const TAG = "esp32.preferences"; struct NVSData { std::string key; - std::vector data; + std::unique_ptr data; + size_t len; + + void set_data(const uint8_t *src, size_t size) { + data = std::make_unique(size); + memcpy(data.get(), src, size); + len = size; + } }; static std::vector s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) @@ -29,26 +37,26 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend { // try find in pending saves and update that for (auto &obj : s_pending_save) { if (obj.key == key) { - obj.data.assign(data, data + len); + obj.set_data(data, len); return true; } } NVSData save{}; save.key = key; - save.data.assign(data, data + len); - s_pending_save.emplace_back(save); - ESP_LOGVV(TAG, "s_pending_save: key: %s, len: %d", key.c_str(), len); + save.set_data(data, len); + s_pending_save.emplace_back(std::move(save)); + ESP_LOGVV(TAG, "s_pending_save: key: %s, len: %zu", key.c_str(), len); return true; } bool load(uint8_t *data, size_t len) override { // try find in pending saves and load from that for (auto &obj : s_pending_save) { if (obj.key == key) { - if (obj.data.size() != len) { + if (obj.len != len) { // size mismatch return false; } - memcpy(data, obj.data.data(), len); + memcpy(data, obj.data.get(), len); return true; } } @@ -60,7 +68,7 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend { return false; } if (actual_len != len) { - ESP_LOGVV(TAG, "NVS length does not match (%u!=%u)", actual_len, len); + ESP_LOGVV(TAG, "NVS length does not match (%zu!=%zu)", actual_len, len); return false; } err = nvs_get_blob(nvs_handle, key.c_str(), data, &len); @@ -68,7 +76,7 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend { ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key.c_str(), esp_err_to_name(err)); return false; } else { - ESP_LOGVV(TAG, "nvs_get_blob: key: %s, len: %d", key.c_str(), len); + ESP_LOGVV(TAG, "nvs_get_blob: key: %s, len: %zu", key.c_str(), len); } return true; } @@ -111,7 +119,7 @@ class ESP32Preferences : public ESPPreferences { if (s_pending_save.empty()) return true; - ESP_LOGV(TAG, "Saving %d items...", s_pending_save.size()); + ESP_LOGV(TAG, "Saving %zu items...", s_pending_save.size()); // goal try write all pending saves even if one fails int cached = 0, written = 0, failed = 0; esp_err_t last_err = ESP_OK; @@ -122,11 +130,10 @@ class ESP32Preferences : public ESPPreferences { const auto &save = s_pending_save[i]; ESP_LOGVV(TAG, "Checking if NVS data %s has changed", save.key.c_str()); if (is_changed(nvs_handle, save)) { - esp_err_t err = nvs_set_blob(nvs_handle, save.key.c_str(), save.data.data(), save.data.size()); - ESP_LOGV(TAG, "sync: key: %s, len: %d", save.key.c_str(), save.data.size()); + esp_err_t err = nvs_set_blob(nvs_handle, save.key.c_str(), save.data.get(), save.len); + ESP_LOGV(TAG, "sync: key: %s, len: %zu", save.key.c_str(), save.len); if (err != 0) { - ESP_LOGV(TAG, "nvs_set_blob('%s', len=%u) failed: %s", save.key.c_str(), save.data.size(), - esp_err_to_name(err)); + ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", save.key.c_str(), save.len, esp_err_to_name(err)); failed++; last_err = err; last_key = save.key; @@ -134,7 +141,7 @@ class ESP32Preferences : public ESPPreferences { } written++; } else { - ESP_LOGV(TAG, "NVS data not changed skipping %s len=%u", save.key.c_str(), save.data.size()); + ESP_LOGV(TAG, "NVS data not changed skipping %s len=%zu", save.key.c_str(), save.len); cached++; } s_pending_save.erase(s_pending_save.begin() + i); @@ -156,20 +163,23 @@ class ESP32Preferences : public ESPPreferences { return failed == 0; } bool is_changed(const uint32_t nvs_handle, const NVSData &to_save) { - NVSData stored_data{}; size_t actual_len; esp_err_t err = nvs_get_blob(nvs_handle, to_save.key.c_str(), nullptr, &actual_len); if (err != 0) { ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", to_save.key.c_str(), esp_err_to_name(err)); return true; } - stored_data.data.resize(actual_len); - err = nvs_get_blob(nvs_handle, to_save.key.c_str(), stored_data.data.data(), &actual_len); + // Check size first before allocating memory + if (actual_len != to_save.len) { + return true; + } + auto stored_data = std::make_unique(actual_len); + err = nvs_get_blob(nvs_handle, to_save.key.c_str(), stored_data.get(), &actual_len); if (err != 0) { ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", to_save.key.c_str(), esp_err_to_name(err)); return true; } - return to_save.data != stored_data.data; + return memcmp(to_save.data.get(), stored_data.get(), to_save.len) != 0; } bool reset() override { diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index 1c7c075cfa..ced7e3fec9 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -1,17 +1,32 @@ +from collections.abc import Callable, MutableMapping +from dataclasses import dataclass from enum import Enum +import logging import re +from typing import Any from esphome import automation import esphome.codegen as cg +from esphome.components import socket from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant import esphome.config_validation as cv -from esphome.const import CONF_ENABLE_ON_BOOT, CONF_ESPHOME, CONF_ID, CONF_NAME -from esphome.core import CORE, TimePeriod -from esphome.core.config import CONF_NAME_ADD_MAC_SUFFIX +from esphome.const import ( + CONF_ENABLE_ON_BOOT, + CONF_ESPHOME, + CONF_ID, + CONF_MAX_CONNECTIONS, + CONF_NAME, + CONF_NAME_ADD_MAC_SUFFIX, +) +from esphome.core import CORE, CoroPriority, TimePeriod, coroutine_with_priority import esphome.final_validate as fv DEPENDENCIES = ["esp32"] -CODEOWNERS = ["@jesserockz", "@Rapsssito"] +AUTO_LOAD = ["socket"] +CODEOWNERS = ["@jesserockz", "@Rapsssito", "@bdraco"] +DOMAIN = "esp32_ble" + +_LOGGER = logging.getLogger(__name__) class BTLoggers(Enum): @@ -95,8 +110,65 @@ class BTLoggers(Enum): """ESP32 WiFi provisioning over Bluetooth""" -# Set to track which loggers are needed by components -_required_loggers: set[BTLoggers] = set() +# Key for storing required loggers in CORE.data +ESP32_BLE_REQUIRED_LOGGERS_KEY = "esp32_ble_required_loggers" + + +def _get_required_loggers() -> set[BTLoggers]: + """Get the set of required Bluetooth loggers from CORE.data.""" + return CORE.data.setdefault(ESP32_BLE_REQUIRED_LOGGERS_KEY, set()) + + +# Dataclass for handler registration counts +@dataclass +class HandlerCounts: + gap_event: int = 0 + gap_scan_event: int = 0 + gattc_event: int = 0 + gatts_event: int = 0 + ble_status_event: int = 0 + + +# Track handler registration counts for StaticVector sizing +_handler_counts = HandlerCounts() + + +def register_gap_event_handler(parent_var: cg.MockObj, handler_var: cg.MockObj) -> None: + """Register a GAP event handler and track the count.""" + _handler_counts.gap_event += 1 + cg.add(parent_var.register_gap_event_handler(handler_var)) + + +def register_gap_scan_event_handler( + parent_var: cg.MockObj, handler_var: cg.MockObj +) -> None: + """Register a GAP scan event handler and track the count.""" + _handler_counts.gap_scan_event += 1 + cg.add(parent_var.register_gap_scan_event_handler(handler_var)) + + +def register_gattc_event_handler( + parent_var: cg.MockObj, handler_var: cg.MockObj +) -> None: + """Register a GATTc event handler and track the count.""" + _handler_counts.gattc_event += 1 + cg.add(parent_var.register_gattc_event_handler(handler_var)) + + +def register_gatts_event_handler( + parent_var: cg.MockObj, handler_var: cg.MockObj +) -> None: + """Register a GATTs event handler and track the count.""" + _handler_counts.gatts_event += 1 + cg.add(parent_var.register_gatts_event_handler(handler_var)) + + +def register_ble_status_event_handler( + parent_var: cg.MockObj, handler_var: cg.MockObj +) -> None: + """Register a BLE status event handler and track the count.""" + _handler_counts.ble_status_event += 1 + cg.add(parent_var.register_ble_status_event_handler(handler_var)) def register_bt_logger(*loggers: BTLoggers) -> None: @@ -105,19 +177,44 @@ def register_bt_logger(*loggers: BTLoggers) -> None: Args: *loggers: One or more BTLoggers enum members """ + required_loggers = _get_required_loggers() for logger in loggers: if not isinstance(logger, BTLoggers): raise TypeError( f"Logger must be a BTLoggers enum member, got {type(logger)}" ) - _required_loggers.add(logger) + required_loggers.add(logger) CONF_BLE_ID = "ble_id" CONF_IO_CAPABILITY = "io_capability" +CONF_ADVERTISING = "advertising" CONF_ADVERTISING_CYCLE_TIME = "advertising_cycle_time" CONF_DISABLE_BT_LOGS = "disable_bt_logs" CONF_CONNECTION_TIMEOUT = "connection_timeout" +CONF_MAX_NOTIFICATIONS = "max_notifications" + +# BLE connection limits +# ESP-IDF CONFIG_BT_ACL_CONNECTIONS has range 1-9, default 4 +# Total instances: 10 (ADV + SCAN + connections) +# - ADV only: up to 9 connections +# - SCAN only: up to 9 connections +# - ADV + SCAN: up to 8 connections +DEFAULT_MAX_CONNECTIONS = 3 +IDF_MAX_CONNECTIONS = 9 + +# Connection slot tracking keys +KEY_ESP32_BLE = "esp32_ble" +KEY_USED_CONNECTION_SLOTS = "used_connection_slots" + +# Export for use by other components (bluetooth_proxy, etc.) +__all__ = [ + "DEFAULT_MAX_CONNECTIONS", + "IDF_MAX_CONNECTIONS", + "KEY_ESP32_BLE", + "KEY_USED_CONNECTION_SLOTS", + "consume_connection_slots", +] NO_BLUETOOTH_VARIANTS = [const.VARIANT_ESP32S2] @@ -162,17 +259,22 @@ CONFIG_SCHEMA = cv.Schema( IO_CAPABILITY, lower=True ), cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean, + cv.Optional(CONF_ADVERTISING, default=False): cv.boolean, cv.Optional( CONF_ADVERTISING_CYCLE_TIME, default="10s" ): cv.positive_time_period_milliseconds, - cv.SplitDefault(CONF_DISABLE_BT_LOGS, esp32_idf=True): cv.All( - cv.only_with_esp_idf, cv.boolean - ), - cv.SplitDefault(CONF_CONNECTION_TIMEOUT, esp32_idf="20s"): cv.All( - cv.only_with_esp_idf, + cv.Optional(CONF_DISABLE_BT_LOGS, default=True): cv.boolean, + cv.Optional(CONF_CONNECTION_TIMEOUT, default="20s"): cv.All( cv.positive_time_period_seconds, cv.Range(min=TimePeriod(seconds=10), max=TimePeriod(seconds=180)), ), + cv.Optional(CONF_MAX_NOTIFICATIONS, default=12): cv.All( + cv.positive_int, + cv.Range(min=1, max=64), + ), + cv.Optional(CONF_MAX_CONNECTIONS, default=DEFAULT_MAX_CONNECTIONS): cv.All( + cv.positive_int, cv.Range(min=1, max=IDF_MAX_CONNECTIONS) + ), } ).extend(cv.COMPONENT_SCHEMA) @@ -220,6 +322,60 @@ def validate_variant(_): raise cv.Invalid(f"{variant} does not support Bluetooth") +def consume_connection_slots( + value: int, consumer: str +) -> Callable[[MutableMapping], MutableMapping]: + """Reserve BLE connection slots for a component. + + Args: + value: Number of connection slots to reserve + consumer: Name of the component consuming the slots + + Returns: + A validator function that records the slot usage + """ + + def _consume_connection_slots(config: MutableMapping) -> MutableMapping: + data: dict[str, Any] = CORE.data.setdefault(KEY_ESP32_BLE, {}) + slots: list[str] = data.setdefault(KEY_USED_CONNECTION_SLOTS, []) + slots.extend([consumer] * value) + return config + + return _consume_connection_slots + + +def validate_connection_slots(max_connections: int) -> None: + """Validate that BLE connection slots don't exceed the configured maximum.""" + # Skip validation in testing mode to allow component grouping + if CORE.testing_mode: + return + + ble_data = CORE.data.get(KEY_ESP32_BLE, {}) + used_slots = ble_data.get(KEY_USED_CONNECTION_SLOTS, []) + num_used = len(used_slots) + + if num_used <= max_connections: + return + + slot_users = ", ".join(used_slots) + + if num_used > IDF_MAX_CONNECTIONS: + raise cv.Invalid( + f"BLE components require {num_used} connection slots but maximum is {IDF_MAX_CONNECTIONS}. " + f"Reduce the number of BLE clients. Components: {slot_users}" + ) + + _LOGGER.warning( + "BLE components require %d connection slot(s) but only %d configured. " + "Please set 'max_connections: %d' in the 'esp32_ble' component. " + "Components: %s", + num_used, + max_connections, + num_used, + slot_users, + ) + + def final_validation(config): validate_variant(config) if (name := config.get(CONF_NAME)) is not None: @@ -232,12 +388,92 @@ def final_validation(config): f"Name '{name}' is too long, maximum length is {max_length} characters" ) + # Set GATT Client/Server sdkconfig options based on which components are loaded + full_config = fv.full_config.get() + + # Validate connection slots usage + max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS) + validate_connection_slots(max_connections) + + # Check if hosted bluetooth is being used + if "esp32_hosted" in full_config: + add_idf_sdkconfig_option("CONFIG_BT_CLASSIC_ENABLED", False) + add_idf_sdkconfig_option("CONFIG_BT_BLE_ENABLED", True) + add_idf_sdkconfig_option("CONFIG_BT_BLUEDROID_ENABLED", True) + add_idf_sdkconfig_option("CONFIG_BT_CONTROLLER_DISABLED", True) + add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID", True) + add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_BLUEDROID_HCI_VHCI", True) + + # Check if BLE Server is needed + has_ble_server = "esp32_ble_server" in full_config + + # Check if BLE Client is needed (via esp32_ble_tracker or esp32_ble_client) + has_ble_client = ( + "esp32_ble_tracker" in full_config or "esp32_ble_client" in full_config + ) + + # ESP-IDF BLE stack requires GATT Server to be enabled when GATT Client is enabled + # This is an internal dependency in the Bluedroid stack (tested ESP-IDF 5.4.2-5.5.1) + # See: https://github.com/espressif/esp-idf/issues/17724 + add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", has_ble_server or has_ble_client) + add_idf_sdkconfig_option("CONFIG_BT_GATTC_ENABLE", has_ble_client) + + # Handle max_connections: check for deprecated location in esp32_ble_tracker + max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS) + + # Use value from tracker if esp32_ble doesn't have it explicitly set (backward compat) + if "esp32_ble_tracker" in full_config: + tracker_config = full_config["esp32_ble_tracker"] + if "max_connections" in tracker_config and CONF_MAX_CONNECTIONS not in config: + max_connections = tracker_config["max_connections"] + + # Set CONFIG_BT_ACL_CONNECTIONS to the maximum connections needed + 1 for ADV/SCAN + # This is the Bluedroid host stack total instance limit (range 1-9, default 4) + # Total instances = ADV/SCAN (1) + connection slots (max_connections) + # Shared between client (tracker/ble_client) and server + add_idf_sdkconfig_option("CONFIG_BT_ACL_CONNECTIONS", max_connections + 1) + + # Set controller-specific max connections for ESP32 (classic) + # CONFIG_BTDM_CTRL_BLE_MAX_CONN is ESP32-specific controller limit (just connections, not ADV/SCAN) + # For newer chips (C3/S3/etc), different configs are used automatically + add_idf_sdkconfig_option("CONFIG_BTDM_CTRL_BLE_MAX_CONN", max_connections) + return config FINAL_VALIDATE_SCHEMA = final_validation +# This needs to be run as a job with CoroPriority.FINAL priority so that all components have +# a chance to register their handlers before the counts are added to defines. +@coroutine_with_priority(CoroPriority.FINAL) +async def _add_ble_handler_defines(): + # Add defines for StaticVector sizing based on handler registration counts + # Only define if count > 0 to avoid allocating unnecessary memory + if _handler_counts.gap_event > 0: + cg.add_define( + "ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT", _handler_counts.gap_event + ) + if _handler_counts.gap_scan_event > 0: + cg.add_define( + "ESPHOME_ESP32_BLE_GAP_SCAN_EVENT_HANDLER_COUNT", + _handler_counts.gap_scan_event, + ) + if _handler_counts.gattc_event > 0: + cg.add_define( + "ESPHOME_ESP32_BLE_GATTC_EVENT_HANDLER_COUNT", _handler_counts.gattc_event + ) + if _handler_counts.gatts_event > 0: + cg.add_define( + "ESPHOME_ESP32_BLE_GATTS_EVENT_HANDLER_COUNT", _handler_counts.gatts_event + ) + if _handler_counts.ble_status_event > 0: + cg.add_define( + "ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT", + _handler_counts.ble_status_event, + ) + + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) cg.add(var.set_enable_on_boot(config[CONF_ENABLE_ON_BOOT])) @@ -247,33 +483,60 @@ async def to_code(config): cg.add(var.set_name(name)) await cg.register_component(var, config) - if CORE.using_esp_idf: - add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True) - add_idf_sdkconfig_option("CONFIG_BT_BLE_42_FEATURES_SUPPORTED", True) + # BLE uses the socket wake_loop_threadsafe() mechanism to wake the main loop from BLE tasks + # This enables low-latency (~12μs) BLE event processing instead of waiting for + # select() timeout (0-16ms). The wake socket is shared across all components. + socket.require_wake_loop_threadsafe() - # Register the core BLE loggers that are always needed - register_bt_logger(BTLoggers.GAP, BTLoggers.BTM, BTLoggers.HCI) + # Define max connections for use in C++ code (e.g., ble_server.h) + max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS) + cg.add_define("USE_ESP32_BLE_MAX_CONNECTIONS", max_connections) - # Apply logger settings if log disabling is enabled - if config.get(CONF_DISABLE_BT_LOGS, False): - # Disable all Bluetooth loggers that are not required - for logger in BTLoggers: - if logger not in _required_loggers: - add_idf_sdkconfig_option(f"{logger.value}_NONE", True) + add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True) + add_idf_sdkconfig_option("CONFIG_BT_BLE_42_FEATURES_SUPPORTED", True) - # Set BLE connection establishment timeout to match aioesphomeapi/bleak-retry-connector - # Default is 20 seconds instead of ESP-IDF's 30 seconds. Because there is no way to - # cancel a BLE connection in progress, when aioesphomeapi times out at 20 seconds, - # the connection slot remains occupied for the remaining time, preventing new connection - # attempts and wasting valuable connection slots. - if CONF_CONNECTION_TIMEOUT in config: - timeout_seconds = int(config[CONF_CONNECTION_TIMEOUT].total_seconds) - add_idf_sdkconfig_option( - "CONFIG_BT_BLE_ESTAB_LINK_CONN_TOUT", timeout_seconds - ) + # Register the core BLE loggers that are always needed + register_bt_logger(BTLoggers.GAP, BTLoggers.BTM, BTLoggers.HCI) + + # Apply logger settings if log disabling is enabled + if config.get(CONF_DISABLE_BT_LOGS, False): + # Disable all Bluetooth loggers that are not required + required_loggers = _get_required_loggers() + for logger in BTLoggers: + if logger not in required_loggers: + add_idf_sdkconfig_option(f"{logger.value}_NONE", True) + + # Set BLE connection establishment timeout to match aioesphomeapi/bleak-retry-connector + # Default is 20 seconds instead of ESP-IDF's 30 seconds. Because there is no way to + # cancel a BLE connection in progress, when aioesphomeapi times out at 20 seconds, + # the connection slot remains occupied for the remaining time, preventing new connection + # attempts and wasting valuable connection slots. + if CONF_CONNECTION_TIMEOUT in config: + timeout_seconds = int(config[CONF_CONNECTION_TIMEOUT].total_seconds) + add_idf_sdkconfig_option("CONFIG_BT_BLE_ESTAB_LINK_CONN_TOUT", timeout_seconds) + # Increase GATT client connection retry count for problematic devices + # Default in ESP-IDF is 3, we increase to 10 for better reliability with + # low-power/timing-sensitive devices + add_idf_sdkconfig_option("CONFIG_BT_GATTC_CONNECT_RETRY_COUNT", 10) + + # Set the maximum number of notification registrations + # This controls how many BLE characteristics can have notifications enabled + # across all connections for a single GATT client interface + # https://github.com/esphome/issues/issues/6808 + if CONF_MAX_NOTIFICATIONS in config: + add_idf_sdkconfig_option( + "CONFIG_BT_GATTC_NOTIF_REG_MAX", config[CONF_MAX_NOTIFICATIONS] + ) cg.add_define("USE_ESP32_BLE") + if config[CONF_ADVERTISING]: + cg.add_define("USE_ESP32_BLE_ADVERTISING") + cg.add_define("USE_ESP32_BLE_UUID") + + # Schedule the handler defines to be added after all components register + CORE.add_job(_add_ble_handler_defines) + @automation.register_condition("ble.enabled", BLEEnabledCondition, cv.Schema({})) async def ble_enabled_to_code(config, condition_id, template_arg, args): diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 33258552c7..a0ed9ee90c 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -1,12 +1,20 @@ -#ifdef USE_ESP32 - #include "ble.h" +#ifdef USE_ESP32 + #include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID #include +#else +extern "C" { +#include +#include +#include +} +#endif #include #include #include @@ -23,6 +31,26 @@ namespace esphome::esp32_ble { static const char *const TAG = "esp32_ble"; +// GAP event groups for deduplication across gap_event_handler and dispatch_gap_event_ +#define GAP_SCAN_COMPLETE_EVENTS \ + case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: \ + case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: \ + case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT + +#define GAP_ADV_COMPLETE_EVENTS \ + case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT: \ + case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT: \ + case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: \ + case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: \ + case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT + +#define GAP_SECURITY_EVENTS \ + case ESP_GAP_BLE_AUTH_CMPL_EVT: \ + case ESP_GAP_BLE_SEC_REQ_EVT: \ + case ESP_GAP_BLE_PASSKEY_NOTIF_EVT: \ + case ESP_GAP_BLE_PASSKEY_REQ_EVT: \ + case ESP_GAP_BLE_NC_REQ_EVT + void ESP32BLE::setup() { global_ble = this; if (!ble_pre_setup_()) { @@ -53,6 +81,7 @@ void ESP32BLE::disable() { bool ESP32BLE::is_active() { return this->state_ == BLE_COMPONENT_STATE_ACTIVE; } +#ifdef USE_ESP32_BLE_ADVERTISING void ESP32BLE::advertising_start() { this->advertising_init_(); if (!this->is_active()) @@ -72,6 +101,28 @@ void ESP32BLE::advertising_set_manufacturer_data(const std::vector &dat this->advertising_start(); } +void ESP32BLE::advertising_set_service_data_and_name(std::span data, bool include_name) { + // This method atomically updates both service data and device name inclusion in BLE advertising. + // When include_name is true, the device name is included in the advertising packet making it + // visible to passive BLE scanners. When false, the name is only visible in scan response + // (requires active scanning). This atomic operation ensures we only restart advertising once + // when changing both properties, avoiding the brief gap that would occur with separate calls. + + this->advertising_init_(); + + if (include_name) { + // When including name, clear service data first to avoid packet overflow + this->advertising_->set_service_data(std::span{}); + this->advertising_->set_include_name(true); + } else { + // When including service data, clear name first to avoid packet overflow + this->advertising_->set_include_name(false); + this->advertising_->set_service_data(data); + } + + this->advertising_start(); +} + void ESP32BLE::advertising_register_raw_advertisement_callback(std::function &&callback) { this->advertising_init_(); this->advertising_->register_raw_advertisement_callback(std::move(callback)); @@ -88,6 +139,7 @@ void ESP32BLE::advertising_remove_service_uuid(ESPBTUUID uuid) { this->advertising_->remove_service_uuid(uuid); this->advertising_start(); } +#endif bool ESP32BLE::ble_pre_setup_() { esp_err_t err = nvs_flash_init(); @@ -98,6 +150,7 @@ bool ESP32BLE::ble_pre_setup_() { return true; } +#ifdef USE_ESP32_BLE_ADVERTISING void ESP32BLE::advertising_init_() { if (this->advertising_ != nullptr) return; @@ -107,9 +160,11 @@ void ESP32BLE::advertising_init_() { this->advertising_->set_min_preferred_interval(0x06); this->advertising_->set_appearance(this->appearance_); } +#endif bool ESP32BLE::ble_setup_() { esp_err_t err; +#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID #ifdef USE_ARDUINO if (!btStart()) { ESP_LOGE(TAG, "btStart failed: %d", esp_bt_controller_get_status()); @@ -143,6 +198,28 @@ bool ESP32BLE::ble_setup_() { #endif esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT); +#else + esp_hosted_connect_to_slave(); // NOLINT + + if (esp_hosted_bt_controller_init() != ESP_OK) { + ESP_LOGW(TAG, "esp_hosted_bt_controller_init failed"); + return false; + } + + if (esp_hosted_bt_controller_enable() != ESP_OK) { + ESP_LOGW(TAG, "esp_hosted_bt_controller_enable failed"); + return false; + } + + hosted_hci_bluedroid_open(); + + esp_bluedroid_hci_driver_operations_t operations = { + .send = hosted_hci_bluedroid_send, + .check_send_available = hosted_hci_bluedroid_check_send_available, + .register_host_callback = hosted_hci_bluedroid_register_host_callback, + }; + esp_bluedroid_attach_hci_driver(&operations); +#endif err = esp_bluedroid_init(); if (err != ESP_OK) { @@ -155,48 +232,62 @@ bool ESP32BLE::ble_setup_() { return false; } - if (!this->gap_event_handlers_.empty()) { - err = esp_ble_gap_register_callback(ESP32BLE::gap_event_handler); - if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_ble_gap_register_callback failed: %d", err); - return false; - } +#ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT + err = esp_ble_gap_register_callback(ESP32BLE::gap_event_handler); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gap_register_callback failed: %d", err); + return false; } +#endif - if (!this->gatts_event_handlers_.empty()) { - err = esp_ble_gatts_register_callback(ESP32BLE::gatts_event_handler); - if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_ble_gatts_register_callback failed: %d", err); - return false; - } +#if defined(USE_ESP32_BLE_SERVER) && defined(ESPHOME_ESP32_BLE_GATTS_EVENT_HANDLER_COUNT) + err = esp_ble_gatts_register_callback(ESP32BLE::gatts_event_handler); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gatts_register_callback failed: %d", err); + return false; } +#endif - if (!this->gattc_event_handlers_.empty()) { - err = esp_ble_gattc_register_callback(ESP32BLE::gattc_event_handler); - if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_ble_gattc_register_callback failed: %d", err); - return false; - } +#if defined(USE_ESP32_BLE_CLIENT) && defined(ESPHOME_ESP32_BLE_GATTC_EVENT_HANDLER_COUNT) + err = esp_ble_gattc_register_callback(ESP32BLE::gattc_event_handler); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gattc_register_callback failed: %d", err); + return false; } +#endif - std::string name; - if (this->name_.has_value()) { - name = this->name_.value(); + const char *device_name; + std::string name_with_suffix; + + if (this->name_ != nullptr) { if (App.is_name_add_mac_suffix_enabled()) { - name += "-" + get_mac_address().substr(6); + // MAC address length: 12 hex chars + null terminator + constexpr size_t mac_address_len = 13; + // MAC address suffix length (last 6 characters of 12-char MAC address string) + constexpr size_t mac_address_suffix_len = 6; + char mac_addr[mac_address_len]; + get_mac_address_into_buffer(mac_addr); + const char *mac_suffix_ptr = mac_addr + mac_address_suffix_len; + name_with_suffix = + make_name_with_suffix(this->name_, strlen(this->name_), '-', mac_suffix_ptr, mac_address_suffix_len); + device_name = name_with_suffix.c_str(); + } else { + device_name = this->name_; } } else { - name = App.get_name(); - if (name.length() > 20) { + name_with_suffix = App.get_name(); + if (name_with_suffix.length() > 20) { if (App.is_name_add_mac_suffix_enabled()) { - name.erase(name.begin() + 13, name.end() - 7); // Remove characters between 13 and the mac address + // Keep first 13 chars and last 7 chars (MAC suffix), remove middle + name_with_suffix.erase(13, name_with_suffix.length() - 20); } else { - name = name.substr(0, 20); + name_with_suffix.resize(20); } } + device_name = name_with_suffix.c_str(); } - err = esp_ble_gap_set_device_name(name.c_str()); + err = esp_ble_gap_set_device_name(device_name); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ble_gap_set_device_name failed: %d", err); return false; @@ -226,6 +317,7 @@ bool ESP32BLE::ble_dismantle_() { return false; } +#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID #ifdef USE_ARDUINO if (!btStop()) { ESP_LOGE(TAG, "btStop failed: %d", esp_bt_controller_get_status()); @@ -255,6 +347,19 @@ bool ESP32BLE::ble_dismantle_() { return false; } } +#endif +#else + if (esp_hosted_bt_controller_disable() != ESP_OK) { + ESP_LOGW(TAG, "esp_hosted_bt_controller_disable failed"); + return false; + } + + if (esp_hosted_bt_controller_deinit(false) != ESP_OK) { + ESP_LOGW(TAG, "esp_hosted_bt_controller_deinit failed"); + return false; + } + + hosted_hci_bluedroid_close(); #endif return true; } @@ -267,9 +372,11 @@ void ESP32BLE::loop() { case BLE_COMPONENT_STATE_DISABLE: { ESP_LOGD(TAG, "Disabling"); +#ifdef ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT for (auto *ble_event_handler : this->ble_status_event_handlers_) { ble_event_handler->ble_before_disabled_event_handler(); } +#endif if (!ble_dismantle_()) { ESP_LOGE(TAG, "Could not be dismantled"); @@ -299,85 +406,87 @@ void ESP32BLE::loop() { BLEEvent *ble_event = this->ble_events_.pop(); while (ble_event != nullptr) { switch (ble_event->type_) { +#if defined(USE_ESP32_BLE_SERVER) && defined(ESPHOME_ESP32_BLE_GATTS_EVENT_HANDLER_COUNT) case BLEEvent::GATTS: { esp_gatts_cb_event_t event = ble_event->event_.gatts.gatts_event; esp_gatt_if_t gatts_if = ble_event->event_.gatts.gatts_if; - esp_ble_gatts_cb_param_t *param = ble_event->event_.gatts.gatts_param; + esp_ble_gatts_cb_param_t *param = &ble_event->event_.gatts.gatts_param; ESP_LOGV(TAG, "gatts_event [esp_gatt_if: %d] - %d", gatts_if, event); for (auto *gatts_handler : this->gatts_event_handlers_) { gatts_handler->gatts_event_handler(event, gatts_if, param); } break; } +#endif +#if defined(USE_ESP32_BLE_CLIENT) && defined(ESPHOME_ESP32_BLE_GATTC_EVENT_HANDLER_COUNT) case BLEEvent::GATTC: { esp_gattc_cb_event_t event = ble_event->event_.gattc.gattc_event; esp_gatt_if_t gattc_if = ble_event->event_.gattc.gattc_if; - esp_ble_gattc_cb_param_t *param = ble_event->event_.gattc.gattc_param; + esp_ble_gattc_cb_param_t *param = &ble_event->event_.gattc.gattc_param; ESP_LOGV(TAG, "gattc_event [esp_gatt_if: %d] - %d", gattc_if, event); for (auto *gattc_handler : this->gattc_event_handlers_) { gattc_handler->gattc_event_handler(event, gattc_if, param); } break; } +#endif case BLEEvent::GAP: { esp_gap_ble_cb_event_t gap_event = ble_event->event_.gap.gap_event; switch (gap_event) { case ESP_GAP_BLE_SCAN_RESULT_EVT: +#ifdef ESPHOME_ESP32_BLE_GAP_SCAN_EVENT_HANDLER_COUNT // Use the new scan event handler - no memcpy! for (auto *scan_handler : this->gap_scan_event_handlers_) { scan_handler->gap_scan_event_handler(ble_event->scan_result()); } +#endif break; // Scan complete events - case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: - case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: - case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: - // All three scan complete events have the same structure with just status - // The scan_complete struct matches ESP-IDF's layout exactly, so this reinterpret_cast is safe - // This is verified at compile-time by static_assert checks in ble_event.h - // The struct already contains our copy of the status (copied in BLEEvent constructor) - ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); - for (auto *gap_handler : this->gap_event_handlers_) { - gap_handler->gap_event_handler( - gap_event, reinterpret_cast(&ble_event->event_.gap.scan_complete)); - } - break; - + GAP_SCAN_COMPLETE_EVENTS: // Advertising complete events - case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT: - case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT: - case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: - case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: - case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT: - // All advertising complete events have the same structure with just status - ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); - for (auto *gap_handler : this->gap_event_handlers_) { - gap_handler->gap_event_handler( - gap_event, reinterpret_cast(&ble_event->event_.gap.adv_complete)); - } - break; - + GAP_ADV_COMPLETE_EVENTS: // RSSI complete event case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT: - ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); - for (auto *gap_handler : this->gap_event_handlers_) { - gap_handler->gap_event_handler( - gap_event, reinterpret_cast(&ble_event->event_.gap.read_rssi_complete)); - } - break; - // Security events - case ESP_GAP_BLE_AUTH_CMPL_EVT: - case ESP_GAP_BLE_SEC_REQ_EVT: - case ESP_GAP_BLE_PASSKEY_NOTIF_EVT: - case ESP_GAP_BLE_PASSKEY_REQ_EVT: - case ESP_GAP_BLE_NC_REQ_EVT: + GAP_SECURITY_EVENTS: ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); - for (auto *gap_handler : this->gap_event_handlers_) { - gap_handler->gap_event_handler( - gap_event, reinterpret_cast(&ble_event->event_.gap.security)); +#ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT + { + esp_ble_gap_cb_param_t *param; + // clang-format off + switch (gap_event) { + // All three scan complete events have the same structure with just status + // The scan_complete struct matches ESP-IDF's layout exactly, so this reinterpret_cast is safe + // This is verified at compile-time by static_assert checks in ble_event.h + // The struct already contains our copy of the status (copied in BLEEvent constructor) + GAP_SCAN_COMPLETE_EVENTS: + param = reinterpret_cast(&ble_event->event_.gap.scan_complete); + break; + + // All advertising complete events have the same structure with just status + GAP_ADV_COMPLETE_EVENTS: + param = reinterpret_cast(&ble_event->event_.gap.adv_complete); + break; + + case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT: + param = reinterpret_cast(&ble_event->event_.gap.read_rssi_complete); + break; + + GAP_SECURITY_EVENTS: + param = reinterpret_cast(&ble_event->event_.gap.security); + break; + + default: + break; + } + // clang-format on + // Dispatch to all registered handlers + for (auto *gap_handler : this->gap_event_handlers_) { + gap_handler->gap_event_handler(gap_event, param); + } } +#endif break; default: @@ -394,9 +503,11 @@ void ESP32BLE::loop() { this->ble_event_pool_.release(ble_event); ble_event = this->ble_events_.pop(); } +#ifdef USE_ESP32_BLE_ADVERTISING if (this->advertising_ != nullptr) { this->advertising_->loop(); } +#endif // Log dropped events periodically uint16_t dropped = this->ble_events_.get_and_reset_dropped_count(); @@ -410,13 +521,17 @@ void load_ble_event(BLEEvent *event, esp_gap_ble_cb_event_t e, esp_ble_gap_cb_pa event->load_gap_event(e, p); } +#ifdef USE_ESP32_BLE_CLIENT void load_ble_event(BLEEvent *event, esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) { event->load_gattc_event(e, i, p); } +#endif +#ifdef USE_ESP32_BLE_SERVER void load_ble_event(BLEEvent *event, esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) { event->load_gatts_event(e, i, p); } +#endif template void enqueue_ble_event(Args... args) { // Allocate an event from the pool @@ -437,34 +552,36 @@ template void enqueue_ble_event(Args... args) { // Explicit template instantiations for the friend function template void enqueue_ble_event(esp_gap_ble_cb_event_t, esp_ble_gap_cb_param_t *); +#ifdef USE_ESP32_BLE_SERVER template void enqueue_ble_event(esp_gatts_cb_event_t, esp_gatt_if_t, esp_ble_gatts_cb_param_t *); +#endif +#ifdef USE_ESP32_BLE_CLIENT template void enqueue_ble_event(esp_gattc_cb_event_t, esp_gatt_if_t, esp_ble_gattc_cb_param_t *); +#endif void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { switch (event) { // Queue GAP events that components need to handle // Scanning events - used by esp32_ble_tracker case ESP_GAP_BLE_SCAN_RESULT_EVT: - case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: - case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: - case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: + GAP_SCAN_COMPLETE_EVENTS: // Advertising events - used by esp32_ble_beacon and esp32_ble server - case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT: - case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT: - case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: - case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: - case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT: + GAP_ADV_COMPLETE_EVENTS: // Connection events - used by ble_client case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT: - // Security events - used by ble_client and bluetooth_proxy - case ESP_GAP_BLE_AUTH_CMPL_EVT: - case ESP_GAP_BLE_SEC_REQ_EVT: - case ESP_GAP_BLE_PASSKEY_NOTIF_EVT: - case ESP_GAP_BLE_PASSKEY_REQ_EVT: - case ESP_GAP_BLE_NC_REQ_EVT: enqueue_ble_event(event, param); return; + // Security events - used by ble_client and bluetooth_proxy + // These are rare but interactive (pairing/bonding), so notify immediately + GAP_SECURITY_EVENTS: + enqueue_ble_event(event, param); + // Wake up main loop to process security event immediately +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + App.wake_loop_threadsafe(); +#endif + return; + // Ignore these GAP events as they are not relevant for our use case case ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT: case ESP_GAP_BLE_SET_PKT_LENGTH_COMPLETE_EVT: @@ -478,15 +595,27 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa ESP_LOGW(TAG, "Ignoring unexpected GAP event type: %d", event); } +#ifdef USE_ESP32_BLE_SERVER void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) { enqueue_ble_event(event, gatts_if, param); + // Wake up main loop to process GATT event immediately +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + App.wake_loop_threadsafe(); +#endif } +#endif +#ifdef USE_ESP32_BLE_CLIENT void ESP32BLE::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { enqueue_ble_event(event, gattc_if, param); + // Wake up main loop to process GATT event immediately +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + App.wake_loop_threadsafe(); +#endif } +#endif float ESP32BLE::get_setup_priority() const { return setup_priority::BLUETOOTH; } @@ -514,11 +643,13 @@ void ESP32BLE::dump_config() { io_capability_s = "invalid"; break; } + char mac_s[18]; + format_mac_addr_upper(mac_address, mac_s); ESP_LOGCONFIG(TAG, "BLE:\n" " MAC address: %s\n" " IO Capability: %s", - format_mac_address_pretty(mac_address).c_str(), io_capability_s); + mac_s, io_capability_s); } else { ESP_LOGCONFIG(TAG, "Bluetooth stack is not enabled"); } diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 543b2f26a3..393ec2e911 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -1,14 +1,18 @@ #pragma once -#include "ble_advertising.h" +#include "esphome/core/defines.h" // Must be included before conditional includes + #include "ble_uuid.h" #include "ble_scan_result.h" +#ifdef USE_ESP32_BLE_ADVERTISING +#include "ble_advertising.h" +#endif #include +#include #include "esphome/core/automation.h" #include "esphome/core/component.h" -#include "esphome/core/defines.h" #include "esphome/core/helpers.h" #include "ble_event.h" @@ -23,21 +27,14 @@ namespace esphome::esp32_ble { -// Maximum number of BLE scan results to buffer -// Sized to handle bursts of advertisements while allowing for processing delays -// With 16 advertisements per batch and some safety margin: -// - Without PSRAM: 24 entries (1.5× batch size) -// - With PSRAM: 36 entries (2.25× batch size) -// The reduced structure size (~80 bytes vs ~400 bytes) allows for larger buffers +// Maximum size of the BLE event queue +// Increased to absorb the ring buffer capacity from esp32_ble_tracker #ifdef USE_PSRAM -static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 36; +static constexpr uint8_t MAX_BLE_QUEUE_SIZE = 100; // 64 + 36 (ring buffer size with PSRAM) #else -static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 24; +static constexpr uint8_t MAX_BLE_QUEUE_SIZE = 88; // 64 + 24 (ring buffer size without PSRAM) #endif -// Maximum size of the BLE event queue - must be power of 2 for lock-free queue -static constexpr size_t MAX_BLE_QUEUE_SIZE = 64; - uint64_t ble_addr_to_uint64(const esp_bd_addr_t address); // NOLINTNEXTLINE(modernize-use-using) @@ -78,17 +75,21 @@ class GAPScanEventHandler { virtual void gap_scan_event_handler(const BLEScanResult &scan_result) = 0; }; +#ifdef USE_ESP32_BLE_CLIENT class GATTcEventHandler { public: virtual void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) = 0; }; +#endif +#ifdef USE_ESP32_BLE_SERVER class GATTsEventHandler { public: virtual void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) = 0; }; +#endif class BLEStatusEventHandler { public: @@ -111,56 +112,90 @@ class ESP32BLE : public Component { void loop() override; void dump_config() override; float get_setup_priority() const override; - void set_name(const std::string &name) { this->name_ = name; } + void set_name(const char *name) { this->name_ = name; } +#ifdef USE_ESP32_BLE_ADVERTISING void advertising_start(); void advertising_set_service_data(const std::vector &data); void advertising_set_manufacturer_data(const std::vector &data); void advertising_set_appearance(uint16_t appearance) { this->appearance_ = appearance; } + void advertising_set_service_data_and_name(std::span data, bool include_name); void advertising_add_service_uuid(ESPBTUUID uuid); void advertising_remove_service_uuid(ESPBTUUID uuid); void advertising_register_raw_advertisement_callback(std::function &&callback); +#endif +#ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT void register_gap_event_handler(GAPEventHandler *handler) { this->gap_event_handlers_.push_back(handler); } +#endif +#ifdef ESPHOME_ESP32_BLE_GAP_SCAN_EVENT_HANDLER_COUNT void register_gap_scan_event_handler(GAPScanEventHandler *handler) { this->gap_scan_event_handlers_.push_back(handler); } +#endif +#if defined(USE_ESP32_BLE_CLIENT) && defined(ESPHOME_ESP32_BLE_GATTC_EVENT_HANDLER_COUNT) void register_gattc_event_handler(GATTcEventHandler *handler) { this->gattc_event_handlers_.push_back(handler); } +#endif +#if defined(USE_ESP32_BLE_SERVER) && defined(ESPHOME_ESP32_BLE_GATTS_EVENT_HANDLER_COUNT) void register_gatts_event_handler(GATTsEventHandler *handler) { this->gatts_event_handlers_.push_back(handler); } +#endif +#ifdef ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT void register_ble_status_event_handler(BLEStatusEventHandler *handler) { this->ble_status_event_handlers_.push_back(handler); } +#endif void set_enable_on_boot(bool enable_on_boot) { this->enable_on_boot_ = enable_on_boot; } protected: +#ifdef USE_ESP32_BLE_SERVER static void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); +#endif +#ifdef USE_ESP32_BLE_CLIENT static void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param); +#endif static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); bool ble_setup_(); bool ble_dismantle_(); bool ble_pre_setup_(); +#ifdef USE_ESP32_BLE_ADVERTISING void advertising_init_(); +#endif + + // BLE uses the core wake_loop_threadsafe() mechanism to wake the main event loop + // from BLE tasks. This enables low-latency (~12μs) event processing instead of + // waiting for select() timeout (0-16ms). The wake socket is shared with other + // components that need this functionality. private: template friend void enqueue_ble_event(Args... args); - // Vectors (12 bytes each on 32-bit, naturally aligned to 4 bytes) - std::vector gap_event_handlers_; - std::vector gap_scan_event_handlers_; - std::vector gattc_event_handlers_; - std::vector gatts_event_handlers_; - std::vector ble_status_event_handlers_; + // Handler vectors - use StaticVector when counts are known at compile time +#ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT + StaticVector gap_event_handlers_; +#endif +#ifdef ESPHOME_ESP32_BLE_GAP_SCAN_EVENT_HANDLER_COUNT + StaticVector gap_scan_event_handlers_; +#endif +#if defined(USE_ESP32_BLE_CLIENT) && defined(ESPHOME_ESP32_BLE_GATTC_EVENT_HANDLER_COUNT) + StaticVector gattc_event_handlers_; +#endif +#if defined(USE_ESP32_BLE_SERVER) && defined(ESPHOME_ESP32_BLE_GATTS_EVENT_HANDLER_COUNT) + StaticVector gatts_event_handlers_; +#endif +#ifdef ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT + StaticVector ble_status_event_handlers_; +#endif // Large objects (size depends on template parameters, but typically aligned to 4 bytes) esphome::LockFreeQueue ble_events_; esphome::EventPool ble_event_pool_; - // optional (typically 16+ bytes on 32-bit, aligned to 4 bytes) - optional name_; - // 4-byte aligned members - BLEAdvertising *advertising_{}; // 4 bytes (pointer) +#ifdef USE_ESP32_BLE_ADVERTISING + BLEAdvertising *advertising_{}; // 4 bytes (pointer) +#endif + const char *name_{nullptr}; // 4 bytes (pointer to string literal in flash) esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; // 4 bytes (enum) uint32_t advertising_cycle_time_{}; // 4 bytes @@ -177,17 +212,17 @@ extern ESP32BLE *global_ble; template class BLEEnabledCondition : public Condition { public: - bool check(Ts... x) override { return global_ble->is_active(); } + bool check(const Ts &...x) override { return global_ble->is_active(); } }; template class BLEEnableAction : public Action { public: - void play(Ts... x) override { global_ble->enable(); } + void play(const Ts &...x) override { global_ble->enable(); } }; template class BLEDisableAction : public Action { public: - void play(Ts... x) override { global_ble->disable(); } + void play(const Ts &...x) override { global_ble->disable(); } }; } // namespace esphome::esp32_ble diff --git a/esphome/components/esp32_ble/ble_advertising.cpp b/esphome/components/esp32_ble/ble_advertising.cpp index 6a0d677aa7..68704e49e2 100644 --- a/esphome/components/esp32_ble/ble_advertising.cpp +++ b/esphome/components/esp32_ble/ble_advertising.cpp @@ -1,6 +1,7 @@ #include "ble_advertising.h" #ifdef USE_ESP32 +#ifdef USE_ESP32_BLE_ADVERTISING #include #include @@ -42,7 +43,7 @@ void BLEAdvertising::remove_service_uuid(ESPBTUUID uuid) { this->advertising_uuids_.end()); } -void BLEAdvertising::set_service_data(const std::vector &data) { +void BLEAdvertising::set_service_data(std::span data) { delete[] this->advertising_data_.p_service_data; this->advertising_data_.p_service_data = nullptr; this->advertising_data_.service_data_len = data.size(); @@ -53,6 +54,10 @@ void BLEAdvertising::set_service_data(const std::vector &data) { } } +void BLEAdvertising::set_service_data(const std::vector &data) { + this->set_service_data(std::span(data)); +} + void BLEAdvertising::set_manufacturer_data(const std::vector &data) { delete[] this->advertising_data_.p_manufacturer_data; this->advertising_data_.p_manufacturer_data = nullptr; @@ -83,7 +88,7 @@ esp_err_t BLEAdvertising::services_advertisement_() { esp_err_t err; this->advertising_data_.set_scan_rsp = false; - this->advertising_data_.include_name = !this->scan_response_; + this->advertising_data_.include_name = this->include_name_in_adv_ || !this->scan_response_; this->advertising_data_.include_txpower = !this->scan_response_; err = esp_ble_gap_config_adv_data(&this->advertising_data_); if (err != ESP_OK) { @@ -147,7 +152,7 @@ void BLEAdvertising::loop() { if (now - this->last_advertisement_time_ > this->advertising_cycle_time_) { this->stop(); this->current_adv_index_ += 1; - if (this->current_adv_index_ >= this->raw_advertisements_callbacks_.size()) { + if (static_cast(this->current_adv_index_) >= this->raw_advertisements_callbacks_.size()) { this->current_adv_index_ = -1; } this->start(); @@ -161,4 +166,5 @@ void BLEAdvertising::register_raw_advertisement_callback(std::function #include +#include #include #ifdef USE_ESP32 +#ifdef USE_ESP32_BLE_ADVERTISING +#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID #include +#endif #include #include @@ -33,6 +39,8 @@ class BLEAdvertising { void set_manufacturer_data(const std::vector &data); void set_appearance(uint16_t appearance) { this->advertising_data_.appearance = appearance; } void set_service_data(const std::vector &data); + void set_service_data(std::span data); + void set_include_name(bool include_name) { this->include_name_in_adv_ = include_name; } void register_raw_advertisement_callback(std::function &&callback); void start(); @@ -42,6 +50,7 @@ class BLEAdvertising { esp_err_t services_advertisement_(); bool scan_response_; + bool include_name_in_adv_{false}; esp_ble_adv_data_t advertising_data_; esp_ble_adv_data_t scan_response_data_; esp_ble_adv_params_t advertising_params_; @@ -56,4 +65,5 @@ class BLEAdvertising { } // namespace esphome::esp32_ble -#endif +#endif // USE_ESP32_BLE_ADVERTISING +#endif // USE_ESP32 diff --git a/esphome/components/esp32_ble/ble_event.h b/esphome/components/esp32_ble/ble_event.h index 884fc9ba65..299fd7705f 100644 --- a/esphome/components/esp32_ble/ble_event.h +++ b/esphome/components/esp32_ble/ble_event.h @@ -3,8 +3,7 @@ #ifdef USE_ESP32 #include // for offsetof -#include - +#include // for memcpy #include #include #include @@ -62,10 +61,24 @@ static_assert(offsetof(esp_ble_gap_cb_param_t, read_rssi_cmpl.rssi) == sizeof(es static_assert(offsetof(esp_ble_gap_cb_param_t, read_rssi_cmpl.remote_addr) == sizeof(esp_bt_status_t) + sizeof(int8_t), "remote_addr must follow rssi in read_rssi_cmpl"); +// Param struct sizes on ESP32 +static constexpr size_t GATTC_PARAM_SIZE = 28; +static constexpr size_t GATTS_PARAM_SIZE = 32; + +// Maximum size for inline storage of data +// GATTC: 80 - 28 (param) - 8 (other fields) = 44 bytes for data +// GATTS: 80 - 32 (param) - 8 (other fields) = 40 bytes for data +static constexpr size_t GATTC_INLINE_DATA_SIZE = 44; +static constexpr size_t GATTS_INLINE_DATA_SIZE = 40; + +// Verify param struct sizes +static_assert(sizeof(esp_ble_gattc_cb_param_t) == GATTC_PARAM_SIZE, "GATTC param size unexpected"); +static_assert(sizeof(esp_ble_gatts_cb_param_t) == GATTS_PARAM_SIZE, "GATTS param size unexpected"); + // Received GAP, GATTC and GATTS events are only queued, and get processed in the main loop(). // This class stores each event with minimal memory usage. -// GAP events (99% of traffic) don't have the vector overhead. -// GATTC/GATTS events use heap allocation for their param and data. +// GAP events (99% of traffic) don't have the heap allocation overhead. +// GATTC/GATTS events use heap allocation for their param and inline storage for small data. // // Event flow: // 1. ESP-IDF BLE stack calls our static handlers in the BLE task context @@ -112,21 +125,21 @@ class BLEEvent { this->init_gap_data_(e, p); } - // Constructor for GATTC events - uses heap allocation - // IMPORTANT: The heap allocation is REQUIRED and must not be removed as an optimization. - // The param pointer from ESP-IDF is only valid during the callback execution. - // Since BLE events are processed asynchronously in the main loop, we must create - // our own copy to ensure the data remains valid until the event is processed. + // Constructor for GATTC events - param stored inline, data may use heap + // IMPORTANT: We MUST copy the param struct because the pointer from ESP-IDF + // is only valid during the callback execution. Since BLE events are processed + // asynchronously in the main loop, we store our own copy inline to ensure + // the data remains valid until the event is processed. BLEEvent(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) { this->type_ = GATTC; this->init_gattc_data_(e, i, p); } - // Constructor for GATTS events - uses heap allocation - // IMPORTANT: The heap allocation is REQUIRED and must not be removed as an optimization. - // The param pointer from ESP-IDF is only valid during the callback execution. - // Since BLE events are processed asynchronously in the main loop, we must create - // our own copy to ensure the data remains valid until the event is processed. + // Constructor for GATTS events - param stored inline, data may use heap + // IMPORTANT: We MUST copy the param struct because the pointer from ESP-IDF + // is only valid during the callback execution. Since BLE events are processed + // asynchronously in the main loop, we store our own copy inline to ensure + // the data remains valid until the event is processed. BLEEvent(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) { this->type_ = GATTS; this->init_gatts_data_(e, i, p); @@ -136,25 +149,32 @@ class BLEEvent { ~BLEEvent() { this->release(); } // Default constructor for pre-allocation in pool - BLEEvent() : type_(GAP) {} + BLEEvent() : event_{}, type_(GAP) {} // Invoked on return to EventPool - clean up any heap-allocated data void release() { - if (this->type_ == GAP) { - return; - } - if (this->type_ == GATTC) { - delete this->event_.gattc.gattc_param; - delete this->event_.gattc.data; - this->event_.gattc.gattc_param = nullptr; - this->event_.gattc.data = nullptr; - return; - } - if (this->type_ == GATTS) { - delete this->event_.gatts.gatts_param; - delete this->event_.gatts.data; - this->event_.gatts.gatts_param = nullptr; - this->event_.gatts.data = nullptr; + switch (this->type_) { + case GAP: + // GAP events don't have heap allocations + break; + case GATTC: + // Param is now stored inline, only delete heap data if it was heap-allocated + if (!this->event_.gattc.is_inline && this->event_.gattc.data.heap_data != nullptr) { + delete[] this->event_.gattc.data.heap_data; + } + // Clear critical fields to prevent issues if type changes + this->event_.gattc.is_inline = false; + this->event_.gattc.data.heap_data = nullptr; + break; + case GATTS: + // Param is now stored inline, only delete heap data if it was heap-allocated + if (!this->event_.gatts.is_inline && this->event_.gatts.data.heap_data != nullptr) { + delete[] this->event_.gatts.data.heap_data; + } + // Clear critical fields to prevent issues if type changes + this->event_.gatts.is_inline = false; + this->event_.gatts.data.heap_data = nullptr; + break; } } @@ -206,20 +226,30 @@ class BLEEvent { // NOLINTNEXTLINE(readability-identifier-naming) struct gattc_event { - esp_gattc_cb_event_t gattc_event; - esp_gatt_if_t gattc_if; - esp_ble_gattc_cb_param_t *gattc_param; // Heap-allocated - std::vector *data; // Heap-allocated - } gattc; // 16 bytes (pointers only) + esp_ble_gattc_cb_param_t gattc_param; // Stored inline (28 bytes) + esp_gattc_cb_event_t gattc_event; // 4 bytes + union { + uint8_t *heap_data; // 4 bytes when heap-allocated + uint8_t inline_data[GATTC_INLINE_DATA_SIZE]; // 44 bytes when stored inline + } data; // 44 bytes total + uint16_t data_len; // 2 bytes + esp_gatt_if_t gattc_if; // 1 byte + bool is_inline; // 1 byte - true when data is stored inline + } gattc; // Total: 80 bytes // NOLINTNEXTLINE(readability-identifier-naming) struct gatts_event { - esp_gatts_cb_event_t gatts_event; - esp_gatt_if_t gatts_if; - esp_ble_gatts_cb_param_t *gatts_param; // Heap-allocated - std::vector *data; // Heap-allocated - } gatts; // 16 bytes (pointers only) - } event_; // 80 bytes + esp_ble_gatts_cb_param_t gatts_param; // Stored inline (32 bytes) + esp_gatts_cb_event_t gatts_event; // 4 bytes + union { + uint8_t *heap_data; // 4 bytes when heap-allocated + uint8_t inline_data[GATTS_INLINE_DATA_SIZE]; // 40 bytes when stored inline + } data; // 40 bytes total + uint16_t data_len; // 2 bytes + esp_gatt_if_t gatts_if; // 1 byte + bool is_inline; // 1 byte - true when data is stored inline + } gatts; // Total: 80 bytes + } event_; // 80 bytes ble_event_t type_; @@ -233,6 +263,29 @@ class BLEEvent { const esp_ble_sec_t &security() const { return event_.gap.security; } private: + // Helper to copy data with inline storage optimization + template + void copy_data_with_inline_storage_(EventStruct &event, const uint8_t *src_data, uint16_t len, + uint8_t **param_value_ptr) { + event.data_len = len; + if (len > 0) { + if (len <= InlineSize) { + event.is_inline = true; + memcpy(event.data.inline_data, src_data, len); + *param_value_ptr = event.data.inline_data; + } else { + event.is_inline = false; + event.data.heap_data = new uint8_t[len]; + memcpy(event.data.heap_data, src_data, len); + *param_value_ptr = event.data.heap_data; + } + } else { + event.is_inline = false; + event.data.heap_data = nullptr; + *param_value_ptr = nullptr; + } + } + // Initialize GAP event data void init_gap_data_(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { this->event_.gap.gap_event = e; @@ -317,35 +370,38 @@ class BLEEvent { this->event_.gattc.gattc_if = i; if (p == nullptr) { - this->event_.gattc.gattc_param = nullptr; - this->event_.gattc.data = nullptr; + // Zero out the param struct when null + memset(&this->event_.gattc.gattc_param, 0, sizeof(this->event_.gattc.gattc_param)); + this->event_.gattc.is_inline = false; + this->event_.gattc.data.heap_data = nullptr; + this->event_.gattc.data_len = 0; return; // Invalid event, but we can't log in header file } - // Heap-allocate param and data - // Heap allocation is used because GATTC/GATTS events are rare (<1% of events) - // while GAP events (99%) are stored inline to minimize memory usage - // IMPORTANT: This heap allocation provides clear ownership semantics: - // - The BLEEvent owns the allocated memory for its lifetime - // - The data remains valid from the BLE callback context until processed in the main loop - // - Without this copy, we'd have use-after-free bugs as ESP-IDF reuses the callback memory - this->event_.gattc.gattc_param = new esp_ble_gattc_cb_param_t(*p); + // Copy param struct inline (no heap allocation!) + // GATTC/GATTS events are rare (<1% of events) but we can still store them inline + // along with small data payloads, eliminating all heap allocations for typical BLE operations + // CRITICAL: This copy is REQUIRED for memory safety - the ESP-IDF param pointer + // is only valid during the callback and will be reused/freed after we return + this->event_.gattc.gattc_param = *p; // Copy data for events that need it // The param struct contains pointers (e.g., notify.value) that point to temporary buffers. // We must copy this data to ensure it remains valid when the event is processed later. switch (e) { case ESP_GATTC_NOTIFY_EVT: - this->event_.gattc.data = new std::vector(p->notify.value, p->notify.value + p->notify.value_len); - this->event_.gattc.gattc_param->notify.value = this->event_.gattc.data->data(); + copy_data_with_inline_storage_event_.gattc), GATTC_INLINE_DATA_SIZE>( + this->event_.gattc, p->notify.value, p->notify.value_len, &this->event_.gattc.gattc_param.notify.value); break; case ESP_GATTC_READ_CHAR_EVT: case ESP_GATTC_READ_DESCR_EVT: - this->event_.gattc.data = new std::vector(p->read.value, p->read.value + p->read.value_len); - this->event_.gattc.gattc_param->read.value = this->event_.gattc.data->data(); + copy_data_with_inline_storage_event_.gattc), GATTC_INLINE_DATA_SIZE>( + this->event_.gattc, p->read.value, p->read.value_len, &this->event_.gattc.gattc_param.read.value); break; default: - this->event_.gattc.data = nullptr; + this->event_.gattc.is_inline = false; + this->event_.gattc.data.heap_data = nullptr; + this->event_.gattc.data_len = 0; break; } } @@ -356,30 +412,33 @@ class BLEEvent { this->event_.gatts.gatts_if = i; if (p == nullptr) { - this->event_.gatts.gatts_param = nullptr; - this->event_.gatts.data = nullptr; + // Zero out the param struct when null + memset(&this->event_.gatts.gatts_param, 0, sizeof(this->event_.gatts.gatts_param)); + this->event_.gatts.is_inline = false; + this->event_.gatts.data.heap_data = nullptr; + this->event_.gatts.data_len = 0; return; // Invalid event, but we can't log in header file } - // Heap-allocate param and data - // Heap allocation is used because GATTC/GATTS events are rare (<1% of events) - // while GAP events (99%) are stored inline to minimize memory usage - // IMPORTANT: This heap allocation provides clear ownership semantics: - // - The BLEEvent owns the allocated memory for its lifetime - // - The data remains valid from the BLE callback context until processed in the main loop - // - Without this copy, we'd have use-after-free bugs as ESP-IDF reuses the callback memory - this->event_.gatts.gatts_param = new esp_ble_gatts_cb_param_t(*p); + // Copy param struct inline (no heap allocation!) + // GATTC/GATTS events are rare (<1% of events) but we can still store them inline + // along with small data payloads, eliminating all heap allocations for typical BLE operations + // CRITICAL: This copy is REQUIRED for memory safety - the ESP-IDF param pointer + // is only valid during the callback and will be reused/freed after we return + this->event_.gatts.gatts_param = *p; // Copy data for events that need it // The param struct contains pointers (e.g., write.value) that point to temporary buffers. // We must copy this data to ensure it remains valid when the event is processed later. switch (e) { case ESP_GATTS_WRITE_EVT: - this->event_.gatts.data = new std::vector(p->write.value, p->write.value + p->write.len); - this->event_.gatts.gatts_param->write.value = this->event_.gatts.data->data(); + copy_data_with_inline_storage_event_.gatts), GATTS_INLINE_DATA_SIZE>( + this->event_.gatts, p->write.value, p->write.len, &this->event_.gatts.gatts_param.write.value); break; default: - this->event_.gatts.data = nullptr; + this->event_.gatts.is_inline = false; + this->event_.gatts.data.heap_data = nullptr; + this->event_.gatts.data_len = 0; break; } } @@ -389,6 +448,15 @@ class BLEEvent { // The gap member in the union should be 80 bytes (including the gap_event enum) static_assert(sizeof(decltype(((BLEEvent *) nullptr)->event_.gap)) <= 80, "gap_event struct has grown beyond 80 bytes"); +// Verify GATTC and GATTS structs don't exceed GAP struct size +// This ensures the union size is determined by GAP (the most common event type) +static_assert(sizeof(decltype(((BLEEvent *) nullptr)->event_.gattc)) <= + sizeof(decltype(((BLEEvent *) nullptr)->event_.gap)), + "gattc_event struct exceeds gap_event size - union size would increase"); +static_assert(sizeof(decltype(((BLEEvent *) nullptr)->event_.gatts)) <= + sizeof(decltype(((BLEEvent *) nullptr)->event_.gap)), + "gatts_event struct exceeds gap_event size - union size would increase"); + // Verify esp_ble_sec_t fits within our union static_assert(sizeof(esp_ble_sec_t) <= 73, "esp_ble_sec_t is larger than BLEScanResult"); diff --git a/esphome/components/esp32_ble/ble_uuid.cpp b/esphome/components/esp32_ble/ble_uuid.cpp index fc6981acd3..dcbb285e07 100644 --- a/esphome/components/esp32_ble/ble_uuid.cpp +++ b/esphome/components/esp32_ble/ble_uuid.cpp @@ -1,11 +1,13 @@ #include "ble_uuid.h" #ifdef USE_ESP32 +#ifdef USE_ESP32_BLE_UUID #include #include #include #include "esphome/core/log.h" +#include "esphome/core/helpers.h" namespace esphome::esp32_ble { @@ -40,32 +42,18 @@ ESPBTUUID ESPBTUUID::from_raw_reversed(const uint8_t *data) { ESPBTUUID ESPBTUUID::from_raw(const std::string &data) { ESPBTUUID ret; if (data.length() == 4) { - ret.uuid_.len = ESP_UUID_LEN_16; - ret.uuid_.uuid.uuid16 = 0; - for (uint i = 0; i < data.length(); i += 2) { - uint8_t msb = data.c_str()[i]; - uint8_t lsb = data.c_str()[i + 1]; - uint8_t lsb_shift = i <= 2 ? (2 - i) * 4 : 0; - - if (msb > '9') - msb -= 7; - if (lsb > '9') - lsb -= 7; - ret.uuid_.uuid.uuid16 += (((msb & 0x0F) << 4) | (lsb & 0x0F)) << lsb_shift; + // 16-bit UUID as 4-character hex string + auto parsed = parse_hex(data); + if (parsed.has_value()) { + ret.uuid_.len = ESP_UUID_LEN_16; + ret.uuid_.uuid.uuid16 = parsed.value(); } } else if (data.length() == 8) { - ret.uuid_.len = ESP_UUID_LEN_32; - ret.uuid_.uuid.uuid32 = 0; - for (uint i = 0; i < data.length(); i += 2) { - uint8_t msb = data.c_str()[i]; - uint8_t lsb = data.c_str()[i + 1]; - uint8_t lsb_shift = i <= 6 ? (6 - i) * 4 : 0; - - if (msb > '9') - msb -= 7; - if (lsb > '9') - lsb -= 7; - ret.uuid_.uuid.uuid32 += (((msb & 0x0F) << 4) | (lsb & 0x0F)) << lsb_shift; + // 32-bit UUID as 8-character hex string + auto parsed = parse_hex(data); + if (parsed.has_value()) { + ret.uuid_.len = ESP_UUID_LEN_32; + ret.uuid_.uuid.uuid32 = parsed.value(); } } else if (data.length() == 16) { // how we can have 16 byte length string reprezenting 128 bit uuid??? needs to be // investigated (lack of time) @@ -143,51 +131,60 @@ bool ESPBTUUID::operator==(const ESPBTUUID &uuid) const { if (this->uuid_.len == uuid.uuid_.len) { switch (this->uuid_.len) { case ESP_UUID_LEN_16: - if (uuid.uuid_.uuid.uuid16 == this->uuid_.uuid.uuid16) { - return true; - } - break; + return this->uuid_.uuid.uuid16 == uuid.uuid_.uuid.uuid16; case ESP_UUID_LEN_32: - if (uuid.uuid_.uuid.uuid32 == this->uuid_.uuid.uuid32) { - return true; - } - break; + return this->uuid_.uuid.uuid32 == uuid.uuid_.uuid.uuid32; case ESP_UUID_LEN_128: - for (uint8_t i = 0; i < ESP_UUID_LEN_128; i++) { - if (uuid.uuid_.uuid.uuid128[i] != this->uuid_.uuid.uuid128[i]) { - return false; - } - } - return true; - break; + return memcmp(this->uuid_.uuid.uuid128, uuid.uuid_.uuid.uuid128, ESP_UUID_LEN_128) == 0; + default: + return false; } - } else { - return this->as_128bit() == uuid.as_128bit(); } - return false; + return this->as_128bit() == uuid.as_128bit(); } esp_bt_uuid_t ESPBTUUID::get_uuid() const { return this->uuid_; } std::string ESPBTUUID::to_string() const { + char buf[40]; // Enough for 128-bit UUID with dashes + char *pos = buf; + switch (this->uuid_.len) { case ESP_UUID_LEN_16: - return str_snprintf("0x%02X%02X", 6, this->uuid_.uuid.uuid16 >> 8, this->uuid_.uuid.uuid16 & 0xff); + *pos++ = '0'; + *pos++ = 'x'; + *pos++ = format_hex_pretty_char(this->uuid_.uuid.uuid16 >> 12); + *pos++ = format_hex_pretty_char((this->uuid_.uuid.uuid16 >> 8) & 0x0F); + *pos++ = format_hex_pretty_char((this->uuid_.uuid.uuid16 >> 4) & 0x0F); + *pos++ = format_hex_pretty_char(this->uuid_.uuid.uuid16 & 0x0F); + *pos = '\0'; + return std::string(buf); + case ESP_UUID_LEN_32: - return str_snprintf("0x%02" PRIX32 "%02" PRIX32 "%02" PRIX32 "%02" PRIX32, 10, (this->uuid_.uuid.uuid32 >> 24), - (this->uuid_.uuid.uuid32 >> 16 & 0xff), (this->uuid_.uuid.uuid32 >> 8 & 0xff), - this->uuid_.uuid.uuid32 & 0xff); + *pos++ = '0'; + *pos++ = 'x'; + for (int shift = 28; shift >= 0; shift -= 4) { + *pos++ = format_hex_pretty_char((this->uuid_.uuid.uuid32 >> shift) & 0x0F); + } + *pos = '\0'; + return std::string(buf); + default: case ESP_UUID_LEN_128: - std::string buf; + // Format: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX for (int8_t i = 15; i >= 0; i--) { - buf += str_snprintf("%02X", 2, this->uuid_.uuid.uuid128[i]); - if (i == 6 || i == 8 || i == 10 || i == 12) - buf += "-"; + uint8_t byte = this->uuid_.uuid.uuid128[i]; + *pos++ = format_hex_pretty_char(byte >> 4); + *pos++ = format_hex_pretty_char(byte & 0x0F); + if (i == 12 || i == 10 || i == 8 || i == 6) { + *pos++ = '-'; + } } - return buf; + *pos = '\0'; + return std::string(buf); } return ""; } } // namespace esphome::esp32_ble -#endif +#endif // USE_ESP32_BLE_UUID +#endif // USE_ESP32 diff --git a/esphome/components/esp32_ble/ble_uuid.h b/esphome/components/esp32_ble/ble_uuid.h index 150ca359d3..4cf2d10abd 100644 --- a/esphome/components/esp32_ble/ble_uuid.h +++ b/esphome/components/esp32_ble/ble_uuid.h @@ -1,9 +1,11 @@ #pragma once +#include "esphome/core/defines.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #ifdef USE_ESP32 +#ifdef USE_ESP32_BLE_UUID #include #include @@ -42,4 +44,5 @@ class ESPBTUUID { } // namespace esphome::esp32_ble -#endif +#endif // USE_ESP32_BLE_UUID +#endif // USE_ESP32 diff --git a/esphome/components/esp32_ble_beacon/__init__.py b/esphome/components/esp32_ble_beacon/__init__.py index 6e0d103aa0..ba5ae4331c 100644 --- a/esphome/components/esp32_ble_beacon/__init__.py +++ b/esphome/components/esp32_ble_beacon/__init__.py @@ -4,7 +4,7 @@ from esphome.components.esp32 import add_idf_sdkconfig_option from esphome.components.esp32_ble import CONF_BLE_ID import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_TX_POWER, CONF_TYPE, CONF_UUID -from esphome.core import CORE, TimePeriod +from esphome.core import TimePeriod AUTO_LOAD = ["esp32_ble"] DEPENDENCIES = ["esp32"] @@ -65,6 +65,8 @@ FINAL_VALIDATE_SCHEMA = esp32_ble.validate_variant async def to_code(config): + cg.add_define("USE_ESP32_BLE_UUID") + uuid = config[CONF_UUID].hex uuid_arr = [ cg.RawExpression(f"0x{uuid[i : i + 2]}") for i in range(0, len(uuid), 2) @@ -72,7 +74,7 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID], uuid_arr) parent = await cg.get_variable(config[esp32_ble.CONF_BLE_ID]) - cg.add(parent.register_gap_event_handler(var)) + esp32_ble.register_gap_event_handler(parent, var) await cg.register_component(var, config) cg.add(var.set_major(config[CONF_MAJOR])) @@ -82,6 +84,7 @@ async def to_code(config): cg.add(var.set_measured_power(config[CONF_MEASURED_POWER])) cg.add(var.set_tx_power(config[CONF_TX_POWER])) - if CORE.using_esp_idf: - add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True) - add_idf_sdkconfig_option("CONFIG_BT_BLE_42_FEATURES_SUPPORTED", True) + cg.add_define("USE_ESP32_BLE_ADVERTISING") + + add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True) + add_idf_sdkconfig_option("CONFIG_BT_BLE_42_FEATURES_SUPPORTED", True) diff --git a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp index 423fe61592..f2aa7e762e 100644 --- a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp +++ b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp @@ -3,7 +3,9 @@ #ifdef USE_ESP32 +#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID #include +#endif #include #include #include @@ -14,10 +16,6 @@ #include "esphome/core/hal.h" #include "esphome/core/helpers.h" -#ifdef USE_ARDUINO -#include -#endif - namespace esphome { namespace esp32_ble_beacon { @@ -31,12 +29,13 @@ void ESP32BLEBeacon::dump_config() { char uuid[37]; char *bpos = uuid; for (int8_t ii = 0; ii < 16; ++ii) { - bpos += sprintf(bpos, "%02X", this->uuid_[ii]); + *bpos++ = format_hex_pretty_char(this->uuid_[ii] >> 4); + *bpos++ = format_hex_pretty_char(this->uuid_[ii] & 0x0F); if (ii == 3 || ii == 5 || ii == 7 || ii == 9) { - bpos += sprintf(bpos, "-"); + *bpos++ = '-'; } } - uuid[36] = '\0'; + *bpos = '\0'; ESP_LOGCONFIG(TAG, " UUID: %s, Major: %u, Minor: %u, Min Interval: %ums, Max Interval: %ums, Measured Power: %d" ", TX Power: %ddBm", diff --git a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h index e37edf6cde..05afdc7379 100644 --- a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h +++ b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h @@ -5,7 +5,9 @@ #ifdef USE_ESP32 +#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID #include +#endif #include namespace esphome { diff --git a/esphome/components/esp32_ble_client/__init__.py b/esphome/components/esp32_ble_client/__init__.py index 25957ed0da..55619f1fc0 100644 --- a/esphome/components/esp32_ble_client/__init__.py +++ b/esphome/components/esp32_ble_client/__init__.py @@ -2,7 +2,7 @@ import esphome.codegen as cg from esphome.components import esp32_ble_tracker AUTO_LOAD = ["esp32_ble_tracker"] -CODEOWNERS = ["@jesserockz"] +CODEOWNERS = ["@jesserockz", "@bdraco"] DEPENDENCIES = ["esp32"] esp32_ble_client_ns = cg.esphome_ns.namespace("esp32_ble_client") diff --git a/esphome/components/esp32_ble_client/ble_characteristic.cpp b/esphome/components/esp32_ble_client/ble_characteristic.cpp index 2fd7fe9871..e0d0174c57 100644 --- a/esphome/components/esp32_ble_client/ble_characteristic.cpp +++ b/esphome/components/esp32_ble_client/ble_characteristic.cpp @@ -5,9 +5,9 @@ #include "esphome/core/log.h" #ifdef USE_ESP32 +#ifdef USE_ESP32_BLE_DEVICE -namespace esphome { -namespace esp32_ble_client { +namespace esphome::esp32_ble_client { static const char *const TAG = "esp32_ble_client"; @@ -38,7 +38,7 @@ void BLECharacteristic::parse_descriptors() { } if (status != ESP_GATT_OK) { ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_get_all_descr error, status=%d", - this->service->client->get_connection_index(), this->service->client->address_str().c_str(), status); + this->service->client->get_connection_index(), this->service->client->address_str(), status); break; } if (count == 0) { @@ -51,7 +51,7 @@ void BLECharacteristic::parse_descriptors() { desc->characteristic = this; this->descriptors.push_back(desc); ESP_LOGV(TAG, "[%d] [%s] descriptor %s, handle 0x%x", this->service->client->get_connection_index(), - this->service->client->address_str().c_str(), desc->uuid.to_string().c_str(), desc->handle); + this->service->client->address_str(), desc->uuid.to_string().c_str(), desc->handle); offset++; } } @@ -84,7 +84,7 @@ esp_err_t BLECharacteristic::write_value(uint8_t *new_val, int16_t new_val_size, new_val, write_type, ESP_GATT_AUTH_REQ_NONE); if (status) { ESP_LOGW(TAG, "[%d] [%s] Error sending write value to BLE gattc server, status=%d", - this->service->client->get_connection_index(), this->service->client->address_str().c_str(), status); + this->service->client->get_connection_index(), this->service->client->address_str(), status); } return status; } @@ -93,7 +93,7 @@ esp_err_t BLECharacteristic::write_value(uint8_t *new_val, int16_t new_val_size) return write_value(new_val, new_val_size, ESP_GATT_WRITE_TYPE_NO_RSP); } -} // namespace esp32_ble_client -} // namespace esphome +} // namespace esphome::esp32_ble_client +#endif // USE_ESP32_BLE_DEVICE #endif // USE_ESP32 diff --git a/esphome/components/esp32_ble_client/ble_characteristic.h b/esphome/components/esp32_ble_client/ble_characteristic.h index a014788e65..1428b42739 100644 --- a/esphome/components/esp32_ble_client/ble_characteristic.h +++ b/esphome/components/esp32_ble_client/ble_characteristic.h @@ -1,6 +1,9 @@ #pragma once +#include "esphome/core/defines.h" + #ifdef USE_ESP32 +#ifdef USE_ESP32_BLE_DEVICE #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" @@ -8,8 +11,7 @@ #include -namespace esphome { -namespace esp32_ble_client { +namespace esphome::esp32_ble_client { namespace espbt = esphome::esp32_ble_tracker; @@ -33,7 +35,7 @@ class BLECharacteristic { BLEService *service; }; -} // namespace esp32_ble_client -} // namespace esphome +} // namespace esphome::esp32_ble_client +#endif // USE_ESP32_BLE_DEVICE #endif // USE_ESP32 diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index 2c13995f76..07e88c7528 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -7,9 +7,9 @@ #include #include +#include -namespace esphome { -namespace esp32_ble_client { +namespace esphome::esp32_ble_client { static const char *const TAG = "esp32_ble_client"; @@ -41,15 +41,8 @@ void BLEClientBase::setup() { } void BLEClientBase::set_state(espbt::ClientState st) { - ESP_LOGV(TAG, "[%d] [%s] Set state %d", this->connection_index_, this->address_str_.c_str(), (int) st); + ESP_LOGV(TAG, "[%d] [%s] Set state %d", this->connection_index_, this->address_str_, (int) st); ESPBTClient::set_state(st); - - if (st == espbt::ClientState::READY_TO_CONNECT) { - // Enable loop for state processing - this->enable_loop(); - // Connect immediately instead of waiting for next loop - this->connect(); - } } void BLEClientBase::loop() { @@ -65,8 +58,8 @@ void BLEClientBase::loop() { } this->set_state(espbt::ClientState::IDLE); } - // If its idle, we can disable the loop as set_state - // will enable it again when we need to connect. + // If idle, we can disable the loop as connect() + // will enable it again when a connection is needed. else if (this->state_ == espbt::ClientState::IDLE) { this->disable_loop(); } @@ -78,41 +71,8 @@ void BLEClientBase::dump_config() { ESP_LOGCONFIG(TAG, " Address: %s\n" " Auto-Connect: %s", - this->address_str().c_str(), TRUEFALSE(this->auto_connect_)); - std::string state_name; - switch (this->state()) { - case espbt::ClientState::INIT: - state_name = "INIT"; - break; - case espbt::ClientState::DISCONNECTING: - state_name = "DISCONNECTING"; - break; - case espbt::ClientState::IDLE: - state_name = "IDLE"; - break; - case espbt::ClientState::SEARCHING: - state_name = "SEARCHING"; - break; - case espbt::ClientState::DISCOVERED: - state_name = "DISCOVERED"; - break; - case espbt::ClientState::READY_TO_CONNECT: - state_name = "READY_TO_CONNECT"; - break; - case espbt::ClientState::CONNECTING: - state_name = "CONNECTING"; - break; - case espbt::ClientState::CONNECTED: - state_name = "CONNECTED"; - break; - case espbt::ClientState::ESTABLISHED: - state_name = "ESTABLISHED"; - break; - default: - state_name = "UNKNOWN_STATE"; - break; - } - ESP_LOGCONFIG(TAG, " State: %s", state_name.c_str()); + this->address_str(), TRUEFALSE(this->auto_connect_)); + ESP_LOGCONFIG(TAG, " State: %s", espbt::client_state_to_string(this->state())); if (this->status_ == ESP_GATT_NO_RESOURCES) { ESP_LOGE(TAG, " Failed due to no resources. Try to reduce number of BLE clients in config."); } else if (this->status_ != ESP_GATT_OK) { @@ -126,7 +86,7 @@ bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) { return false; if (this->address_ == 0 || device.address_uint64() != this->address_) return false; - if (this->state_ != espbt::ClientState::IDLE && this->state_ != espbt::ClientState::SEARCHING) + if (this->state_ != espbt::ClientState::IDLE) return false; this->log_event_("Found device"); @@ -141,65 +101,46 @@ bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) { #endif void BLEClientBase::connect() { - ESP_LOGI(TAG, "[%d] [%s] 0x%02x Attempting BLE connection", this->connection_index_, this->address_str_.c_str(), - this->remote_addr_type_); - this->paired_ = false; - - auto ret = esp_ble_gattc_open(this->gattc_if_, this->remote_bda_, this->remote_addr_type_, true); - if (ret) { - ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_open error, status=%d", this->connection_index_, this->address_str_.c_str(), - ret); - this->set_state(espbt::ClientState::IDLE); - } else { - this->set_state(espbt::ClientState::CONNECTING); - - // Always set connection parameters to ensure stable operation - // Use FAST for all V3 connections (better latency and reliability) - // Use MEDIUM for V1/legacy connections (balanced performance) - uint16_t min_interval, max_interval, timeout; - const char *param_type; - - if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE || - this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) { - min_interval = FAST_MIN_CONN_INTERVAL; - max_interval = FAST_MAX_CONN_INTERVAL; - timeout = FAST_CONN_TIMEOUT; - param_type = "fast"; - } else { - min_interval = MEDIUM_MIN_CONN_INTERVAL; - max_interval = MEDIUM_MAX_CONN_INTERVAL; - timeout = MEDIUM_CONN_TIMEOUT; - param_type = "medium"; - } - - auto param_ret = esp_ble_gap_set_prefer_conn_params(this->remote_bda_, min_interval, max_interval, - 0, // latency: 0 - timeout); - if (param_ret != ESP_OK) { - ESP_LOGW(TAG, "[%d] [%s] esp_ble_gap_set_prefer_conn_params failed: %d", this->connection_index_, - this->address_str_.c_str(), param_ret); - } else { - ESP_LOGD(TAG, "[%d] [%s] Set %s conn params", this->connection_index_, this->address_str_.c_str(), param_type); - } + // Prevent duplicate connection attempts + if (this->state_ == espbt::ClientState::CONNECTING || this->state_ == espbt::ClientState::CONNECTED || + this->state_ == espbt::ClientState::ESTABLISHED) { + ESP_LOGW(TAG, "[%d] [%s] Connection already in progress, state=%s", this->connection_index_, this->address_str_, + espbt::client_state_to_string(this->state_)); + return; } + ESP_LOGI(TAG, "[%d] [%s] 0x%02x Connecting", this->connection_index_, this->address_str_, this->remote_addr_type_); + this->paired_ = false; + // Enable loop for state processing + this->enable_loop(); + // Immediately transition to CONNECTING to prevent duplicate connection attempts + this->set_state(espbt::ClientState::CONNECTING); + + // Determine connection parameters based on connection type + if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) { + // V3 without cache needs fast params for service discovery + this->set_conn_params_(FAST_MIN_CONN_INTERVAL, FAST_MAX_CONN_INTERVAL, 0, FAST_CONN_TIMEOUT, "fast"); + } else if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) { + // V3 with cache can use medium params + this->set_conn_params_(MEDIUM_MIN_CONN_INTERVAL, MEDIUM_MAX_CONN_INTERVAL, 0, MEDIUM_CONN_TIMEOUT, "medium"); + } + // For V1/Legacy, don't set params - use ESP-IDF defaults + + // Open the connection + auto ret = esp_ble_gattc_open(this->gattc_if_, this->remote_bda_, this->remote_addr_type_, true); + this->handle_connection_result_(ret); } esp_err_t BLEClientBase::pair() { return esp_ble_set_encryption(this->remote_bda_, ESP_BLE_SEC_ENCRYPT); } void BLEClientBase::disconnect() { - if (this->state_ == espbt::ClientState::IDLE) { - ESP_LOGI(TAG, "[%d] [%s] Disconnect requested, but already idle.", this->connection_index_, - this->address_str_.c_str()); - return; - } - if (this->state_ == espbt::ClientState::DISCONNECTING) { - ESP_LOGI(TAG, "[%d] [%s] Disconnect requested, but already disconnecting.", this->connection_index_, - this->address_str_.c_str()); + if (this->state_ == espbt::ClientState::IDLE || this->state_ == espbt::ClientState::DISCONNECTING) { + ESP_LOGI(TAG, "[%d] [%s] Disconnect requested, but already %s", this->connection_index_, this->address_str_, + espbt::client_state_to_string(this->state_)); return; } if (this->state_ == espbt::ClientState::CONNECTING || this->conn_id_ == UNSET_CONN_ID) { - ESP_LOGW(TAG, "[%d] [%s] Disconnecting before connected, disconnect scheduled.", this->connection_index_, - this->address_str_.c_str()); + ESP_LOGD(TAG, "[%d] [%s] Disconnect before connected, disconnect scheduled", this->connection_index_, + this->address_str_); this->want_disconnect_ = true; return; } @@ -208,16 +149,13 @@ void BLEClientBase::disconnect() { void BLEClientBase::unconditional_disconnect() { // Disconnect without checking the state. - ESP_LOGI(TAG, "[%d] [%s] Disconnecting (conn_id: %d).", this->connection_index_, this->address_str_.c_str(), - this->conn_id_); + ESP_LOGI(TAG, "[%d] [%s] Disconnecting (conn_id: %d).", this->connection_index_, this->address_str_, this->conn_id_); if (this->state_ == espbt::ClientState::DISCONNECTING) { - ESP_LOGE(TAG, "[%d] [%s] Tried to disconnect while already disconnecting.", this->connection_index_, - this->address_str_.c_str()); + this->log_error_("Already disconnecting"); return; } if (this->conn_id_ == UNSET_CONN_ID) { - ESP_LOGE(TAG, "[%d] [%s] No connection ID set, cannot disconnect.", this->connection_index_, - this->address_str_.c_str()); + this->log_error_("conn id unset, cannot disconnect"); return; } auto err = esp_ble_gattc_close(this->gattc_if_, this->conn_id_); @@ -229,12 +167,10 @@ void BLEClientBase::unconditional_disconnect() { // In the future we might consider App.reboot() here since // the BLE stack is in an indeterminate state. // - ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_close error, err=%d", this->connection_index_, this->address_str_.c_str(), - err); + this->log_gattc_warning_("esp_ble_gattc_close", err); } - if (this->state_ == espbt::ClientState::SEARCHING || this->state_ == espbt::ClientState::READY_TO_CONNECT || - this->state_ == espbt::ClientState::DISCOVERED) { + if (this->state_ == espbt::ClientState::DISCOVERED) { this->set_address(0); this->set_state(espbt::ClientState::IDLE); } else { @@ -243,16 +179,79 @@ void BLEClientBase::unconditional_disconnect() { } void BLEClientBase::release_services() { +#ifdef USE_ESP32_BLE_DEVICE for (auto &svc : this->services_) delete svc; // NOLINT(cppcoreguidelines-owning-memory) this->services_.clear(); +#endif #ifndef CONFIG_BT_GATTC_CACHE_NVS_FLASH esp_ble_gattc_cache_clean(this->remote_bda_); #endif } void BLEClientBase::log_event_(const char *name) { - ESP_LOGD(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_.c_str(), name); + ESP_LOGD(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_, name); +} + +void BLEClientBase::log_gattc_event_(const char *name) { + ESP_LOGD(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); +} + +void BLEClientBase::log_gattc_warning_(const char *operation, esp_err_t err) { + ESP_LOGW(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str_, operation, err); +} + +void BLEClientBase::log_connection_params_(const char *param_type) { + ESP_LOGD(TAG, "[%d] [%s] %s conn params", this->connection_index_, this->address_str_, param_type); +} + +void BLEClientBase::handle_connection_result_(esp_err_t ret) { + if (ret) { + this->log_gattc_warning_("esp_ble_gattc_open", ret); + this->set_state(espbt::ClientState::IDLE); + } +} + +void BLEClientBase::log_error_(const char *message) { + ESP_LOGE(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_, message); +} + +void BLEClientBase::log_error_(const char *message, int code) { + ESP_LOGE(TAG, "[%d] [%s] %s=%d", this->connection_index_, this->address_str_, message, code); +} + +void BLEClientBase::log_warning_(const char *message) { + ESP_LOGW(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_, message); +} + +void BLEClientBase::update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, + uint16_t timeout, const char *param_type) { + esp_ble_conn_update_params_t conn_params = {{0}}; + memcpy(conn_params.bda, this->remote_bda_, sizeof(esp_bd_addr_t)); + conn_params.min_int = min_interval; + conn_params.max_int = max_interval; + conn_params.latency = latency; + conn_params.timeout = timeout; + this->log_connection_params_(param_type); + esp_err_t err = esp_ble_gap_update_conn_params(&conn_params); + if (err != ESP_OK) { + this->log_gattc_warning_("esp_ble_gap_update_conn_params", err); + } +} + +void BLEClientBase::set_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout, + const char *param_type) { + // Set preferred connection parameters before connecting + // These will be used when establishing the connection + this->log_connection_params_(param_type); + esp_err_t err = esp_ble_gap_set_prefer_conn_params(this->remote_bda_, min_interval, max_interval, latency, timeout); + if (err != ESP_OK) { + this->log_gattc_warning_("esp_ble_gap_set_prefer_conn_params", err); + } } bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t esp_gattc_if, @@ -262,18 +261,17 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ if (event != ESP_GATTC_REG_EVT && esp_gattc_if != ESP_GATT_IF_NONE && esp_gattc_if != this->gattc_if_) return false; - ESP_LOGV(TAG, "[%d] [%s] gattc_event_handler: event=%d gattc_if=%d", this->connection_index_, - this->address_str_.c_str(), event, esp_gattc_if); + ESP_LOGV(TAG, "[%d] [%s] gattc_event_handler: event=%d gattc_if=%d", this->connection_index_, this->address_str_, + event, esp_gattc_if); switch (event) { case ESP_GATTC_REG_EVT: { if (param->reg.status == ESP_GATT_OK) { - ESP_LOGV(TAG, "[%d] [%s] gattc registered app id %d", this->connection_index_, this->address_str_.c_str(), + ESP_LOGV(TAG, "[%d] [%s] gattc registered app id %d", this->connection_index_, this->address_str_, this->app_id); this->gattc_if_ = esp_gattc_if; } else { - ESP_LOGE(TAG, "[%d] [%s] gattc app registration failed id=%d code=%d", this->connection_index_, - this->address_str_.c_str(), param->reg.app_id, param->reg.status); + this->log_error_("gattc app registration failed status", param->reg.status); this->status_ = param->reg.status; this->mark_failed(); } @@ -282,30 +280,28 @@ 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_event_("ESP_GATTC_OPEN_EVT"); - this->conn_id_ = param->open.conn_id; + this->log_gattc_event_("OPEN"); + // conn_id was already set in ESP_GATTC_CONNECT_EVT this->service_count_ = 0; + + // ESP-IDF's BLE stack may send ESP_GATTC_OPEN_EVT after esp_ble_gattc_open() returns an + // error, if the error occurred at the BTA/GATT layer. This can result in the event + // arriving after we've already transitioned to IDLE state. + if (this->state_ == espbt::ClientState::IDLE) { + ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in IDLE state (status=%d), ignoring", this->connection_index_, + this->address_str_, param->open.status); + break; + } + if (this->state_ != espbt::ClientState::CONNECTING) { // This should not happen but lets log it in case it does // because it means we have a bad assumption about how the // ESP BT stack works. - if (this->state_ == espbt::ClientState::CONNECTED) { - ESP_LOGE(TAG, "[%d] [%s] Got ESP_GATTC_OPEN_EVT while already connected, status=%d", this->connection_index_, - this->address_str_.c_str(), param->open.status); - } else if (this->state_ == espbt::ClientState::ESTABLISHED) { - ESP_LOGE(TAG, "[%d] [%s] Got ESP_GATTC_OPEN_EVT while already established, status=%d", - this->connection_index_, this->address_str_.c_str(), param->open.status); - } else if (this->state_ == espbt::ClientState::DISCONNECTING) { - ESP_LOGE(TAG, "[%d] [%s] Got ESP_GATTC_OPEN_EVT while disconnecting, status=%d", this->connection_index_, - this->address_str_.c_str(), param->open.status); - } else { - ESP_LOGE(TAG, "[%d] [%s] Got ESP_GATTC_OPEN_EVT while not in connecting state, status=%d", - this->connection_index_, this->address_str_.c_str(), param->open.status); - } + ESP_LOGE(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in %s state (status=%d)", this->connection_index_, + this->address_str_, espbt::client_state_to_string(this->state_), param->open.status); } if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) { - ESP_LOGW(TAG, "[%d] [%s] Connection failed, status=%d", this->connection_index_, this->address_str_.c_str(), - param->open.status); + this->log_gattc_warning_("Connection open", param->open.status); this->set_state(espbt::ClientState::IDLE); break; } @@ -317,27 +313,34 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ this->conn_id_ = UNSET_CONN_ID; break; } - auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if_, param->open.conn_id); - if (ret) { - ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_send_mtu_req failed, status=%x", this->connection_index_, - this->address_str_.c_str(), ret); - } + // MTU negotiation already started in ESP_GATTC_CONNECT_EVT this->set_state(espbt::ClientState::CONNECTED); - ESP_LOGI(TAG, "[%d] [%s] Connection open", this->connection_index_, this->address_str_.c_str()); + ESP_LOGI(TAG, "[%d] [%s] Connection open", this->connection_index_, this->address_str_); if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) { - ESP_LOGI(TAG, "[%d] [%s] Using cached services", this->connection_index_, this->address_str_.c_str()); + // Cached connections already connected with medium parameters, no update needed // only set our state, subclients might have more stuff to do yet. this->state_ = espbt::ClientState::ESTABLISHED; break; } - ESP_LOGD(TAG, "[%d] [%s] Searching for services", this->connection_index_, this->address_str_.c_str()); + // For V3_WITHOUT_CACHE, we already set fast params before connecting + // No need to update them again here + this->log_event_("Searching for services"); esp_ble_gattc_search_service(esp_gattc_if, param->cfg_mtu.conn_id, nullptr); break; } case ESP_GATTC_CONNECT_EVT: { if (!this->check_addr(param->connect.remote_bda)) return false; - this->log_event_("ESP_GATTC_CONNECT_EVT"); + this->log_gattc_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 + // ESP_GATTC_CONNECT_EVT instead of waiting for ESP_GATTC_OPEN_EVT. + // This saves ~3ms in the connection process. + auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if_, param->connect.conn_id); + if (ret) { + this->log_gattc_warning_("esp_ble_gattc_send_mtu_req", ret); + } break; } case ESP_GATTC_DISCONNECT_EVT: { @@ -346,11 +349,10 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ // Check if we were disconnected while waiting for service discovery if (param->disconnect.reason == ESP_GATT_CONN_TERMINATE_PEER_USER && this->state_ == espbt::ClientState::CONNECTED) { - ESP_LOGW(TAG, "[%d] [%s] Disconnected by remote during service discovery", this->connection_index_, - this->address_str_.c_str()); + this->log_warning_("Remote closed during discovery"); } else { - ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason 0x%02x", this->connection_index_, - this->address_str_.c_str(), param->disconnect.reason); + ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason 0x%02x", this->connection_index_, this->address_str_, + param->disconnect.reason); } this->release_services(); this->set_state(espbt::ClientState::IDLE); @@ -361,12 +363,12 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ if (this->conn_id_ != param->cfg_mtu.conn_id) return false; if (param->cfg_mtu.status != ESP_GATT_OK) { - ESP_LOGW(TAG, "[%d] [%s] cfg_mtu failed, mtu %d, status %d", this->connection_index_, - this->address_str_.c_str(), param->cfg_mtu.mtu, param->cfg_mtu.status); + ESP_LOGW(TAG, "[%d] [%s] cfg_mtu failed, mtu %d, status %d", this->connection_index_, this->address_str_, + param->cfg_mtu.mtu, param->cfg_mtu.status); // No state change required here - disconnect event will follow if needed. break; } - ESP_LOGD(TAG, "[%d] [%s] cfg_mtu status %d, mtu %d", this->connection_index_, this->address_str_.c_str(), + ESP_LOGD(TAG, "[%d] [%s] cfg_mtu status %d, mtu %d", this->connection_index_, this->address_str_, param->cfg_mtu.status, param->cfg_mtu.mtu); this->mtu_ = param->cfg_mtu.mtu; break; @@ -374,7 +376,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_event_("ESP_GATTC_CLOSE_EVT"); + this->log_gattc_event_("CLOSE"); this->release_services(); this->set_state(espbt::ClientState::IDLE); this->conn_id_ = UNSET_CONN_ID; @@ -386,79 +388,73 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ this->service_count_++; if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) { // V3 clients don't need services initialized since - // they only request by handle after receiving the services. + // as they use the ESP APIs to get services. break; } +#ifdef USE_ESP32_BLE_DEVICE BLEService *ble_service = new BLEService(); // NOLINT(cppcoreguidelines-owning-memory) ble_service->uuid = espbt::ESPBTUUID::from_uuid(param->search_res.srvc_id.uuid); ble_service->start_handle = param->search_res.start_handle; ble_service->end_handle = param->search_res.end_handle; ble_service->client = this; this->services_.push_back(ble_service); +#endif break; } case ESP_GATTC_SEARCH_CMPL_EVT: { if (this->conn_id_ != param->search_cmpl.conn_id) return false; - this->log_event_("ESP_GATTC_SEARCH_CMPL_EVT"); - for (auto &svc : this->services_) { - ESP_LOGV(TAG, "[%d] [%s] Service UUID: %s", this->connection_index_, this->address_str_.c_str(), - svc->uuid.to_string().c_str()); - ESP_LOGV(TAG, "[%d] [%s] start_handle: 0x%x end_handle: 0x%x", this->connection_index_, - this->address_str_.c_str(), svc->start_handle, svc->end_handle); - } - ESP_LOGI(TAG, "[%d] [%s] Service discovery complete", this->connection_index_, this->address_str_.c_str()); - - // For V3 connections, restore to medium connection parameters after service discovery + this->log_gattc_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 || - this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) { - esp_ble_conn_update_params_t conn_params = {{0}}; - memcpy(conn_params.bda, this->remote_bda_, sizeof(esp_bd_addr_t)); - conn_params.min_int = MEDIUM_MIN_CONN_INTERVAL; - conn_params.max_int = MEDIUM_MAX_CONN_INTERVAL; - conn_params.latency = 0; - conn_params.timeout = MEDIUM_CONN_TIMEOUT; - ESP_LOGD(TAG, "[%d] [%s] Restored medium conn params after service discovery", this->connection_index_, - this->address_str_.c_str()); - esp_ble_gap_update_conn_params(&conn_params); + if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) { + this->update_conn_params_(MEDIUM_MIN_CONN_INTERVAL, MEDIUM_MAX_CONN_INTERVAL, 0, MEDIUM_CONN_TIMEOUT, "medium"); + } else if (this->connection_type_ != espbt::ConnectionType::V3_WITH_CACHE) { +#ifdef USE_ESP32_BLE_DEVICE + for (auto &svc : this->services_) { + ESP_LOGV(TAG, "[%d] [%s] Service UUID: %s", this->connection_index_, this->address_str_, + svc->uuid.to_string().c_str()); + ESP_LOGV(TAG, "[%d] [%s] start_handle: 0x%x end_handle: 0x%x", this->connection_index_, this->address_str_, + svc->start_handle, svc->end_handle); + } +#endif } - + ESP_LOGI(TAG, "[%d] [%s] Service discovery complete", this->connection_index_, this->address_str_); this->state_ = espbt::ClientState::ESTABLISHED; break; } case ESP_GATTC_READ_DESCR_EVT: { if (this->conn_id_ != param->write.conn_id) return false; - this->log_event_("ESP_GATTC_READ_DESCR_EVT"); + this->log_gattc_event_("READ_DESCR"); break; } case ESP_GATTC_WRITE_DESCR_EVT: { if (this->conn_id_ != param->write.conn_id) return false; - this->log_event_("ESP_GATTC_WRITE_DESCR_EVT"); + this->log_gattc_event_("WRITE_DESCR"); break; } case ESP_GATTC_WRITE_CHAR_EVT: { if (this->conn_id_ != param->write.conn_id) return false; - this->log_event_("ESP_GATTC_WRITE_CHAR_EVT"); + this->log_gattc_event_("WRITE_CHAR"); break; } case ESP_GATTC_READ_CHAR_EVT: { if (this->conn_id_ != param->read.conn_id) return false; - this->log_event_("ESP_GATTC_READ_CHAR_EVT"); + this->log_gattc_event_("READ_CHAR"); break; } case ESP_GATTC_NOTIFY_EVT: { if (this->conn_id_ != param->notify.conn_id) return false; - this->log_event_("ESP_GATTC_NOTIFY_EVT"); + this->log_gattc_event_("NOTIFY"); break; } case ESP_GATTC_REG_FOR_NOTIFY_EVT: { - this->log_event_("ESP_GATTC_REG_FOR_NOTIFY_EVT"); + this->log_gattc_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 @@ -470,8 +466,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ esp_gatt_status_t descr_status = esp_ble_gattc_get_descr_by_char_handle( this->gattc_if_, this->conn_id_, param->reg_for_notify.handle, NOTIFY_DESC_UUID, &desc_result, &count); if (descr_status != ESP_GATT_OK) { - ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_get_descr_by_char_handle error, status=%d", this->connection_index_, - this->address_str_.c_str(), descr_status); + this->log_gattc_warning_("esp_ble_gattc_get_descr_by_char_handle", descr_status); break; } esp_gattc_char_elem_t char_result; @@ -479,8 +474,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ esp_ble_gattc_get_all_char(this->gattc_if_, this->conn_id_, param->reg_for_notify.handle, param->reg_for_notify.handle, &char_result, &count, 0); if (char_status != ESP_GATT_OK) { - ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_get_all_char error, status=%d", this->connection_index_, - this->address_str_.c_str(), char_status); + this->log_gattc_warning_("esp_ble_gattc_get_all_char", char_status); break; } @@ -494,15 +488,19 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ (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); if (status) { - ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_write_char_descr error, status=%d", this->connection_index_, - this->address_str_.c_str(), status); + this->log_gattc_warning_("esp_ble_gattc_write_char_descr", status); } break; } + case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: { + this->log_gattc_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_.c_str(), event); + ESP_LOGD(TAG, "[%d] [%s] Event %d", this->connection_index_, this->address_str_, event); break; } return true; @@ -519,7 +517,7 @@ void BLEClientBase::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_ case ESP_GAP_BLE_SEC_REQ_EVT: if (!this->check_addr(param->ble_security.auth_cmpl.bd_addr)) return; - ESP_LOGV(TAG, "[%d] [%s] ESP_GAP_BLE_SEC_REQ_EVT %x", this->connection_index_, this->address_str_.c_str(), event); + ESP_LOGV(TAG, "[%d] [%s] ESP_GAP_BLE_SEC_REQ_EVT %x", this->connection_index_, this->address_str_, event); esp_ble_gap_security_rsp(param->ble_security.ble_req.bd_addr, true); break; // This event is sent once authentication has completed @@ -528,16 +526,14 @@ void BLEClientBase::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_ return; esp_bd_addr_t bd_addr; memcpy(bd_addr, param->ble_security.auth_cmpl.bd_addr, sizeof(esp_bd_addr_t)); - ESP_LOGI(TAG, "[%d] [%s] auth complete. remote BD_ADDR: %s", this->connection_index_, this->address_str_.c_str(), + ESP_LOGI(TAG, "[%d] [%s] auth complete addr: %s", this->connection_index_, this->address_str_, format_hex(bd_addr, 6).c_str()); if (!param->ble_security.auth_cmpl.success) { - ESP_LOGE(TAG, "[%d] [%s] auth fail reason = 0x%x", this->connection_index_, this->address_str_.c_str(), - param->ble_security.auth_cmpl.fail_reason); + this->log_error_("auth fail reason", param->ble_security.auth_cmpl.fail_reason); } else { this->paired_ = true; - ESP_LOGD(TAG, "[%d] [%s] auth success. address type = %d auth mode = %d", this->connection_index_, - this->address_str_.c_str(), param->ble_security.auth_cmpl.addr_type, - param->ble_security.auth_cmpl.auth_mode); + ESP_LOGD(TAG, "[%d] [%s] auth success type = %d mode = %d", this->connection_index_, this->address_str_, + param->ble_security.auth_cmpl.addr_type, param->ble_security.auth_cmpl.auth_mode); } break; @@ -599,10 +595,11 @@ float BLEClientBase::parse_char_value(uint8_t *value, uint16_t length) { } } ESP_LOGW(TAG, "[%d] [%s] Cannot parse characteristic value of type 0x%x length %d", this->connection_index_, - this->address_str_.c_str(), value[0], length); + this->address_str_, value[0], length); return NAN; } +#ifdef USE_ESP32_BLE_DEVICE BLEService *BLEClientBase::get_service(espbt::ESPBTUUID uuid) { for (auto *svc : this->services_) { if (svc->uuid == uuid) @@ -679,8 +676,8 @@ BLEDescriptor *BLEClientBase::get_descriptor(uint16_t handle) { } return nullptr; } +#endif // USE_ESP32_BLE_DEVICE -} // namespace esp32_ble_client -} // namespace esphome +} // namespace esphome::esp32_ble_client #endif // USE_ESP32 diff --git a/esphome/components/esp32_ble_client/ble_client_base.h b/esphome/components/esp32_ble_client/ble_client_base.h index 0a2fda4476..7786495915 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.h +++ b/esphome/components/esp32_ble_client/ble_client_base.h @@ -5,10 +5,11 @@ #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" #include "esphome/core/component.h" +#ifdef USE_ESP32_BLE_DEVICE #include "ble_service.h" +#endif #include -#include #include #include @@ -16,12 +17,12 @@ #include #include -namespace esphome { -namespace esp32_ble_client { +namespace esphome::esp32_ble_client { namespace espbt = esphome::esp32_ble_tracker; static const int UNSET_CONN_ID = 0xFFFF; +static constexpr size_t MAC_ADDR_STR_LEN = 18; // "AA:BB:CC:DD:EE:FF\0" class BLEClientBase : public espbt::ESPBTClient, public Component { public: @@ -57,17 +58,14 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { this->remote_bda_[4] = (address >> 8) & 0xFF; this->remote_bda_[5] = (address >> 0) & 0xFF; if (address == 0) { - this->address_str_ = ""; + this->address_str_[0] = '\0'; } else { - this->address_str_ = - str_snprintf("%02X:%02X:%02X:%02X:%02X:%02X", 17, (uint8_t) (this->address_ >> 40) & 0xff, - (uint8_t) (this->address_ >> 32) & 0xff, (uint8_t) (this->address_ >> 24) & 0xff, - (uint8_t) (this->address_ >> 16) & 0xff, (uint8_t) (this->address_ >> 8) & 0xff, - (uint8_t) (this->address_ >> 0) & 0xff); + format_mac_addr_upper(this->remote_bda_, this->address_str_); } } - std::string address_str() const { return this->address_str_; } + const char *address_str() const { return this->address_str_; } +#ifdef USE_ESP32_BLE_DEVICE BLEService *get_service(espbt::ESPBTUUID uuid); BLEService *get_service(uint16_t uuid); BLECharacteristic *get_characteristic(espbt::ESPBTUUID service, espbt::ESPBTUUID chr); @@ -78,6 +76,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { BLEDescriptor *get_descriptor(uint16_t handle); // Get the configuration descriptor for the given characteristic handle. BLEDescriptor *get_config_descriptor(uint16_t handle); +#endif float parse_char_value(uint8_t *value, uint16_t length); @@ -103,15 +102,17 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { uint64_t address_{0}; // Group 2: Container types (grouped for memory optimization) - std::string address_str_{}; +#ifdef USE_ESP32_BLE_DEVICE std::vector services_; +#endif // Group 3: 4-byte types int gattc_if_; esp_gatt_status_t status_{ESP_GATT_OK}; - // Group 4: Arrays (6 bytes) - esp_bd_addr_t remote_bda_; + // Group 4: Arrays + char address_str_[MAC_ADDR_STR_LEN]{}; // 18 bytes: "AA:BB:CC:DD:EE:FF\0" + esp_bd_addr_t remote_bda_; // 6 bytes // Group 5: 2-byte types uint16_t conn_id_{UNSET_CONN_ID}; @@ -127,9 +128,21 @@ 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 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, + const char *param_type); + void log_gattc_warning_(const char *operation, esp_gatt_status_t status); + void log_gattc_warning_(const char *operation, esp_err_t err); + void log_connection_params_(const char *param_type); + void handle_connection_result_(esp_err_t ret); + // Compact error logging helpers to reduce flash usage + void log_error_(const char *message); + void log_error_(const char *message, int code); + void log_warning_(const char *message); }; -} // namespace esp32_ble_client -} // namespace esphome +} // namespace esphome::esp32_ble_client #endif // USE_ESP32 diff --git a/esphome/components/esp32_ble_client/ble_descriptor.h b/esphome/components/esp32_ble_client/ble_descriptor.h index c05430144f..fb2b78a7b1 100644 --- a/esphome/components/esp32_ble_client/ble_descriptor.h +++ b/esphome/components/esp32_ble_client/ble_descriptor.h @@ -1,11 +1,13 @@ #pragma once +#include "esphome/core/defines.h" + #ifdef USE_ESP32 +#ifdef USE_ESP32_BLE_DEVICE #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" -namespace esphome { -namespace esp32_ble_client { +namespace esphome::esp32_ble_client { namespace espbt = esphome::esp32_ble_tracker; @@ -19,7 +21,7 @@ class BLEDescriptor { BLECharacteristic *characteristic; }; -} // namespace esp32_ble_client -} // namespace esphome +} // namespace esphome::esp32_ble_client +#endif // USE_ESP32_BLE_DEVICE #endif // USE_ESP32 diff --git a/esphome/components/esp32_ble_client/ble_service.cpp b/esphome/components/esp32_ble_client/ble_service.cpp index b22d2a1788..deaaa3de02 100644 --- a/esphome/components/esp32_ble_client/ble_service.cpp +++ b/esphome/components/esp32_ble_client/ble_service.cpp @@ -4,9 +4,9 @@ #include "esphome/core/log.h" #ifdef USE_ESP32 +#ifdef USE_ESP32_BLE_DEVICE -namespace esphome { -namespace esp32_ble_client { +namespace esphome::esp32_ble_client { static const char *const TAG = "esp32_ble_client"; @@ -51,7 +51,7 @@ void BLEService::parse_characteristics() { } if (status != ESP_GATT_OK) { ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_get_all_char error, status=%d", this->client->get_connection_index(), - this->client->address_str().c_str(), status); + this->client->address_str(), status); break; } if (count == 0) { @@ -65,13 +65,13 @@ void BLEService::parse_characteristics() { characteristic->service = this; this->characteristics.push_back(characteristic); ESP_LOGV(TAG, "[%d] [%s] characteristic %s, handle 0x%x, properties 0x%x", this->client->get_connection_index(), - this->client->address_str().c_str(), characteristic->uuid.to_string().c_str(), characteristic->handle, + this->client->address_str(), characteristic->uuid.to_string().c_str(), characteristic->handle, characteristic->properties); offset++; } } -} // namespace esp32_ble_client -} // namespace esphome +} // namespace esphome::esp32_ble_client +#endif // USE_ESP32_BLE_DEVICE #endif // USE_ESP32 diff --git a/esphome/components/esp32_ble_client/ble_service.h b/esphome/components/esp32_ble_client/ble_service.h index 41fc3e838b..00ecc777e7 100644 --- a/esphome/components/esp32_ble_client/ble_service.h +++ b/esphome/components/esp32_ble_client/ble_service.h @@ -1,6 +1,9 @@ #pragma once +#include "esphome/core/defines.h" + #ifdef USE_ESP32 +#ifdef USE_ESP32_BLE_DEVICE #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" @@ -8,8 +11,7 @@ #include -namespace esphome { -namespace esp32_ble_client { +namespace esphome::esp32_ble_client { namespace espbt = esphome::esp32_ble_tracker; @@ -30,7 +32,7 @@ class BLEService { BLECharacteristic *get_characteristic(uint16_t uuid); }; -} // namespace esp32_ble_client -} // namespace esphome +} // namespace esphome::esp32_ble_client +#endif // USE_ESP32_BLE_DEVICE #endif // USE_ESP32 diff --git a/esphome/components/esp32_ble_server/__init__.py b/esphome/components/esp32_ble_server/__init__.py index 6f16d76a32..a7e2522fac 100644 --- a/esphome/components/esp32_ble_server/__init__.py +++ b/esphome/components/esp32_ble_server/__init__.py @@ -26,7 +26,7 @@ from esphome.const import ( from esphome.core import CORE from esphome.schema_extractors import SCHEMA_EXTRACT -AUTO_LOAD = ["esp32_ble", "bytebuffer", "event_emitter"] +AUTO_LOAD = ["esp32_ble", "bytebuffer"] CODEOWNERS = ["@jesserockz", "@clydebarrow", "@Rapsssito"] DEPENDENCIES = ["esp32"] DOMAIN = "esp32_ble_server" @@ -461,7 +461,9 @@ async def parse_value(value_config, args): if isinstance(value, str): value = list(value.encode(value_config[CONF_STRING_ENCODING])) if isinstance(value, list): - return cg.std_vector.template(cg.uint8)(value) + # Generate initializer list {1, 2, 3} instead of std::vector({1, 2, 3}) + # This calls the set_value(std::initializer_list) overload + return cg.ArrayInitializer(*value) val = cg.RawExpression(f"{value_config[CONF_TYPE]}({cg.safe_exp(value)})") return ByteBuffer_ns.wrap(val, value_config[CONF_ENDIANNESS]) @@ -488,6 +490,7 @@ async def to_code_descriptor(descriptor_conf, char_var): cg.add(desc_var.set_value(value)) if CONF_ON_WRITE in descriptor_conf: on_write_conf = descriptor_conf[CONF_ON_WRITE] + cg.add_define("USE_ESP32_BLE_SERVER_DESCRIPTOR_ON_WRITE") await automation.build_automation( BLETriggers_ns.create_descriptor_on_write_trigger(desc_var), [(cg.std_vector.template(cg.uint8), "x"), (cg.uint16, "id")], @@ -505,23 +508,32 @@ async def to_code_characteristic(service_var, char_conf): ) if CONF_ON_WRITE in char_conf: on_write_conf = char_conf[CONF_ON_WRITE] + cg.add_define("USE_ESP32_BLE_SERVER_CHARACTERISTIC_ON_WRITE") await automation.build_automation( BLETriggers_ns.create_characteristic_on_write_trigger(char_var), [(cg.std_vector.template(cg.uint8), "x"), (cg.uint16, "id")], on_write_conf, ) if CONF_VALUE in char_conf: - action_conf = { - CONF_ID: char_conf[CONF_ID], - CONF_VALUE: char_conf[CONF_VALUE], - } - value_action = await ble_server_characteristic_set_value( - action_conf, - char_conf[CONF_CHAR_VALUE_ACTION_ID_], - cg.TemplateArguments(), - {}, - ) - cg.add(value_action.play()) + # Check if the value is templated (Lambda) + value_data = char_conf[CONF_VALUE][CONF_DATA] + if isinstance(value_data, cv.Lambda): + # Templated value - need the full action infrastructure + action_conf = { + CONF_ID: char_conf[CONF_ID], + CONF_VALUE: char_conf[CONF_VALUE], + } + value_action = await ble_server_characteristic_set_value( + action_conf, + char_conf[CONF_CHAR_VALUE_ACTION_ID_], + cg.TemplateArguments(), + {}, + ) + cg.add(value_action.play()) + else: + # Static value - just set it directly without action infrastructure + value = await parse_value(char_conf[CONF_VALUE], {}) + cg.add(char_var.set_value(value)) for descriptor_conf in char_conf[CONF_DESCRIPTORS]: await to_code_descriptor(descriptor_conf, char_var) @@ -529,14 +541,15 @@ async def to_code_characteristic(service_var, char_conf): async def to_code(config): # Register the loggers this component needs esp32_ble.register_bt_logger(BTLoggers.GATT, BTLoggers.SMP) + cg.add_define("USE_ESP32_BLE_UUID") var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) parent = await cg.get_variable(config[esp32_ble.CONF_BLE_ID]) - cg.add(parent.register_gatts_event_handler(var)) - cg.add(parent.register_ble_status_event_handler(var)) + esp32_ble.register_gatts_event_handler(parent, var) + esp32_ble.register_ble_status_event_handler(parent, var) cg.add(var.set_parent(parent)) cg.add(parent.advertising_set_appearance(config[CONF_APPEARANCE])) if CONF_MANUFACTURER_DATA in config: @@ -559,20 +572,22 @@ async def to_code(config): else: cg.add(var.enqueue_start_service(service_var)) if CONF_ON_CONNECT in config: + cg.add_define("USE_ESP32_BLE_SERVER_ON_CONNECT") await automation.build_automation( BLETriggers_ns.create_server_on_connect_trigger(var), [(cg.uint16, "id")], config[CONF_ON_CONNECT], ) if CONF_ON_DISCONNECT in config: + cg.add_define("USE_ESP32_BLE_SERVER_ON_DISCONNECT") await automation.build_automation( BLETriggers_ns.create_server_on_disconnect_trigger(var), [(cg.uint16, "id")], config[CONF_ON_DISCONNECT], ) cg.add_define("USE_ESP32_BLE_SERVER") - if CORE.using_esp_idf: - add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True) + cg.add_define("USE_ESP32_BLE_ADVERTISING") + add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True) @automation.register_action( @@ -593,6 +608,7 @@ async def ble_server_characteristic_set_value(config, action_id, template_arg, a var = cg.new_Pvariable(action_id, template_arg, paren) value = await parse_value(config[CONF_VALUE], args) cg.add(var.set_buffer(value)) + cg.add_define("USE_ESP32_BLE_SERVER_SET_VALUE_ACTION") return var @@ -611,6 +627,7 @@ async def ble_server_descriptor_set_value(config, action_id, template_arg, args) var = cg.new_Pvariable(action_id, template_arg, paren) value = await parse_value(config[CONF_VALUE], args) cg.add(var.set_buffer(value)) + cg.add_define("USE_ESP32_BLE_SERVER_DESCRIPTOR_SET_VALUE_ACTION") return var @@ -628,4 +645,5 @@ async def ble_server_descriptor_set_value(config, action_id, template_arg, args) ) async def ble_server_characteristic_notify(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) + cg.add_define("USE_ESP32_BLE_SERVER_NOTIFY_ACTION") return cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/esp32_ble_server/ble_characteristic.cpp b/esphome/components/esp32_ble_server/ble_characteristic.cpp index 373d57436e..7627a58338 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.cpp +++ b/esphome/components/esp32_ble_server/ble_characteristic.cpp @@ -35,13 +35,18 @@ BLECharacteristic::BLECharacteristic(const ESPBTUUID uuid, uint32_t properties) void BLECharacteristic::set_value(ByteBuffer buffer) { this->set_value(buffer.get_data()); } -void BLECharacteristic::set_value(const std::vector &buffer) { +void BLECharacteristic::set_value(std::vector &&buffer) { xSemaphoreTake(this->set_value_lock_, 0L); - this->value_ = buffer; + this->value_ = std::move(buffer); xSemaphoreGive(this->set_value_lock_); } + +void BLECharacteristic::set_value(std::initializer_list data) { + this->set_value(std::vector(data)); // Delegate to move overload +} + void BLECharacteristic::set_value(const std::string &buffer) { - this->set_value(std::vector(buffer.begin(), buffer.end())); + this->set_value(std::vector(buffer.begin(), buffer.end())); // Delegate to move overload } void BLECharacteristic::notify() { @@ -49,13 +54,17 @@ void BLECharacteristic::notify() { this->service_->get_server()->get_connected_client_count() == 0) return; - for (auto &client : this->service_->get_server()->get_clients()) { + const uint16_t *clients = this->service_->get_server()->get_clients(); + uint8_t client_count = this->service_->get_server()->get_client_count(); + + for (uint8_t i = 0; i < client_count; i++) { + uint16_t client = clients[i]; size_t length = this->value_.size(); - // If the client is not in the list of clients to notify, skip it - if (this->clients_to_notify_.count(client) == 0) + // Find the client in the list of clients to notify + auto *entry = this->find_client_in_notify_list_(client); + if (entry == nullptr) continue; - // If the client is in the list of clients to notify, check if it requires an ack (i.e. INDICATE) - bool require_ack = this->clients_to_notify_[client]; + bool require_ack = entry->indicate; // TODO: Remove this block when INDICATE acknowledgment is supported if (require_ack) { ESP_LOGW(TAG, "INDICATE acknowledgment is not yet supported (i.e. it works as a NOTIFY)"); @@ -73,16 +82,17 @@ void BLECharacteristic::notify() { void BLECharacteristic::add_descriptor(BLEDescriptor *descriptor) { // If the descriptor is the CCCD descriptor, listen to its write event to know if the client wants to be notified if (descriptor->get_uuid() == ESPBTUUID::from_uint16(ESP_GATT_UUID_CHAR_CLIENT_CONFIG)) { - descriptor->on(BLEDescriptorEvt::VectorEvt::ON_WRITE, [this](const std::vector &value, uint16_t conn_id) { + descriptor->on_write([this](std::span value, uint16_t conn_id) { if (value.size() != 2) return; uint16_t cccd = encode_uint16(value[1], value[0]); bool notify = (cccd & 1) != 0; bool indicate = (cccd & 2) != 0; + // Remove existing entry if present + this->remove_client_from_notify_list_(conn_id); + // Add new entry if needed if (notify || indicate) { - this->clients_to_notify_[conn_id] = indicate; - } else { - this->clients_to_notify_.erase(conn_id); + this->clients_to_notify_.push_back({conn_id, indicate}); } }); } @@ -120,69 +130,49 @@ bool BLECharacteristic::is_created() { if (this->state_ != CREATING_DEPENDENTS) return false; - bool created = true; for (auto *descriptor : this->descriptors_) { - created &= descriptor->is_created(); + if (!descriptor->is_created()) + return false; } - if (created) - this->state_ = CREATED; - return this->state_ == CREATED; + // All descriptors are created if we reach here + this->state_ = CREATED; + return true; } bool BLECharacteristic::is_failed() { if (this->state_ == FAILED) return true; - bool failed = false; for (auto *descriptor : this->descriptors_) { - failed |= descriptor->is_failed(); + if (descriptor->is_failed()) { + this->state_ = FAILED; + return true; + } + } + return false; +} + +void BLECharacteristic::set_property_bit_(esp_gatt_char_prop_t bit, bool value) { + if (value) { + this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | bit); + } else { + this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~bit); } - if (failed) - this->state_ = FAILED; - return this->state_ == FAILED; } void BLECharacteristic::set_broadcast_property(bool value) { - if (value) { - this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_BROADCAST); - } else { - this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_BROADCAST); - } + this->set_property_bit_(ESP_GATT_CHAR_PROP_BIT_BROADCAST, value); } void BLECharacteristic::set_indicate_property(bool value) { - if (value) { - this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_INDICATE); - } else { - this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_INDICATE); - } + this->set_property_bit_(ESP_GATT_CHAR_PROP_BIT_INDICATE, value); } void BLECharacteristic::set_notify_property(bool value) { - if (value) { - this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_NOTIFY); - } else { - this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_NOTIFY); - } -} -void BLECharacteristic::set_read_property(bool value) { - if (value) { - this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_READ); - } else { - this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_READ); - } -} -void BLECharacteristic::set_write_property(bool value) { - if (value) { - this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_WRITE); - } else { - this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_WRITE); - } + this->set_property_bit_(ESP_GATT_CHAR_PROP_BIT_NOTIFY, value); } +void BLECharacteristic::set_read_property(bool value) { this->set_property_bit_(ESP_GATT_CHAR_PROP_BIT_READ, value); } +void BLECharacteristic::set_write_property(bool value) { this->set_property_bit_(ESP_GATT_CHAR_PROP_BIT_WRITE, value); } void BLECharacteristic::set_write_no_response_property(bool value) { - if (value) { - this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_WRITE_NR); - } else { - this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_WRITE_NR); - } + this->set_property_bit_(ESP_GATT_CHAR_PROP_BIT_WRITE_NR, value); } void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, @@ -207,8 +197,9 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt if (!param->read.need_rsp) break; // For some reason you can request a read but not want a response - this->EventEmitter::emit_(BLECharacteristicEvt::EmptyEvt::ON_READ, - param->read.conn_id); + if (this->on_read_callback_) { + (*this->on_read_callback_)(param->read.conn_id); + } uint16_t max_offset = 22; @@ -276,8 +267,9 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt } if (!param->write.is_prep) { - this->EventEmitter, uint16_t>::emit_( - BLECharacteristicEvt::VectorEvt::ON_WRITE, this->value_, param->write.conn_id); + if (this->on_write_callback_) { + (*this->on_write_callback_)(this->value_, param->write.conn_id); + } } break; @@ -288,8 +280,9 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt break; this->write_event_ = false; if (param->exec_write.exec_write_flag == ESP_GATT_PREP_WRITE_EXEC) { - this->EventEmitter, uint16_t>::emit_( - BLECharacteristicEvt::VectorEvt::ON_WRITE, this->value_, param->exec_write.conn_id); + if (this->on_write_callback_) { + (*this->on_write_callback_)(this->value_, param->exec_write.conn_id); + } } esp_err_t err = esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, nullptr); @@ -307,6 +300,28 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt } } +void BLECharacteristic::remove_client_from_notify_list_(uint16_t conn_id) { + // Since we typically have very few clients (often just 1), we can optimize + // for the common case by swapping with the last element and popping + for (size_t i = 0; i < this->clients_to_notify_.size(); i++) { + if (this->clients_to_notify_[i].conn_id == conn_id) { + // Swap with last element and pop (safe even when i is the last element) + this->clients_to_notify_[i] = this->clients_to_notify_.back(); + this->clients_to_notify_.pop_back(); + return; + } + } +} + +BLECharacteristic::ClientNotificationEntry *BLECharacteristic::find_client_in_notify_list_(uint16_t conn_id) { + for (auto &entry : this->clients_to_notify_) { + if (entry.conn_id == conn_id) { + return &entry; + } + } + return nullptr; +} + } // namespace esp32_ble_server } // namespace esphome diff --git a/esphome/components/esp32_ble_server/ble_characteristic.h b/esphome/components/esp32_ble_server/ble_characteristic.h index 3698b8c4aa..b913915789 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.h +++ b/esphome/components/esp32_ble_server/ble_characteristic.h @@ -2,11 +2,12 @@ #include "ble_descriptor.h" #include "esphome/components/esp32_ble/ble_uuid.h" -#include "esphome/components/event_emitter/event_emitter.h" #include "esphome/components/bytebuffer/bytebuffer.h" #include -#include +#include +#include +#include #ifdef USE_ESP32 @@ -23,28 +24,17 @@ namespace esp32_ble_server { using namespace esp32_ble; using namespace bytebuffer; -using namespace event_emitter; class BLEService; -namespace BLECharacteristicEvt { -enum VectorEvt { - ON_WRITE, -}; - -enum EmptyEvt { - ON_READ, -}; -} // namespace BLECharacteristicEvt - -class BLECharacteristic : public EventEmitter, uint16_t>, - public EventEmitter { +class BLECharacteristic { public: BLECharacteristic(ESPBTUUID uuid, uint32_t properties); ~BLECharacteristic(); void set_value(ByteBuffer buffer); - void set_value(const std::vector &buffer); + void set_value(std::vector &&buffer); + void set_value(std::initializer_list data); void set_value(const std::string &buffer); void set_broadcast_property(bool value); @@ -77,6 +67,15 @@ class BLECharacteristic : public EventEmitter, uint16_t)> &&callback) { + this->on_write_callback_ = + std::make_unique, uint16_t)>>(std::move(callback)); + } + void on_read(std::function &&callback) { + this->on_read_callback_ = std::make_unique>(std::move(callback)); + } + protected: bool write_event_{false}; BLEService *service_{}; @@ -89,7 +88,20 @@ class BLECharacteristic : public EventEmitter descriptors_; - std::unordered_map clients_to_notify_; + + struct ClientNotificationEntry { + uint16_t conn_id; + bool indicate; // true = indicate, false = notify + }; + std::vector clients_to_notify_; + + void remove_client_from_notify_list_(uint16_t conn_id); + ClientNotificationEntry *find_client_in_notify_list_(uint16_t conn_id); + + void set_property_bit_(esp_gatt_char_prop_t bit, bool value); + + std::unique_ptr, uint16_t)>> on_write_callback_; + std::unique_ptr> on_read_callback_; esp_gatt_perm_t permissions_ = ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE; diff --git a/esphome/components/esp32_ble_server/ble_descriptor.cpp b/esphome/components/esp32_ble_server/ble_descriptor.cpp index afbe579513..2d053c09bd 100644 --- a/esphome/components/esp32_ble_server/ble_descriptor.cpp +++ b/esphome/components/esp32_ble_server/ble_descriptor.cpp @@ -46,15 +46,17 @@ void BLEDescriptor::do_create(BLECharacteristic *characteristic) { this->state_ = CREATING; } -void BLEDescriptor::set_value(std::vector buffer) { - size_t length = buffer.size(); +void BLEDescriptor::set_value(std::vector &&buffer) { this->set_value_impl_(buffer.data(), buffer.size()); } +void BLEDescriptor::set_value(std::initializer_list data) { this->set_value_impl_(data.begin(), data.size()); } + +void BLEDescriptor::set_value_impl_(const uint8_t *data, size_t length) { if (length > this->value_.attr_max_len) { ESP_LOGE(TAG, "Size %d too large, must be no bigger than %d", length, this->value_.attr_max_len); return; } this->value_.attr_len = length; - memcpy(this->value_.attr_value, buffer.data(), length); + memcpy(this->value_.attr_value, data, length); } void BLEDescriptor::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, @@ -74,9 +76,10 @@ void BLEDescriptor::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_ break; this->value_.attr_len = param->write.len; memcpy(this->value_.attr_value, param->write.value, param->write.len); - this->emit_(BLEDescriptorEvt::VectorEvt::ON_WRITE, - std::vector(param->write.value, param->write.value + param->write.len), - param->write.conn_id); + if (this->on_write_callback_) { + (*this->on_write_callback_)(std::span(param->write.value, param->write.len), + param->write.conn_id); + } break; } default: diff --git a/esphome/components/esp32_ble_server/ble_descriptor.h b/esphome/components/esp32_ble_server/ble_descriptor.h index 8d3c22c5a1..5f4f146d6f 100644 --- a/esphome/components/esp32_ble_server/ble_descriptor.h +++ b/esphome/components/esp32_ble_server/ble_descriptor.h @@ -1,37 +1,34 @@ #pragma once #include "esphome/components/esp32_ble/ble_uuid.h" -#include "esphome/components/event_emitter/event_emitter.h" #include "esphome/components/bytebuffer/bytebuffer.h" #ifdef USE_ESP32 #include #include +#include +#include +#include namespace esphome { namespace esp32_ble_server { using namespace esp32_ble; using namespace bytebuffer; -using namespace event_emitter; class BLECharacteristic; -namespace BLEDescriptorEvt { -enum VectorEvt { - ON_WRITE, -}; -} // namespace BLEDescriptorEvt - -class BLEDescriptor : public EventEmitter, uint16_t> { +// Base class for BLE descriptors +class BLEDescriptor { public: BLEDescriptor(ESPBTUUID uuid, uint16_t max_len = 100, bool read = true, bool write = true); virtual ~BLEDescriptor(); void do_create(BLECharacteristic *characteristic); ESPBTUUID get_uuid() const { return this->uuid_; } - void set_value(std::vector buffer); + void set_value(std::vector &&buffer); + void set_value(std::initializer_list data); void set_value(ByteBuffer buffer) { this->set_value(buffer.get_data()); } void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); @@ -39,13 +36,23 @@ class BLEDescriptor : public EventEmitterstate_ == CREATED; } bool is_failed() { return this->state_ == FAILED; } + // Direct callback registration - only allocates when callback is set + void on_write(std::function, uint16_t)> &&callback) { + this->on_write_callback_ = + std::make_unique, uint16_t)>>(std::move(callback)); + } + protected: + void set_value_impl_(const uint8_t *data, size_t length); + BLECharacteristic *characteristic_{nullptr}; ESPBTUUID uuid_; uint16_t handle_{0xFFFF}; esp_attr_value_t value_{}; + std::unique_ptr, uint16_t)>> on_write_callback_; + esp_gatt_perm_t permissions_{}; enum State : uint8_t { diff --git a/esphome/components/esp32_ble_server/ble_server.cpp b/esphome/components/esp32_ble_server/ble_server.cpp index 5339bf8aed..0e58224a5a 100644 --- a/esphome/components/esp32_ble_server/ble_server.cpp +++ b/esphome/components/esp32_ble_server/ble_server.cpp @@ -10,7 +10,9 @@ #include #include #include +#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID #include +#endif #include #include @@ -70,11 +72,11 @@ void BLEServer::loop() { // it is at the top of the GATT table this->device_information_service_->do_create(this); // Create all services previously created - for (auto &pair : this->services_) { - if (pair.second == this->device_information_service_) { + for (auto &entry : this->services_) { + if (entry.service == this->device_information_service_) { continue; } - pair.second->do_create(this); + entry.service->do_create(this); } this->state_ = STARTING_SERVICE; } @@ -118,7 +120,7 @@ BLEService *BLEServer::create_service(ESPBTUUID uuid, bool advertise, uint16_t n } BLEService *service = // NOLINT(cppcoreguidelines-owning-memory) new BLEService(uuid, num_handles, inst_id, advertise); - this->services_.emplace(BLEServer::get_service_key(uuid, inst_id), service); + this->services_.push_back({uuid, inst_id, service}); if (this->parent_->is_active() && this->registered_) { service->do_create(this); } @@ -127,26 +129,32 @@ BLEService *BLEServer::create_service(ESPBTUUID uuid, bool advertise, uint16_t n void BLEServer::remove_service(ESPBTUUID uuid, uint8_t inst_id) { ESP_LOGV(TAG, "Removing BLE service - %s %d", uuid.to_string().c_str(), inst_id); - BLEService *service = this->get_service(uuid, inst_id); - if (service == nullptr) { - ESP_LOGW(TAG, "BLE service %s %d does not exist", uuid.to_string().c_str(), inst_id); - return; + for (auto it = this->services_.begin(); it != this->services_.end(); ++it) { + if (it->uuid == uuid && it->inst_id == inst_id) { + it->service->do_delete(); + delete it->service; // NOLINT(cppcoreguidelines-owning-memory) + this->services_.erase(it); + return; + } } - service->do_delete(); - delete service; // NOLINT(cppcoreguidelines-owning-memory) - this->services_.erase(BLEServer::get_service_key(uuid, inst_id)); + ESP_LOGW(TAG, "BLE service %s %d does not exist", uuid.to_string().c_str(), inst_id); } BLEService *BLEServer::get_service(ESPBTUUID uuid, uint8_t inst_id) { - BLEService *service = nullptr; - if (this->services_.count(BLEServer::get_service_key(uuid, inst_id)) > 0) { - service = this->services_.at(BLEServer::get_service_key(uuid, inst_id)); + for (auto &entry : this->services_) { + if (entry.uuid == uuid && entry.inst_id == inst_id) { + return entry.service; + } } - return service; + return nullptr; } -std::string BLEServer::get_service_key(ESPBTUUID uuid, uint8_t inst_id) { - return uuid.to_string() + std::to_string(inst_id); +void BLEServer::dispatch_callbacks_(CallbackType type, uint16_t conn_id) { + for (auto &entry : this->callbacks_) { + if (entry.type == type) { + entry.callback(conn_id); + } + } } void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, @@ -155,14 +163,14 @@ void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t ga case ESP_GATTS_CONNECT_EVT: { ESP_LOGD(TAG, "BLE Client connected"); this->add_client_(param->connect.conn_id); - this->emit_(BLEServerEvt::EmptyEvt::ON_CONNECT, param->connect.conn_id); + this->dispatch_callbacks_(CallbackType::ON_CONNECT, param->connect.conn_id); break; } case ESP_GATTS_DISCONNECT_EVT: { ESP_LOGD(TAG, "BLE Client disconnected"); this->remove_client_(param->disconnect.conn_id); this->parent_->advertising_start(); - this->emit_(BLEServerEvt::EmptyEvt::ON_DISCONNECT, param->disconnect.conn_id); + this->dispatch_callbacks_(CallbackType::ON_DISCONNECT, param->disconnect.conn_id); break; } case ESP_GATTS_REG_EVT: { @@ -174,17 +182,46 @@ void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t ga break; } - for (const auto &pair : this->services_) { - pair.second->gatts_event_handler(event, gatts_if, param); + for (auto &entry : this->services_) { + entry.service->gatts_event_handler(event, gatts_if, param); + } +} + +int8_t BLEServer::find_client_index_(uint16_t conn_id) const { + for (uint8_t i = 0; i < this->client_count_; i++) { + if (this->clients_[i] == conn_id) + return i; + } + return -1; +} + +void BLEServer::add_client_(uint16_t conn_id) { + // Check if already in list + if (this->find_client_index_(conn_id) >= 0) + return; + // Add if there's space + if (this->client_count_ < USE_ESP32_BLE_MAX_CONNECTIONS) { + this->clients_[this->client_count_++] = conn_id; + } else { + // This should never happen since max clients is known at compile time + ESP_LOGE(TAG, "Client array full"); + } +} + +void BLEServer::remove_client_(uint16_t conn_id) { + int8_t index = this->find_client_index_(conn_id); + if (index >= 0) { + // Replace with last element and decrement count (client order not preserved) + this->clients_[index] = this->clients_[--this->client_count_]; } } void BLEServer::ble_before_disabled_event_handler() { // Delete all clients - this->clients_.clear(); + this->client_count_ = 0; // Delete all services - for (auto &pair : this->services_) { - pair.second->do_delete(); + for (auto &entry : this->services_) { + entry.service->do_delete(); } this->registered_ = false; this->state_ = INIT; diff --git a/esphome/components/esp32_ble_server/ble_server.h b/esphome/components/esp32_ble_server/ble_server.h index 531b52d6b9..6fa86dd67f 100644 --- a/esphome/components/esp32_ble_server/ble_server.h +++ b/esphome/components/esp32_ble_server/ble_server.h @@ -12,7 +12,7 @@ #include #include #include -#include +#include #ifdef USE_ESP32 @@ -24,18 +24,7 @@ namespace esp32_ble_server { using namespace esp32_ble; using namespace bytebuffer; -namespace BLEServerEvt { -enum EmptyEvt { - ON_CONNECT, - ON_DISCONNECT, -}; -} // namespace BLEServerEvt - -class BLEServer : public Component, - public GATTsEventHandler, - public BLEStatusEventHandler, - public Parented, - public EventEmitter { +class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEventHandler, public Parented { public: void setup() override; void loop() override; @@ -57,27 +46,56 @@ class BLEServer : public Component, void set_device_information_service(BLEService *service) { this->device_information_service_ = service; } esp_gatt_if_t get_gatts_if() { return this->gatts_if_; } - uint32_t get_connected_client_count() { return this->clients_.size(); } - const std::unordered_set &get_clients() { return this->clients_; } + uint32_t get_connected_client_count() { return this->client_count_; } + const uint16_t *get_clients() const { return this->clients_; } + uint8_t get_client_count() const { return this->client_count_; } void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) override; void ble_before_disabled_event_handler() override; + // Direct callback registration - supports multiple callbacks + void on_connect(std::function &&callback) { + this->callbacks_.push_back({CallbackType::ON_CONNECT, std::move(callback)}); + } + void on_disconnect(std::function &&callback) { + this->callbacks_.push_back({CallbackType::ON_DISCONNECT, std::move(callback)}); + } + protected: - static std::string get_service_key(ESPBTUUID uuid, uint8_t inst_id); + enum class CallbackType : uint8_t { + ON_CONNECT, + ON_DISCONNECT, + }; + + struct CallbackEntry { + CallbackType type; + std::function callback; + }; + + struct ServiceEntry { + ESPBTUUID uuid; + uint8_t inst_id; + BLEService *service; + }; + void restart_advertising_(); - void add_client_(uint16_t conn_id) { this->clients_.insert(conn_id); } - void remove_client_(uint16_t conn_id) { this->clients_.erase(conn_id); } + int8_t find_client_index_(uint16_t conn_id) const; + void add_client_(uint16_t conn_id); + void remove_client_(uint16_t conn_id); + void dispatch_callbacks_(CallbackType type, uint16_t conn_id); + + std::vector callbacks_; std::vector manufacturer_data_{}; esp_gatt_if_t gatts_if_{0}; bool registered_{false}; - std::unordered_set clients_; - std::unordered_map services_{}; + uint16_t clients_[USE_ESP32_BLE_MAX_CONNECTIONS]{}; + uint8_t client_count_{0}; + std::vector services_{}; std::vector services_to_start_{}; BLEService *device_information_service_{}; diff --git a/esphome/components/esp32_ble_server/ble_server_automations.cpp b/esphome/components/esp32_ble_server/ble_server_automations.cpp index 41ef2b8bfe..0761de994a 100644 --- a/esphome/components/esp32_ble_server/ble_server_automations.cpp +++ b/esphome/components/esp32_ble_server/ble_server_automations.cpp @@ -9,67 +9,83 @@ namespace esp32_ble_server_automations { using namespace esp32_ble; +#ifdef USE_ESP32_BLE_SERVER_CHARACTERISTIC_ON_WRITE Trigger, uint16_t> *BLETriggers::create_characteristic_on_write_trigger( BLECharacteristic *characteristic) { Trigger, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory) new Trigger, uint16_t>(); - characteristic->EventEmitter, uint16_t>::on( - BLECharacteristicEvt::VectorEvt::ON_WRITE, - [on_write_trigger](const std::vector &data, uint16_t id) { on_write_trigger->trigger(data, id); }); + characteristic->on_write([on_write_trigger](std::span data, uint16_t id) { + // Convert span to vector for trigger + on_write_trigger->trigger(std::vector(data.begin(), data.end()), id); + }); return on_write_trigger; } +#endif +#ifdef USE_ESP32_BLE_SERVER_DESCRIPTOR_ON_WRITE Trigger, uint16_t> *BLETriggers::create_descriptor_on_write_trigger(BLEDescriptor *descriptor) { Trigger, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory) new Trigger, uint16_t>(); - descriptor->on( - BLEDescriptorEvt::VectorEvt::ON_WRITE, - [on_write_trigger](const std::vector &data, uint16_t id) { on_write_trigger->trigger(data, id); }); + descriptor->on_write([on_write_trigger](std::span data, uint16_t id) { + // Convert span to vector for trigger + on_write_trigger->trigger(std::vector(data.begin(), data.end()), id); + }); return on_write_trigger; } +#endif +#ifdef USE_ESP32_BLE_SERVER_ON_CONNECT Trigger *BLETriggers::create_server_on_connect_trigger(BLEServer *server) { Trigger *on_connect_trigger = new Trigger(); // NOLINT(cppcoreguidelines-owning-memory) - server->on(BLEServerEvt::EmptyEvt::ON_CONNECT, - [on_connect_trigger](uint16_t conn_id) { on_connect_trigger->trigger(conn_id); }); + server->on_connect([on_connect_trigger](uint16_t conn_id) { on_connect_trigger->trigger(conn_id); }); return on_connect_trigger; } +#endif +#ifdef USE_ESP32_BLE_SERVER_ON_DISCONNECT Trigger *BLETriggers::create_server_on_disconnect_trigger(BLEServer *server) { Trigger *on_disconnect_trigger = new Trigger(); // NOLINT(cppcoreguidelines-owning-memory) - server->on(BLEServerEvt::EmptyEvt::ON_DISCONNECT, - [on_disconnect_trigger](uint16_t conn_id) { on_disconnect_trigger->trigger(conn_id); }); + server->on_disconnect([on_disconnect_trigger](uint16_t conn_id) { on_disconnect_trigger->trigger(conn_id); }); return on_disconnect_trigger; } +#endif +#ifdef USE_ESP32_BLE_SERVER_SET_VALUE_ACTION void BLECharacteristicSetValueActionManager::set_listener(BLECharacteristic *characteristic, - EventEmitterListenerID listener_id, const std::function &pre_notify_listener) { - // Check if there is already a listener for this characteristic - if (this->listeners_.count(characteristic) > 0) { - // Unpack the pair listener_id, pre_notify_listener_id - auto listener_pairs = this->listeners_[characteristic]; - EventEmitterListenerID old_listener_id = listener_pairs.first; - EventEmitterListenerID old_pre_notify_listener_id = listener_pairs.second; - // Remove the previous listener - characteristic->EventEmitter::off(BLECharacteristicEvt::EmptyEvt::ON_READ, - old_listener_id); - // Remove the pre-notify listener - this->off(BLECharacteristicSetValueActionEvt::PRE_NOTIFY, old_pre_notify_listener_id); + // Find and remove existing listener for this characteristic + auto *existing = this->find_listener_(characteristic); + if (existing != nullptr) { + // Remove from vector + this->remove_listener_(characteristic); } - // Create a new listener for the pre-notify event - EventEmitterListenerID pre_notify_listener_id = - this->on(BLECharacteristicSetValueActionEvt::PRE_NOTIFY, - [pre_notify_listener, characteristic](const BLECharacteristic *evt_characteristic) { - // Only call the pre-notify listener if the characteristic is the one we are interested in - if (characteristic == evt_characteristic) { - pre_notify_listener(); - } - }); - // Save the pair listener_id, pre_notify_listener_id to the map - this->listeners_[characteristic] = std::make_pair(listener_id, pre_notify_listener_id); + // Save the entry to the vector + this->listeners_.push_back({characteristic, pre_notify_listener}); } +BLECharacteristicSetValueActionManager::ListenerEntry *BLECharacteristicSetValueActionManager::find_listener_( + BLECharacteristic *characteristic) { + for (auto &entry : this->listeners_) { + if (entry.characteristic == characteristic) { + return &entry; + } + } + return nullptr; +} + +void BLECharacteristicSetValueActionManager::remove_listener_(BLECharacteristic *characteristic) { + // Since we typically have very few listeners, optimize by swapping with back and popping + for (size_t i = 0; i < this->listeners_.size(); i++) { + if (this->listeners_[i].characteristic == characteristic) { + // Swap with last element and pop (safe even when i is the last element) + this->listeners_[i] = this->listeners_.back(); + this->listeners_.pop_back(); + return; + } + } +} +#endif + } // namespace esp32_ble_server_automations } // namespace esp32_ble_server } // namespace esphome diff --git a/esphome/components/esp32_ble_server/ble_server_automations.h b/esphome/components/esp32_ble_server/ble_server_automations.h index eab6b05f05..fe18600280 100644 --- a/esphome/components/esp32_ble_server/ble_server_automations.h +++ b/esphome/components/esp32_ble_server/ble_server_automations.h @@ -4,11 +4,9 @@ #include "ble_characteristic.h" #include "ble_descriptor.h" -#include "esphome/components/event_emitter/event_emitter.h" #include "esphome/core/automation.h" #include -#include #include #ifdef USE_ESP32 @@ -19,41 +17,53 @@ namespace esp32_ble_server { namespace esp32_ble_server_automations { using namespace esp32_ble; -using namespace event_emitter; class BLETriggers { public: +#ifdef USE_ESP32_BLE_SERVER_CHARACTERISTIC_ON_WRITE static Trigger, uint16_t> *create_characteristic_on_write_trigger( BLECharacteristic *characteristic); +#endif +#ifdef USE_ESP32_BLE_SERVER_DESCRIPTOR_ON_WRITE static Trigger, uint16_t> *create_descriptor_on_write_trigger(BLEDescriptor *descriptor); +#endif +#ifdef USE_ESP32_BLE_SERVER_ON_CONNECT static Trigger *create_server_on_connect_trigger(BLEServer *server); +#endif +#ifdef USE_ESP32_BLE_SERVER_ON_DISCONNECT static Trigger *create_server_on_disconnect_trigger(BLEServer *server); +#endif }; -enum BLECharacteristicSetValueActionEvt { - PRE_NOTIFY, -}; - +#ifdef USE_ESP32_BLE_SERVER_SET_VALUE_ACTION // Class to make sure only one BLECharacteristicSetValueAction is active at a time for each characteristic -class BLECharacteristicSetValueActionManager - : public EventEmitter { +class BLECharacteristicSetValueActionManager { public: // Singleton pattern static BLECharacteristicSetValueActionManager *get_instance() { static BLECharacteristicSetValueActionManager instance; return &instance; } - void set_listener(BLECharacteristic *characteristic, EventEmitterListenerID listener_id, - const std::function &pre_notify_listener); - EventEmitterListenerID get_listener(BLECharacteristic *characteristic) { - return this->listeners_[characteristic].first; - } + void set_listener(BLECharacteristic *characteristic, const std::function &pre_notify_listener); + bool has_listener(BLECharacteristic *characteristic) { return this->find_listener_(characteristic) != nullptr; } void emit_pre_notify(BLECharacteristic *characteristic) { - this->emit_(BLECharacteristicSetValueActionEvt::PRE_NOTIFY, characteristic); + for (const auto &entry : this->listeners_) { + if (entry.characteristic == characteristic) { + entry.pre_notify_listener(); + break; + } + } } private: - std::unordered_map> listeners_; + struct ListenerEntry { + BLECharacteristic *characteristic; + std::function pre_notify_listener; + }; + std::vector listeners_; + + ListenerEntry *find_listener_(BLECharacteristic *characteristic); + void remove_listener_(BLECharacteristic *characteristic); }; template class BLECharacteristicSetValueAction : public Action { @@ -61,34 +71,36 @@ template class BLECharacteristicSetValueAction : public Action, buffer) void set_buffer(ByteBuffer buffer) { this->set_buffer(buffer.get_data()); } - void play(Ts... x) override { + void play(const Ts &...x) override { // If the listener is already set, do nothing - if (BLECharacteristicSetValueActionManager::get_instance()->get_listener(this->parent_) == this->listener_id_) + if (BLECharacteristicSetValueActionManager::get_instance()->has_listener(this->parent_)) return; // Set initial value this->parent_->set_value(this->buffer_.value(x...)); // Set the listener for read events - this->listener_id_ = this->parent_->EventEmitter::on( - BLECharacteristicEvt::EmptyEvt::ON_READ, [this, x...](uint16_t id) { - // Set the value of the characteristic every time it is read - this->parent_->set_value(this->buffer_.value(x...)); - }); + this->parent_->on_read([this, x...](uint16_t id) { + // Set the value of the characteristic every time it is read + this->parent_->set_value(this->buffer_.value(x...)); + }); // Set the listener in the global manager so only one BLECharacteristicSetValueAction is set for each characteristic BLECharacteristicSetValueActionManager::get_instance()->set_listener( - this->parent_, this->listener_id_, [this, x...]() { this->parent_->set_value(this->buffer_.value(x...)); }); + this->parent_, [this, x...]() { this->parent_->set_value(this->buffer_.value(x...)); }); } protected: BLECharacteristic *parent_; - EventEmitterListenerID listener_id_; }; +#endif // USE_ESP32_BLE_SERVER_SET_VALUE_ACTION +#ifdef USE_ESP32_BLE_SERVER_NOTIFY_ACTION template class BLECharacteristicNotifyAction : public Action { public: BLECharacteristicNotifyAction(BLECharacteristic *characteristic) : parent_(characteristic) {} - void play(Ts... x) override { + void play(const Ts &...x) override { +#ifdef USE_ESP32_BLE_SERVER_SET_VALUE_ACTION // Call the pre-notify event BLECharacteristicSetValueActionManager::get_instance()->emit_pre_notify(this->parent_); +#endif // Notify the characteristic this->parent_->notify(); } @@ -96,17 +108,20 @@ template class BLECharacteristicNotifyAction : public Action class BLEDescriptorSetValueAction : public Action { public: BLEDescriptorSetValueAction(BLEDescriptor *descriptor) : parent_(descriptor) {} TEMPLATABLE_VALUE(std::vector, buffer) void set_buffer(ByteBuffer buffer) { this->set_buffer(buffer.get_data()); } - void play(Ts... x) override { this->parent_->set_value(this->buffer_.value(x...)); } + void play(const Ts &...x) override { this->parent_->set_value(this->buffer_.value(x...)); } protected: BLEDescriptor *parent_; }; +#endif // USE_ESP32_BLE_SERVER_DESCRIPTOR_SET_VALUE_ACTION } // namespace esp32_ble_server_automations } // namespace esp32_ble_server diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 9daa6ee34e..4e25434aad 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -1,14 +1,14 @@ from __future__ import annotations -from collections.abc import Callable, MutableMapping +from dataclasses import dataclass import logging -from typing import Any from esphome import automation import esphome.codegen as cg from esphome.components import esp32_ble from esphome.components.esp32 import add_idf_sdkconfig_option from esphome.components.esp32_ble import ( + IDF_MAX_CONNECTIONS, BTLoggers, bt_uuid, bt_uuid16_format, @@ -24,32 +24,27 @@ from esphome.const import ( CONF_INTERVAL, CONF_MAC_ADDRESS, CONF_MANUFACTURER_ID, + CONF_MAX_CONNECTIONS, CONF_ON_BLE_ADVERTISE, CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE, CONF_ON_BLE_SERVICE_DATA_ADVERTISE, CONF_SERVICE_UUID, CONF_TRIGGER_ID, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.enum import StrEnum from esphome.types import ConfigType AUTO_LOAD = ["esp32_ble"] DEPENDENCIES = ["esp32"] +CODEOWNERS = ["@bdraco"] -KEY_ESP32_BLE_TRACKER = "esp32_ble_tracker" -KEY_USED_CONNECTION_SLOTS = "used_connection_slots" - -CONF_MAX_CONNECTIONS = "max_connections" CONF_ESP32_BLE_ID = "esp32_ble_id" CONF_SCAN_PARAMETERS = "scan_parameters" CONF_WINDOW = "window" CONF_ON_SCAN_END = "on_scan_end" CONF_SOFTWARE_COEXISTENCE = "software_coexistence" -DEFAULT_MAX_CONNECTIONS = 3 -IDF_MAX_CONNECTIONS = 9 - _LOGGER = logging.getLogger(__name__) @@ -58,8 +53,28 @@ class BLEFeatures(StrEnum): ESP_BT_DEVICE = "ESP_BT_DEVICE" -# Set to track which features are needed by components -_required_features: set[BLEFeatures] = set() +# Dataclass for registration counts +@dataclass +class RegistrationCounts: + listeners: int = 0 + clients: int = 0 + + +# CORE.data keys for state management +ESP32_BLE_TRACKER_REQUIRED_FEATURES_KEY = "esp32_ble_tracker_required_features" +ESP32_BLE_TRACKER_REGISTRATION_COUNTS_KEY = "esp32_ble_tracker_registration_counts" + + +def _get_required_features() -> set[BLEFeatures]: + """Get the set of required BLE features from CORE.data.""" + return CORE.data.setdefault(ESP32_BLE_TRACKER_REQUIRED_FEATURES_KEY, set()) + + +def _get_registration_counts() -> RegistrationCounts: + """Get the registration counts from CORE.data.""" + return CORE.data.setdefault( + ESP32_BLE_TRACKER_REGISTRATION_COUNTS_KEY, RegistrationCounts() + ) def register_ble_features(features: set[BLEFeatures]) -> None: @@ -68,7 +83,7 @@ def register_ble_features(features: set[BLEFeatures]) -> None: Args: features: Set of BLEFeatures enum members """ - _required_features.update(features) + _get_required_features().update(features) esp32_ble_tracker_ns = cg.esphome_ns.namespace("esp32_ble_tracker") @@ -127,6 +142,15 @@ def validate_scan_parameters(config): return config +def validate_max_connections_deprecated(config: ConfigType) -> ConfigType: + if CONF_MAX_CONNECTIONS in config: + _LOGGER.warning( + "The 'max_connections' option in 'esp32_ble_tracker' is deprecated. " + "Please move it to the 'esp32_ble' component instead." + ) + return config + + def as_hex(value): return cg.RawExpression(f"0x{value}ULL") @@ -149,29 +173,13 @@ def as_reversed_hex_array(value): ) -def max_connections() -> int: - return IDF_MAX_CONNECTIONS if CORE.using_esp_idf else DEFAULT_MAX_CONNECTIONS - - -def consume_connection_slots( - value: int, consumer: str -) -> Callable[[MutableMapping], MutableMapping]: - def _consume_connection_slots(config: MutableMapping) -> MutableMapping: - data: dict[str, Any] = CORE.data.setdefault(KEY_ESP32_BLE_TRACKER, {}) - slots: list[str] = data.setdefault(KEY_USED_CONNECTION_SLOTS, []) - slots.extend([consumer] * value) - return config - - return _consume_connection_slots - - CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(ESP32BLETracker), cv.GenerateID(esp32_ble.CONF_BLE_ID): cv.use_id(esp32_ble.ESP32BLE), - cv.Optional(CONF_MAX_CONNECTIONS, default=DEFAULT_MAX_CONNECTIONS): cv.All( - cv.positive_int, cv.Range(min=0, max=max_connections()) + cv.Optional(CONF_MAX_CONNECTIONS): cv.All( + cv.positive_int, cv.Range(min=0, max=IDF_MAX_CONNECTIONS) ), cv.Optional(CONF_SCAN_PARAMETERS, default={}): cv.All( cv.Schema( @@ -227,49 +235,11 @@ CONFIG_SCHEMA = cv.All( cv.OnlyWith(CONF_SOFTWARE_COEXISTENCE, "wifi", default=True): bool, } ).extend(cv.COMPONENT_SCHEMA), + validate_max_connections_deprecated, ) -def validate_remaining_connections(config): - data: dict[str, Any] = CORE.data.get(KEY_ESP32_BLE_TRACKER, {}) - slots: list[str] = data.get(KEY_USED_CONNECTION_SLOTS, []) - used_slots = len(slots) - if used_slots <= config[CONF_MAX_CONNECTIONS]: - return config - slot_users = ", ".join(slots) - hard_limit = max_connections() - - if used_slots < hard_limit: - _LOGGER.warning( - "esp32_ble_tracker exceeded `%s`: components attempted to consume %d " - "connection slot(s) out of available configured maximum %d connection " - "slot(s); The system automatically increased `%s` to %d to match the " - "number of used connection slot(s) by components: %s.", - CONF_MAX_CONNECTIONS, - used_slots, - config[CONF_MAX_CONNECTIONS], - CONF_MAX_CONNECTIONS, - used_slots, - slot_users, - ) - config[CONF_MAX_CONNECTIONS] = used_slots - return config - - msg = ( - f"esp32_ble_tracker exceeded `{CONF_MAX_CONNECTIONS}`: " - f"components attempted to consume {used_slots} connection slot(s) " - f"out of available configured maximum {config[CONF_MAX_CONNECTIONS]} " - f"connection slot(s); Decrease the number of BLE clients ({slot_users})" - ) - if config[CONF_MAX_CONNECTIONS] < hard_limit: - msg += f" or increase {CONF_MAX_CONNECTIONS}` to {used_slots}" - msg += f" to stay under the {hard_limit} connection slot(s) limit." - raise cv.Invalid(msg) - - -FINAL_VALIDATE_SCHEMA = cv.All( - validate_remaining_connections, esp32_ble.validate_variant -) +FINAL_VALIDATE_SCHEMA = esp32_ble.validate_variant ESP_BLE_DEVICE_SCHEMA = cv.Schema( { @@ -286,10 +256,10 @@ async def to_code(config): await cg.register_component(var, config) parent = await cg.get_variable(config[esp32_ble.CONF_BLE_ID]) - cg.add(parent.register_gap_event_handler(var)) - cg.add(parent.register_gap_scan_event_handler(var)) - cg.add(parent.register_gattc_event_handler(var)) - cg.add(parent.register_ble_status_event_handler(var)) + esp32_ble.register_gap_event_handler(parent, var) + esp32_ble.register_gap_scan_event_handler(parent, var) + esp32_ble.register_gattc_event_handler(parent, var) + esp32_ble.register_ble_status_event_handler(parent, var) cg.add(var.set_parent(parent)) params = config[CONF_SCAN_PARAMETERS] @@ -307,13 +277,17 @@ async def to_code(config): ): register_ble_features({BLEFeatures.ESP_BT_DEVICE}) + registration_counts = _get_registration_counts() + for conf in config.get(CONF_ON_BLE_ADVERTISE, []): + registration_counts.listeners += 1 trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) if CONF_MAC_ADDRESS in conf: addr_list = [it.as_hex for it in conf[CONF_MAC_ADDRESS]] cg.add(trigger.set_addresses(addr_list)) await automation.build_automation(trigger, [(ESPBTDeviceConstRef, "x")], conf) for conf in config.get(CONF_ON_BLE_SERVICE_DATA_ADVERTISE, []): + registration_counts.listeners += 1 trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) if len(conf[CONF_SERVICE_UUID]) == len(bt_uuid16_format): cg.add(trigger.set_service_uuid16(as_hex(conf[CONF_SERVICE_UUID]))) @@ -326,6 +300,7 @@ async def to_code(config): cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex)) await automation.build_automation(trigger, [(adv_data_t_const_ref, "x")], conf) for conf in config.get(CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE, []): + registration_counts.listeners += 1 trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) if len(conf[CONF_MANUFACTURER_ID]) == len(bt_uuid16_format): cg.add(trigger.set_manufacturer_uuid16(as_hex(conf[CONF_MANUFACTURER_ID]))) @@ -338,27 +313,20 @@ async def to_code(config): cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex)) await automation.build_automation(trigger, [(adv_data_t_const_ref, "x")], conf) for conf in config.get(CONF_ON_SCAN_END, []): + registration_counts.listeners += 1 trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) - if CORE.using_esp_idf: - add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True) - if config.get(CONF_SOFTWARE_COEXISTENCE): - add_idf_sdkconfig_option("CONFIG_SW_COEXIST_ENABLE", True) - # https://github.com/espressif/esp-idf/issues/4101 - # https://github.com/espressif/esp-idf/issues/2503 - # Match arduino CONFIG_BTU_TASK_STACK_SIZE - # https://github.com/espressif/arduino-esp32/blob/fd72cf46ad6fc1a6de99c1d83ba8eba17d80a4ee/tools/sdk/esp32/sdkconfig#L1866 - add_idf_sdkconfig_option("CONFIG_BT_BTU_TASK_STACK_SIZE", 8192) - add_idf_sdkconfig_option("CONFIG_BT_ACL_CONNECTIONS", 9) - add_idf_sdkconfig_option( - "CONFIG_BTDM_CTRL_BLE_MAX_CONN", config[CONF_MAX_CONNECTIONS] - ) - # CONFIG_BT_GATTC_NOTIF_REG_MAX controls the number of - # max notifications in 5.x, setting CONFIG_BT_ACL_CONNECTIONS - # is enough in 4.x - # https://github.com/esphome/issues/issues/6808 - add_idf_sdkconfig_option("CONFIG_BT_GATTC_NOTIF_REG_MAX", 9) + add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True) + if config.get(CONF_SOFTWARE_COEXISTENCE): + add_idf_sdkconfig_option("CONFIG_SW_COEXIST_ENABLE", True) + # https://github.com/espressif/esp-idf/issues/4101 + # https://github.com/espressif/esp-idf/issues/2503 + # Match arduino CONFIG_BTU_TASK_STACK_SIZE + # https://github.com/espressif/arduino-esp32/blob/fd72cf46ad6fc1a6de99c1d83ba8eba17d80a4ee/tools/sdk/esp32/sdkconfig#L1866 + add_idf_sdkconfig_option("CONFIG_BT_BTU_TASK_STACK_SIZE", 8192) + # Note: CONFIG_BT_ACL_CONNECTIONS and CONFIG_BTDM_CTRL_BLE_MAX_CONN are now + # configured in esp32_ble component based on max_connections setting cg.add_define("USE_OTA_STATE_CALLBACK") # To be notified when an OTA update starts cg.add_define("USE_ESP32_BLE_CLIENT") @@ -372,11 +340,25 @@ async def to_code(config): # This needs to be run as a job with very low priority so that all components have # chance to call register_ble_tracker and register_client before the list is checked # and added to the global defines list. -@coroutine_with_priority(-1000) +@coroutine_with_priority(CoroPriority.FINAL) async def _add_ble_features(): # Add feature-specific defines based on what's needed - if BLEFeatures.ESP_BT_DEVICE in _required_features: + required_features = _get_required_features() + if BLEFeatures.ESP_BT_DEVICE in required_features: cg.add_define("USE_ESP32_BLE_DEVICE") + cg.add_define("USE_ESP32_BLE_UUID") + + # Add defines for StaticVector sizing based on registration counts + # Only define if count > 0 to avoid allocating unnecessary memory + registration_counts = _get_registration_counts() + if registration_counts.listeners > 0: + cg.add_define( + "ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT", registration_counts.listeners + ) + if registration_counts.clients > 0: + cg.add_define( + "ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT", registration_counts.clients + ) ESP32_BLE_START_SCAN_ACTION_SCHEMA = cv.Schema( @@ -427,6 +409,7 @@ async def register_ble_device( var: cg.SafeExpType, config: ConfigType ) -> cg.SafeExpType: register_ble_features({BLEFeatures.ESP_BT_DEVICE}) + _get_registration_counts().listeners += 1 paren = await cg.get_variable(config[CONF_ESP32_BLE_ID]) cg.add(paren.register_listener(var)) return var @@ -434,6 +417,7 @@ async def register_ble_device( async def register_client(var: cg.SafeExpType, config: ConfigType) -> cg.SafeExpType: register_ble_features({BLEFeatures.ESP_BT_DEVICE}) + _get_registration_counts().clients += 1 paren = await cg.get_variable(config[CONF_ESP32_BLE_ID]) cg.add(paren.register_client(var)) return var @@ -447,6 +431,7 @@ async def register_raw_ble_device( This does NOT register the ESP_BT_DEVICE feature, meaning ESPBTDevice will not be compiled in if this is the only registration method used. """ + _get_registration_counts().listeners += 1 paren = await cg.get_variable(config[CONF_ESP32_BLE_ID]) cg.add(paren.register_listener(var)) return var @@ -460,6 +445,7 @@ async def register_raw_client( This does NOT register the ESP_BT_DEVICE feature, meaning ESPBTDevice will not be compiled in if this is the only registration method used. """ + _get_registration_counts().clients += 1 paren = await cg.get_variable(config[CONF_ESP32_BLE_ID]) cg.add(paren.register_client(var)) return var diff --git a/esphome/components/esp32_ble_tracker/automation.h b/esphome/components/esp32_ble_tracker/automation.h index c0e6eee138..bbf7992fa4 100644 --- a/esphome/components/esp32_ble_tracker/automation.h +++ b/esphome/components/esp32_ble_tracker/automation.h @@ -10,7 +10,7 @@ namespace esphome::esp32_ble_tracker { class ESPBTAdvertiseTrigger : public Trigger, public ESPBTDeviceListener { public: explicit ESPBTAdvertiseTrigger(ESP32BLETracker *parent) { parent->register_listener(this); } - void set_addresses(const std::vector &addresses) { this->address_vec_ = addresses; } + void set_addresses(std::initializer_list addresses) { this->address_vec_ = addresses; } bool parse_device(const ESPBTDevice &device) override { uint64_t u64_addr = device.address_uint64(); @@ -80,20 +80,23 @@ class BLEManufacturerDataAdvertiseTrigger : public Trigger, ESPBTUUID uuid_; }; +#endif // USE_ESP32_BLE_DEVICE + class BLEEndOfScanTrigger : public Trigger<>, public ESPBTDeviceListener { public: explicit BLEEndOfScanTrigger(ESP32BLETracker *parent) { parent->register_listener(this); } +#ifdef USE_ESP32_BLE_DEVICE bool parse_device(const ESPBTDevice &device) override { return false; } +#endif void on_scan_end() override { this->trigger(); } }; -#endif // USE_ESP32_BLE_DEVICE template class ESP32BLEStartScanAction : public Action { public: ESP32BLEStartScanAction(ESP32BLETracker *parent) : parent_(parent) {} TEMPLATABLE_VALUE(bool, continuous) - void play(Ts... x) override { + void play(const Ts &...x) override { this->parent_->set_scan_continuous(this->continuous_.value(x...)); this->parent_->start_scan(); } @@ -104,7 +107,7 @@ template class ESP32BLEStartScanAction : public Action { template class ESP32BLEStopScanAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->stop_scan(); } + void play(const Ts &...x) override { this->parent_->stop_scan(); } }; } // namespace esphome::esp32_ble_tracker diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 254eddd1d9..8577f12a92 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -7,7 +7,9 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID #include +#endif #include #include #include @@ -25,10 +27,6 @@ #include #endif -#ifdef USE_ARDUINO -#include -#endif - #define MBEDTLS_AES_ALT #include @@ -41,6 +39,27 @@ static const char *const TAG = "esp32_ble_tracker"; ESP32BLETracker *global_esp32_ble_tracker = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +const char *client_state_to_string(ClientState state) { + switch (state) { + case ClientState::INIT: + return "INIT"; + case ClientState::DISCONNECTING: + return "DISCONNECTING"; + case ClientState::IDLE: + return "IDLE"; + case ClientState::DISCOVERED: + return "DISCOVERED"; + case ClientState::CONNECTING: + return "CONNECTING"; + case ClientState::CONNECTED: + return "CONNECTED"; + case ClientState::ESTABLISHED: + return "ESTABLISHED"; + default: + return "UNKNOWN"; + } +} + float ESP32BLETracker::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; } void ESP32BLETracker::setup() { @@ -49,13 +68,6 @@ void ESP32BLETracker::setup() { ESP_LOGE(TAG, "BLE Tracker was marked failed by ESP32BLE"); return; } - RAMAllocator allocator; - this->scan_ring_buffer_ = allocator.allocate(SCAN_RESULT_BUFFER_SIZE); - - if (this->scan_ring_buffer_ == nullptr) { - ESP_LOGE(TAG, "Could not allocate ring buffer for BLE Tracker!"); - this->mark_failed(); - } global_esp32_ble_tracker = this; @@ -64,9 +76,11 @@ void ESP32BLETracker::setup() { [this](ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) { if (state == ota::OTA_STARTED) { this->stop_scan(); +#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT for (auto *client : this->clients_) { client->disconnect(); } +#endif } }); #endif @@ -83,124 +97,48 @@ void ESP32BLETracker::loop() { this->start_scan(); } } - int connecting = 0; - int discovered = 0; - int searching = 0; - int disconnecting = 0; - for (auto *client : this->clients_) { - switch (client->state()) { - case ClientState::DISCONNECTING: - disconnecting++; - break; - case ClientState::DISCOVERED: - discovered++; - break; - case ClientState::SEARCHING: - searching++; - break; - case ClientState::CONNECTING: - case ClientState::READY_TO_CONNECT: - connecting++; - break; - default: - break; - } - } - if (connecting != connecting_ || discovered != discovered_ || searching != searching_ || - disconnecting != disconnecting_) { - connecting_ = connecting; - discovered_ = discovered; - searching_ = searching; - disconnecting_ = disconnecting; - ESP_LOGD(TAG, "connecting: %d, discovered: %d, searching: %d, disconnecting: %d", connecting_, discovered_, - searching_, disconnecting_); - } - bool promote_to_connecting = discovered && !searching && !connecting; - // Process scan results from lock-free SPSC ring buffer - // Consumer side: This runs in the main loop thread + // Check for scan timeout - moved here from scheduler to avoid false reboots + // when the loop is blocked if (this->scanner_state_ == ScannerState::RUNNING) { - // Load our own index with relaxed ordering (we're the only writer) - uint8_t read_idx = this->ring_read_index_.load(std::memory_order_relaxed); - - // Load producer's index with acquire to see their latest writes - uint8_t write_idx = this->ring_write_index_.load(std::memory_order_acquire); - - while (read_idx != write_idx) { - // Calculate how many contiguous results we can process in one batch - // If write > read: process all results from read to write - // If write <= read (wraparound): process from read to end of buffer first - size_t batch_size = (write_idx > read_idx) ? (write_idx - read_idx) : (SCAN_RESULT_BUFFER_SIZE - read_idx); - - // Process the batch for raw advertisements - if (this->raw_advertisements_) { - for (auto *listener : this->listeners_) { - listener->parse_devices(&this->scan_ring_buffer_[read_idx], batch_size); - } - for (auto *client : this->clients_) { - client->parse_devices(&this->scan_ring_buffer_[read_idx], batch_size); + switch (this->scan_timeout_state_) { + case ScanTimeoutState::MONITORING: { + uint32_t now = App.get_loop_component_start_time(); + uint32_t timeout_ms = this->scan_duration_ * 2000; + // Robust time comparison that handles rollover correctly + // This works because unsigned arithmetic wraps around predictably + if ((now - this->scan_start_time_) > timeout_ms) { + // First time we've seen the timeout exceeded - wait one more loop iteration + // This ensures all components have had a chance to process pending events + // This is because esp32_ble may not have run yet and called + // gap_scan_event_handler yet when the loop unblocks + ESP_LOGW(TAG, "Scan timeout exceeded"); + this->scan_timeout_state_ = ScanTimeoutState::EXCEEDED_WAIT; } + break; } + case ScanTimeoutState::EXCEEDED_WAIT: + // We've waited at least one full loop iteration, and scan is still running + ESP_LOGE(TAG, "Scan never terminated, rebooting"); + App.reboot(); + break; - // Process individual results for parsed advertisements - if (this->parse_advertisements_) { -#ifdef USE_ESP32_BLE_DEVICE - for (size_t i = 0; i < batch_size; i++) { - BLEScanResult &scan_result = this->scan_ring_buffer_[read_idx + i]; - ESPBTDevice device; - device.parse_scan_rst(scan_result); - - bool found = false; - for (auto *listener : this->listeners_) { - if (listener->parse_device(device)) - found = true; - } - - for (auto *client : this->clients_) { - if (client->parse_device(device)) { - found = true; - if (!connecting && client->state() == ClientState::DISCOVERED) { - promote_to_connecting = true; - } - } - } - - if (!found && !this->scan_continuous_) { - this->print_bt_device_info(device); - } - } -#endif // USE_ESP32_BLE_DEVICE - } - - // Update read index for entire batch - read_idx = (read_idx + batch_size) % SCAN_RESULT_BUFFER_SIZE; - - // Store with release to ensure reads complete before index update - this->ring_read_index_.store(read_idx, std::memory_order_release); - } - - // Log dropped results periodically - size_t dropped = this->scan_results_dropped_.exchange(0, std::memory_order_relaxed); - if (dropped > 0) { - ESP_LOGW(TAG, "Dropped %zu BLE scan results due to buffer overflow", dropped); + case ScanTimeoutState::INACTIVE: + // This case should be unreachable - scanner and timeout states are always synchronized + break; } } + + ClientStateCounts counts = this->count_client_states_(); + if (counts != this->client_state_counts_) { + this->client_state_counts_ = counts; + ESP_LOGD(TAG, "connecting: %d, discovered: %d, disconnecting: %d", this->client_state_counts_.connecting, + this->client_state_counts_.discovered, this->client_state_counts_.disconnecting); + } + if (this->scanner_state_ == ScannerState::FAILED || (this->scan_set_param_failed_ && this->scanner_state_ == ScannerState::RUNNING)) { - this->stop_scan_(); - if (this->scan_start_fail_count_ == std::numeric_limits::max()) { - ESP_LOGE(TAG, "Scan could not restart after %d attempts, rebooting to restore stack (IDF)", - std::numeric_limits::max()); - App.reboot(); - } - if (this->scan_start_failed_) { - ESP_LOGE(TAG, "Scan start failed: %d", this->scan_start_failed_); - this->scan_start_failed_ = ESP_BT_STATUS_SUCCESS; - } - if (this->scan_set_param_failed_) { - ESP_LOGE(TAG, "Scan set param failed: %d", this->scan_set_param_failed_); - this->scan_set_param_failed_ = ESP_BT_STATUS_SUCCESS; - } + this->handle_scanner_failure_(); } /* @@ -215,45 +153,23 @@ void ESP32BLETracker::loop() { https://github.com/espressif/esp-idf/issues/6688 */ - if (this->scanner_state_ == ScannerState::IDLE && !connecting && !disconnecting && !promote_to_connecting) { + + if (this->scanner_state_ == ScannerState::IDLE && !counts.connecting && !counts.disconnecting && !counts.discovered) { #ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE - if (this->coex_prefer_ble_) { - this->coex_prefer_ble_ = false; - ESP_LOGD(TAG, "Setting coexistence preference to balanced."); - esp_coex_preference_set(ESP_COEX_PREFER_BALANCE); // Reset to default - } + this->update_coex_preference_(false); #endif if (this->scan_continuous_) { this->start_scan_(false); // first = false } } // If there is a discovered client and no connecting - // clients and no clients using the scanner to search for - // devices, then stop scanning and promote the discovered - // client to ready to connect. - if (promote_to_connecting && + // clients, then promote the discovered client to ready to connect. + // We check both RUNNING and IDLE states because: + // - RUNNING: gap_scan_event_handler initiates stop_scan_() but promotion can happen immediately + // - IDLE: Scanner has already stopped (naturally or by gap_scan_event_handler) + if (counts.discovered && !counts.connecting && (this->scanner_state_ == ScannerState::RUNNING || this->scanner_state_ == ScannerState::IDLE)) { - for (auto *client : this->clients_) { - if (client->state() == ClientState::DISCOVERED) { - if (this->scanner_state_ == ScannerState::RUNNING) { - ESP_LOGD(TAG, "Stopping scan to make connection"); - this->stop_scan_(); - } else if (this->scanner_state_ == ScannerState::IDLE) { - ESP_LOGD(TAG, "Promoting client to connect"); - // We only want to promote one client at a time. - // once the scanner is fully stopped. -#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE - ESP_LOGD(TAG, "Setting coexistence to Bluetooth to make connection."); - if (!this->coex_prefer_ble_) { - this->coex_prefer_ble_ = true; - esp_coex_preference_set(ESP_COEX_PREFER_BT); // Prioritize Bluetooth - } -#endif - client->set_state(ClientState::READY_TO_CONNECT); - } - break; - } - } + this->try_promote_discovered_clients_(); } } @@ -269,16 +185,11 @@ void ESP32BLETracker::ble_before_disabled_event_handler() { this->stop_scan_(); void ESP32BLETracker::stop_scan_() { if (this->scanner_state_ != ScannerState::RUNNING && this->scanner_state_ != ScannerState::FAILED) { - if (this->scanner_state_ == ScannerState::IDLE) { - ESP_LOGE(TAG, "Scan is already stopped while trying to stop."); - } else if (this->scanner_state_ == ScannerState::STARTING) { - ESP_LOGE(TAG, "Scan is starting while trying to stop."); - } else if (this->scanner_state_ == ScannerState::STOPPING) { - ESP_LOGE(TAG, "Scan is already stopping while trying to stop."); - } + ESP_LOGE(TAG, "Cannot stop scan: %s", this->scanner_state_to_string_(this->scanner_state_)); return; } - this->cancel_timeout("scan"); + // Reset timeout state machine when stopping scan + this->scan_timeout_state_ = ScanTimeoutState::INACTIVE; this->set_scanner_state_(ScannerState::STOPPING); esp_err_t err = esp_ble_gap_stop_scanning(); if (err != ESP_OK) { @@ -293,35 +204,30 @@ void ESP32BLETracker::start_scan_(bool first) { return; } if (this->scanner_state_ != ScannerState::IDLE) { - if (this->scanner_state_ == ScannerState::STARTING) { - ESP_LOGE(TAG, "Cannot start scan while already starting."); - } else if (this->scanner_state_ == ScannerState::RUNNING) { - ESP_LOGE(TAG, "Cannot start scan while already running."); - } else if (this->scanner_state_ == ScannerState::STOPPING) { - ESP_LOGE(TAG, "Cannot start scan while already stopping."); - } else if (this->scanner_state_ == ScannerState::FAILED) { - ESP_LOGE(TAG, "Cannot start scan while already failed."); - } + this->log_unexpected_state_("start scan", ScannerState::IDLE); return; } this->set_scanner_state_(ScannerState::STARTING); ESP_LOGD(TAG, "Starting scan, set scanner state to STARTING."); if (!first) { +#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT for (auto *listener : this->listeners_) listener->on_scan_end(); +#endif } +#ifdef USE_ESP32_BLE_DEVICE this->already_discovered_.clear(); +#endif this->scan_params_.scan_type = this->scan_active_ ? BLE_SCAN_TYPE_ACTIVE : BLE_SCAN_TYPE_PASSIVE; this->scan_params_.own_addr_type = BLE_ADDR_TYPE_PUBLIC; this->scan_params_.scan_filter_policy = BLE_SCAN_FILTER_ALLOW_ALL; this->scan_params_.scan_interval = this->scan_interval_; this->scan_params_.scan_window = this->scan_window_; - // Start timeout before scan is started. Otherwise scan never starts if any error. - this->set_timeout("scan", this->scan_duration_ * 2000, []() { - ESP_LOGE(TAG, "Scan never terminated, rebooting to restore stack (IDF)"); - App.reboot(); - }); + // Start timeout monitoring in loop() instead of using scheduler + // This prevents false reboots when the loop is blocked + this->scan_start_time_ = App.get_loop_component_start_time(); + this->scan_timeout_state_ = ScanTimeoutState::MONITORING; esp_err_t err = esp_ble_gap_set_scan_params(&this->scan_params_); if (err != ESP_OK) { @@ -336,20 +242,25 @@ void ESP32BLETracker::start_scan_(bool first) { } void ESP32BLETracker::register_client(ESPBTClient *client) { +#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT client->app_id = ++this->app_id_; this->clients_.push_back(client); this->recalculate_advertisement_parser_types(); +#endif } void ESP32BLETracker::register_listener(ESPBTDeviceListener *listener) { +#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT listener->set_parent(this); this->listeners_.push_back(listener); this->recalculate_advertisement_parser_types(); +#endif } void ESP32BLETracker::recalculate_advertisement_parser_types() { this->raw_advertisements_ = false; this->parse_advertisements_ = false; +#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT for (auto *listener : this->listeners_) { if (listener->get_advertisement_parser_type() == AdvertisementParserType::PARSED_ADVERTISEMENTS) { this->parse_advertisements_ = true; @@ -357,6 +268,8 @@ void ESP32BLETracker::recalculate_advertisement_parser_types() { this->raw_advertisements_ = true; } } +#endif +#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT for (auto *client : this->clients_) { if (client->get_advertisement_parser_type() == AdvertisementParserType::PARSED_ADVERTISEMENTS) { this->parse_advertisements_ = true; @@ -364,6 +277,7 @@ void ESP32BLETracker::recalculate_advertisement_parser_types() { this->raw_advertisements_ = true; } } +#endif } void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { @@ -382,52 +296,26 @@ void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_ga default: break; } - // Forward all events to clients (scan results are handled separately via gap_scan_event_handler) + // Forward all events to clients (scan results are handled separately via gap_scan_event_handler) +#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT for (auto *client : this->clients_) { client->gap_event_handler(event, param); } +#endif } void ESP32BLETracker::gap_scan_event_handler(const BLEScanResult &scan_result) { // Note: This handler is called from the main loop context via esp32_ble's event queue. - // However, we still use a lock-free ring buffer to batch results efficiently. - ESP_LOGV(TAG, "gap_scan_result - event %d", scan_result.search_evt); + // We process advertisements immediately instead of buffering them. + ESP_LOGVV(TAG, "gap_scan_result - event %d", scan_result.search_evt); if (scan_result.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) { - // Ring buffer write (Producer side) - // Even though we're in the main loop, the ring buffer design allows efficient batching - // IMPORTANT: Only this thread writes to ring_write_index_ - - // Load our own index with relaxed ordering (we're the only writer) - uint8_t write_idx = this->ring_write_index_.load(std::memory_order_relaxed); - uint8_t next_write_idx = (write_idx + 1) % SCAN_RESULT_BUFFER_SIZE; - - // Load consumer's index with acquire to see their latest updates - uint8_t read_idx = this->ring_read_index_.load(std::memory_order_acquire); - - // Check if buffer is full - if (next_write_idx != read_idx) { - // Write to ring buffer - this->scan_ring_buffer_[write_idx] = scan_result; - - // Store with release to ensure the write is visible before index update - this->ring_write_index_.store(next_write_idx, std::memory_order_release); - } else { - // Buffer full, track dropped results - this->scan_results_dropped_.fetch_add(1, std::memory_order_relaxed); - } + // Process the scan result immediately + this->process_scan_result_(scan_result); } else if (scan_result.search_evt == ESP_GAP_SEARCH_INQ_CMPL_EVT) { // Scan finished on its own if (this->scanner_state_ != ScannerState::RUNNING) { - if (this->scanner_state_ == ScannerState::STOPPING) { - ESP_LOGE(TAG, "Scan was not running when scan completed."); - } else if (this->scanner_state_ == ScannerState::STARTING) { - ESP_LOGE(TAG, "Scan was not started when scan completed."); - } else if (this->scanner_state_ == ScannerState::FAILED) { - ESP_LOGE(TAG, "Scan was in failed state when scan completed."); - } else if (this->scanner_state_ == ScannerState::IDLE) { - ESP_LOGE(TAG, "Scan was idle when scan completed."); - } + this->log_unexpected_state_("scan complete", ScannerState::RUNNING); } // Scan completed naturally, perform cleanup and transition to IDLE this->cleanup_scan_state_(false); @@ -449,15 +337,7 @@ void ESP32BLETracker::gap_scan_start_complete_(const esp_ble_gap_cb_param_t::ble ESP_LOGV(TAG, "gap_scan_start_complete - status %d", param.status); this->scan_start_failed_ = param.status; if (this->scanner_state_ != ScannerState::STARTING) { - if (this->scanner_state_ == ScannerState::RUNNING) { - ESP_LOGE(TAG, "Scan was already running when start complete."); - } else if (this->scanner_state_ == ScannerState::STOPPING) { - ESP_LOGE(TAG, "Scan was stopping when start complete."); - } else if (this->scanner_state_ == ScannerState::FAILED) { - ESP_LOGE(TAG, "Scan was in failed state when start complete."); - } else if (this->scanner_state_ == ScannerState::IDLE) { - ESP_LOGE(TAG, "Scan was idle when start complete."); - } + this->log_unexpected_state_("start complete", ScannerState::STARTING); } if (param.status == ESP_BT_STATUS_SUCCESS) { this->scan_start_fail_count_ = 0; @@ -475,15 +355,7 @@ void ESP32BLETracker::gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_ // This allows us to safely transition to IDLE state and perform cleanup without race conditions ESP_LOGV(TAG, "gap_scan_stop_complete - status %d", param.status); if (this->scanner_state_ != ScannerState::STOPPING) { - if (this->scanner_state_ == ScannerState::RUNNING) { - ESP_LOGE(TAG, "Scan was not running when stop complete."); - } else if (this->scanner_state_ == ScannerState::STARTING) { - ESP_LOGE(TAG, "Scan was not started when stop complete."); - } else if (this->scanner_state_ == ScannerState::FAILED) { - ESP_LOGE(TAG, "Scan was in failed state when stop complete."); - } else if (this->scanner_state_ == ScannerState::IDLE) { - ESP_LOGE(TAG, "Scan was idle when stop complete."); - } + this->log_unexpected_state_("stop complete", ScannerState::STOPPING); } // Perform cleanup and transition to IDLE @@ -492,9 +364,11 @@ void ESP32BLETracker::gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_ void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { +#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT for (auto *client : this->clients_) { client->gattc_event_handler(event, gattc_if, param); } +#endif } void ESP32BLETracker::set_scanner_state_(ScannerState state) { @@ -745,9 +619,8 @@ void ESPBTDevice::parse_adv_(const uint8_t *payload, uint8_t len) { } std::string ESPBTDevice::address_str() const { - char mac[24]; - snprintf(mac, sizeof(mac), "%02X:%02X:%02X:%02X:%02X:%02X", this->address_[0], this->address_[1], this->address_[2], - this->address_[3], this->address_[4], this->address_[5]); + char mac[18]; + format_mac_addr_upper(this->address_, mac); return mac; } @@ -764,25 +637,9 @@ void ESP32BLETracker::dump_config() { " Continuous Scanning: %s", this->scan_duration_, this->scan_interval_ * 0.625f, this->scan_window_ * 0.625f, this->scan_active_ ? "ACTIVE" : "PASSIVE", YESNO(this->scan_continuous_)); - switch (this->scanner_state_) { - case ScannerState::IDLE: - ESP_LOGCONFIG(TAG, " Scanner State: IDLE"); - break; - case ScannerState::STARTING: - ESP_LOGCONFIG(TAG, " Scanner State: STARTING"); - break; - case ScannerState::RUNNING: - ESP_LOGCONFIG(TAG, " Scanner State: RUNNING"); - break; - case ScannerState::STOPPING: - ESP_LOGCONFIG(TAG, " Scanner State: STOPPING"); - break; - case ScannerState::FAILED: - ESP_LOGCONFIG(TAG, " Scanner State: FAILED"); - break; - } - ESP_LOGCONFIG(TAG, " Connecting: %d, discovered: %d, searching: %d, disconnecting: %d", connecting_, discovered_, - searching_, disconnecting_); + ESP_LOGCONFIG(TAG, " Scanner State: %s", this->scanner_state_to_string_(this->scanner_state_)); + ESP_LOGCONFIG(TAG, " Connecting: %d, discovered: %d, disconnecting: %d", this->client_state_counts_.connecting, + this->client_state_counts_.discovered, this->client_state_counts_.disconnecting); if (this->scan_start_fail_count_) { ESP_LOGCONFIG(TAG, " Scan Start Fail Count: %d", this->scan_start_fail_count_); } @@ -859,19 +716,151 @@ bool ESPBTDevice::resolve_irk(const uint8_t *irk) const { return ecb_ciphertext[15] == (addr64 & 0xff) && ecb_ciphertext[14] == ((addr64 >> 8) & 0xff) && ecb_ciphertext[13] == ((addr64 >> 16) & 0xff); } + #endif // USE_ESP32_BLE_DEVICE +void ESP32BLETracker::process_scan_result_(const BLEScanResult &scan_result) { + // Process raw advertisements + if (this->raw_advertisements_) { +#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT + for (auto *listener : this->listeners_) { + listener->parse_devices(&scan_result, 1); + } +#endif +#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT + for (auto *client : this->clients_) { + client->parse_devices(&scan_result, 1); + } +#endif + } + + // Process parsed advertisements + if (this->parse_advertisements_) { +#ifdef USE_ESP32_BLE_DEVICE + ESPBTDevice device; + device.parse_scan_rst(scan_result); + + bool found = false; +#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT + for (auto *listener : this->listeners_) { + if (listener->parse_device(device)) + found = true; + } +#endif + +#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT + for (auto *client : this->clients_) { + if (client->parse_device(device)) { + found = true; + } + } +#endif + + if (!found && !this->scan_continuous_) { + this->print_bt_device_info(device); + } +#endif // USE_ESP32_BLE_DEVICE + } +} + void ESP32BLETracker::cleanup_scan_state_(bool is_stop_complete) { ESP_LOGD(TAG, "Scan %scomplete, set scanner state to IDLE.", is_stop_complete ? "stop " : ""); +#ifdef USE_ESP32_BLE_DEVICE this->already_discovered_.clear(); - this->cancel_timeout("scan"); +#endif + // Reset timeout state machine instead of cancelling scheduler timeout + this->scan_timeout_state_ = ScanTimeoutState::INACTIVE; +#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT for (auto *listener : this->listeners_) listener->on_scan_end(); +#endif this->set_scanner_state_(ScannerState::IDLE); } +void ESP32BLETracker::handle_scanner_failure_() { + this->stop_scan_(); + if (this->scan_start_fail_count_ == std::numeric_limits::max()) { + ESP_LOGE(TAG, "Scan could not restart after %d attempts, rebooting to restore stack (IDF)", + std::numeric_limits::max()); + App.reboot(); + } + if (this->scan_start_failed_) { + ESP_LOGE(TAG, "Scan start failed: %d", this->scan_start_failed_); + this->scan_start_failed_ = ESP_BT_STATUS_SUCCESS; + } + if (this->scan_set_param_failed_) { + ESP_LOGE(TAG, "Scan set param failed: %d", this->scan_set_param_failed_); + this->scan_set_param_failed_ = ESP_BT_STATUS_SUCCESS; + } +} + +void ESP32BLETracker::try_promote_discovered_clients_() { + // Only promote the first discovered client to avoid multiple simultaneous connections +#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT + for (auto *client : this->clients_) { + if (client->state() != ClientState::DISCOVERED) { + continue; + } + + if (this->scanner_state_ == ScannerState::RUNNING) { + ESP_LOGD(TAG, "Stopping scan to make connection"); + this->stop_scan_(); + // Don't wait for scan stop complete - promote immediately. + // This is safe because ESP-IDF processes BLE commands sequentially through its internal mailbox queue. + // This guarantees that the stop scan command will be fully processed before any subsequent connect command, + // preventing race conditions or overlapping operations. + } + + ESP_LOGD(TAG, "Promoting client to connect"); +#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE + this->update_coex_preference_(true); +#endif + client->connect(); + break; + } +#endif +} + +const char *ESP32BLETracker::scanner_state_to_string_(ScannerState state) const { + switch (state) { + case ScannerState::IDLE: + return "IDLE"; + case ScannerState::STARTING: + return "STARTING"; + case ScannerState::RUNNING: + return "RUNNING"; + case ScannerState::STOPPING: + return "STOPPING"; + case ScannerState::FAILED: + return "FAILED"; + default: + return "UNKNOWN"; + } +} + +void ESP32BLETracker::log_unexpected_state_(const char *operation, ScannerState expected_state) const { + ESP_LOGE(TAG, "Unexpected state: %s on %s, expected: %s", this->scanner_state_to_string_(this->scanner_state_), + operation, this->scanner_state_to_string_(expected_state)); +} + +#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE +void ESP32BLETracker::update_coex_preference_(bool force_ble) { +#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID + if (force_ble && !this->coex_prefer_ble_) { + ESP_LOGD(TAG, "Setting coexistence to Bluetooth to make connection."); + this->coex_prefer_ble_ = true; + esp_coex_preference_set(ESP_COEX_PREFER_BT); // Prioritize Bluetooth + } else if (!force_ble && this->coex_prefer_ble_) { + ESP_LOGD(TAG, "Setting coexistence preference to balanced."); + this->coex_prefer_ble_ = false; + esp_coex_preference_set(ESP_COEX_PREFER_BALANCE); // Reset to default + } +#endif // CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID +} +#endif + } // namespace esphome::esp32_ble_tracker #endif // USE_ESP32 diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index c274e64b12..f80f3e2670 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -6,7 +6,6 @@ #include "esphome/core/helpers.h" #include -#include #include #include @@ -21,6 +20,7 @@ #include "esphome/components/esp32_ble/ble.h" #include "esphome/components/esp32_ble/ble_uuid.h" +#include "esphome/components/esp32_ble/ble_scan_result.h" namespace esphome::esp32_ble_tracker { @@ -33,10 +33,12 @@ enum AdvertisementParserType { RAW_ADVERTISEMENTS, }; +#ifdef USE_ESP32_BLE_UUID struct ServiceData { ESPBTUUID uuid; adv_data_t data; }; +#endif #ifdef USE_ESP32_BLE_DEVICE class ESPBLEiBeacon { @@ -136,6 +138,18 @@ class ESPBTDeviceListener { ESP32BLETracker *parent_{nullptr}; }; +struct ClientStateCounts { + uint8_t connecting = 0; + uint8_t discovered = 0; + uint8_t disconnecting = 0; + + bool operator==(const ClientStateCounts &other) const { + return connecting == other.connecting && discovered == other.discovered && disconnecting == other.disconnecting; + } + + bool operator!=(const ClientStateCounts &other) const { return !(*this == other); } +}; + enum class ClientState : uint8_t { // Connection is allocated INIT, @@ -143,12 +157,8 @@ enum class ClientState : uint8_t { DISCONNECTING, // Connection is idle, no device detected. IDLE, - // Searching for device. - SEARCHING, // Device advertisement found. DISCOVERED, - // Device is discovered and the scanner is stopped - READY_TO_CONNECT, // Connection in progress. CONNECTING, // Initial connection established. @@ -170,6 +180,9 @@ enum class ScannerState { STOPPING, }; +// Helper function to convert ClientState to string +const char *client_state_to_string(ClientState state); + enum class ConnectionType : uint8_t { // The default connection type, we hold all the services in ram // for the duration of the connection. @@ -272,47 +285,89 @@ class ESP32BLETracker : public Component, void set_scanner_state_(ScannerState state); /// Common cleanup logic when transitioning scanner to IDLE state void cleanup_scan_state_(bool is_stop_complete); + /// Process a single scan result immediately + void process_scan_result_(const BLEScanResult &scan_result); + /// Handle scanner failure states + void handle_scanner_failure_(); + /// Try to promote discovered clients to ready to connect + void try_promote_discovered_clients_(); + /// Convert scanner state enum to string for logging + const char *scanner_state_to_string_(ScannerState state) const; + /// Log an unexpected scanner state + void log_unexpected_state_(const char *operation, ScannerState expected_state) const; +#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE + /// Update BLE coexistence preference + void update_coex_preference_(bool force_ble); +#endif + /// Count clients in each state + ClientStateCounts count_client_states_() const { + ClientStateCounts counts; +#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT + for (auto *client : this->clients_) { + switch (client->state()) { + case ClientState::DISCONNECTING: + counts.disconnecting++; + break; + case ClientState::DISCOVERED: + counts.discovered++; + break; + case ClientState::CONNECTING: + counts.connecting++; + break; + default: + break; + } + } +#endif + return counts; + } - uint8_t app_id_{0}; - + // Group 1: Large objects (12+ bytes) - vectors and callback manager +#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT + StaticVector listeners_; +#endif +#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT + StaticVector clients_; +#endif + CallbackManager scanner_state_callbacks_; +#ifdef USE_ESP32_BLE_DEVICE /// Vector of addresses that have already been printed in print_bt_device_info std::vector already_discovered_; - std::vector listeners_; - /// Client parameters. - std::vector clients_; +#endif + + // Group 2: Structs (aligned to 4 bytes) /// A structure holding the ESP BLE scan parameters. esp_ble_scan_params_t scan_params_; + ClientStateCounts client_state_counts_; + + // Group 3: 4-byte types /// The interval in seconds to perform scans. uint32_t scan_duration_; uint32_t scan_interval_; uint32_t scan_window_; + esp_bt_status_t scan_start_failed_{ESP_BT_STATUS_SUCCESS}; + esp_bt_status_t scan_set_param_failed_{ESP_BT_STATUS_SUCCESS}; + + // Group 4: 1-byte types (enums, uint8_t, bool) + uint8_t app_id_{0}; uint8_t scan_start_fail_count_{0}; + ScannerState scanner_state_{ScannerState::IDLE}; bool scan_continuous_; bool scan_active_; - ScannerState scanner_state_{ScannerState::IDLE}; - CallbackManager scanner_state_callbacks_; bool ble_was_disabled_{true}; bool raw_advertisements_{false}; bool parse_advertisements_{false}; - - // Lock-free Single-Producer Single-Consumer (SPSC) ring buffer for scan results - // Producer: ESP-IDF Bluetooth stack callback (gap_scan_event_handler) - // Consumer: ESPHome main loop (loop() method) - // This design ensures zero blocking in the BT callback and prevents scan result loss - BLEScanResult *scan_ring_buffer_; - std::atomic ring_write_index_{0}; // Written only by BT callback (producer) - std::atomic ring_read_index_{0}; // Written only by main loop (consumer) - std::atomic scan_results_dropped_{0}; // Tracks buffer overflow events - - esp_bt_status_t scan_start_failed_{ESP_BT_STATUS_SUCCESS}; - esp_bt_status_t scan_set_param_failed_{ESP_BT_STATUS_SUCCESS}; - int connecting_{0}; - int discovered_{0}; - int searching_{0}; - int disconnecting_{0}; #ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE bool coex_prefer_ble_{false}; #endif + // Scan timeout state machine + enum class ScanTimeoutState : uint8_t { + INACTIVE, // No timeout monitoring + MONITORING, // Actively monitoring for timeout + EXCEEDED_WAIT, // Timeout exceeded, waiting one loop before reboot + }; + uint32_t scan_start_time_{0}; + ScanTimeoutState scan_timeout_state_{ScanTimeoutState::INACTIVE}; }; // NOLINTNEXTLINE diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index bfb66ff83a..d9d9bc0a56 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -4,6 +4,7 @@ from esphome import automation, pins import esphome.codegen as cg from esphome.components import i2c from esphome.components.esp32 import add_idf_component +from esphome.components.psram import DOMAIN as psram_domain import esphome.config_validation as cv from esphome.const import ( CONF_BRIGHTNESS, @@ -21,16 +22,14 @@ from esphome.const import ( CONF_TRIGGER_ID, CONF_VSYNC_PIN, ) -from esphome.core import CORE from esphome.core.entity_helpers import setup_entity import esphome.final_validate as fv _LOGGER = logging.getLogger(__name__) +AUTO_LOAD = ["camera"] DEPENDENCIES = ["esp32"] -AUTO_LOAD = ["camera", "psram"] - esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera") ESP32Camera = esp32_camera_ns.class_("ESP32Camera", cg.PollingComponent, cg.EntityBase) ESP32CameraImageData = esp32_camera_ns.struct("CameraImageData") @@ -164,6 +163,14 @@ CONF_ON_IMAGE = "on_image" camera_range_param = cv.int_range(min=-2, max=2) + +def validate_fb_location_(value): + validator = cv.enum(ENUM_FB_LOCATION, upper=True) + if value.lower() == psram_domain: + validator = cv.All(validator, cv.requires_component(psram_domain)) + return validator(value) + + CONFIG_SCHEMA = cv.All( cv.ENTITY_BASE_SCHEMA.extend( { @@ -237,9 +244,9 @@ CONFIG_SCHEMA = cv.All( cv.framerate, cv.Range(min=0, max=1) ), cv.Optional(CONF_FRAME_BUFFER_COUNT, default=1): cv.int_range(min=1, max=2), - cv.Optional(CONF_FRAME_BUFFER_LOCATION, default="PSRAM"): cv.enum( - ENUM_FB_LOCATION, upper=True - ), + cv.Optional( + CONF_FRAME_BUFFER_LOCATION, default="PSRAM" + ): validate_fb_location_, cv.Optional(CONF_ON_STREAM_START): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( @@ -344,8 +351,7 @@ async def to_code(config): cg.add_define("USE_CAMERA") - if CORE.using_esp_idf: - add_idf_component(name="espressif/esp32-camera", ref="2.1.0") + add_idf_component(name="espressif/esp32-camera", ref="2.1.1") for conf in config.get(CONF_ON_STREAM_START, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) diff --git a/esphome/components/esp32_camera_web_server/__init__.py b/esphome/components/esp32_camera_web_server/__init__.py index a6a7ac3630..ed1aaa2e07 100644 --- a/esphome/components/esp32_camera_web_server/__init__.py +++ b/esphome/components/esp32_camera_web_server/__init__.py @@ -1,6 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_MODE, CONF_PORT +from esphome.types import ConfigType CODEOWNERS = ["@ayufan"] AUTO_LOAD = ["camera"] @@ -13,13 +14,27 @@ Mode = esp32_camera_web_server_ns.enum("Mode") MODES = {"STREAM": Mode.STREAM, "SNAPSHOT": Mode.SNAPSHOT} -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(CameraWebServer), - cv.Required(CONF_PORT): cv.port, - cv.Required(CONF_MODE): cv.enum(MODES, upper=True), - }, -).extend(cv.COMPONENT_SCHEMA) + +def _consume_camera_web_server_sockets(config: ConfigType) -> ConfigType: + """Register socket needs for camera web server.""" + from esphome.components import socket + + # Each camera web server instance needs 1 listening socket + 2 client connections + sockets_needed = 3 + socket.consume_sockets(sockets_needed, "esp32_camera_web_server")(config) + return config + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(CameraWebServer), + cv.Required(CONF_PORT): cv.port, + cv.Required(CONF_MODE): cv.enum(MODES, upper=True), + }, + ).extend(cv.COMPONENT_SCHEMA), + _consume_camera_web_server_sockets, +) async def to_code(config): diff --git a/esphome/components/esp32_can/esp32_can.cpp b/esphome/components/esp32_can/esp32_can.cpp index b5e72497ce..cdef7b1930 100644 --- a/esphome/components/esp32_can/esp32_can.cpp +++ b/esphome/components/esp32_can/esp32_can.cpp @@ -67,8 +67,16 @@ static bool get_bitrate(canbus::CanSpeed bitrate, twai_timing_config_t *t_config } bool ESP32Can::setup_internal() { + static int next_twai_ctrl_num = 0; + if (static_cast(next_twai_ctrl_num) >= SOC_TWAI_CONTROLLER_NUM) { + ESP_LOGW(TAG, "Maximum number of esp32_can components created already"); + this->mark_failed(); + return false; + } + twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT((gpio_num_t) this->tx_, (gpio_num_t) this->rx_, TWAI_MODE_NORMAL); + g_config.controller_id = next_twai_ctrl_num++; if (this->tx_queue_len_.has_value()) { g_config.tx_queue_len = this->tx_queue_len_.value(); } @@ -86,14 +94,14 @@ bool ESP32Can::setup_internal() { } // Install TWAI driver - if (twai_driver_install(&g_config, &t_config, &f_config) != ESP_OK) { + if (twai_driver_install_v2(&g_config, &t_config, &f_config, &(this->twai_handle_)) != ESP_OK) { // Failed to install driver this->mark_failed(); return false; } // Start TWAI driver - if (twai_start() != ESP_OK) { + if (twai_start_v2(this->twai_handle_) != ESP_OK) { // Failed to start driver this->mark_failed(); return false; @@ -102,6 +110,11 @@ bool ESP32Can::setup_internal() { } canbus::Error ESP32Can::send_message(struct canbus::CanFrame *frame) { + if (this->twai_handle_ == nullptr) { + // not setup yet or setup failed + return canbus::ERROR_FAIL; + } + if (frame->can_data_length_code > canbus::CAN_MAX_DATA_LENGTH) { return canbus::ERROR_FAILTX; } @@ -124,7 +137,7 @@ canbus::Error ESP32Can::send_message(struct canbus::CanFrame *frame) { memcpy(message.data, frame->data, frame->can_data_length_code); } - if (twai_transmit(&message, this->tx_enqueue_timeout_ticks_) == ESP_OK) { + if (twai_transmit_v2(this->twai_handle_, &message, this->tx_enqueue_timeout_ticks_) == ESP_OK) { return canbus::ERROR_OK; } else { return canbus::ERROR_ALLTXBUSY; @@ -132,9 +145,14 @@ canbus::Error ESP32Can::send_message(struct canbus::CanFrame *frame) { } canbus::Error ESP32Can::read_message(struct canbus::CanFrame *frame) { + if (this->twai_handle_ == nullptr) { + // not setup yet or setup failed + return canbus::ERROR_FAIL; + } + twai_message_t message; - if (twai_receive(&message, 0) != ESP_OK) { + if (twai_receive_v2(this->twai_handle_, &message, 0) != ESP_OK) { return canbus::ERROR_NOMSG; } diff --git a/esphome/components/esp32_can/esp32_can.h b/esphome/components/esp32_can/esp32_can.h index 416f037083..dc44aceb36 100644 --- a/esphome/components/esp32_can/esp32_can.h +++ b/esphome/components/esp32_can/esp32_can.h @@ -5,6 +5,8 @@ #include "esphome/components/canbus/canbus.h" #include "esphome/core/component.h" +#include + namespace esphome { namespace esp32_can { @@ -29,6 +31,7 @@ class ESP32Can : public canbus::Canbus { TickType_t tx_enqueue_timeout_ticks_{}; optional tx_queue_len_{}; optional rx_queue_len_{}; + twai_handle_t twai_handle_{nullptr}; }; } // namespace esp32_can diff --git a/esphome/components/esp32_dac/esp32_dac.cpp b/esphome/components/esp32_dac/esp32_dac.cpp index 7d8507c566..8f226a5cc2 100644 --- a/esphome/components/esp32_dac/esp32_dac.cpp +++ b/esphome/components/esp32_dac/esp32_dac.cpp @@ -2,11 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#ifdef USE_ESP32 - -#ifdef USE_ARDUINO -#include -#endif +#if defined(USE_ESP32_VARIANT_ESP32) || defined(USE_ESP32_VARIANT_ESP32S2) namespace esphome { namespace esp32_dac { @@ -23,18 +19,12 @@ void ESP32DAC::setup() { this->pin_->setup(); this->turn_off(); -#ifdef USE_ESP_IDF const dac_channel_t channel = this->pin_->get_pin() == DAC0_PIN ? DAC_CHAN_0 : DAC_CHAN_1; const dac_oneshot_config_t oneshot_cfg{channel}; dac_oneshot_new_channel(&oneshot_cfg, &this->dac_handle_); -#endif } -void ESP32DAC::on_safe_shutdown() { -#ifdef USE_ESP_IDF - dac_oneshot_del_channel(this->dac_handle_); -#endif -} +void ESP32DAC::on_safe_shutdown() { dac_oneshot_del_channel(this->dac_handle_); } void ESP32DAC::dump_config() { ESP_LOGCONFIG(TAG, "ESP32 DAC:"); @@ -48,15 +38,10 @@ void ESP32DAC::write_state(float state) { state = state * 255; -#ifdef USE_ESP_IDF dac_oneshot_output_voltage(this->dac_handle_, state); -#endif -#ifdef USE_ARDUINO - dacWrite(this->pin_->get_pin(), state); -#endif } } // namespace esp32_dac } // namespace esphome -#endif +#endif // USE_ESP32_VARIANT_ESP32 || USE_ESP32_VARIANT_ESP32S2 diff --git a/esphome/components/esp32_dac/esp32_dac.h b/esphome/components/esp32_dac/esp32_dac.h index 63d0c914a1..95c687d307 100644 --- a/esphome/components/esp32_dac/esp32_dac.h +++ b/esphome/components/esp32_dac/esp32_dac.h @@ -1,15 +1,13 @@ #pragma once +#include "esphome/components/output/float_output.h" +#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/hal.h" -#include "esphome/core/automation.h" -#include "esphome/components/output/float_output.h" -#ifdef USE_ESP32 +#if defined(USE_ESP32_VARIANT_ESP32) || defined(USE_ESP32_VARIANT_ESP32S2) -#ifdef USE_ESP_IDF #include -#endif namespace esphome { namespace esp32_dac { @@ -29,12 +27,10 @@ class ESP32DAC : public output::FloatOutput, public Component { void write_state(float state) override; InternalGPIOPin *pin_; -#ifdef USE_ESP_IDF dac_oneshot_handle_t dac_handle_; -#endif }; } // namespace esp32_dac } // namespace esphome -#endif +#endif // USE_ESP32_VARIANT_ESP32 || USE_ESP32_VARIANT_ESP32S2 diff --git a/esphome/components/esp32_hosted/__init__.py b/esphome/components/esp32_hosted/__init__.py index 330800df12..fde75517eb 100644 --- a/esphome/components/esp32_hosted/__init__.py +++ b/esphome/components/esp32_hosted/__init__.py @@ -1,4 +1,5 @@ import os +from pathlib import Path from esphome import pins from esphome.components import esp32 @@ -91,11 +92,16 @@ async def to_code(config): framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] os.environ["ESP_IDF_VERSION"] = f"{framework_ver.major}.{framework_ver.minor}" - esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.10.2") - esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0") - esp32.add_idf_component(name="espressif/esp_hosted", ref="2.0.11") + if framework_ver >= cv.Version(5, 5, 0): + esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.1.5") + esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.3") + esp32.add_idf_component(name="espressif/esp_hosted", ref="2.6.1") + else: + esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.13.0") + esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0") + esp32.add_idf_component(name="espressif/esp_hosted", ref="2.0.11") esp32.add_extra_script( "post", "esp32_hosted.py", - os.path.join(os.path.dirname(__file__), "esp32_hosted.py.script"), + Path(__file__).parent / "esp32_hosted.py.script", ) diff --git a/esphome/components/esp32_hosted/update/__init__.py b/esphome/components/esp32_hosted/update/__init__.py new file mode 100644 index 0000000000..040f989a64 --- /dev/null +++ b/esphome/components/esp32_hosted/update/__init__.py @@ -0,0 +1,78 @@ +import hashlib +from typing import Any + +import esphome.codegen as cg +from esphome.components import esp32, update +import esphome.config_validation as cv +from esphome.const import CONF_PATH, CONF_RAW_DATA_ID +from esphome.core import CORE, HexInt + +CODEOWNERS = ["@swoboda1337"] +AUTO_LOAD = ["sha256", "watchdog"] +DEPENDENCIES = ["esp32_hosted"] + +CONF_SHA256 = "sha256" + +esp32_hosted_ns = cg.esphome_ns.namespace("esp32_hosted") +Esp32HostedUpdate = esp32_hosted_ns.class_( + "Esp32HostedUpdate", update.UpdateEntity, cg.Component +) + + +def _validate_sha256(value: Any) -> str: + value = cv.string_strict(value) + if len(value) != 64: + raise cv.Invalid("SHA256 must be 64 hexadecimal characters") + try: + bytes.fromhex(value) + except ValueError as e: + raise cv.Invalid(f"SHA256 must be valid hexadecimal: {e}") from e + return value + + +CONFIG_SCHEMA = cv.All( + update.update_schema(Esp32HostedUpdate, device_class="firmware").extend( + { + cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), + cv.Required(CONF_PATH): cv.file_, + cv.Required(CONF_SHA256): _validate_sha256, + } + ), + esp32.only_on_variant( + supported=[ + esp32.const.VARIANT_ESP32H2, + esp32.const.VARIANT_ESP32P4, + ] + ), +) + + +def _validate_firmware(config: dict[str, Any]) -> None: + path = CORE.relative_config_path(config[CONF_PATH]) + with open(path, "rb") as f: + firmware_data = f.read() + calculated = hashlib.sha256(firmware_data).hexdigest() + expected = config[CONF_SHA256].lower() + if calculated != expected: + raise cv.Invalid( + f"SHA256 mismatch for {config[CONF_PATH]}: expected {expected}, got {calculated}" + ) + + +FINAL_VALIDATE_SCHEMA = _validate_firmware + + +async def to_code(config: dict[str, Any]) -> None: + var = await update.new_update(config) + + path = config[CONF_PATH] + with open(CORE.relative_config_path(path), "rb") as f: + firmware_data = f.read() + rhs = [HexInt(x) for x in firmware_data] + prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) + + sha256_bytes = bytes.fromhex(config[CONF_SHA256]) + cg.add(var.set_firmware_sha256([HexInt(b) for b in sha256_bytes])) + cg.add(var.set_firmware_data(prog_arr)) + cg.add(var.set_firmware_size(len(firmware_data))) + await cg.register_component(var, config) diff --git a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp new file mode 100644 index 0000000000..de130ca71f --- /dev/null +++ b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp @@ -0,0 +1,169 @@ +#if defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32P4) +#include "esp32_hosted_update.h" +#include "esphome/components/watchdog/watchdog.h" +#include "esphome/components/sha256/sha256.h" +#include "esphome/core/application.h" +#include "esphome/core/log.h" +#include +#include +#include + +extern "C" { +#include +} + +namespace esphome::esp32_hosted { + +static const char *const TAG = "esp32_hosted.update"; + +// older coprocessor firmware versions have a 1500-byte limit per RPC call +constexpr size_t CHUNK_SIZE = 1500; + +void Esp32HostedUpdate::setup() { + this->update_info_.title = "ESP32 Hosted Coprocessor"; + + // if wifi is not present, connect to the coprocessor +#ifndef USE_WIFI + esp_hosted_connect_to_slave(); // NOLINT +#endif + + // get coprocessor version + esp_hosted_coprocessor_fwver_t ver_info; + if (esp_hosted_get_coprocessor_fwversion(&ver_info) == ESP_OK) { + this->update_info_.current_version = str_sprintf("%d.%d.%d", ver_info.major1, ver_info.minor1, ver_info.patch1); + } else { + this->update_info_.current_version = "unknown"; + } + ESP_LOGD(TAG, "Coprocessor version: %s", this->update_info_.current_version.c_str()); + + // get image version + const int app_desc_offset = sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t); + if (this->firmware_size_ >= app_desc_offset + sizeof(esp_app_desc_t)) { + esp_app_desc_t *app_desc = (esp_app_desc_t *) (this->firmware_data_ + app_desc_offset); + if (app_desc->magic_word == ESP_APP_DESC_MAGIC_WORD) { + ESP_LOGD(TAG, "Firmware version: %s", app_desc->version); + ESP_LOGD(TAG, "Project name: %s", app_desc->project_name); + ESP_LOGD(TAG, "Build date: %s", app_desc->date); + ESP_LOGD(TAG, "Build time: %s", app_desc->time); + ESP_LOGD(TAG, "IDF version: %s", app_desc->idf_ver); + this->update_info_.latest_version = app_desc->version; + if (this->update_info_.latest_version != this->update_info_.current_version) { + this->state_ = update::UPDATE_STATE_AVAILABLE; + } else { + this->state_ = update::UPDATE_STATE_NO_UPDATE; + } + } else { + ESP_LOGW(TAG, "Invalid app description magic word: 0x%08x (expected 0x%08x)", app_desc->magic_word, + ESP_APP_DESC_MAGIC_WORD); + this->state_ = update::UPDATE_STATE_NO_UPDATE; + } + } else { + ESP_LOGW(TAG, "Firmware too small to contain app description"); + this->state_ = update::UPDATE_STATE_NO_UPDATE; + } + + // publish state + this->status_clear_error(); + this->publish_state(); +} + +void Esp32HostedUpdate::dump_config() { + ESP_LOGCONFIG(TAG, + "ESP32 Hosted Update:\n" + " Current Version: %s\n" + " Latest Version: %s\n" + " Latest Size: %zu bytes", + this->update_info_.current_version.c_str(), this->update_info_.latest_version.c_str(), + this->firmware_size_); +} + +void Esp32HostedUpdate::perform(bool force) { + if (this->state_ != update::UPDATE_STATE_AVAILABLE && !force) { + ESP_LOGW(TAG, "Update not available"); + return; + } + + if (this->firmware_data_ == nullptr || this->firmware_size_ == 0) { + ESP_LOGE(TAG, "No firmware data available"); + return; + } + + sha256::SHA256 hasher; + hasher.init(); + hasher.add(this->firmware_data_, this->firmware_size_); + hasher.calculate(); + if (!hasher.equals_bytes(this->firmware_sha256_.data())) { + this->status_set_error(LOG_STR("SHA256 verification failed")); + this->publish_state(); + return; + } + + ESP_LOGI(TAG, "Starting OTA update (%zu bytes)", this->firmware_size_); + + watchdog::WatchdogManager watchdog(20000); + update::UpdateState prev_state = this->state_; + this->state_ = update::UPDATE_STATE_INSTALLING; + this->update_info_.has_progress = false; + this->publish_state(); + + esp_err_t err = esp_hosted_slave_ota_begin(); // NOLINT + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to begin OTA: %s", esp_err_to_name(err)); + this->state_ = prev_state; + this->status_set_error(LOG_STR("Failed to begin OTA")); + this->publish_state(); + return; + } + + uint8_t chunk[CHUNK_SIZE]; + const uint8_t *data_ptr = this->firmware_data_; + size_t remaining = this->firmware_size_; + while (remaining > 0) { + size_t chunk_size = std::min(remaining, static_cast(CHUNK_SIZE)); + memcpy(chunk, data_ptr, chunk_size); + err = esp_hosted_slave_ota_write(chunk, chunk_size); // NOLINT + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to write OTA data: %s", esp_err_to_name(err)); + esp_hosted_slave_ota_end(); // NOLINT + this->state_ = prev_state; + this->status_set_error(LOG_STR("Failed to write OTA data")); + this->publish_state(); + return; + } + data_ptr += chunk_size; + remaining -= chunk_size; + App.feed_wdt(); + } + + err = esp_hosted_slave_ota_end(); // NOLINT + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to end OTA: %s", esp_err_to_name(err)); + this->state_ = prev_state; + this->status_set_error(LOG_STR("Failed to end OTA")); + this->publish_state(); + return; + } + + // activate new firmware + err = esp_hosted_slave_ota_activate(); // NOLINT + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to activate OTA: %s", esp_err_to_name(err)); + this->state_ = prev_state; + this->status_set_error(LOG_STR("Failed to activate OTA")); + this->publish_state(); + return; + } + + // update state + ESP_LOGI(TAG, "OTA update successful"); + this->state_ = update::UPDATE_STATE_NO_UPDATE; + this->status_clear_error(); + this->publish_state(); + + // schedule a restart to ensure everything is in sync + ESP_LOGI(TAG, "Restarting in 1 second"); + this->set_timeout(1000, []() { App.safe_reboot(); }); +} + +} // namespace esphome::esp32_hosted +#endif diff --git a/esphome/components/esp32_hosted/update/esp32_hosted_update.h b/esphome/components/esp32_hosted/update/esp32_hosted_update.h new file mode 100644 index 0000000000..9c087bf72a --- /dev/null +++ b/esphome/components/esp32_hosted/update/esp32_hosted_update.h @@ -0,0 +1,32 @@ +#pragma once + +#if defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32P4) + +#include "esphome/core/component.h" +#include "esphome/components/update/update_entity.h" +#include + +namespace esphome::esp32_hosted { + +class Esp32HostedUpdate : public update::UpdateEntity, public Component { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + + void perform(bool force) override; + void check() override {} + + void set_firmware_data(const uint8_t *data) { this->firmware_data_ = data; } + void set_firmware_size(size_t size) { this->firmware_size_ = size; } + void set_firmware_sha256(const std::array &sha256) { this->firmware_sha256_ = sha256; } + + protected: + const uint8_t *firmware_data_{nullptr}; + size_t firmware_size_{0}; + std::array firmware_sha256_; +}; + +} // namespace esphome::esp32_hosted + +#endif diff --git a/esphome/components/esp32_improv/__init__.py b/esphome/components/esp32_improv/__init__.py index fa33bd947a..2e69d400ca 100644 --- a/esphome/components/esp32_improv/__init__.py +++ b/esphome/components/esp32_improv/__init__.py @@ -1,11 +1,11 @@ from esphome import automation import esphome.codegen as cg -from esphome.components import binary_sensor, esp32_ble, output +from esphome.components import binary_sensor, esp32_ble, improv_base, output from esphome.components.esp32_ble import BTLoggers import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_ON_STATE, CONF_TRIGGER_ID -AUTO_LOAD = ["esp32_ble_server"] +AUTO_LOAD = ["esp32_ble_server", "improv_base"] CODEOWNERS = ["@jesserockz"] DEPENDENCIES = ["wifi", "esp32"] @@ -20,6 +20,11 @@ CONF_ON_STOP = "on_stop" CONF_STATUS_INDICATOR = "status_indicator" CONF_WIFI_TIMEOUT = "wifi_timeout" +# Default WiFi timeout - aligned with WiFi component ap_timeout +# Allows sufficient time to try all BSSIDs before starting provisioning mode +DEFAULT_WIFI_TIMEOUT = "90s" + + improv_ns = cg.esphome_ns.namespace("improv") Error = improv_ns.enum("Error") State = improv_ns.enum("State") @@ -43,55 +48,63 @@ ESP32ImprovStoppedTrigger = esp32_improv_ns.class_( ) -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(ESP32ImprovComponent), - cv.Required(CONF_AUTHORIZER): cv.Any( - cv.none, cv.use_id(binary_sensor.BinarySensor) - ), - cv.Optional(CONF_STATUS_INDICATOR): cv.use_id(output.BinaryOutput), - cv.Optional( - CONF_IDENTIFY_DURATION, default="10s" - ): cv.positive_time_period_milliseconds, - cv.Optional( - CONF_AUTHORIZED_DURATION, default="1min" - ): cv.positive_time_period_milliseconds, - cv.Optional( - CONF_WIFI_TIMEOUT, default="1min" - ): cv.positive_time_period_milliseconds, - cv.Optional(CONF_ON_PROVISIONED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - ESP32ImprovProvisionedTrigger - ), - } - ), - cv.Optional(CONF_ON_PROVISIONING): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - ESP32ImprovProvisioningTrigger - ), - } - ), - cv.Optional(CONF_ON_START): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESP32ImprovStartTrigger), - } - ), - cv.Optional(CONF_ON_STATE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESP32ImprovStateTrigger), - } - ), - cv.Optional(CONF_ON_STOP): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - ESP32ImprovStoppedTrigger - ), - } - ), - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ESP32ImprovComponent), + cv.Required(CONF_AUTHORIZER): cv.Any( + cv.none, cv.use_id(binary_sensor.BinarySensor) + ), + cv.Optional(CONF_STATUS_INDICATOR): cv.use_id(output.BinaryOutput), + cv.Optional( + CONF_IDENTIFY_DURATION, default="10s" + ): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_AUTHORIZED_DURATION, default="1min" + ): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_WIFI_TIMEOUT, default=DEFAULT_WIFI_TIMEOUT + ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_ON_PROVISIONED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + ESP32ImprovProvisionedTrigger + ), + } + ), + cv.Optional(CONF_ON_PROVISIONING): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + ESP32ImprovProvisioningTrigger + ), + } + ), + cv.Optional(CONF_ON_START): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + ESP32ImprovStartTrigger + ), + } + ), + cv.Optional(CONF_ON_STATE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + ESP32ImprovStateTrigger + ), + } + ), + cv.Optional(CONF_ON_STOP): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + ESP32ImprovStoppedTrigger + ), + } + ), + } + ) + .extend(improv_base.IMPROV_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) async def to_code(config): @@ -102,7 +115,8 @@ async def to_code(config): await cg.register_component(var, config) cg.add_define("USE_IMPROV") - cg.add_library("improv/Improv", "1.2.4") + + await improv_base.setup_improv_core(var, config, "esp32_improv") cg.add(var.set_identify_duration(config[CONF_IDENTIFY_DURATION])) cg.add(var.set_authorized_duration(config[CONF_AUTHORIZED_DURATION])) diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index d41094fda1..0ad54bbb15 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -1,10 +1,10 @@ #include "esp32_improv_component.h" +#include "esphome/components/bytebuffer/bytebuffer.h" #include "esphome/components/esp32_ble/ble.h" #include "esphome/components/esp32_ble_server/ble_2902.h" #include "esphome/core/application.h" #include "esphome/core/log.h" -#include "esphome/components/bytebuffer/bytebuffer.h" #ifdef USE_ESP32 @@ -15,6 +15,15 @@ using namespace bytebuffer; static const char *const TAG = "esp32_improv.component"; static const char *const ESPHOME_MY_LINK = "https://my.home-assistant.io/redirect/config_flow_start?domain=esphome"; +static constexpr uint16_t STOP_ADVERTISING_DELAY = + 10000; // Delay (ms) before stopping service to allow BLE clients to read the final state +static constexpr uint16_t NAME_ADVERTISING_INTERVAL = 60000; // Advertise name every 60 seconds +static constexpr uint16_t NAME_ADVERTISING_DURATION = 1000; // Advertise name for 1 second + +// Improv service data constants +static constexpr uint8_t IMPROV_SERVICE_DATA_SIZE = 8; +static constexpr uint8_t IMPROV_PROTOCOL_ID_1 = 0x77; // 'P' << 1 | 'R' >> 7 +static constexpr uint8_t IMPROV_PROTOCOL_ID_2 = 0x46; // 'I' << 1 | 'M' >> 7 ESP32ImprovComponent::ESP32ImprovComponent() { global_improv_component = this; } @@ -29,8 +38,10 @@ void ESP32ImprovComponent::setup() { }); } #endif - global_ble_server->on(BLEServerEvt::EmptyEvt::ON_DISCONNECT, - [this](uint16_t conn_id) { this->set_error_(improv::ERROR_NONE); }); + global_ble_server->on_disconnect([this](uint16_t conn_id) { this->set_error_(improv::ERROR_NONE); }); + + // Start with loop disabled - will be enabled by start() when needed + this->disable_loop(); } void ESP32ImprovComponent::setup_characteristics() { @@ -45,12 +56,11 @@ void ESP32ImprovComponent::setup_characteristics() { this->error_->add_descriptor(error_descriptor); this->rpc_ = this->service_->create_characteristic(improv::RPC_COMMAND_UUID, BLECharacteristic::PROPERTY_WRITE); - this->rpc_->EventEmitter, uint16_t>::on( - BLECharacteristicEvt::VectorEvt::ON_WRITE, [this](const std::vector &data, uint16_t id) { - if (!data.empty()) { - this->incoming_data_.insert(this->incoming_data_.end(), data.begin(), data.end()); - } - }); + this->rpc_->on_write([this](std::span data, uint16_t id) { + if (!data.empty()) { + this->incoming_data_.insert(this->incoming_data_.end(), data.begin(), data.end()); + } + }); BLEDescriptor *rpc_descriptor = new BLE2902(); this->rpc_->add_descriptor(rpc_descriptor); @@ -94,6 +104,11 @@ void ESP32ImprovComponent::loop() { this->process_incoming_data_(); uint32_t now = App.get_loop_component_start_time(); + // Check if we need to update advertising type + if (this->state_ != improv::STATE_STOPPED && this->state_ != improv::STATE_PROVISIONED) { + this->update_advertising_type_(); + } + switch (this->state_) { case improv::STATE_STOPPED: this->set_status_indicator_state_(false); @@ -102,10 +117,17 @@ void ESP32ImprovComponent::loop() { if (this->service_->is_created()) { this->service_->start(); } else if (this->service_->is_running()) { + // Start by advertising the device name first BEFORE setting any state + ESP_LOGV(TAG, "Starting with device name advertising"); + this->advertising_device_name_ = true; + this->last_name_adv_time_ = App.get_loop_component_start_time(); + esp32_ble::global_ble->advertising_set_service_data_and_name(std::span{}, true); esp32_ble::global_ble->advertising_start(); - this->set_state_(improv::STATE_AWAITING_AUTHORIZATION); + // Set initial state based on whether we have an authorizer + this->set_state_(this->get_initial_state_(), false); this->set_error_(improv::ERROR_NONE); + this->should_start_ = false; // Clear flag after starting ESP_LOGD(TAG, "Service started!"); } } @@ -115,54 +137,33 @@ void ESP32ImprovComponent::loop() { if (this->authorizer_ == nullptr || (this->authorized_start_ != 0 && ((now - this->authorized_start_) < this->authorized_duration_))) { this->set_state_(improv::STATE_AUTHORIZED); - } else -#else - { this->set_state_(improv::STATE_AUTHORIZED); } -#endif - { + } else { if (!this->check_identify_()) this->set_status_indicator_state_(true); } +#else + this->set_state_(improv::STATE_AUTHORIZED); +#endif + this->check_wifi_connection_(); break; } case improv::STATE_AUTHORIZED: { #ifdef USE_BINARY_SENSOR - if (this->authorizer_ != nullptr) { - if (now - this->authorized_start_ > this->authorized_duration_) { - ESP_LOGD(TAG, "Authorization timeout"); - this->set_state_(improv::STATE_AWAITING_AUTHORIZATION); - return; - } + if (this->authorizer_ != nullptr && now - this->authorized_start_ > this->authorized_duration_) { + ESP_LOGD(TAG, "Authorization timeout"); + this->set_state_(improv::STATE_AWAITING_AUTHORIZATION); + return; } #endif if (!this->check_identify_()) { this->set_status_indicator_state_((now % 1000) < 500); } + this->check_wifi_connection_(); break; } case improv::STATE_PROVISIONING: { this->set_status_indicator_state_((now % 200) < 100); - if (wifi::global_wifi_component->is_connected()) { - wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(), - this->connecting_sta_.get_password()); - this->connecting_sta_ = {}; - this->cancel_timeout("wifi-connect-timeout"); - this->set_state_(improv::STATE_PROVISIONED); - - std::vector urls = {ESPHOME_MY_LINK}; -#ifdef USE_WEBSERVER - for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) { - if (ip.is_ip4()) { - std::string webserver_url = "http://" + ip.str() + ":" + to_string(USE_WEBSERVER_PORT); - urls.push_back(webserver_url); - break; - } - } -#endif - std::vector data = improv::build_rpc_response(improv::WIFI_SETTINGS, urls); - this->send_response_(data); - this->stop(); - } + this->check_wifi_connection_(); break; } case improv::STATE_PROVISIONED: { @@ -190,6 +191,25 @@ void ESP32ImprovComponent::set_status_indicator_state_(bool state) { #endif } +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG +const char *ESP32ImprovComponent::state_to_string_(improv::State state) { + switch (state) { + case improv::STATE_STOPPED: + return "STOPPED"; + case improv::STATE_AWAITING_AUTHORIZATION: + return "AWAITING_AUTHORIZATION"; + case improv::STATE_AUTHORIZED: + return "AUTHORIZED"; + case improv::STATE_PROVISIONING: + return "PROVISIONING"; + case improv::STATE_PROVISIONED: + return "PROVISIONED"; + default: + return "UNKNOWN"; + } +} +#endif + bool ESP32ImprovComponent::check_identify_() { uint32_t now = millis(); @@ -202,32 +222,34 @@ bool ESP32ImprovComponent::check_identify_() { return identify; } -void ESP32ImprovComponent::set_state_(improv::State state) { - ESP_LOGV(TAG, "Setting state: %d", state); +void ESP32ImprovComponent::set_state_(improv::State state, bool update_advertising) { + // Skip if state hasn't changed + if (this->state_ == state) { + return; + } + +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG + ESP_LOGD(TAG, "State transition: %s (0x%02X) -> %s (0x%02X)", this->state_to_string_(this->state_), this->state_, + this->state_to_string_(state), state); +#endif this->state_ = state; - if (this->status_->get_value().empty() || this->status_->get_value()[0] != state) { + if (this->status_ != nullptr && (this->status_->get_value().empty() || this->status_->get_value()[0] != state)) { this->status_->set_value(ByteBuffer::wrap(static_cast(state))); if (state != improv::STATE_STOPPED) this->status_->notify(); } - std::vector service_data(8, 0); - service_data[0] = 0x77; // PR - service_data[1] = 0x46; // IM - service_data[2] = static_cast(state); - - uint8_t capabilities = 0x00; -#ifdef USE_OUTPUT - if (this->status_indicator_ != nullptr) - capabilities |= improv::CAPABILITY_IDENTIFY; -#endif - - service_data[3] = capabilities; - service_data[4] = 0x00; // Reserved - service_data[5] = 0x00; // Reserved - service_data[6] = 0x00; // Reserved - service_data[7] = 0x00; // Reserved - - esp32_ble::global_ble->advertising_set_service_data(service_data); + // Only advertise valid Improv states (0x01-0x04). + // STATE_STOPPED (0x00) is internal only and not part of the Improv spec. + // Advertising 0x00 causes undefined behavior in some clients and makes them + // repeatedly connect trying to determine the actual state. + if (state != improv::STATE_STOPPED && update_advertising) { + // State change always overrides name advertising and resets the timer + this->advertising_device_name_ = false; + // Reset the timer so we wait another 60 seconds before advertising name + this->last_name_adv_time_ = App.get_loop_component_start_time(); + // Advertise the new state via service data + this->advertise_service_data_(); + } #ifdef USE_ESP32_IMPROV_STATE_CALLBACK this->state_callback_.call(this->state_, this->error_state_); #endif @@ -237,15 +259,20 @@ void ESP32ImprovComponent::set_error_(improv::Error error) { if (error != improv::ERROR_NONE) { ESP_LOGE(TAG, "Error: %d", error); } - if (this->error_->get_value().empty() || this->error_->get_value()[0] != error) { + // The error_ characteristic is initialized in setup_characteristics() which is called + // from the loop, while the BLE disconnect callback is registered in setup(). + // error_ can be nullptr if: + // 1. A client connects/disconnects before setup_characteristics() is called + // 2. The device is already provisioned so the service never starts (should_start_ is false) + if (this->error_ != nullptr && (this->error_->get_value().empty() || this->error_->get_value()[0] != error)) { this->error_->set_value(ByteBuffer::wrap(static_cast(error))); if (this->state_ != improv::STATE_STOPPED) this->error_->notify(); } } -void ESP32ImprovComponent::send_response_(std::vector &response) { - this->rpc_response_->set_value(ByteBuffer::wrap(response)); +void ESP32ImprovComponent::send_response_(std::vector &&response) { + this->rpc_response_->set_value(std::move(response)); if (this->state_ != improv::STATE_STOPPED) this->rpc_response_->notify(); } @@ -261,7 +288,10 @@ void ESP32ImprovComponent::start() { void ESP32ImprovComponent::stop() { this->should_start_ = false; - this->set_timeout("end-service", 1000, [this] { + // Wait before stopping the service to ensure all BLE clients see the state change. + // This prevents clients from repeatedly reconnecting and wasting resources by allowing + // them to observe that the device is provisioned before the service disappears. + this->set_timeout("end-service", STOP_ADVERTISING_DELAY, [this] { if (this->state_ == improv::STATE_STOPPED || this->service_ == nullptr) return; this->service_->stop(); @@ -307,7 +337,7 @@ void ESP32ImprovComponent::process_incoming_data_() { this->connecting_sta_ = sta; wifi::global_wifi_component->set_sta(sta); - wifi::global_wifi_component->start_connecting(sta, false); + wifi::global_wifi_component->start_connecting(sta); this->set_state_(improv::STATE_PROVISIONING); ESP_LOGD(TAG, "Received Improv Wi-Fi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(), command.password.c_str()); @@ -345,6 +375,105 @@ void ESP32ImprovComponent::on_wifi_connect_timeout_() { wifi::global_wifi_component->clear_sta(); } +void ESP32ImprovComponent::check_wifi_connection_() { + if (!wifi::global_wifi_component->is_connected()) { + return; + } + + if (this->state_ == improv::STATE_PROVISIONING) { + wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(), this->connecting_sta_.get_password()); + this->connecting_sta_ = {}; + this->cancel_timeout("wifi-connect-timeout"); + + // Build URL list with minimal allocations + // Maximum 3 URLs: custom next_url + ESPHOME_MY_LINK + webserver URL + std::string url_strings[3]; + size_t url_count = 0; + +#ifdef USE_ESP32_IMPROV_NEXT_URL + // Add next_url if configured (should be first per Improv BLE spec) + std::string next_url = this->get_formatted_next_url_(); + if (!next_url.empty()) { + url_strings[url_count++] = std::move(next_url); + } +#endif + + // Add default URLs for backward compatibility + url_strings[url_count++] = ESPHOME_MY_LINK; +#ifdef USE_WEBSERVER + for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) { + if (ip.is_ip4()) { + char url_buffer[64]; + snprintf(url_buffer, sizeof(url_buffer), "http://%s:%d", ip.str().c_str(), USE_WEBSERVER_PORT); + url_strings[url_count++] = url_buffer; + break; + } + } +#endif + this->send_response_(improv::build_rpc_response(improv::WIFI_SETTINGS, + std::vector(url_strings, url_strings + url_count))); + } else if (this->is_active() && this->state_ != improv::STATE_PROVISIONED) { + ESP_LOGD(TAG, "WiFi provisioned externally"); + } + + this->set_state_(improv::STATE_PROVISIONED); + this->stop(); +} + +void ESP32ImprovComponent::advertise_service_data_() { + uint8_t service_data[IMPROV_SERVICE_DATA_SIZE] = {}; + service_data[0] = IMPROV_PROTOCOL_ID_1; // PR + service_data[1] = IMPROV_PROTOCOL_ID_2; // IM + service_data[2] = static_cast(this->state_); + + uint8_t capabilities = 0x00; +#ifdef USE_OUTPUT + if (this->status_indicator_ != nullptr) + capabilities |= improv::CAPABILITY_IDENTIFY; +#endif + + service_data[3] = capabilities; + // service_data[4-7] are already 0 (Reserved) + + // Atomically set service data and disable name in advertising + esp32_ble::global_ble->advertising_set_service_data_and_name(std::span(service_data), false); +} + +void ESP32ImprovComponent::update_advertising_type_() { + uint32_t now = App.get_loop_component_start_time(); + + // If we're advertising the device name and it's been more than NAME_ADVERTISING_DURATION, switch back to service data + if (this->advertising_device_name_) { + if (now - this->last_name_adv_time_ >= NAME_ADVERTISING_DURATION) { + ESP_LOGV(TAG, "Switching back to service data advertising"); + this->advertising_device_name_ = false; + // Restore service data advertising + this->advertise_service_data_(); + } + return; + } + + // Check if it's time to advertise the device name (every NAME_ADVERTISING_INTERVAL) + if (now - this->last_name_adv_time_ >= NAME_ADVERTISING_INTERVAL) { + ESP_LOGV(TAG, "Switching to device name advertising"); + this->advertising_device_name_ = true; + this->last_name_adv_time_ = now; + + // Atomically clear service data and enable name in advertising data + esp32_ble::global_ble->advertising_set_service_data_and_name(std::span{}, true); + } +} + +improv::State ESP32ImprovComponent::get_initial_state_() const { +#ifdef USE_BINARY_SENSOR + // If we have an authorizer, start in awaiting authorization state + return this->authorizer_ == nullptr ? improv::STATE_AUTHORIZED : improv::STATE_AWAITING_AUTHORIZATION; +#else + // No binary_sensor support = no authorizer possible, start as authorized + return improv::STATE_AUTHORIZED; +#endif +} + ESP32ImprovComponent *global_improv_component = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) } // namespace esp32_improv diff --git a/esphome/components/esp32_improv/esp32_improv_component.h b/esphome/components/esp32_improv/esp32_improv_component.h index 87cec23876..8f4cfd7958 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.h +++ b/esphome/components/esp32_improv/esp32_improv_component.h @@ -7,6 +7,7 @@ #include "esphome/components/esp32_ble_server/ble_characteristic.h" #include "esphome/components/esp32_ble_server/ble_server.h" +#include "esphome/components/improv_base/improv_base.h" #include "esphome/components/wifi/wifi_component.h" #ifdef USE_ESP32_IMPROV_STATE_CALLBACK @@ -32,7 +33,7 @@ namespace esp32_improv { using namespace esp32_ble_server; -class ESP32ImprovComponent : public Component { +class ESP32ImprovComponent : public Component, public improv_base::ImprovBase { public: ESP32ImprovComponent(); void dump_config() override; @@ -44,6 +45,7 @@ class ESP32ImprovComponent : public Component { void start(); void stop(); bool is_active() const { return this->state_ != improv::STATE_STOPPED; } + bool should_start() const { return this->should_start_; } #ifdef USE_ESP32_IMPROV_STATE_CALLBACK void add_on_state_callback(std::function &&callback) { @@ -79,12 +81,12 @@ class ESP32ImprovComponent : public Component { std::vector incoming_data_; wifi::WiFiAP connecting_sta_; - BLEService *service_ = nullptr; - BLECharacteristic *status_; - BLECharacteristic *error_; - BLECharacteristic *rpc_; - BLECharacteristic *rpc_response_; - BLECharacteristic *capabilities_; + BLEService *service_{nullptr}; + BLECharacteristic *status_{nullptr}; + BLECharacteristic *error_{nullptr}; + BLECharacteristic *rpc_{nullptr}; + BLECharacteristic *rpc_response_{nullptr}; + BLECharacteristic *capabilities_{nullptr}; #ifdef USE_BINARY_SENSOR binary_sensor::BinarySensor *authorizer_{nullptr}; @@ -100,14 +102,23 @@ class ESP32ImprovComponent : public Component { #endif bool status_indicator_state_{false}; + uint32_t last_name_adv_time_{0}; + bool advertising_device_name_{false}; void set_status_indicator_state_(bool state); + void update_advertising_type_(); - void set_state_(improv::State state); + void set_state_(improv::State state, bool update_advertising = true); void set_error_(improv::Error error); - void send_response_(std::vector &response); + improv::State get_initial_state_() const; + void send_response_(std::vector &&response); void process_incoming_data_(); void on_wifi_connect_timeout_(); + void check_wifi_connection_(); bool check_identify_(); + void advertise_service_data_(); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG + const char *state_to_string_(improv::State state); +#endif }; // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/esp32_rmt_led_strip/led_strip.cpp b/esphome/components/esp32_rmt_led_strip/led_strip.cpp index e22bb605e2..2c7963b366 100644 --- a/esphome/components/esp32_rmt_led_strip/led_strip.cpp +++ b/esphome/components/esp32_rmt_led_strip/led_strip.cpp @@ -35,16 +35,18 @@ static size_t IRAM_ATTR HOT encoder_callback(const void *data, size_t size, size if (symbols_free < RMT_SYMBOLS_PER_BYTE) { return 0; } - for (int32_t i = 0; i < RMT_SYMBOLS_PER_BYTE; i++) { + for (size_t i = 0; i < RMT_SYMBOLS_PER_BYTE; i++) { if (bytes[index] & (1 << (7 - i))) { symbols[i] = params->bit1; } else { symbols[i] = params->bit0; } } +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) if ((index + 1) >= size && params->reset.duration0 == 0 && params->reset.duration1 == 0) { *done = true; } +#endif return RMT_SYMBOLS_PER_BYTE; } @@ -110,7 +112,7 @@ void ESP32RMTLEDStripLightOutput::setup() { memset(&encoder, 0, sizeof(encoder)); encoder.callback = encoder_callback; encoder.arg = &this->params_; - encoder.min_chunk_size = 8; + encoder.min_chunk_size = RMT_SYMBOLS_PER_BYTE; if (rmt_new_simple_encoder(&encoder, &this->encoder_) != ESP_OK) { ESP_LOGE(TAG, "Encoder creation failed"); this->mark_failed(); diff --git a/esphome/components/esp32_touch/esp32_touch.h b/esphome/components/esp32_touch/esp32_touch.h index 5a91b1c750..fb1973e26f 100644 --- a/esphome/components/esp32_touch/esp32_touch.h +++ b/esphome/components/esp32_touch/esp32_touch.h @@ -171,8 +171,8 @@ class ESP32TouchComponent : public Component { // based on the filter configuration uint32_t read_touch_value(touch_pad_t pad) const; - // Helper to update touch state with a known state - void update_touch_state_(ESP32TouchBinarySensor *child, bool is_touched); + // Helper to update touch state with a known state and value + void update_touch_state_(ESP32TouchBinarySensor *child, bool is_touched, uint32_t value); // Helper to read touch value and update state for a given child bool check_and_update_touch_state_(ESP32TouchBinarySensor *child); @@ -234,9 +234,13 @@ class ESP32TouchBinarySensor : public binary_sensor::BinarySensor { touch_pad_t get_touch_pad() const { return this->touch_pad_; } uint32_t get_threshold() const { return this->threshold_; } void set_threshold(uint32_t threshold) { this->threshold_ = threshold; } -#ifdef USE_ESP32_VARIANT_ESP32 + + /// Get the raw touch measurement value. + /// @note Although this method may appear unused within the component, it is a public API + /// used by lambdas in user configurations for custom touch value processing. + /// @return The current raw touch sensor reading uint32_t get_value() const { return this->value_; } -#endif + uint32_t get_wakeup_threshold() const { return this->wakeup_threshold_; } protected: @@ -245,9 +249,8 @@ class ESP32TouchBinarySensor : public binary_sensor::BinarySensor { touch_pad_t touch_pad_{TOUCH_PAD_MAX}; uint32_t threshold_{0}; uint32_t benchmark_{}; -#ifdef USE_ESP32_VARIANT_ESP32 + /// Stores the last raw touch measurement value. uint32_t value_{0}; -#endif bool last_state_{false}; const uint32_t wakeup_threshold_{0}; diff --git a/esphome/components/esp32_touch/esp32_touch_common.cpp b/esphome/components/esp32_touch/esp32_touch_common.cpp index 2d93de077e..a0b1df38c1 100644 --- a/esphome/components/esp32_touch/esp32_touch_common.cpp +++ b/esphome/components/esp32_touch/esp32_touch_common.cpp @@ -100,6 +100,8 @@ void ESP32TouchComponent::process_setup_mode_logging_(uint32_t now) { #else // Read the value being used for touch detection uint32_t value = this->read_touch_value(child->get_touch_pad()); + // Store the value for get_value() access in lambdas + child->value_ = value; ESP_LOGD(TAG, "Touch Pad '%s' (T%d): %d", child->get_name().c_str(), child->get_touch_pad(), value); #endif } diff --git a/esphome/components/esp32_touch/esp32_touch_v2.cpp b/esphome/components/esp32_touch/esp32_touch_v2.cpp index afd2655fd7..9662b009f6 100644 --- a/esphome/components/esp32_touch/esp32_touch_v2.cpp +++ b/esphome/components/esp32_touch/esp32_touch_v2.cpp @@ -10,8 +10,11 @@ namespace esp32_touch { static const char *const TAG = "esp32_touch"; -// Helper to update touch state with a known state -void ESP32TouchComponent::update_touch_state_(ESP32TouchBinarySensor *child, bool is_touched) { +// Helper to update touch state with a known state and value +void ESP32TouchComponent::update_touch_state_(ESP32TouchBinarySensor *child, bool is_touched, uint32_t value) { + // Store the value for get_value() access in lambdas + child->value_ = value; + // Always update timer when touched if (is_touched) { child->last_touch_time_ = App.get_loop_component_start_time(); @@ -21,9 +24,8 @@ void ESP32TouchComponent::update_touch_state_(ESP32TouchBinarySensor *child, boo child->last_state_ = is_touched; child->publish_state(is_touched); if (is_touched) { - // ESP32-S2/S3 v2: touched when value > threshold ESP_LOGV(TAG, "Touch Pad '%s' state: ON (value: %" PRIu32 " > threshold: %" PRIu32 ")", child->get_name().c_str(), - this->read_touch_value(child->touch_pad_), child->threshold_ + child->benchmark_); + value, child->threshold_ + child->benchmark_); } else { ESP_LOGV(TAG, "Touch Pad '%s' state: OFF", child->get_name().c_str()); } @@ -41,7 +43,7 @@ bool ESP32TouchComponent::check_and_update_touch_state_(ESP32TouchBinarySensor * child->get_name().c_str(), child->touch_pad_, value, child->threshold_, child->benchmark_); bool is_touched = value > child->benchmark_ + child->threshold_; - this->update_touch_state_(child, is_touched); + this->update_touch_state_(child, is_touched, value); return is_touched; } @@ -296,7 +298,9 @@ void ESP32TouchComponent::loop() { this->check_and_update_touch_state_(child); } else if (event.intr_mask & TOUCH_PAD_INTR_MASK_ACTIVE) { // We only get ACTIVE interrupts now, releases are detected by timeout - this->update_touch_state_(child, true); // Always touched for ACTIVE interrupts + // Read the current value + uint32_t value = this->read_touch_value(child->touch_pad_); + this->update_touch_state_(child, true, value); // Always touched for ACTIVE interrupts } break; } diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 33a4149571..a74f9ee8ce 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -1,5 +1,5 @@ import logging -import os +from pathlib import Path import esphome.codegen as cg import esphome.config_validation as cv @@ -17,7 +17,7 @@ from esphome.const import ( PLATFORM_ESP8266, ThreadModel, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.helpers import copy_file_if_changed from .boards import BOARDS, ESP8266_LD_SCRIPTS @@ -176,7 +176,7 @@ CONFIG_SCHEMA = cv.All( ) -@coroutine_with_priority(1000) +@coroutine_with_priority(CoroPriority.PLATFORM) async def to_code(config): cg.add(esp8266_ns.setup_preferences()) @@ -190,7 +190,9 @@ async def to_code(config): cg.add_define("ESPHOME_VARIANT", "ESP8266") cg.add_define(ThreadModel.SINGLE) - cg.add_platformio_option("extra_scripts", ["post:post_build.py"]) + cg.add_platformio_option( + "extra_scripts", ["pre:testing_mode.py", "post:post_build.py"] + ) conf = config[CONF_FRAMEWORK] cg.add_platformio_option("framework", "arduino") @@ -230,6 +232,12 @@ async def to_code(config): # For cases where nullptrs can be handled, use nothrow: `new (std::nothrow) T;` cg.add_build_flag("-DNEW_OOM_ABORT") + # In testing mode, fake larger memory to allow linking grouped component tests + # Real ESP8266 hardware only has 32KB IRAM and ~80KB RAM, but for CI testing + # we pretend it has much larger memory to test that components compile together + if CORE.testing_mode: + cg.add_build_flag("-DESPHOME_TESTING_MODE") + cg.add_platformio_option("board_build.flash_mode", config[CONF_BOARD_FLASH_MODE]) ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] @@ -259,9 +267,14 @@ async def to_code(config): # Called by writer.py def copy_files(): - dir = os.path.dirname(__file__) - post_build_file = os.path.join(dir, "post_build.py.script") + dir = Path(__file__).parent + post_build_file = dir / "post_build.py.script" copy_file_if_changed( post_build_file, CORE.relative_build_path("post_build.py"), ) + testing_mode_file = dir / "testing_mode.py.script" + copy_file_if_changed( + testing_mode_file, + CORE.relative_build_path("testing_mode.py"), + ) diff --git a/esphome/components/esp8266/core.cpp b/esphome/components/esp8266/core.cpp index 2d3959b031..200ca567c2 100644 --- a/esphome/components/esp8266/core.cpp +++ b/esphome/components/esp8266/core.cpp @@ -58,8 +58,8 @@ extern "C" void resetPins() { // NOLINT #ifdef USE_ESP8266_EARLY_PIN_INIT for (int i = 0; i < 16; i++) { - uint8_t mode = ESPHOME_ESP8266_GPIO_INITIAL_MODE[i]; - uint8_t level = ESPHOME_ESP8266_GPIO_INITIAL_LEVEL[i]; + uint8_t mode = progmem_read_byte(&ESPHOME_ESP8266_GPIO_INITIAL_MODE[i]); + uint8_t level = progmem_read_byte(&ESPHOME_ESP8266_GPIO_INITIAL_LEVEL[i]); if (mode != 255) pinMode(i, mode); // NOLINT if (level != 255) diff --git a/esphome/components/esp8266/core.h b/esphome/components/esp8266/core.h index ac33305669..1abe67be86 100644 --- a/esphome/components/esp8266/core.h +++ b/esphome/components/esp8266/core.h @@ -7,8 +7,6 @@ extern const uint8_t ESPHOME_ESP8266_GPIO_INITIAL_MODE[16]; extern const uint8_t ESPHOME_ESP8266_GPIO_INITIAL_LEVEL[16]; -namespace esphome { -namespace esp8266 {} // namespace esp8266 -} // namespace esphome +namespace esphome::esp8266 {} // namespace esphome::esp8266 #endif // USE_ESP8266 diff --git a/esphome/components/esp8266/gpio.cpp b/esphome/components/esp8266/gpio.cpp index ee3683c67d..17a495bc1d 100644 --- a/esphome/components/esp8266/gpio.cpp +++ b/esphome/components/esp8266/gpio.cpp @@ -3,8 +3,7 @@ #include "gpio.h" #include "esphome/core/log.h" -namespace esphome { -namespace esp8266 { +namespace esphome::esp8266 { static const char *const TAG = "esp8266"; @@ -110,9 +109,11 @@ void ESP8266GPIOPin::digital_write(bool value) { } void ESP8266GPIOPin::detach_interrupt() const { detachInterrupt(pin_); } -} // namespace esp8266 +} // namespace esphome::esp8266 -using namespace esp8266; +namespace esphome { + +using esp8266::ISRPinArg; bool IRAM_ATTR ISRInternalGPIOPin::digital_read() { auto *arg = reinterpret_cast(this->arg_); diff --git a/esphome/components/esp8266/gpio.h b/esphome/components/esp8266/gpio.h index dd6407885e..213a5c54be 100644 --- a/esphome/components/esp8266/gpio.h +++ b/esphome/components/esp8266/gpio.h @@ -5,8 +5,7 @@ #include "esphome/core/hal.h" #include -namespace esphome { -namespace esp8266 { +namespace esphome::esp8266 { class ESP8266GPIOPin : public InternalGPIOPin { public: @@ -29,11 +28,10 @@ class ESP8266GPIOPin : public InternalGPIOPin { void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; uint8_t pin_; - bool inverted_; - gpio::Flags flags_; + bool inverted_{}; + gpio::Flags flags_{}; }; -} // namespace esp8266 -} // namespace esphome +} // namespace esphome::esp8266 #endif // USE_ESP8266 diff --git a/esphome/components/esp8266/gpio.py b/esphome/components/esp8266/gpio.py index 050efaacae..2e8d6496bc 100644 --- a/esphome/components/esp8266/gpio.py +++ b/esphome/components/esp8266/gpio.py @@ -17,7 +17,7 @@ from esphome.const import ( CONF_PULLUP, PLATFORM_ESP8266, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority from . import boards from .const import KEY_BOARD, KEY_ESP8266, KEY_PIN_INITIAL_STATES, esp8266_ns @@ -165,7 +165,10 @@ async def esp8266_pin_to_code(config): num = config[CONF_NUMBER] mode = config[CONF_MODE] cg.add(var.set_pin(num)) - cg.add(var.set_inverted(config[CONF_INVERTED])) + # Only set if true to avoid bloating setup() function + # (inverted bit in pin_flags_ bitfield is zero-initialized to false) + if config[CONF_INVERTED]: + cg.add(var.set_inverted(True)) cg.add(var.set_flags(pins.gpio_flags_expr(mode))) if num < 16: initial_state: PinInitialState = CORE.data[KEY_ESP8266][KEY_PIN_INITIAL_STATES][ @@ -188,7 +191,7 @@ async def esp8266_pin_to_code(config): return var -@coroutine_with_priority(-999.0) +@coroutine_with_priority(CoroPriority.WORKAROUNDS) async def add_pin_initial_states_array(): # Add includes at the very end, so that they override everything initial_states: list[PinInitialState] = CORE.data[KEY_ESP8266][ @@ -199,11 +202,11 @@ async def add_pin_initial_states_array(): cg.add_global( cg.RawExpression( - f"const uint8_t ESPHOME_ESP8266_GPIO_INITIAL_MODE[16] = {{{initial_modes_s}}}" + f"const uint8_t ESPHOME_ESP8266_GPIO_INITIAL_MODE[16] PROGMEM = {{{initial_modes_s}}}" ) ) cg.add_global( cg.RawExpression( - f"const uint8_t ESPHOME_ESP8266_GPIO_INITIAL_LEVEL[16] = {{{initial_levels_s}}}" + f"const uint8_t ESPHOME_ESP8266_GPIO_INITIAL_LEVEL[16] PROGMEM = {{{initial_levels_s}}}" ) ) diff --git a/esphome/components/esp8266/preferences.cpp b/esphome/components/esp8266/preferences.cpp index efd226e8f8..197d244dc4 100644 --- a/esphome/components/esp8266/preferences.cpp +++ b/esphome/components/esp8266/preferences.cpp @@ -1,6 +1,7 @@ #ifdef USE_ESP8266 #include +#include extern "C" { #include "spi_flash.h" } @@ -12,26 +13,26 @@ extern "C" { #include "preferences.h" #include -#include +#include -namespace esphome { -namespace esp8266 { +namespace esphome::esp8266 { static const char *const TAG = "esp8266.preferences"; -static bool s_prevent_write = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static uint32_t *s_flash_storage = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static bool s_prevent_write = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static bool s_flash_dirty = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static const uint32_t ESP_RTC_USER_MEM_START = 0x60001200; +static constexpr uint32_t ESP_RTC_USER_MEM_START = 0x60001200; +static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_WORDS = 128; +static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_BYTES = ESP_RTC_USER_MEM_SIZE_WORDS * 4; + #define ESP_RTC_USER_MEM ((uint32_t *) ESP_RTC_USER_MEM_START) -static const uint32_t ESP_RTC_USER_MEM_SIZE_WORDS = 128; -static const uint32_t ESP_RTC_USER_MEM_SIZE_BYTES = ESP_RTC_USER_MEM_SIZE_WORDS * 4; #ifdef USE_ESP8266_PREFERENCES_FLASH -static const uint32_t ESP8266_FLASH_STORAGE_SIZE = 128; +static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = 128; #else -static const uint32_t ESP8266_FLASH_STORAGE_SIZE = 64; +static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = 64; #endif static inline bool esp_rtc_user_mem_read(uint32_t index, uint32_t *dest) { @@ -67,6 +68,8 @@ static uint32_t get_esp8266_flash_sector() { } static uint32_t get_esp8266_flash_address() { return get_esp8266_flash_sector() * SPI_FLASH_SEC_SIZE; } +static inline size_t bytes_to_words(size_t bytes) { return (bytes + 3) / 4; } + template uint32_t calculate_crc(It first, It last, uint32_t type) { uint32_t crc = type; while (first != last) { @@ -117,47 +120,42 @@ static bool load_from_rtc(size_t offset, uint32_t *data, size_t len) { class ESP8266PreferenceBackend : public ESPPreferenceBackend { public: - size_t offset = 0; uint32_t type = 0; + uint16_t offset = 0; + uint8_t length_words = 0; // Max 255 words (1020 bytes of data) bool in_flash = false; - size_t length_words = 0; bool save(const uint8_t *data, size_t len) override { - if ((len + 3) / 4 != length_words) { + if (bytes_to_words(len) != length_words) { return false; } - std::vector buffer; - buffer.resize(length_words + 1); - memcpy(buffer.data(), data, len); - buffer[buffer.size() - 1] = calculate_crc(buffer.begin(), buffer.end() - 1, type); + size_t buffer_size = static_cast(length_words) + 1; + std::unique_ptr buffer(new uint32_t[buffer_size]()); // Note the () for zero-initialization + memcpy(buffer.get(), data, len); + buffer[length_words] = calculate_crc(buffer.get(), buffer.get() + length_words, type); if (in_flash) { - return save_to_flash(offset, buffer.data(), buffer.size()); - } else { - return save_to_rtc(offset, buffer.data(), buffer.size()); + return save_to_flash(offset, buffer.get(), buffer_size); } + return save_to_rtc(offset, buffer.get(), buffer_size); } bool load(uint8_t *data, size_t len) override { - if ((len + 3) / 4 != length_words) { + if (bytes_to_words(len) != length_words) { return false; } - std::vector buffer; - buffer.resize(length_words + 1); - bool ret; - if (in_flash) { - ret = load_from_flash(offset, buffer.data(), buffer.size()); - } else { - ret = load_from_rtc(offset, buffer.data(), buffer.size()); - } + size_t buffer_size = static_cast(length_words) + 1; + std::unique_ptr buffer(new uint32_t[buffer_size]()); + bool ret = in_flash ? load_from_flash(offset, buffer.get(), buffer_size) + : load_from_rtc(offset, buffer.get(), buffer_size); if (!ret) return false; - uint32_t crc = calculate_crc(buffer.begin(), buffer.end() - 1, type); - if (buffer[buffer.size() - 1] != crc) { + uint32_t crc = calculate_crc(buffer.get(), buffer.get() + length_words, type); + if (buffer[length_words] != crc) { return false; } - memcpy(data, buffer.data(), len); + memcpy(data, buffer.get(), len); return true; } }; @@ -178,16 +176,20 @@ class ESP8266Preferences : public ESPPreferences { } ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) override { - uint32_t length_words = (length + 3) / 4; + uint32_t length_words = bytes_to_words(length); + if (length_words > 255) { + ESP_LOGE(TAG, "Preference too large: %" PRIu32 " words > 255", length_words); + return {}; + } if (in_flash) { uint32_t start = current_flash_offset; uint32_t end = start + length_words + 1; if (end > ESP8266_FLASH_STORAGE_SIZE) return {}; auto *pref = new ESP8266PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory) - pref->offset = start; + pref->offset = static_cast(start); pref->type = type; - pref->length_words = length_words; + pref->length_words = static_cast(length_words); pref->in_flash = true; current_flash_offset = end; return {pref}; @@ -213,9 +215,9 @@ class ESP8266Preferences : public ESPPreferences { uint32_t rtc_offset = in_normal ? start + 32 : start - 96; auto *pref = new ESP8266PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory) - pref->offset = rtc_offset; + pref->offset = static_cast(rtc_offset); pref->type = type; - pref->length_words = length_words; + pref->length_words = static_cast(length_words); pref->in_flash = false; current_offset += length_words + 1; return pref; @@ -282,10 +284,10 @@ void setup_preferences() { } void preferences_prevent_write(bool prevent) { s_prevent_write = prevent; } -} // namespace esp8266 +} // namespace esphome::esp8266 +namespace esphome { ESPPreferences *global_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - } // namespace esphome #endif // USE_ESP8266 diff --git a/esphome/components/esp8266/preferences.h b/esphome/components/esp8266/preferences.h index edec915794..16cf80a129 100644 --- a/esphome/components/esp8266/preferences.h +++ b/esphome/components/esp8266/preferences.h @@ -2,13 +2,11 @@ #ifdef USE_ESP8266 -namespace esphome { -namespace esp8266 { +namespace esphome::esp8266 { void setup_preferences(); void preferences_prevent_write(bool prevent); -} // namespace esp8266 -} // namespace esphome +} // namespace esphome::esp8266 #endif // USE_ESP8266 diff --git a/esphome/components/esp8266/testing_mode.py.script b/esphome/components/esp8266/testing_mode.py.script new file mode 100644 index 0000000000..44d84b765c --- /dev/null +++ b/esphome/components/esp8266/testing_mode.py.script @@ -0,0 +1,166 @@ +import os +import re + +# pylint: disable=E0602 +Import("env") # noqa + + +# Memory sizes for testing mode (allow larger builds for CI component grouping) +TESTING_IRAM_SIZE = "0x200000" # 2MB +TESTING_DRAM_SIZE = "0x200000" # 2MB +TESTING_FLASH_SIZE = "0x2000000" # 32MB + + +def patch_segment_size(content, segment_name, new_size, label): + """Patch a memory segment's length in linker script. + + Args: + content: Linker script content + segment_name: Name of the segment (e.g., 'iram1_0_seg') + new_size: New size as hex string (e.g., '0x200000') + label: Human-readable label for logging (e.g., 'IRAM') + + Returns: + Tuple of (patched_content, was_patched) + """ + # Match: segment_name : org = 0x..., len = 0x... + pattern = rf"({segment_name}\s*:\s*org\s*=\s*0x[0-9a-fA-F]+\s*,\s*len\s*=\s*)0x[0-9a-fA-F]+" + new_content = re.sub(pattern, rf"\g<1>{new_size}", content) + return new_content, new_content != content + + +def apply_memory_patches(content): + """Apply IRAM, DRAM, and Flash patches to linker script content. + + Args: + content: Linker script content as string + + Returns: + Patched content as string + """ + patches_applied = [] + + # Patch IRAM (for larger code in IRAM) + content, patched = patch_segment_size(content, "iram1_0_seg", TESTING_IRAM_SIZE, "IRAM") + if patched: + patches_applied.append("IRAM") + + # Patch DRAM (for larger BSS/data sections) + content, patched = patch_segment_size(content, "dram0_0_seg", TESTING_DRAM_SIZE, "DRAM") + if patched: + patches_applied.append("DRAM") + + # Patch Flash (for larger code sections) + content, patched = patch_segment_size(content, "irom0_0_seg", TESTING_FLASH_SIZE, "Flash") + if patched: + patches_applied.append("Flash") + + if patches_applied: + iram_mb = int(TESTING_IRAM_SIZE, 16) // (1024 * 1024) + dram_mb = int(TESTING_DRAM_SIZE, 16) // (1024 * 1024) + flash_mb = int(TESTING_FLASH_SIZE, 16) // (1024 * 1024) + print(f" Patched memory segments: {', '.join(patches_applied)} (IRAM/DRAM: {iram_mb}MB, Flash: {flash_mb}MB)") + + return content + + +def patch_linker_script_file(filepath, description): + """Patch a linker script file in the build directory with enlarged memory segments. + + This function modifies linker scripts in the build directory only (never SDK files). + It patches IRAM, DRAM, and Flash segments to allow larger builds in testing mode. + + Args: + filepath: Path to the linker script file in the build directory + description: Human-readable description for logging + + Returns: + True if the file was patched, False if already patched or not found + """ + if not os.path.exists(filepath): + print(f"ESPHome: {description} not found at {filepath}") + return False + + print(f"ESPHome: Patching {description}...") + with open(filepath, "r") as f: + content = f.read() + + patched_content = apply_memory_patches(content) + + if patched_content != content: + with open(filepath, "w") as f: + f.write(patched_content) + print(f"ESPHome: Successfully patched {description}") + return True + else: + print(f"ESPHome: {description} already patched or no changes needed") + return False + + +def patch_local_linker_script(source, target, env): + """Patch the local.eagle.app.v6.common.ld in build directory. + + This patches the preprocessed linker script that PlatformIO creates in the build + directory, enlarging IRAM, DRAM, and Flash segments for testing mode. + + Args: + source: SCons source nodes + target: SCons target nodes + env: SCons environment + """ + # Check if we're in testing mode + build_flags = env.get("BUILD_FLAGS", []) + testing_mode = any("-DESPHOME_TESTING_MODE" in flag for flag in build_flags) + + if not testing_mode: + return + + # Patch the local linker script if it exists + build_dir = env.subst("$BUILD_DIR") + ld_dir = os.path.join(build_dir, "ld") + if os.path.exists(ld_dir): + local_ld = os.path.join(ld_dir, "local.eagle.app.v6.common.ld") + if os.path.exists(local_ld): + patch_linker_script_file(local_ld, "local.eagle.app.v6.common.ld") + + +# Check if we're in testing mode +build_flags = env.get("BUILD_FLAGS", []) +testing_mode = any("-DESPHOME_TESTING_MODE" in flag for flag in build_flags) + +if testing_mode: + # Create a custom linker script in the build directory with patched memory limits + # This allows larger IRAM/DRAM/Flash for CI component grouping tests + build_dir = env.subst("$BUILD_DIR") + ldscript = env.GetProjectOption("board_build.ldscript", "") + assert ldscript, "No linker script configured in board_build.ldscript" + + framework_dir = env.PioPlatform().get_package_dir("framework-arduinoespressif8266") + assert framework_dir is not None, "Could not find framework-arduinoespressif8266 package" + + # Read the original SDK linker script (read-only, SDK is never modified) + sdk_ld = os.path.join(framework_dir, "tools", "sdk", "ld", ldscript) + # Create a custom version in the build directory (isolated, temporary) + custom_ld = os.path.join(build_dir, f"testing_{ldscript}") + + if os.path.exists(sdk_ld) and not os.path.exists(custom_ld): + # Read the SDK linker script + with open(sdk_ld, "r") as f: + content = f.read() + + # Apply memory patches (IRAM: 2MB, DRAM: 2MB, Flash: 32MB) + patched_content = apply_memory_patches(content) + + # Write the patched linker script to the build directory + with open(custom_ld, "w") as f: + f.write(patched_content) + + print(f"ESPHome: Created custom linker script: {custom_ld}") + + # Tell the linker to use our custom script from the build directory + assert os.path.exists(custom_ld), f"Custom linker script not found: {custom_ld}" + env.Replace(LDSCRIPT_PATH=custom_ld) + print(f"ESPHome: Using custom linker script with patched memory limits") + + # Also patch local.eagle.app.v6.common.ld after PlatformIO creates it + env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", patch_local_linker_script) diff --git a/esphome/components/esp8266_pwm/esp8266_pwm.h b/esphome/components/esp8266_pwm/esp8266_pwm.h index 79530aacd4..4b021fc462 100644 --- a/esphome/components/esp8266_pwm/esp8266_pwm.h +++ b/esphome/components/esp8266_pwm/esp8266_pwm.h @@ -40,7 +40,7 @@ template class SetFrequencyAction : public Action { SetFrequencyAction(ESP8266PWM *parent) : parent_(parent) {} TEMPLATABLE_VALUE(float, frequency); - void play(Ts... x) { + void play(const Ts &...x) { float freq = this->frequency_.value(x...); this->parent_->update_frequency(freq); } diff --git a/esphome/components/esp_ldo/esp_ldo.cpp b/esphome/components/esp_ldo/esp_ldo.cpp index eb04670d7e..5e3d4159f3 100644 --- a/esphome/components/esp_ldo/esp_ldo.cpp +++ b/esphome/components/esp_ldo/esp_ldo.cpp @@ -14,8 +14,8 @@ void EspLdo::setup() { config.flags.adjustable = this->adjustable_; auto err = esp_ldo_acquire_channel(&config, &this->handle_); if (err != ESP_OK) { - auto msg = str_sprintf("Failed to acquire LDO channel %d with voltage %fV", this->channel_, this->voltage_); - this->mark_failed(msg.c_str()); + ESP_LOGE(TAG, "Failed to acquire LDO channel %d with voltage %fV", this->channel_, this->voltage_); + this->mark_failed(LOG_STR("Failed to acquire LDO channel")); } else { ESP_LOGD(TAG, "Acquired LDO channel %d with voltage %fV", this->channel_, this->voltage_); } diff --git a/esphome/components/esp_ldo/esp_ldo.h b/esphome/components/esp_ldo/esp_ldo.h index bafa32db6b..9edd303e16 100644 --- a/esphome/components/esp_ldo/esp_ldo.h +++ b/esphome/components/esp_ldo/esp_ldo.h @@ -34,7 +34,7 @@ template class AdjustAction : public Action { TEMPLATABLE_VALUE(float, voltage) - void play(Ts... x) override { this->ldo_->adjust_voltage(this->voltage_.value(x...)); } + void play(const Ts &...x) override { this->ldo_->adjust_voltage(this->voltage_.value(x...)); } protected: EspLdo *ldo_; diff --git a/esphome/components/esphome/ota/__init__.py b/esphome/components/esphome/ota/__init__.py index 9facdc3bc6..e56e85b231 100644 --- a/esphome/components/esphome/ota/__init__.py +++ b/esphome/components/esphome/ota/__init__.py @@ -16,16 +16,31 @@ from esphome.const import ( CONF_SAFE_MODE, CONF_VERSION, ) -from esphome.core import coroutine_with_priority +from esphome.core import CORE, coroutine_with_priority +from esphome.coroutine import CoroPriority import esphome.final_validate as fv +from esphome.types import ConfigType _LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@esphome/core"] -AUTO_LOAD = ["md5", "socket"] DEPENDENCIES = ["network"] + +def supports_sha256() -> bool: + """Check if the current platform supports SHA256 for OTA authentication.""" + return bool(CORE.is_esp32 or CORE.is_esp8266 or CORE.is_rp2040 or CORE.is_libretiny) + + +def AUTO_LOAD() -> list[str]: + """Conditionally auto-load sha256 only on platforms that support it.""" + base_components = ["md5", "socket"] + if supports_sha256(): + return base_components + ["sha256"] + return base_components + + esphome = cg.esphome_ns.namespace("esphome") ESPHomeOTAComponent = esphome.class_("ESPHomeOTAComponent", OTAComponent) @@ -88,7 +103,16 @@ def ota_esphome_final_validate(config): ) -CONFIG_SCHEMA = ( +def _consume_ota_sockets(config: ConfigType) -> ConfigType: + """Register socket needs for OTA component.""" + from esphome.components import socket + + # OTA needs 1 listening socket (client connections are temporary during updates) + socket.consume_sockets(1, "ota")(config) + return config + + +CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(ESPHomeOTAComponent), @@ -115,19 +139,27 @@ CONFIG_SCHEMA = ( } ) .extend(BASE_OTA_SCHEMA) - .extend(cv.COMPONENT_SCHEMA) + .extend(cv.COMPONENT_SCHEMA), + _consume_ota_sockets, ) FINAL_VALIDATE_SCHEMA = ota_esphome_final_validate -@coroutine_with_priority(52.0) -async def to_code(config): +@coroutine_with_priority(CoroPriority.OTA_UPDATES) +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) cg.add(var.set_port(config[CONF_PORT])) - if CONF_PASSWORD in config: + + # Password could be set to an empty string and we can assume that means no password + if config.get(CONF_PASSWORD): cg.add(var.set_auth_password(config[CONF_PASSWORD])) cg.add_define("USE_OTA_PASSWORD") + # Only include hash algorithms when password is configured + cg.add_define("USE_OTA_MD5") + # Only include SHA256 support on platforms that have it + if supports_sha256(): + cg.add_define("USE_OTA_SHA256") cg.add_define("USE_OTA_VERSION", config[CONF_VERSION]) await cg.register_component(var, config) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 4cc82b9094..eb6c61a69b 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -1,6 +1,13 @@ #include "ota_esphome.h" #ifdef USE_OTA +#ifdef USE_OTA_PASSWORD +#ifdef USE_OTA_MD5 #include "esphome/components/md5/md5.h" +#endif +#ifdef USE_OTA_SHA256 +#include "esphome/components/sha256/sha256.h" +#endif +#endif #include "esphome/components/network/util.h" #include "esphome/components/ota/ota_backend.h" #include "esphome/components/ota/ota_backend_arduino_esp32.h" @@ -10,6 +17,7 @@ #include "esphome/components/ota/ota_backend_esp_idf.h" #include "esphome/core/application.h" #include "esphome/core/hal.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "esphome/core/util.h" @@ -19,7 +27,19 @@ namespace esphome { static const char *const TAG = "esphome.ota"; -static constexpr u_int16_t OTA_BLOCK_SIZE = 8192; +static constexpr uint16_t OTA_BLOCK_SIZE = 8192; +static constexpr size_t OTA_BUFFER_SIZE = 1024; // buffer size for OTA data transfer +static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 20000; // milliseconds for initial handshake +static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 90000; // milliseconds for data transfer + +#ifdef USE_OTA_PASSWORD +#ifdef USE_OTA_MD5 +static constexpr size_t MD5_HEX_SIZE = 32; // MD5 hash as hex string (16 bytes * 2) +#endif +#ifdef USE_OTA_SHA256 +static constexpr size_t SHA256_HEX_SIZE = 64; // SHA256 hash as hex string (32 bytes * 2) +#endif +#endif // USE_OTA_PASSWORD void ESPHomeOTAComponent::setup() { #ifdef USE_OTA_STATE_CALLBACK @@ -28,19 +48,19 @@ void ESPHomeOTAComponent::setup() { this->server_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections if (this->server_ == nullptr) { - ESP_LOGW(TAG, "Could not create socket"); + this->log_socket_error_(LOG_STR("creation")); this->mark_failed(); return; } int enable = 1; int err = this->server_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)); if (err != 0) { - ESP_LOGW(TAG, "Socket unable to set reuseaddr: errno %d", err); + this->log_socket_error_(LOG_STR("reuseaddr")); // we can still continue } err = this->server_->setblocking(false); if (err != 0) { - ESP_LOGW(TAG, "Socket unable to set nonblocking mode: errno %d", err); + this->log_socket_error_(LOG_STR("non-blocking")); this->mark_failed(); return; } @@ -49,21 +69,21 @@ void ESPHomeOTAComponent::setup() { socklen_t sl = socket::set_sockaddr_any((struct sockaddr *) &server, sizeof(server), this->port_); if (sl == 0) { - ESP_LOGW(TAG, "Socket unable to set sockaddr: errno %d", errno); + this->log_socket_error_(LOG_STR("set sockaddr")); this->mark_failed(); return; } err = this->server_->bind((struct sockaddr *) &server, sizeof(server)); if (err != 0) { - ESP_LOGW(TAG, "Socket unable to bind: errno %d", errno); + this->log_socket_error_(LOG_STR("bind")); this->mark_failed(); return; } - err = this->server_->listen(4); + err = this->server_->listen(1); // Only one client at a time if (err != 0) { - ESP_LOGW(TAG, "Socket unable to listen: errno %d", errno); + this->log_socket_error_(LOG_STR("listen")); this->mark_failed(); return; } @@ -74,7 +94,7 @@ void ESPHomeOTAComponent::dump_config() { "Over-The-Air updates:\n" " Address: %s:%u\n" " Version: %d", - network::get_use_address().c_str(), this->port_, USE_OTA_VERSION); + network::get_use_address(), this->port_, USE_OTA_VERSION); #ifdef USE_OTA_PASSWORD if (!this->password_.empty()) { ESP_LOGCONFIG(TAG, " Password configured"); @@ -83,217 +103,252 @@ void ESPHomeOTAComponent::dump_config() { } void ESPHomeOTAComponent::loop() { - // Skip handle_() call if no client connected and no incoming connections + // Skip handle_handshake_() call if no client connected and no incoming connections // This optimization reduces idle loop overhead when OTA is not active - // Note: No need to check server_ for null as the component is marked failed in setup() if server_ creation fails + // Note: No need to check server_ for null as the component is marked failed in setup() + // if server_ creation fails if (this->client_ != nullptr || this->server_->ready()) { - this->handle_(); + this->handle_handshake_(); } } static const uint8_t FEATURE_SUPPORTS_COMPRESSION = 0x01; - -void ESPHomeOTAComponent::handle_() { - ota::OTAResponseTypes error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN; - bool update_started = false; - size_t total = 0; - uint32_t last_progress = 0; - uint8_t buf[1024]; - char *sbuf = reinterpret_cast(buf); - size_t ota_size; - uint8_t ota_features; - std::unique_ptr backend; - (void) ota_features; -#if USE_OTA_VERSION == 2 - size_t size_acknowledged = 0; +#ifdef USE_OTA_SHA256 +static const uint8_t FEATURE_SUPPORTS_SHA256_AUTH = 0x02; #endif +// Temporary flag to allow MD5 downgrade for ~3 versions (until 2026.1.0) +// This allows users to downgrade via OTA if they encounter issues after updating. +// Without this, users would need to do a serial flash to downgrade. +// TODO: Remove this flag and all associated code in 2026.1.0 +#define ALLOW_OTA_DOWNGRADE_MD5 + +void ESPHomeOTAComponent::handle_handshake_() { + /// Handle the OTA handshake and authentication. + /// + /// This method is non-blocking and will return immediately if no data is available. + /// It manages the state machine through connection, magic bytes validation, feature + /// negotiation, and authentication before entering the blocking data transfer phase. + if (this->client_ == nullptr) { // We already checked server_->ready() in loop(), so we can accept directly struct sockaddr_storage source_addr; socklen_t addr_len = sizeof(source_addr); - this->client_ = this->server_->accept((struct sockaddr *) &source_addr, &addr_len); + int enable = 1; + + this->client_ = this->server_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len); if (this->client_ == nullptr) return; + int err = this->client_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int)); + if (err != 0) { + this->log_socket_error_(LOG_STR("nodelay")); + this->cleanup_connection_(); + return; + } + err = this->client_->setblocking(false); + if (err != 0) { + this->log_socket_error_(LOG_STR("non-blocking")); + this->cleanup_connection_(); + return; + } + this->log_start_(LOG_STR("handshake")); + this->client_connect_time_ = App.get_loop_component_start_time(); + this->handshake_buf_pos_ = 0; // Reset handshake buffer position + this->ota_state_ = OTAState::MAGIC_READ; } - int enable = 1; - int err = this->client_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int)); - if (err != 0) { - ESP_LOGW(TAG, "Socket could not enable TCP nodelay, errno %d", errno); - this->client_->close(); - this->client_ = nullptr; + // Check for handshake timeout + uint32_t now = App.get_loop_component_start_time(); + if (now - this->client_connect_time_ > OTA_SOCKET_TIMEOUT_HANDSHAKE) { + ESP_LOGW(TAG, "Handshake timeout"); + this->cleanup_connection_(); return; } - ESP_LOGD(TAG, "Starting update from %s", this->client_->getpeername().c_str()); + switch (this->ota_state_) { + case OTAState::MAGIC_READ: { + // Try to read remaining magic bytes (5 total) + if (!this->try_read_(5, LOG_STR("read magic"))) { + return; + } + + // Validate magic bytes + static const uint8_t MAGIC_BYTES[5] = {0x6C, 0x26, 0xF7, 0x5C, 0x45}; + if (memcmp(this->handshake_buf_, MAGIC_BYTES, 5) != 0) { + ESP_LOGW(TAG, "Magic bytes mismatch! 0x%02X-0x%02X-0x%02X-0x%02X-0x%02X", this->handshake_buf_[0], + this->handshake_buf_[1], this->handshake_buf_[2], this->handshake_buf_[3], this->handshake_buf_[4]); + this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_MAGIC); + return; + } + + // Magic bytes valid, move to next state + this->transition_ota_state_(OTAState::MAGIC_ACK); + this->handshake_buf_[0] = ota::OTA_RESPONSE_OK; + this->handshake_buf_[1] = USE_OTA_VERSION; + [[fallthrough]]; + } + + case OTAState::MAGIC_ACK: { + // Send OK and version - 2 bytes + if (!this->try_write_(2, LOG_STR("ack magic"))) { + return; + } + // All bytes sent, create backend and move to next state + this->backend_ = ota::make_ota_backend(); + this->transition_ota_state_(OTAState::FEATURE_READ); + [[fallthrough]]; + } + + case OTAState::FEATURE_READ: { + // Read features - 1 byte + if (!this->try_read_(1, LOG_STR("read feature"))) { + return; + } + this->ota_features_ = this->handshake_buf_[0]; + ESP_LOGV(TAG, "Features: 0x%02X", this->ota_features_); + this->transition_ota_state_(OTAState::FEATURE_ACK); + this->handshake_buf_[0] = + ((this->ota_features_ & FEATURE_SUPPORTS_COMPRESSION) != 0 && this->backend_->supports_compression()) + ? ota::OTA_RESPONSE_SUPPORTS_COMPRESSION + : ota::OTA_RESPONSE_HEADER_OK; + [[fallthrough]]; + } + + case OTAState::FEATURE_ACK: { + // Acknowledge header - 1 byte + if (!this->try_write_(1, LOG_STR("ack feature"))) { + return; + } +#ifdef USE_OTA_PASSWORD + // If password is set, move to auth phase + if (!this->password_.empty()) { + this->transition_ota_state_(OTAState::AUTH_SEND); + } else +#endif + { + // No password, move directly to data phase + this->transition_ota_state_(OTAState::DATA); + } + [[fallthrough]]; + } + +#ifdef USE_OTA_PASSWORD + case OTAState::AUTH_SEND: { + // Non-blocking authentication send + if (!this->handle_auth_send_()) { + return; + } + this->transition_ota_state_(OTAState::AUTH_READ); + [[fallthrough]]; + } + + case OTAState::AUTH_READ: { + // Non-blocking authentication read & verify + if (!this->handle_auth_read_()) { + return; + } + this->transition_ota_state_(OTAState::DATA); + [[fallthrough]]; + } +#endif + + case OTAState::DATA: + this->handle_data_(); + return; + + default: + break; + } +} + +void ESPHomeOTAComponent::handle_data_() { + /// Handle the OTA data transfer and update process. + /// + /// This method is blocking and will not return until the OTA update completes, + /// fails, or times out. It receives the firmware data, writes it to flash, + /// and reboots on success. + /// + /// Authentication has already been handled in the non-blocking states AUTH_SEND/AUTH_READ. + ota::OTAResponseTypes error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN; + bool update_started = false; + size_t total = 0; + uint32_t last_progress = 0; + uint8_t buf[OTA_BUFFER_SIZE]; + char *sbuf = reinterpret_cast(buf); + size_t ota_size; +#if USE_OTA_VERSION == 2 + size_t size_acknowledged = 0; +#endif + + // Acknowledge auth OK - 1 byte + this->write_byte_(ota::OTA_RESPONSE_AUTH_OK); + + // Read size, 4 bytes MSB first + if (!this->readall_(buf, 4)) { + this->log_read_error_(LOG_STR("size")); + goto error; // NOLINT(cppcoreguidelines-avoid-goto) + } + ota_size = (static_cast(buf[0]) << 24) | (static_cast(buf[1]) << 16) | + (static_cast(buf[2]) << 8) | buf[3]; + ESP_LOGV(TAG, "Size is %u bytes", ota_size); + + // Now that we've passed authentication and are actually + // starting the update, set the warning status and notify + // listeners. This ensures that port scanners do not + // accidentally trigger the update process. + this->log_start_(LOG_STR("update")); this->status_set_warning(); #ifdef USE_OTA_STATE_CALLBACK this->state_callback_.call(ota::OTA_STARTED, 0.0f, 0); #endif - if (!this->readall_(buf, 5)) { - ESP_LOGW(TAG, "Reading magic bytes failed"); - goto error; // NOLINT(cppcoreguidelines-avoid-goto) - } - // 0x6C, 0x26, 0xF7, 0x5C, 0x45 - if (buf[0] != 0x6C || buf[1] != 0x26 || buf[2] != 0xF7 || buf[3] != 0x5C || buf[4] != 0x45) { - ESP_LOGW(TAG, "Magic bytes do not match! 0x%02X-0x%02X-0x%02X-0x%02X-0x%02X", buf[0], buf[1], buf[2], buf[3], - buf[4]); - error_code = ota::OTA_RESPONSE_ERROR_MAGIC; - goto error; // NOLINT(cppcoreguidelines-avoid-goto) - } - - // Send OK and version - 2 bytes - buf[0] = ota::OTA_RESPONSE_OK; - buf[1] = USE_OTA_VERSION; - this->writeall_(buf, 2); - - backend = ota::make_ota_backend(); - - // Read features - 1 byte - if (!this->readall_(buf, 1)) { - ESP_LOGW(TAG, "Reading features failed"); - goto error; // NOLINT(cppcoreguidelines-avoid-goto) - } - ota_features = buf[0]; // NOLINT - ESP_LOGV(TAG, "Features: 0x%02X", ota_features); - - // Acknowledge header - 1 byte - buf[0] = ota::OTA_RESPONSE_HEADER_OK; - if ((ota_features & FEATURE_SUPPORTS_COMPRESSION) != 0 && backend->supports_compression()) { - buf[0] = ota::OTA_RESPONSE_SUPPORTS_COMPRESSION; - } - - this->writeall_(buf, 1); - -#ifdef USE_OTA_PASSWORD - if (!this->password_.empty()) { - buf[0] = ota::OTA_RESPONSE_REQUEST_AUTH; - this->writeall_(buf, 1); - md5::MD5Digest md5{}; - md5.init(); - sprintf(sbuf, "%08" PRIx32, random_uint32()); - md5.add(sbuf, 8); - md5.calculate(); - md5.get_hex(sbuf); - ESP_LOGV(TAG, "Auth: Nonce is %s", sbuf); - - // Send nonce, 32 bytes hex MD5 - if (!this->writeall_(reinterpret_cast(sbuf), 32)) { - ESP_LOGW(TAG, "Auth: Writing nonce failed"); - goto error; // NOLINT(cppcoreguidelines-avoid-goto) - } - - // prepare challenge - md5.init(); - md5.add(this->password_.c_str(), this->password_.length()); - // add nonce - md5.add(sbuf, 32); - - // Receive cnonce, 32 bytes hex MD5 - if (!this->readall_(buf, 32)) { - ESP_LOGW(TAG, "Auth: Reading cnonce failed"); - goto error; // NOLINT(cppcoreguidelines-avoid-goto) - } - sbuf[32] = '\0'; - ESP_LOGV(TAG, "Auth: CNonce is %s", sbuf); - // add cnonce - md5.add(sbuf, 32); - - // calculate result - md5.calculate(); - md5.get_hex(sbuf); - ESP_LOGV(TAG, "Auth: Result is %s", sbuf); - - // Receive result, 32 bytes hex MD5 - if (!this->readall_(buf + 64, 32)) { - ESP_LOGW(TAG, "Auth: Reading response failed"); - goto error; // NOLINT(cppcoreguidelines-avoid-goto) - } - sbuf[64 + 32] = '\0'; - ESP_LOGV(TAG, "Auth: Response is %s", sbuf + 64); - - bool matches = true; - for (uint8_t i = 0; i < 32; i++) - matches = matches && buf[i] == buf[64 + i]; - - if (!matches) { - ESP_LOGW(TAG, "Auth failed! Passwords do not match"); - error_code = ota::OTA_RESPONSE_ERROR_AUTH_INVALID; - goto error; // NOLINT(cppcoreguidelines-avoid-goto) - } - } -#endif // USE_OTA_PASSWORD - - // Acknowledge auth OK - 1 byte - buf[0] = ota::OTA_RESPONSE_AUTH_OK; - this->writeall_(buf, 1); - - // Read size, 4 bytes MSB first - if (!this->readall_(buf, 4)) { - ESP_LOGW(TAG, "Reading size failed"); - goto error; // NOLINT(cppcoreguidelines-avoid-goto) - } - ota_size = 0; - for (uint8_t i = 0; i < 4; i++) { - ota_size <<= 8; - ota_size |= buf[i]; - } - ESP_LOGV(TAG, "Size is %u bytes", ota_size); - - error_code = backend->begin(ota_size); + // This will block for a few seconds as it locks flash + error_code = this->backend_->begin(ota_size); if (error_code != ota::OTA_RESPONSE_OK) goto error; // NOLINT(cppcoreguidelines-avoid-goto) update_started = true; // Acknowledge prepare OK - 1 byte - buf[0] = ota::OTA_RESPONSE_UPDATE_PREPARE_OK; - this->writeall_(buf, 1); + this->write_byte_(ota::OTA_RESPONSE_UPDATE_PREPARE_OK); // Read binary MD5, 32 bytes if (!this->readall_(buf, 32)) { - ESP_LOGW(TAG, "Reading binary MD5 checksum failed"); + this->log_read_error_(LOG_STR("MD5 checksum")); goto error; // NOLINT(cppcoreguidelines-avoid-goto) } sbuf[32] = '\0'; ESP_LOGV(TAG, "Update: Binary MD5 is %s", sbuf); - backend->set_update_md5(sbuf); + this->backend_->set_update_md5(sbuf); // Acknowledge MD5 OK - 1 byte - buf[0] = ota::OTA_RESPONSE_BIN_MD5_OK; - this->writeall_(buf, 1); + this->write_byte_(ota::OTA_RESPONSE_BIN_MD5_OK); while (total < ota_size) { // TODO: timeout check - size_t requested = std::min(sizeof(buf), ota_size - total); + size_t remaining = ota_size - total; + size_t requested = remaining < OTA_BUFFER_SIZE ? remaining : OTA_BUFFER_SIZE; ssize_t read = this->client_->read(buf, requested); if (read == -1) { - if (errno == EAGAIN || errno == EWOULDBLOCK) { - App.feed_wdt(); - delay(1); + if (this->would_block_(errno)) { + this->yield_and_feed_watchdog_(); continue; } - ESP_LOGW(TAG, "Error receiving data for update, errno %d", errno); + ESP_LOGW(TAG, "Read err %d", errno); goto error; // NOLINT(cppcoreguidelines-avoid-goto) } else if (read == 0) { - // $ man recv - // "When a stream socket peer has performed an orderly shutdown, the return value will - // be 0 (the traditional "end-of-file" return)." - ESP_LOGW(TAG, "Remote end closed connection"); + ESP_LOGW(TAG, "Remote closed"); goto error; // NOLINT(cppcoreguidelines-avoid-goto) } - error_code = backend->write(buf, read); + error_code = this->backend_->write(buf, read); if (error_code != ota::OTA_RESPONSE_OK) { - ESP_LOGW(TAG, "Error writing binary data to flash!, error_code: %d", error_code); + ESP_LOGW(TAG, "Flash write err %d", error_code); goto error; // NOLINT(cppcoreguidelines-avoid-goto) } total += read; #if USE_OTA_VERSION == 2 while (size_acknowledged + OTA_BLOCK_SIZE <= total || (total == ota_size && size_acknowledged < ota_size)) { - buf[0] = ota::OTA_RESPONSE_CHUNK_OK; - this->writeall_(buf, 1); + this->write_byte_(ota::OTA_RESPONSE_CHUNK_OK); size_acknowledged += OTA_BLOCK_SIZE; } #endif @@ -307,33 +362,29 @@ void ESPHomeOTAComponent::handle_() { this->state_callback_.call(ota::OTA_IN_PROGRESS, percentage, 0); #endif // feed watchdog and give other tasks a chance to run - App.feed_wdt(); - yield(); + this->yield_and_feed_watchdog_(); } } // Acknowledge receive OK - 1 byte - buf[0] = ota::OTA_RESPONSE_RECEIVE_OK; - this->writeall_(buf, 1); + this->write_byte_(ota::OTA_RESPONSE_RECEIVE_OK); - error_code = backend->end(); + error_code = this->backend_->end(); if (error_code != ota::OTA_RESPONSE_OK) { - ESP_LOGW(TAG, "Error ending update! error_code: %d", error_code); + ESP_LOGW(TAG, "End update err %d", error_code); goto error; // NOLINT(cppcoreguidelines-avoid-goto) } // Acknowledge Update end OK - 1 byte - buf[0] = ota::OTA_RESPONSE_UPDATE_END_OK; - this->writeall_(buf, 1); + this->write_byte_(ota::OTA_RESPONSE_UPDATE_END_OK); // Read ACK if (!this->readall_(buf, 1) || buf[0] != ota::OTA_RESPONSE_OK) { - ESP_LOGW(TAG, "Reading back acknowledgement failed"); + this->log_read_error_(LOG_STR("ack")); // do not go to error, this is not fatal } - this->client_->close(); - this->client_ = nullptr; + this->cleanup_connection_(); delay(10); ESP_LOGI(TAG, "Update complete"); this->status_clear_warning(); @@ -344,13 +395,11 @@ void ESPHomeOTAComponent::handle_() { App.safe_reboot(); error: - buf[0] = static_cast(error_code); - this->writeall_(buf, 1); - this->client_->close(); - this->client_ = nullptr; + this->write_byte_(static_cast(error_code)); + this->cleanup_connection_(); - if (backend != nullptr && update_started) { - backend->abort(); + if (this->backend_ != nullptr && update_started) { + this->backend_->abort(); } this->status_momentary_error("onerror", 5000); @@ -364,28 +413,24 @@ bool ESPHomeOTAComponent::readall_(uint8_t *buf, size_t len) { uint32_t at = 0; while (len - at > 0) { uint32_t now = millis(); - if (now - start > 1000) { - ESP_LOGW(TAG, "Timed out reading %d bytes of data", len); + if (now - start > OTA_SOCKET_TIMEOUT_DATA) { + ESP_LOGW(TAG, "Timeout reading %d bytes", len); return false; } ssize_t read = this->client_->read(buf + at, len - at); if (read == -1) { - if (errno == EAGAIN || errno == EWOULDBLOCK) { - App.feed_wdt(); - delay(1); - continue; + if (!this->would_block_(errno)) { + ESP_LOGW(TAG, "Read err %d bytes, errno %d", len, errno); + return false; } - ESP_LOGW(TAG, "Failed to read %d bytes of data, errno %d", len, errno); - return false; } else if (read == 0) { - ESP_LOGW(TAG, "Remote closed connection"); + ESP_LOGW(TAG, "Remote closed"); return false; } else { at += read; } - App.feed_wdt(); - delay(1); + this->yield_and_feed_watchdog_(); } return true; @@ -395,25 +440,21 @@ bool ESPHomeOTAComponent::writeall_(const uint8_t *buf, size_t len) { uint32_t at = 0; while (len - at > 0) { uint32_t now = millis(); - if (now - start > 1000) { - ESP_LOGW(TAG, "Timed out writing %d bytes of data", len); + if (now - start > OTA_SOCKET_TIMEOUT_DATA) { + ESP_LOGW(TAG, "Timeout writing %d bytes", len); return false; } ssize_t written = this->client_->write(buf + at, len - at); if (written == -1) { - if (errno == EAGAIN || errno == EWOULDBLOCK) { - App.feed_wdt(); - delay(1); - continue; + if (!this->would_block_(errno)) { + ESP_LOGW(TAG, "Write err %d bytes, errno %d", len, errno); + return false; } - ESP_LOGW(TAG, "Failed to write %d bytes of data, errno %d", len, errno); - return false; } else { at += written; } - App.feed_wdt(); - delay(1); + this->yield_and_feed_watchdog_(); } return true; } @@ -421,5 +462,342 @@ bool ESPHomeOTAComponent::writeall_(const uint8_t *buf, size_t len) { float ESPHomeOTAComponent::get_setup_priority() const { return setup_priority::AFTER_WIFI; } uint16_t ESPHomeOTAComponent::get_port() const { return this->port_; } void ESPHomeOTAComponent::set_port(uint16_t port) { this->port_ = port; } + +void ESPHomeOTAComponent::log_socket_error_(const LogString *msg) { + ESP_LOGW(TAG, "Socket %s: errno %d", LOG_STR_ARG(msg), errno); +} + +void ESPHomeOTAComponent::log_read_error_(const LogString *what) { ESP_LOGW(TAG, "Read %s failed", LOG_STR_ARG(what)); } + +void ESPHomeOTAComponent::log_start_(const LogString *phase) { + ESP_LOGD(TAG, "Starting %s from %s", LOG_STR_ARG(phase), this->client_->getpeername().c_str()); +} + +void ESPHomeOTAComponent::log_remote_closed_(const LogString *during) { + ESP_LOGW(TAG, "Remote closed at %s", LOG_STR_ARG(during)); +} + +bool ESPHomeOTAComponent::handle_read_error_(ssize_t read, const LogString *desc) { + if (read == -1 && this->would_block_(errno)) { + return false; // No data yet, try again next loop + } + + if (read <= 0) { + read == 0 ? this->log_remote_closed_(desc) : this->log_socket_error_(desc); + this->cleanup_connection_(); + return false; + } + return true; +} + +bool ESPHomeOTAComponent::handle_write_error_(ssize_t written, const LogString *desc) { + if (written == -1) { + if (this->would_block_(errno)) { + return false; // Try again next loop + } + this->log_socket_error_(desc); + this->cleanup_connection_(); + return false; + } + return true; +} + +bool ESPHomeOTAComponent::try_read_(size_t to_read, const LogString *desc) { + // Read bytes into handshake buffer, starting at handshake_buf_pos_ + size_t bytes_to_read = to_read - this->handshake_buf_pos_; + ssize_t read = this->client_->read(this->handshake_buf_ + this->handshake_buf_pos_, bytes_to_read); + + if (!this->handle_read_error_(read, desc)) { + return false; + } + + this->handshake_buf_pos_ += read; + // Return true only if we have all the requested bytes + return this->handshake_buf_pos_ >= to_read; +} + +bool ESPHomeOTAComponent::try_write_(size_t to_write, const LogString *desc) { + // Write bytes from handshake buffer, starting at handshake_buf_pos_ + size_t bytes_to_write = to_write - this->handshake_buf_pos_; + ssize_t written = this->client_->write(this->handshake_buf_ + this->handshake_buf_pos_, bytes_to_write); + + if (!this->handle_write_error_(written, desc)) { + return false; + } + + this->handshake_buf_pos_ += written; + // Return true only if we have written all the requested bytes + return this->handshake_buf_pos_ >= to_write; +} + +void ESPHomeOTAComponent::cleanup_connection_() { + this->client_->close(); + this->client_ = nullptr; + this->client_connect_time_ = 0; + this->handshake_buf_pos_ = 0; + this->ota_state_ = OTAState::IDLE; + this->ota_features_ = 0; + this->backend_ = nullptr; +#ifdef USE_OTA_PASSWORD + this->cleanup_auth_(); +#endif +} + +void ESPHomeOTAComponent::yield_and_feed_watchdog_() { + App.feed_wdt(); + delay(1); +} + +#ifdef USE_OTA_PASSWORD +void ESPHomeOTAComponent::log_auth_warning_(const LogString *msg) { ESP_LOGW(TAG, "Auth: %s", LOG_STR_ARG(msg)); } + +bool ESPHomeOTAComponent::select_auth_type_() { +#ifdef USE_OTA_SHA256 + bool client_supports_sha256 = (this->ota_features_ & FEATURE_SUPPORTS_SHA256_AUTH) != 0; + +#ifdef ALLOW_OTA_DOWNGRADE_MD5 + // Allow fallback to MD5 if client doesn't support SHA256 + if (client_supports_sha256) { + this->auth_type_ = ota::OTA_RESPONSE_REQUEST_SHA256_AUTH; + return true; + } +#ifdef USE_OTA_MD5 + this->log_auth_warning_(LOG_STR("Using deprecated MD5")); + this->auth_type_ = ota::OTA_RESPONSE_REQUEST_AUTH; + return true; +#else + this->log_auth_warning_(LOG_STR("SHA256 required")); + this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID); + return false; +#endif // USE_OTA_MD5 + +#else // !ALLOW_OTA_DOWNGRADE_MD5 + // Require SHA256 + if (!client_supports_sha256) { + this->log_auth_warning_(LOG_STR("SHA256 required")); + this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID); + return false; + } + this->auth_type_ = ota::OTA_RESPONSE_REQUEST_SHA256_AUTH; + return true; +#endif // ALLOW_OTA_DOWNGRADE_MD5 + +#else // !USE_OTA_SHA256 +#ifdef USE_OTA_MD5 + // Only MD5 available + this->auth_type_ = ota::OTA_RESPONSE_REQUEST_AUTH; + return true; +#else + // No auth methods available + this->log_auth_warning_(LOG_STR("No auth methods available")); + this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID); + return false; +#endif // USE_OTA_MD5 +#endif // USE_OTA_SHA256 +} + +bool ESPHomeOTAComponent::handle_auth_send_() { + // Initialize auth buffer if not already done + if (!this->auth_buf_) { + // Select auth type based on client capabilities and configuration + if (!this->select_auth_type_()) { + return false; + } + + // Generate nonce - hasher must be created and used in same stack frame + // CRITICAL ESP32-S3 HARDWARE SHA ACCELERATION REQUIREMENTS: + // 1. Hash objects must NEVER be passed to another function (different stack frame) + // 2. NO Variable Length Arrays (VLAs) - they corrupt the stack with hardware DMA + // 3. All hash operations (init/add/calculate) must happen in the SAME function where object is created + // Violating these causes truncated hash output (20 bytes instead of 32) or memory corruption. + // + // Buffer layout after AUTH_READ completes: + // [0]: auth_type (1 byte) + // [1...hex_size]: nonce (hex_size bytes) - our random nonce sent in 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 + + // Declare both hash objects in same stack frame, use pointer to select. + // NOTE: Both objects are declared here even though only one is used. This is REQUIRED for ESP32-S3 + // hardware SHA acceleration - the object must exist in this stack frame for all operations. + // Do NOT try to "optimize" by creating the object inside the if block, as it would go out of scope. +#ifdef USE_OTA_SHA256 + sha256::SHA256 sha_hasher; +#endif +#ifdef USE_OTA_MD5 + md5::MD5Digest md5_hasher; +#endif + HashBase *hasher = nullptr; + +#ifdef USE_OTA_SHA256 + if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_SHA256_AUTH) { + hasher = &sha_hasher; + } +#endif +#ifdef USE_OTA_MD5 + if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_AUTH) { + hasher = &md5_hasher; + } +#endif + + const size_t hex_size = hasher->get_size() * 2; + const size_t nonce_len = hasher->get_size() / 4; + const size_t auth_buf_size = 1 + 3 * hex_size; + this->auth_buf_ = std::make_unique(auth_buf_size); + this->auth_buf_pos_ = 0; + + char *buf = reinterpret_cast(this->auth_buf_.get() + 1); + if (!random_bytes(reinterpret_cast(buf), nonce_len)) { + this->log_auth_warning_(LOG_STR("Random failed")); + this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_UNKNOWN); + return false; + } + + hasher->init(); + hasher->add(buf, nonce_len); + hasher->calculate(); + this->auth_buf_[0] = this->auth_type_; + hasher->get_hex(buf); + +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char log_buf[65]; // Fixed size for SHA256 hex (64) + null, works for MD5 (32) too + memcpy(log_buf, buf, hex_size); + log_buf[hex_size] = '\0'; + ESP_LOGV(TAG, "Auth: Nonce is %s", log_buf); +#endif + } + + // Try to write auth_type + nonce + size_t hex_size = this->get_auth_hex_size_(); + const size_t to_write = 1 + hex_size; + size_t remaining = to_write - this->auth_buf_pos_; + + ssize_t written = this->client_->write(this->auth_buf_.get() + this->auth_buf_pos_, remaining); + if (!this->handle_write_error_(written, LOG_STR("ack auth"))) { + return false; + } + + this->auth_buf_pos_ += written; + + // Check if we still have more to write + if (this->auth_buf_pos_ < to_write) { + return false; // More to write, try again next loop + } + + // All written, prepare for reading phase + this->auth_buf_pos_ = 0; + return true; +} + +bool ESPHomeOTAComponent::handle_auth_read_() { + size_t hex_size = this->get_auth_hex_size_(); + const size_t to_read = hex_size * 2; // CNonce + Response + + // Try to read remaining bytes (CNonce + Response) + // We read cnonce+response starting at offset 1+hex_size (after auth_type and our nonce) + size_t cnonce_offset = 1 + hex_size; // Offset where cnonce should be stored in buffer + size_t remaining = to_read - this->auth_buf_pos_; + ssize_t read = this->client_->read(this->auth_buf_.get() + cnonce_offset + this->auth_buf_pos_, remaining); + + if (!this->handle_read_error_(read, LOG_STR("read auth"))) { + return false; + } + + this->auth_buf_pos_ += read; + + // Check if we still need more data + if (this->auth_buf_pos_ < to_read) { + return false; // More to read, try again next loop + } + + // We have all the data, verify it + const char *nonce = reinterpret_cast(this->auth_buf_.get() + 1); + const char *cnonce = nonce + hex_size; + const char *response = cnonce + hex_size; + + // CRITICAL ESP32-S3: Hash objects must stay in same stack frame (no passing to other functions). + // Declare both hash objects in same stack frame, use pointer to select. + // NOTE: Both objects are declared here even though only one is used. This is REQUIRED for ESP32-S3 + // hardware SHA acceleration - the object must exist in this stack frame for all operations. + // Do NOT try to "optimize" by creating the object inside the if block, as it would go out of scope. +#ifdef USE_OTA_SHA256 + sha256::SHA256 sha_hasher; +#endif +#ifdef USE_OTA_MD5 + md5::MD5Digest md5_hasher; +#endif + HashBase *hasher = nullptr; + +#ifdef USE_OTA_SHA256 + if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_SHA256_AUTH) { + hasher = &sha_hasher; + } +#endif +#ifdef USE_OTA_MD5 + if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_AUTH) { + hasher = &md5_hasher; + } +#endif + + hasher->init(); + hasher->add(this->password_.c_str(), this->password_.length()); + hasher->add(nonce, hex_size * 2); // Add both nonce and cnonce (contiguous in buffer) + hasher->calculate(); + +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char log_buf[65]; // Fixed size for SHA256 hex (64) + null, works for MD5 (32) too + // Log CNonce + memcpy(log_buf, cnonce, hex_size); + log_buf[hex_size] = '\0'; + ESP_LOGV(TAG, "Auth: CNonce is %s", log_buf); + + // Log computed hash + hasher->get_hex(log_buf); + log_buf[hex_size] = '\0'; + ESP_LOGV(TAG, "Auth: Result is %s", log_buf); + + // Log received response + memcpy(log_buf, response, hex_size); + log_buf[hex_size] = '\0'; + ESP_LOGV(TAG, "Auth: Response is %s", log_buf); +#endif + + // Compare response + bool matches = hasher->equals_hex(response); + + if (!matches) { + this->log_auth_warning_(LOG_STR("Password mismatch")); + this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID); + return false; + } + + // Authentication successful - clean up auth state + this->cleanup_auth_(); + + return true; +} + +size_t ESPHomeOTAComponent::get_auth_hex_size_() const { +#ifdef USE_OTA_SHA256 + if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_SHA256_AUTH) { + return SHA256_HEX_SIZE; + } +#endif +#ifdef USE_OTA_MD5 + return MD5_HEX_SIZE; +#else +#ifndef USE_OTA_SHA256 +#error "Either USE_OTA_MD5 or USE_OTA_SHA256 must be defined when USE_OTA_PASSWORD is enabled" +#endif +#endif +} + +void ESPHomeOTAComponent::cleanup_auth_() { + this->auth_buf_ = nullptr; + this->auth_buf_pos_ = 0; + this->auth_type_ = 0; +} +#endif // USE_OTA_PASSWORD + } // namespace esphome #endif diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index e0d09ff37e..057461e6a4 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -2,16 +2,30 @@ #include "esphome/core/defines.h" #ifdef USE_OTA -#include "esphome/core/helpers.h" -#include "esphome/core/preferences.h" #include "esphome/components/ota/ota_backend.h" #include "esphome/components/socket/socket.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include "esphome/core/preferences.h" +#include "esphome/core/hash_base.h" namespace esphome { /// ESPHomeOTAComponent provides a simple way to integrate Over-the-Air updates into your app using ArduinoOTA. class ESPHomeOTAComponent : public ota::OTAComponent { public: + enum class OTAState : uint8_t { + IDLE, + MAGIC_READ, // Reading magic bytes + MAGIC_ACK, // Sending OK and version after magic bytes + FEATURE_READ, // Reading feature flags from client + FEATURE_ACK, // Sending feature acknowledgment +#ifdef USE_OTA_PASSWORD + AUTH_SEND, // Sending authentication request + AUTH_READ, // Reading authentication data +#endif // USE_OTA_PASSWORD + DATA, // BLOCKING! Processing OTA data (update, etc.) + }; #ifdef USE_OTA_PASSWORD void set_auth_password(const std::string &password) { password_ = password; } #endif // USE_OTA_PASSWORD @@ -27,18 +41,62 @@ class ESPHomeOTAComponent : public ota::OTAComponent { uint16_t get_port() const; protected: - void handle_(); + void handle_handshake_(); + void handle_data_(); +#ifdef USE_OTA_PASSWORD + bool handle_auth_send_(); + bool handle_auth_read_(); + bool select_auth_type_(); + size_t get_auth_hex_size_() const; + void cleanup_auth_(); + void log_auth_warning_(const LogString *msg); +#endif // USE_OTA_PASSWORD bool readall_(uint8_t *buf, size_t len); bool writeall_(const uint8_t *buf, size_t len); + inline bool write_byte_(uint8_t byte) { return this->writeall_(&byte, 1); } + + bool try_read_(size_t to_read, const LogString *desc); + bool try_write_(size_t to_write, const LogString *desc); + + inline bool would_block_(int error_code) const { return error_code == EAGAIN || error_code == EWOULDBLOCK; } + bool handle_read_error_(ssize_t read, const LogString *desc); + bool handle_write_error_(ssize_t written, const LogString *desc); + inline void transition_ota_state_(OTAState next_state) { + this->ota_state_ = next_state; + this->handshake_buf_pos_ = 0; // Reset buffer position for next state + } + + void log_socket_error_(const LogString *msg); + void log_read_error_(const LogString *what); + void log_start_(const LogString *phase); + void log_remote_closed_(const LogString *during); + void cleanup_connection_(); + inline void send_error_and_cleanup_(ota::OTAResponseTypes error) { + uint8_t error_byte = static_cast(error); + this->client_->write(&error_byte, 1); // Best effort, non-blocking + this->cleanup_connection_(); + } + void yield_and_feed_watchdog_(); #ifdef USE_OTA_PASSWORD std::string password_; #endif // USE_OTA_PASSWORD - uint16_t port_; - std::unique_ptr server_; std::unique_ptr client_; + std::unique_ptr backend_; + + uint32_t client_connect_time_{0}; + uint16_t port_; + uint8_t handshake_buf_[5]; + OTAState ota_state_{OTAState::IDLE}; + uint8_t handshake_buf_pos_{0}; + uint8_t ota_features_{0}; +#ifdef USE_OTA_PASSWORD + std::unique_ptr auth_buf_; + uint8_t auth_buf_pos_{0}; + uint8_t auth_type_{0}; // Store auth type to know which hasher to use +#endif // USE_OTA_PASSWORD }; } // namespace esphome diff --git a/esphome/components/espnow/__init__.py b/esphome/components/espnow/__init__.py index d15817cf92..cc2c02d4c0 100644 --- a/esphome/components/espnow/__init__.py +++ b/esphome/components/espnow/__init__.py @@ -1,6 +1,6 @@ from esphome import automation, core import esphome.codegen as cg -from esphome.components import wifi +from esphome.components import socket, wifi from esphome.components.udp import CONF_ON_RECEIVE import esphome.config_validation as cv from esphome.const import ( @@ -17,6 +17,7 @@ from esphome.core import CORE, HexInt from esphome.types import ConfigType CODEOWNERS = ["@jesserockz"] +AUTO_LOAD = ["socket"] byte_vector = cg.std_vector.template(cg.uint8) peer_address_t = cg.std_ns.class_("array").template(cg.uint8, 6) @@ -65,15 +66,6 @@ CONF_WAIT_FOR_SENT = "wait_for_sent" MAX_ESPNOW_PACKET_SIZE = 250 # Maximum size of the payload in bytes -def _validate_unknown_peer(config): - if config[CONF_AUTO_ADD_PEER] and config.get(CONF_ON_UNKNOWN_PEER): - raise cv.Invalid( - f"'{CONF_ON_UNKNOWN_PEER}' cannot be used when '{CONF_AUTO_ADD_PEER}' is enabled.", - path=[CONF_ON_UNKNOWN_PEER], - ) - return config - - CONFIG_SCHEMA = cv.All( cv.Schema( { @@ -103,7 +95,6 @@ CONFIG_SCHEMA = cv.All( }, ).extend(cv.COMPONENT_SCHEMA), cv.only_on_esp32, - _validate_unknown_peer, ) @@ -124,13 +115,16 @@ async def _trigger_to_code(config): async def to_code(config): - print(config) var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) if CORE.using_arduino: cg.add_library("WiFi", None) + # ESP-NOW uses wake_loop_threadsafe() to wake the main loop from ESP-NOW callbacks + # This enables low-latency event processing instead of waiting for select() timeout + socket.require_wake_loop_threadsafe() + cg.add_define("USE_ESPNOW") if wifi_channel := config.get(CONF_CHANNEL): cg.add(var.set_wifi_channel(wifi_channel)) diff --git a/esphome/components/espnow/automation.h b/esphome/components/espnow/automation.h index ad534b279a..0b26681400 100644 --- a/esphome/components/espnow/automation.h +++ b/esphome/components/espnow/automation.h @@ -14,13 +14,13 @@ template class SendAction : public Action, public Parente TEMPLATABLE_VALUE(std::vector, data); public: - void add_on_sent(const std::vector *> &actions) { + void add_on_sent(const std::initializer_list *> &actions) { this->sent_.add_actions(actions); if (this->flags_.wait_for_sent) { this->sent_.add_action(new LambdaAction([this](Ts... x) { this->play_next_(x...); })); } } - void add_on_error(const std::vector *> &actions) { + void add_on_error(const std::initializer_list *> &actions) { this->error_.add_actions(actions); if (this->flags_.wait_for_sent) { this->error_.add_action(new LambdaAction([this](Ts... x) { @@ -36,24 +36,24 @@ template class SendAction : public Action, public Parente void set_wait_for_sent(bool wait_for_sent) { this->flags_.wait_for_sent = wait_for_sent; } void set_continue_on_error(bool continue_on_error) { this->flags_.continue_on_error = continue_on_error; } - void play_complex(Ts... x) override { + void play_complex(const Ts &...x) override { this->num_running_++; send_callback_t send_callback = [this, x...](esp_err_t status) { if (status == ESP_OK) { - if (this->sent_.empty() && this->flags_.wait_for_sent) { - this->play_next_(x...); - } else if (!this->sent_.empty()) { + if (!this->sent_.empty()) { this->sent_.play(x...); + } else if (this->flags_.wait_for_sent) { + this->play_next_(x...); } } else { - if (this->error_.empty() && this->flags_.wait_for_sent) { + if (!this->error_.empty()) { + this->error_.play(x...); + } else if (this->flags_.wait_for_sent) { if (this->flags_.continue_on_error) { this->play_next_(x...); } else { this->stop_complex(); } - } else if (!this->error_.empty()) { - this->error_.play(x...); } } }; @@ -67,7 +67,7 @@ template class SendAction : public Action, public Parente } } - void play(Ts... x) override { /* ignore - see play_complex */ + void play(const Ts &...x) override { /* ignore - see play_complex */ } void stop() override { @@ -90,7 +90,7 @@ template class AddPeerAction : public Action, public Pare TEMPLATABLE_VALUE(peer_address_t, address); public: - void play(Ts... x) override { + void play(const Ts &...x) override { peer_address_t address = this->address_.value(x...); this->parent_->add_peer(address.data()); } @@ -100,7 +100,7 @@ template class DeletePeerAction : public Action, public P TEMPLATABLE_VALUE(peer_address_t, address); public: - void play(Ts... x) override { + void play(const Ts &...x) override { peer_address_t address = this->address_.value(x...); this->parent_->del_peer(address.data()); } @@ -109,7 +109,7 @@ template class DeletePeerAction : public Action, public P template class SetChannelAction : public Action, public Parented { public: TEMPLATABLE_VALUE(uint8_t, channel) - void play(Ts... x) override { + void play(const Ts &...x) override { if (this->parent_->is_wifi_enabled()) { return; } diff --git a/esphome/components/espnow/espnow_component.cpp b/esphome/components/espnow/espnow_component.cpp index dab8e2b726..d2f136d1c7 100644 --- a/esphome/components/espnow/espnow_component.cpp +++ b/esphome/components/espnow/espnow_component.cpp @@ -4,6 +4,7 @@ #include "espnow_err.h" +#include "esphome/core/application.h" #include "esphome/core/defines.h" #include "esphome/core/log.h" @@ -97,6 +98,11 @@ void on_send_report(const uint8_t *mac_addr, esp_now_send_status_t status) // Push the packet to the queue global_esp_now->receive_packet_queue_.push(packet); // Push always because we're the only producer and the pool ensures we never exceed queue size + + // Wake main loop immediately to process ESP-NOW send event instead of waiting for select() timeout +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + App.wake_loop_threadsafe(); +#endif } void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int size) { @@ -114,6 +120,11 @@ void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int // Push the packet to the queue global_esp_now->receive_packet_queue_.push(packet); // Push always because we're the only producer and the pool ensures we never exceed queue size + + // Wake main loop immediately to process ESP-NOW receive event instead of waiting for select() timeout +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + App.wake_loop_threadsafe(); +#endif } ESPNowComponent::ESPNowComponent() { global_esp_now = this; } @@ -154,7 +165,7 @@ void ESPNowComponent::setup() { } void ESPNowComponent::enable() { - if (this->state_ != ESPNOW_STATE_ENABLED) + if (this->state_ == ESPNOW_STATE_ENABLED) return; ESP_LOGD(TAG, "Enabling"); @@ -178,11 +189,7 @@ void ESPNowComponent::enable_() { this->apply_wifi_channel(); } -#ifdef USE_WIFI - else { - this->wifi_channel_ = wifi::global_wifi_component->get_wifi_channel(); - } -#endif + this->get_wifi_channel(); esp_err_t err = esp_now_init(); if (err != ESP_OK) { @@ -212,10 +219,11 @@ void ESPNowComponent::enable_() { esp_wifi_connectionless_module_set_wake_interval(CONFIG_ESPNOW_WAKE_INTERVAL); #endif + this->state_ = ESPNOW_STATE_ENABLED; + for (auto peer : this->peers_) { this->add_peer(peer.address); } - this->state_ = ESPNOW_STATE_ENABLED; } void ESPNowComponent::disable() { @@ -228,10 +236,6 @@ void ESPNowComponent::disable() { esp_now_unregister_recv_cb(); esp_now_unregister_send_cb(); - for (auto peer : this->peers_) { - this->del_peer(peer.address); - } - esp_err_t err = esp_now_deinit(); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_now_deinit failed! 0x%x", err); @@ -267,7 +271,6 @@ void ESPNowComponent::loop() { } } #endif - // Process received packets ESPNowPacket *packet = this->receive_packet_queue_.pop(); while (packet != nullptr) { @@ -275,14 +278,16 @@ void ESPNowComponent::loop() { case ESPNowPacket::RECEIVED: { const ESPNowRecvInfo info = packet->get_receive_info(); if (!esp_now_is_peer_exist(info.src_addr)) { - if (this->auto_add_peer_) { - this->add_peer(info.src_addr); - } else { - for (auto *handler : this->unknown_peer_handlers_) { - if (handler->on_unknown_peer(info, packet->packet_.receive.data, packet->packet_.receive.size)) - break; // If a handler returns true, stop processing further handlers + bool handled = false; + for (auto *handler : this->unknown_peer_handlers_) { + if (handler->on_unknown_peer(info, packet->packet_.receive.data, packet->packet_.receive.size)) { + handled = true; + break; // If a handler returns true, stop processing further handlers } } + if (!handled && this->auto_add_peer_) { + this->add_peer(info.src_addr); + } } // Intentionally left as if instead of else in case the peer is added above if (esp_now_is_peer_exist(info.src_addr)) { @@ -343,6 +348,12 @@ void ESPNowComponent::loop() { } } +uint8_t ESPNowComponent::get_wifi_channel() { + wifi_second_chan_t dummy; + esp_wifi_get_channel(&this->wifi_channel_, &dummy); + return this->wifi_channel_; +} + esp_err_t ESPNowComponent::send(const uint8_t *peer_address, const uint8_t *payload, size_t size, const send_callback_t &callback) { if (this->state_ != ESPNOW_STATE_ENABLED) { @@ -407,7 +418,7 @@ esp_err_t ESPNowComponent::add_peer(const uint8_t *peer) { } if (memcmp(peer, this->own_address_, ESP_NOW_ETH_ALEN) == 0) { - this->mark_failed(); + this->status_momentary_warning("peer-add-failed"); return ESP_ERR_INVALID_MAC; } diff --git a/esphome/components/espnow/espnow_component.h b/esphome/components/espnow/espnow_component.h index 3a523d1f7e..9941e97227 100644 --- a/esphome/components/espnow/espnow_component.h +++ b/esphome/components/espnow/espnow_component.h @@ -110,6 +110,7 @@ class ESPNowComponent : public Component { void set_wifi_channel(uint8_t channel) { this->wifi_channel_ = channel; } void apply_wifi_channel(); + uint8_t get_wifi_channel(); void set_auto_add_peer(bool value) { this->auto_add_peer_ = value; } diff --git a/esphome/components/espnow/espnow_packet.h b/esphome/components/espnow/espnow_packet.h index d39f7d2c24..b6192a0d41 100644 --- a/esphome/components/espnow/espnow_packet.h +++ b/esphome/components/espnow/espnow_packet.h @@ -49,7 +49,7 @@ class ESPNowPacket { #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) // Constructor for sent data ESPNowPacket(const esp_now_send_info_t *info, esp_now_send_status_t status) { - this->init_sent_data(info->src_addr, status); + this->init_sent_data_(info->src_addr, status); } #else // Constructor for sent data diff --git a/esphome/components/espnow/packet_transport/__init__.py b/esphome/components/espnow/packet_transport/__init__.py new file mode 100644 index 0000000000..e6d66440db --- /dev/null +++ b/esphome/components/espnow/packet_transport/__init__.py @@ -0,0 +1,39 @@ +"""ESP-NOW transport platform for packet_transport component.""" + +import esphome.codegen as cg +from esphome.components.packet_transport import ( + PacketTransport, + new_packet_transport, + transport_schema, +) +import esphome.config_validation as cv +from esphome.core import HexInt +from esphome.cpp_types import PollingComponent + +from .. import ESPNowComponent, espnow_ns + +CODEOWNERS = ["@EasilyBoredEngineer"] +DEPENDENCIES = ["espnow"] + +ESPNowTransport = espnow_ns.class_("ESPNowTransport", PacketTransport, PollingComponent) + +CONF_ESPNOW_ID = "espnow_id" +CONF_PEER_ADDRESS = "peer_address" + +CONFIG_SCHEMA = transport_schema(ESPNowTransport).extend( + { + cv.GenerateID(CONF_ESPNOW_ID): cv.use_id(ESPNowComponent), + cv.Optional(CONF_PEER_ADDRESS, default="FF:FF:FF:FF:FF:FF"): cv.mac_address, + } +) + + +async def to_code(config): + """Set up the ESP-NOW transport component.""" + var, _ = await new_packet_transport(config) + + await cg.register_parented(var, config[CONF_ESPNOW_ID]) + + # Set peer address - convert MAC to parts array like ESP-NOW does + mac = config[CONF_PEER_ADDRESS] + cg.add(var.set_peer_address([HexInt(x) for x in mac.parts])) diff --git a/esphome/components/espnow/packet_transport/espnow_transport.cpp b/esphome/components/espnow/packet_transport/espnow_transport.cpp new file mode 100644 index 0000000000..d30e9447a0 --- /dev/null +++ b/esphome/components/espnow/packet_transport/espnow_transport.cpp @@ -0,0 +1,97 @@ +#include "espnow_transport.h" + +#ifdef USE_ESP32 + +#include "esphome/core/application.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace espnow { + +static const char *const TAG = "espnow.transport"; + +bool ESPNowTransport::should_send() { return this->parent_ != nullptr && !this->parent_->is_failed(); } + +void ESPNowTransport::setup() { + packet_transport::PacketTransport::setup(); + + if (this->parent_ == nullptr) { + ESP_LOGE(TAG, "ESPNow component not set"); + this->mark_failed(); + return; + } + + ESP_LOGI(TAG, "Registering ESP-NOW handlers"); + ESP_LOGI(TAG, "Peer address: %02X:%02X:%02X:%02X:%02X:%02X", this->peer_address_[0], this->peer_address_[1], + this->peer_address_[2], this->peer_address_[3], this->peer_address_[4], this->peer_address_[5]); + + // Register received handler + this->parent_->register_received_handler(static_cast(this)); + + // Register broadcasted handler + this->parent_->register_broadcasted_handler(static_cast(this)); +} + +void ESPNowTransport::update() { + packet_transport::PacketTransport::update(); + this->updated_ = true; +} + +void ESPNowTransport::send_packet(const std::vector &buf) const { + if (this->parent_ == nullptr) { + ESP_LOGE(TAG, "ESPNow component not set"); + return; + } + + if (buf.empty()) { + ESP_LOGW(TAG, "Attempted to send empty packet"); + return; + } + + if (buf.size() > ESP_NOW_MAX_DATA_LEN) { + ESP_LOGE(TAG, "Packet too large: %zu bytes (max %d)", buf.size(), ESP_NOW_MAX_DATA_LEN); + return; + } + + // Send to configured peer address + this->parent_->send(this->peer_address_.data(), buf.data(), buf.size(), [](esp_err_t err) { + if (err != ESP_OK) { + ESP_LOGW(TAG, "Send failed: %d", err); + } + }); +} + +bool ESPNowTransport::on_received(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) { + ESP_LOGV(TAG, "Received packet of size %u from %02X:%02X:%02X:%02X:%02X:%02X", size, info.src_addr[0], + info.src_addr[1], info.src_addr[2], info.src_addr[3], info.src_addr[4], info.src_addr[5]); + + if (data == nullptr || size == 0) { + ESP_LOGW(TAG, "Received empty or null packet"); + return false; + } + + this->packet_buffer_.resize(size); + memcpy(this->packet_buffer_.data(), data, size); + this->process_(this->packet_buffer_); + return false; // Allow other handlers to run +} + +bool ESPNowTransport::on_broadcasted(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) { + ESP_LOGV(TAG, "Received broadcast packet of size %u from %02X:%02X:%02X:%02X:%02X:%02X", size, info.src_addr[0], + info.src_addr[1], info.src_addr[2], info.src_addr[3], info.src_addr[4], info.src_addr[5]); + + if (data == nullptr || size == 0) { + ESP_LOGW(TAG, "Received empty or null broadcast packet"); + return false; + } + + this->packet_buffer_.resize(size); + memcpy(this->packet_buffer_.data(), data, size); + this->process_(this->packet_buffer_); + return false; // Allow other handlers to run +} + +} // namespace espnow +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/espnow/packet_transport/espnow_transport.h b/esphome/components/espnow/packet_transport/espnow_transport.h new file mode 100644 index 0000000000..3629fad2cd --- /dev/null +++ b/esphome/components/espnow/packet_transport/espnow_transport.h @@ -0,0 +1,44 @@ +#pragma once + +#include "../espnow_component.h" + +#ifdef USE_ESP32 + +#include "esphome/core/component.h" +#include "esphome/components/packet_transport/packet_transport.h" + +#include + +namespace esphome { +namespace espnow { + +class ESPNowTransport : public packet_transport::PacketTransport, + public Parented, + public ESPNowReceivedPacketHandler, + public ESPNowBroadcastedHandler { + public: + void setup() override; + void update() override; + float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + + void set_peer_address(peer_address_t address) { + memcpy(this->peer_address_.data(), address.data(), ESP_NOW_ETH_ALEN); + } + + // ESPNow handler interface + bool on_received(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override; + bool on_broadcasted(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override; + + protected: + void send_packet(const std::vector &buf) const override; + size_t get_max_packet_size() override { return ESP_NOW_MAX_DATA_LEN; } + bool should_send() override; + + peer_address_t peer_address_{{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}}; + std::vector packet_buffer_; +}; + +} // namespace espnow +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 7a412a643d..b4d67635c1 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -2,13 +2,19 @@ import logging from esphome import pins import esphome.codegen as cg -from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant +from esphome.components.esp32 import ( + add_idf_component, + add_idf_sdkconfig_option, + get_esp32_variant, +) from esphome.components.esp32.const import ( + VARIANT_ESP32, VARIANT_ESP32C3, + VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3, ) -from esphome.components.network import IPAddress +from esphome.components.network import ip_address_literal from esphome.components.spi import CONF_INTERFACE_INDEX, get_spi_interface import esphome.config_validation as cv from esphome.const import ( @@ -21,10 +27,12 @@ from esphome.const import ( CONF_GATEWAY, CONF_ID, CONF_INTERRUPT_PIN, + CONF_MAC_ADDRESS, CONF_MANUAL_IP, CONF_MISO_PIN, CONF_MODE, CONF_MOSI_PIN, + CONF_NUMBER, CONF_PAGE_ID, CONF_PIN, CONF_POLLING_INTERVAL, @@ -38,14 +46,43 @@ from esphome.const import ( KEY_CORE, KEY_FRAMEWORK_VERSION, ) -from esphome.core import CORE, TimePeriodMilliseconds, coroutine_with_priority +from esphome.core import ( + CORE, + CoroPriority, + TimePeriodMilliseconds, + coroutine_with_priority, +) import esphome.final_validate as fv +from esphome.types import ConfigType CONFLICTS_WITH = ["wifi"] DEPENDENCIES = ["esp32"] AUTO_LOAD = ["network"] LOGGER = logging.getLogger(__name__) +# RMII pins that are hardcoded on ESP32 classic and cannot be changed +# These pins are used by the internal Ethernet MAC when using RMII PHYs +ESP32_RMII_FIXED_PINS = { + 19: "EMAC_TXD0", + 21: "EMAC_TX_EN", + 22: "EMAC_TXD1", + 25: "EMAC_RXD0", + 26: "EMAC_RXD1", + 27: "EMAC_RX_CRS_DV", +} + +# RMII default pins for ESP32-P4 +# These are the default pins used by ESP-IDF and are configurable in principle, +# but ESPHome's ethernet component currently has no way to change them +ESP32P4_RMII_DEFAULT_PINS = { + 34: "EMAC_TXD0", + 35: "EMAC_TXD1", + 28: "EMAC_RX_CRS_DV", + 29: "EMAC_RXD0", + 30: "EMAC_RXD1", + 49: "EMAC_TX_EN", +} + ethernet_ns = cg.esphome_ns.namespace("ethernet") PHYRegister = ethernet_ns.struct("PHYRegister") CONF_PHY_ADDR = "phy_addr" @@ -70,6 +107,15 @@ ETHERNET_TYPES = { "W5500": EthernetType.ETHERNET_TYPE_W5500, "OPENETH": EthernetType.ETHERNET_TYPE_OPENETH, "DM9051": EthernetType.ETHERNET_TYPE_DM9051, + "LAN8670": EthernetType.ETHERNET_TYPE_LAN8670, +} + +# PHY types that need compile-time defines for conditional compilation +_PHY_TYPE_TO_DEFINE = { + "KSZ8081": "USE_ETHERNET_KSZ8081", + "KSZ8081RNA": "USE_ETHERNET_KSZ8081", + "LAN8670": "USE_ETHERNET_LAN8670", + # Add other PHY types here only if they need conditional compilation } SPI_ETHERNET_TYPES = ["W5500", "DM9051"] @@ -105,19 +151,15 @@ ManualIP = ethernet_ns.struct("ManualIP") def _is_framework_spi_polling_mode_supported(): # SPI Ethernet without IRQ feature is added in - # esp-idf >= (5.3+ ,5.2.1+, 5.1.4) and arduino-esp32 >= 3.0.0 + # esp-idf >= (5.3+ ,5.2.1+, 5.1.4) + # Note: Arduino now uses ESP-IDF as a component, so we only check IDF version framework_version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] - if CORE.using_esp_idf: - if framework_version >= cv.Version(5, 3, 0): - return True - if cv.Version(5, 3, 0) > framework_version >= cv.Version(5, 2, 1): - return True - if cv.Version(5, 2, 0) > framework_version >= cv.Version(5, 1, 4): # noqa: SIM103 - return True - return False - if CORE.using_arduino: - return framework_version >= cv.Version(3, 0, 0) - # fail safe: Unknown framework + if framework_version >= cv.Version(5, 3, 0): + return True + if cv.Version(5, 3, 0) > framework_version >= cv.Version(5, 2, 1): + return True + if cv.Version(5, 2, 0) > framework_version >= cv.Version(5, 1, 4): # noqa: SIM103 + return True return False @@ -128,6 +170,7 @@ def _validate(config): else: use_address = CORE.name + config[CONF_DOMAIN] config[CONF_USE_ADDRESS] = use_address + if config[CONF_TYPE] in SPI_ETHERNET_TYPES: if _is_framework_spi_polling_mode_supported(): if CONF_POLLING_INTERVAL in config and CONF_INTERRUPT_PIN in config: @@ -160,6 +203,12 @@ def _validate(config): del config[CONF_CLK_MODE] elif CONF_CLK not in config: raise cv.Invalid("'clk' is a required option for [ethernet].") + variant = get_esp32_variant() + if variant not in (VARIANT_ESP32, VARIANT_ESP32P4): + raise cv.Invalid( + f"{config[CONF_TYPE]} PHY requires RMII interface and is only supported " + f"on ESP32 classic and ESP32-P4, not {variant}" + ) return config @@ -174,6 +223,7 @@ BASE_SCHEMA = cv.Schema( "This option has been removed. Please use the [disabled] option under the " "new mdns component instead." ), + cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, } ).extend(cv.COMPONENT_SCHEMA) @@ -240,6 +290,7 @@ CONFIG_SCHEMA = cv.All( "W5500": SPI_SCHEMA, "OPENETH": BASE_SCHEMA, "DM9051": SPI_SCHEMA, + "LAN8670": RMII_SCHEMA, }, upper=True, ), @@ -247,7 +298,7 @@ CONFIG_SCHEMA = cv.All( ) -def _final_validate(config): +def _final_validate_spi(config): if config[CONF_TYPE] not in SPI_ETHERNET_TYPES: return if spi_configs := fv.full_config.get().get(CONF_SPI): @@ -266,17 +317,14 @@ def _final_validate(config): ) -FINAL_VALIDATE_SCHEMA = _final_validate - - def manual_ip(config): return cg.StructInitializer( ManualIP, - ("static_ip", IPAddress(str(config[CONF_STATIC_IP]))), - ("gateway", IPAddress(str(config[CONF_GATEWAY]))), - ("subnet", IPAddress(str(config[CONF_SUBNET]))), - ("dns1", IPAddress(str(config[CONF_DNS1]))), - ("dns2", IPAddress(str(config[CONF_DNS2]))), + ("static_ip", ip_address_literal(config[CONF_STATIC_IP])), + ("gateway", ip_address_literal(config[CONF_GATEWAY])), + ("subnet", ip_address_literal(config[CONF_SUBNET])), + ("dns1", ip_address_literal(config[CONF_DNS1])), + ("dns2", ip_address_literal(config[CONF_DNS2])), ) @@ -289,7 +337,7 @@ def phy_register(address: int, value: int, page: int): ) -@coroutine_with_priority(60.0) +@coroutine_with_priority(CoroPriority.COMMUNICATION) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) @@ -310,11 +358,8 @@ async def to_code(config): cg.add(var.set_clock_speed(config[CONF_CLOCK_SPEED])) cg.add_define("USE_ETHERNET_SPI") - if CORE.using_esp_idf: - add_idf_sdkconfig_option("CONFIG_ETH_USE_SPI_ETHERNET", True) - add_idf_sdkconfig_option( - f"CONFIG_ETH_SPI_ETHERNET_{config[CONF_TYPE]}", True - ) + add_idf_sdkconfig_option("CONFIG_ETH_USE_SPI_ETHERNET", True) + add_idf_sdkconfig_option(f"CONFIG_ETH_SPI_ETHERNET_{config[CONF_TYPE]}", True) elif config[CONF_TYPE] == "OPENETH": cg.add_define("USE_ETHERNET_OPENETH") add_idf_sdkconfig_option("CONFIG_ETH_USE_OPENETH", True) @@ -338,15 +383,80 @@ async def to_code(config): cg.add(var.set_use_address(config[CONF_USE_ADDRESS])) if CONF_MANUAL_IP in config: + cg.add_define("USE_ETHERNET_MANUAL_IP") cg.add(var.set_manual_ip(manual_ip(config[CONF_MANUAL_IP]))) + # Add compile-time define for PHY types with specific code + if phy_define := _PHY_TYPE_TO_DEFINE.get(config[CONF_TYPE]): + cg.add_define(phy_define) + + if mac_address := config.get(CONF_MAC_ADDRESS): + cg.add(var.set_fixed_mac(mac_address.parts)) + cg.add_define("USE_ETHERNET") # Disable WiFi when using Ethernet to save memory - if CORE.using_esp_idf: - add_idf_sdkconfig_option("CONFIG_ESP_WIFI_ENABLED", False) - # Also disable WiFi/BT coexistence since WiFi is disabled - add_idf_sdkconfig_option("CONFIG_SW_COEXIST_ENABLE", False) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_ENABLED", False) + # Also disable WiFi/BT coexistence since WiFi is disabled + add_idf_sdkconfig_option("CONFIG_SW_COEXIST_ENABLE", False) + + if config[CONF_TYPE] == "LAN8670": + # Add LAN867x 10BASE-T1S PHY support component + add_idf_component(name="espressif/lan867x", ref="2.0.0") if CORE.using_arduino: cg.add_library("WiFi", None) + + +def _final_validate_rmii_pins(config: ConfigType) -> None: + """Validate that RMII pins are not used by other components.""" + # Only validate for RMII-based PHYs on ESP32/ESP32P4 + if config[CONF_TYPE] in SPI_ETHERNET_TYPES or config[CONF_TYPE] == "OPENETH": + return # SPI and OPENETH don't use RMII + + variant = get_esp32_variant() + if variant == VARIANT_ESP32: + rmii_pins = ESP32_RMII_FIXED_PINS + is_configurable = False + elif variant == VARIANT_ESP32P4: + rmii_pins = ESP32P4_RMII_DEFAULT_PINS + is_configurable = True + else: + return # No RMII validation needed for other variants + + # Check all used pins against RMII reserved pins + for pin_list in pins.PIN_SCHEMA_REGISTRY.pins_used.values(): + for pin_path, _, pin_config in pin_list: + pin_num = pin_config.get(CONF_NUMBER) + if pin_num not in rmii_pins: + continue + # Found a conflict - show helpful error message + pin_function = rmii_pins[pin_num] + component_path = ".".join(str(p) for p in pin_path) + if is_configurable: + error_msg = ( + f"GPIO{pin_num} is used by Ethernet RMII " + f"({pin_function}) with the current default " + f"configuration. This conflicts with '{component_path}'. " + f"Please choose a different GPIO pin for " + f"'{component_path}'." + ) + else: + error_msg = ( + f"GPIO{pin_num} is reserved for Ethernet RMII " + f"({pin_function}) and cannot be used. This pin is " + f"hardcoded by ESP-IDF and cannot be changed when using " + f"RMII Ethernet PHYs. Please choose a different GPIO pin " + f"for '{component_path}'." + ) + raise cv.Invalid(error_msg, path=pin_path) + + +def _final_validate(config: ConfigType) -> ConfigType: + """Final validation for Ethernet component.""" + _final_validate_spi(config) + _final_validate_rmii_pins(config) + return config + + +FINAL_VALIDATE_SCHEMA = _final_validate diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 87913488da..9a46aa2687 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -9,6 +9,10 @@ #include #include "esp_event.h" +#ifdef USE_ETHERNET_LAN8670 +#include "esp_eth_phy_lan867x.h" +#endif + #ifdef USE_ETHERNET_SPI #include #include @@ -37,17 +41,20 @@ static const char *const TAG = "ethernet"; EthernetComponent *global_eth_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +void EthernetComponent::log_error_and_mark_failed_(esp_err_t err, const char *message) { + ESP_LOGE(TAG, "%s: (%d) %s", message, err, esp_err_to_name(err)); + this->mark_failed(); +} + #define ESPHL_ERROR_CHECK(err, message) \ if ((err) != ESP_OK) { \ - ESP_LOGE(TAG, message ": (%d) %s", err, esp_err_to_name(err)); \ - this->mark_failed(); \ + this->log_error_and_mark_failed_(err, message); \ return; \ } #define ESPHL_ERROR_CHECK_RET(err, message, ret) \ if ((err) != ESP_OK) { \ - ESP_LOGE(TAG, message ": (%d) %s", err, esp_err_to_name(err)); \ - this->mark_failed(); \ + this->log_error_and_mark_failed_(err, message); \ return ret; \ } @@ -200,6 +207,12 @@ void EthernetComponent::setup() { this->phy_ = esp_eth_phy_new_ksz80xx(&phy_config); break; } +#ifdef USE_ETHERNET_LAN8670 + case ETHERNET_TYPE_LAN8670: { + this->phy_ = esp_eth_phy_new_lan867x(&phy_config); + break; + } +#endif #endif #ifdef USE_ETHERNET_SPI #if CONFIG_ETH_SPI_ETHERNET_W5500 @@ -229,10 +242,12 @@ void EthernetComponent::setup() { ESPHL_ERROR_CHECK(err, "ETH driver install error"); #ifndef USE_ETHERNET_SPI +#ifdef USE_ETHERNET_KSZ8081 if (this->type_ == ETHERNET_TYPE_KSZ8081RNA && this->clk_mode_ == EMAC_CLK_OUT) { // KSZ8081RNA default is incorrect. It expects a 25MHz clock instead of the 50MHz we provide. this->ksz8081_set_clock_reference_(mac); } +#endif // USE_ETHERNET_KSZ8081 for (const auto &phy_register : this->phy_registers_) { this->write_phy_register_(mac, phy_register); @@ -241,7 +256,11 @@ void EthernetComponent::setup() { // use ESP internal eth mac uint8_t mac_addr[6]; - esp_read_mac(mac_addr, ESP_MAC_ETH); + if (this->fixed_mac_.has_value()) { + memcpy(mac_addr, this->fixed_mac_->data(), 6); + } else { + esp_read_mac(mac_addr, ESP_MAC_ETH); + } err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_S_MAC_ADDR, mac_addr); ESPHL_ERROR_CHECK(err, "set mac address error"); @@ -300,6 +319,7 @@ void EthernetComponent::loop() { this->state_ = EthernetComponentState::CONNECTING; this->start_connect_(); } else { + this->finish_connect_(); // When connected and stable, disable the loop to save CPU cycles this->disable_loop(); } @@ -350,12 +370,21 @@ void EthernetComponent::dump_config() { eth_type = "DM9051"; break; +#ifdef USE_ETHERNET_LAN8670 + case ETHERNET_TYPE_LAN8670: + eth_type = "LAN8670"; + break; +#endif + default: eth_type = "Unknown"; break; } - ESP_LOGCONFIG(TAG, "Ethernet:"); + ESP_LOGCONFIG(TAG, + "Ethernet:\n" + " Connected: %s", + YESNO(this->is_connected())); this->dump_connect_params_(); #ifdef USE_ETHERNET_SPI ESP_LOGCONFIG(TAG, @@ -392,8 +421,6 @@ void EthernetComponent::dump_config() { float EthernetComponent::get_setup_priority() const { return setup_priority::WIFI; } -bool EthernetComponent::can_proceed() { return this->is_connected(); } - network::IPAddresses EthernetComponent::get_ip_addresses() { network::IPAddresses addresses; esp_netif_ip_info_t ip; @@ -486,13 +513,38 @@ void EthernetComponent::got_ip6_event_handler(void *arg, esp_event_base_t event_ } #endif /* USE_NETWORK_IPV6 */ +void EthernetComponent::finish_connect_() { +#if USE_NETWORK_IPV6 + // Retry IPv6 link-local setup if it failed during initial connect + // This handles the case where min_ipv6_addr_count is NOT set (or is 0), + // allowing us to reach CONNECTED state with just IPv4. + // If IPv6 setup failed in start_connect_() because the interface wasn't ready: + // - Bootup timing issues (#10281) + // - Cable unplugged/network interruption (#10705) + // We can now retry since we're in CONNECTED state and the interface is definitely up. + if (!this->ipv6_setup_done_) { + esp_err_t err = esp_netif_create_ip6_linklocal(this->eth_netif_); + if (err == ESP_OK) { + ESP_LOGD(TAG, "IPv6 link-local address created (retry succeeded)"); + } + // Always set the flag to prevent continuous retries + // If IPv6 setup fails here with the interface up and stable, it's + // likely a persistent issue (IPv6 disabled at router, hardware + // limitation, etc.) that won't be resolved by further retries. + // The device continues to work with IPv4. + this->ipv6_setup_done_ = true; + } +#endif /* USE_NETWORK_IPV6 */ +} + void EthernetComponent::start_connect_() { global_eth_component->got_ipv4_address_ = false; #if USE_NETWORK_IPV6 global_eth_component->ipv6_count_ = 0; + this->ipv6_setup_done_ = false; #endif /* USE_NETWORK_IPV6 */ this->connect_begin_ = millis(); - this->status_set_warning("waiting for IP configuration"); + this->status_set_warning(LOG_STR("waiting for IP configuration")); esp_err_t err; err = esp_netif_set_hostname(this->eth_netif_, App.get_name().c_str()); @@ -501,11 +553,14 @@ void EthernetComponent::start_connect_() { } esp_netif_ip_info_t info; +#ifdef USE_ETHERNET_MANUAL_IP if (this->manual_ip_.has_value()) { info.ip = this->manual_ip_->static_ip; info.gw = this->manual_ip_->gateway; info.netmask = this->manual_ip_->subnet; - } else { + } else +#endif + { info.ip.addr = 0; info.gw.addr = 0; info.netmask.addr = 0; @@ -526,6 +581,7 @@ void EthernetComponent::start_connect_() { err = esp_netif_set_ip_info(this->eth_netif_, &info); ESPHL_ERROR_CHECK(err, "DHCPC set IP info error"); +#ifdef USE_ETHERNET_MANUAL_IP if (this->manual_ip_.has_value()) { LwIPLock lock; if (this->manual_ip_->dns1.is_set()) { @@ -538,16 +594,36 @@ void EthernetComponent::start_connect_() { d = this->manual_ip_->dns2; dns_setserver(1, &d); } - } else { + } else +#endif + { err = esp_netif_dhcpc_start(this->eth_netif_); if (err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STARTED) { ESPHL_ERROR_CHECK(err, "DHCPC start error"); } } #if USE_NETWORK_IPV6 + // Attempt to create IPv6 link-local address + // We MUST attempt this here, not just in finish_connect_(), because with + // min_ipv6_addr_count set, the component won't reach CONNECTED state without IPv6. + // However, this may fail with ESP_FAIL if the interface is not up yet: + // - At bootup when link isn't ready (#10281) + // - After disconnection/cable unplugged (#10705) + // We'll retry in finish_connect_() if it fails here. err = esp_netif_create_ip6_linklocal(this->eth_netif_); if (err != ESP_OK) { - ESPHL_ERROR_CHECK(err, "Enable IPv6 link local failed"); + if (err == ESP_ERR_ESP_NETIF_INVALID_PARAMS) { + // This is a programming error, not a transient failure + ESPHL_ERROR_CHECK(err, "esp_netif_create_ip6_linklocal invalid parameters"); + } else { + // ESP_FAIL means the interface isn't up yet + // This is expected and non-fatal, happens in multiple scenarios: + // - During reconnection after network interruptions (#10705) + // - At bootup when the link isn't ready yet (#10281) + // We'll retry once we reach CONNECTED state and the interface is up + ESP_LOGW(TAG, "esp_netif_create_ip6_linklocal failed: %s", esp_err_to_name(err)); + // Don't mark component as failed - this is a transient error + } } #endif /* USE_NETWORK_IPV6 */ @@ -618,16 +694,15 @@ void EthernetComponent::set_clk_mode(emac_rmii_clock_mode_t clk_mode) { this->cl void EthernetComponent::add_phy_register(PHYRegister register_value) { this->phy_registers_.push_back(register_value); } #endif void EthernetComponent::set_type(EthernetType type) { this->type_ = type; } +#ifdef USE_ETHERNET_MANUAL_IP void EthernetComponent::set_manual_ip(const ManualIP &manual_ip) { this->manual_ip_ = manual_ip; } +#endif -std::string EthernetComponent::get_use_address() const { - if (this->use_address_.empty()) { - return App.get_name() + ".local"; - } - return this->use_address_; -} +// set_use_address() is guaranteed to be called during component setup by Python code generation, +// so use_address_ will always be valid when get_use_address() is called - no fallback needed. +const char *EthernetComponent::get_use_address() const { return this->use_address_; } -void EthernetComponent::set_use_address(const std::string &use_address) { this->use_address_ = use_address; } +void EthernetComponent::set_use_address(const char *use_address) { this->use_address_ = use_address; } void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) { esp_err_t err; @@ -638,7 +713,9 @@ void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) { std::string EthernetComponent::get_eth_mac_address_pretty() { uint8_t mac[6]; get_eth_mac_address_raw(mac); - return str_snprintf("%02X:%02X:%02X:%02X:%02X:%02X", 17, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + char buf[18]; + format_mac_addr_upper(mac, buf); + return std::string(buf); } eth_duplex_t EthernetComponent::get_duplex_mode() { @@ -675,6 +752,7 @@ bool EthernetComponent::powerdown() { #ifndef USE_ETHERNET_SPI +#ifdef USE_ETHERNET_KSZ8081 constexpr uint8_t KSZ80XX_PC2R_REG_ADDR = 0x1F; void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) { @@ -703,6 +781,7 @@ void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) { ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty((u_int8_t *) &phy_control_2, 2).c_str()); } } +#endif // USE_ETHERNET_KSZ8081 void EthernetComponent::write_phy_register_(esp_eth_mac_t *mac, PHYRegister register_data) { esp_err_t err; diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index bdcda6afb4..bffed4dc4a 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -28,6 +28,7 @@ enum EthernetType : uint8_t { ETHERNET_TYPE_W5500, ETHERNET_TYPE_OPENETH, ETHERNET_TYPE_DM9051, + ETHERNET_TYPE_LAN8670, }; struct ManualIP { @@ -57,7 +58,6 @@ class EthernetComponent : public Component { void loop() override; void dump_config() override; float get_setup_priority() const override; - bool can_proceed() override; void on_powerdown() override { powerdown(); } bool is_connected(); @@ -82,12 +82,15 @@ class EthernetComponent : public Component { void add_phy_register(PHYRegister register_value); #endif void set_type(EthernetType type); +#ifdef USE_ETHERNET_MANUAL_IP void set_manual_ip(const ManualIP &manual_ip); +#endif + void set_fixed_mac(const std::array &mac) { this->fixed_mac_ = mac; } network::IPAddresses get_ip_addresses(); network::IPAddress get_dns_address(uint8_t num); - std::string get_use_address() const; - void set_use_address(const std::string &use_address); + const char *get_use_address() const; + void set_use_address(const char *use_address); void get_eth_mac_address_raw(uint8_t *mac); std::string get_eth_mac_address_pretty(); eth_duplex_t get_duplex_mode(); @@ -102,13 +105,16 @@ class EthernetComponent : public Component { #endif /* LWIP_IPV6 */ void start_connect_(); + void finish_connect_(); void dump_connect_params_(); + void log_error_and_mark_failed_(esp_err_t err, const char *message); +#ifdef USE_ETHERNET_KSZ8081 /// @brief Set `RMII Reference Clock Select` bit for KSZ8081. void ksz8081_set_clock_reference_(esp_eth_mac_t *mac); +#endif /// @brief Set arbitratry PHY registers from config. void write_phy_register_(esp_eth_mac_t *mac, PHYRegister register_data); - std::string use_address_; #ifdef USE_ETHERNET_SPI uint8_t clk_pin_; uint8_t miso_pin_; @@ -133,7 +139,9 @@ class EthernetComponent : public Component { uint8_t mdc_pin_{23}; uint8_t mdio_pin_{18}; #endif +#ifdef USE_ETHERNET_MANUAL_IP optional manual_ip_{}; +#endif uint32_t connect_begin_; // Group all uint8_t types together (enums and bools) @@ -144,18 +152,25 @@ class EthernetComponent : public Component { bool got_ipv4_address_{false}; #if LWIP_IPV6 uint8_t ipv6_count_{0}; + bool ipv6_setup_done_{false}; #endif /* LWIP_IPV6 */ // Pointers at the end (naturally aligned) esp_netif_t *eth_netif_{nullptr}; esp_eth_handle_t eth_handle_; esp_eth_phy_t *phy_{nullptr}; + optional> fixed_mac_; + + private: + // Stores a pointer to a string literal (static storage duration). + // ONLY set from Python-generated code with string literals - never dynamic strings. + const char *use_address_{""}; }; // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) extern EthernetComponent *global_eth_component; -#if defined(USE_ARDUINO) || ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) extern "C" esp_eth_phy_t *esp_eth_phy_new_jl1101(const eth_phy_config_t *config); #endif diff --git a/esphome/components/event/__init__.py b/esphome/components/event/__init__.py index 1948570ecd..e2b69ba872 100644 --- a/esphome/components/event/__init__.py +++ b/esphome/components/event/__init__.py @@ -17,7 +17,7 @@ from esphome.const import ( DEVICE_CLASS_EMPTY, DEVICE_CLASS_MOTION, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass @@ -85,11 +85,6 @@ def event_schema( return _EVENT_SCHEMA.extend(schema) -# Remove before 2025.11.0 -EVENT_SCHEMA = event_schema() -EVENT_SCHEMA.add_extra(cv.deprecated_schema_constant("event")) - - async def setup_event_core_(var, config, *, event_types: list[str]): await setup_entity(var, config, "event") @@ -143,6 +138,6 @@ async def event_fire_to_code(config, action_id, template_arg, args): return var -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(event_ns.using) diff --git a/esphome/components/event/automation.h b/esphome/components/event/automation.h index 9ebcb654a0..5bdba18687 100644 --- a/esphome/components/event/automation.h +++ b/esphome/components/event/automation.h @@ -11,7 +11,7 @@ template class TriggerEventAction : public Action, public public: TEMPLATABLE_VALUE(std::string, event_type) - void play(Ts... x) override { this->parent_->trigger(this->event_type_.value(x...)); } + void play(const Ts &...x) override { this->parent_->trigger(this->event_type_.value(x...)); } }; class EventTrigger : public Trigger { diff --git a/esphome/components/event/event.cpp b/esphome/components/event/event.cpp index d27b3b378e..4c74a11388 100644 --- a/esphome/components/event/event.cpp +++ b/esphome/components/event/event.cpp @@ -1,5 +1,6 @@ #include "event.h" - +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" namespace esphome { @@ -8,14 +9,40 @@ namespace event { static const char *const TAG = "event"; void Event::trigger(const std::string &event_type) { - auto found = types_.find(event_type); - if (found == types_.end()) { + // Linear search with strcmp - faster than std::set for small datasets (1-5 items typical) + const char *found = nullptr; + for (const char *type : this->types_) { + if (strcmp(type, event_type.c_str()) == 0) { + found = type; + break; + } + } + if (found == nullptr) { ESP_LOGE(TAG, "'%s': invalid event type for trigger(): %s", this->get_name().c_str(), event_type.c_str()); return; } - last_event_type = &(*found); - ESP_LOGD(TAG, "'%s' Triggered event '%s'", this->get_name().c_str(), last_event_type->c_str()); + this->last_event_type_ = found; + ESP_LOGD(TAG, "'%s' Triggered event '%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); +#endif +} + +void Event::set_event_types(const FixedVector &event_types) { + this->types_.init(event_types.size()); + for (const char *type : event_types) { + this->types_.push_back(type); + } + this->last_event_type_ = nullptr; // Reset when types change +} + +void Event::set_event_types(const std::vector &event_types) { + this->types_.init(event_types.size()); + for (const char *type : event_types) { + this->types_.push_back(type); + } + this->last_event_type_ = nullptr; // Reset when types change } void Event::add_on_event_callback(std::function &&callback) { diff --git a/esphome/components/event/event.h b/esphome/components/event/event.h index 03c3c8d95a..e4b2e0b845 100644 --- a/esphome/components/event/event.h +++ b/esphome/components/event/event.h @@ -1,7 +1,8 @@ #pragma once -#include +#include #include +#include #include "esphome/core/component.h" #include "esphome/core/entity_base.h" @@ -13,26 +14,49 @@ namespace event { #define LOG_EVENT(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - if (!(obj)->get_icon().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \ + if (!(obj)->get_icon_ref().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon_ref().c_str()); \ } \ - if (!(obj)->get_device_class().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class().c_str()); \ + if (!(obj)->get_device_class_ref().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class_ref().c_str()); \ } \ } class Event : public EntityBase, public EntityBase_DeviceClass { public: - const std::string *last_event_type; - void trigger(const std::string &event_type); - void set_event_types(const std::set &event_types) { this->types_ = event_types; } - std::set get_event_types() const { return this->types_; } + + /// Set the event types supported by this event (from initializer list). + void set_event_types(std::initializer_list event_types) { + this->types_ = event_types; + this->last_event_type_ = nullptr; // Reset when types change + } + /// Set the event types supported by this event (from FixedVector). + void set_event_types(const FixedVector &event_types); + /// Set the event types supported by this event (from vector). + void set_event_types(const std::vector &event_types); + + // Deleted overloads to catch incorrect std::string usage at compile time with clear error messages + void set_event_types(std::initializer_list event_types) = delete; + void set_event_types(const FixedVector &event_types) = delete; + void set_event_types(const std::vector &event_types) = delete; + + /// Return the event types supported by this event. + const FixedVector &get_event_types() const { return this->types_; } + + /// Return the last triggered event type (pointer to string in types_), or nullptr if no event triggered yet. + const char *get_last_event_type() const { return this->last_event_type_; } + void add_on_event_callback(std::function &&callback); protected: CallbackManager event_callback_; - std::set types_; + FixedVector types_; + + private: + /// Last triggered event type - must point to entry in types_ to ensure valid lifetime. + /// Set by trigger() after validation, reset to nullptr when types_ changes. + const char *last_event_type_{nullptr}; }; } // namespace event diff --git a/esphome/components/event_emitter/__init__.py b/esphome/components/event_emitter/__init__.py deleted file mode 100644 index fcbbf26f02..0000000000 --- a/esphome/components/event_emitter/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -CODEOWNERS = ["@Rapsssito"] - -# Allows event_emitter to be configured in yaml, to allow use of the C++ api. - -CONFIG_SCHEMA = {} diff --git a/esphome/components/event_emitter/event_emitter.cpp b/esphome/components/event_emitter/event_emitter.cpp deleted file mode 100644 index 8487e19c2f..0000000000 --- a/esphome/components/event_emitter/event_emitter.cpp +++ /dev/null @@ -1,14 +0,0 @@ -#include "event_emitter.h" - -namespace esphome { -namespace event_emitter { - -static const char *const TAG = "event_emitter"; - -void raise_event_emitter_full_error() { - ESP_LOGE(TAG, "EventEmitter has reached the maximum number of listeners for event"); - ESP_LOGW(TAG, "Removing listener to make space for new listener"); -} - -} // namespace event_emitter -} // namespace esphome diff --git a/esphome/components/event_emitter/event_emitter.h b/esphome/components/event_emitter/event_emitter.h deleted file mode 100644 index 3876a2cc14..0000000000 --- a/esphome/components/event_emitter/event_emitter.h +++ /dev/null @@ -1,63 +0,0 @@ -#pragma once -#include -#include -#include -#include - -#include "esphome/core/log.h" - -namespace esphome { -namespace event_emitter { - -using EventEmitterListenerID = uint32_t; -void raise_event_emitter_full_error(); - -// EventEmitter class that can emit events with a specific name (it is highly recommended to use an enum class for this) -// and a list of arguments. Supports multiple listeners for each event. -template class EventEmitter { - public: - EventEmitterListenerID on(EvtType event, std::function listener) { - EventEmitterListenerID listener_id = get_next_id_(event); - listeners_[event][listener_id] = listener; - return listener_id; - } - - void off(EvtType event, EventEmitterListenerID id) { - if (listeners_.count(event) == 0) - return; - listeners_[event].erase(id); - } - - protected: - void emit_(EvtType event, Args... args) { - if (listeners_.count(event) == 0) - return; - for (const auto &listener : listeners_[event]) { - listener.second(args...); - } - } - - EventEmitterListenerID get_next_id_(EvtType event) { - // Check if the map is full - if (listeners_[event].size() == std::numeric_limits::max()) { - // Raise an error if the map is full - raise_event_emitter_full_error(); - off(event, 0); - return 0; - } - // Get the next ID for the given event. - EventEmitterListenerID next_id = (current_id_ + 1) % std::numeric_limits::max(); - while (listeners_[event].count(next_id) > 0) { - next_id = (next_id + 1) % std::numeric_limits::max(); - } - current_id_ = next_id; - return current_id_; - } - - private: - std::unordered_map>> listeners_; - EventEmitterListenerID current_id_ = 0; -}; - -} // namespace event_emitter -} // namespace esphome diff --git a/esphome/components/external_components/__init__.py b/esphome/components/external_components/__init__.py index a09217fd21..ceb402c5b7 100644 --- a/esphome/components/external_components/__init__.py +++ b/esphome/components/external_components/__init__.py @@ -39,11 +39,13 @@ async def to_code(config): pass -def _process_git_config(config: dict, refresh) -> str: +def _process_git_config(config: dict, refresh, skip_update: bool = False) -> str: + # When skip_update is True, use NEVER_REFRESH to prevent updates + actual_refresh = git.NEVER_REFRESH if skip_update else refresh repo_dir, _ = git.clone_or_update( url=config[CONF_URL], ref=config.get(CONF_REF), - refresh=refresh, + refresh=actual_refresh, domain=DOMAIN, username=config.get(CONF_USERNAME), password=config.get(CONF_PASSWORD), @@ -70,12 +72,12 @@ def _process_git_config(config: dict, refresh) -> str: return components_dir -def _process_single_config(config: dict): +def _process_single_config(config: dict, skip_update: bool = False): conf = config[CONF_SOURCE] if conf[CONF_TYPE] == TYPE_GIT: with cv.prepend_path([CONF_SOURCE]): components_dir = _process_git_config( - config[CONF_SOURCE], config[CONF_REFRESH] + config[CONF_SOURCE], config[CONF_REFRESH], skip_update ) elif conf[CONF_TYPE] == TYPE_LOCAL: components_dir = Path(CORE.relative_config_path(conf[CONF_PATH])) @@ -105,7 +107,7 @@ def _process_single_config(config: dict): loader.install_meta_finder(components_dir, allowed_components=allowed_components) -def do_external_components_pass(config: dict) -> None: +def do_external_components_pass(config: dict, skip_update: bool = False) -> None: conf = config.get(DOMAIN) if conf is None: return @@ -113,4 +115,4 @@ def do_external_components_pass(config: dict) -> None: conf = CONFIG_SCHEMA(conf) for i, c in enumerate(conf): with cv.prepend_path(i): - _process_single_config(c) + _process_single_config(c, skip_update) diff --git a/esphome/components/ezo/automation.h b/esphome/components/ezo/automation.h index 19427b9159..a4a6fa3014 100644 --- a/esphome/components/ezo/automation.h +++ b/esphome/components/ezo/automation.h @@ -17,35 +17,35 @@ class LedTrigger : public Trigger { class CustomTrigger : public Trigger { public: explicit CustomTrigger(EZOSensor *ezo) { - ezo->add_custom_callback([this](std::string value) { this->trigger(std::move(value)); }); + ezo->add_custom_callback([this](const std::string &value) { this->trigger(value); }); } }; class TTrigger : public Trigger { public: explicit TTrigger(EZOSensor *ezo) { - ezo->add_t_callback([this](std::string value) { this->trigger(std::move(value)); }); + ezo->add_t_callback([this](const std::string &value) { this->trigger(value); }); } }; class CalibrationTrigger : public Trigger { public: explicit CalibrationTrigger(EZOSensor *ezo) { - ezo->add_calibration_callback([this](std::string value) { this->trigger(std::move(value)); }); + ezo->add_calibration_callback([this](const std::string &value) { this->trigger(value); }); } }; class SlopeTrigger : public Trigger { public: explicit SlopeTrigger(EZOSensor *ezo) { - ezo->add_slope_callback([this](std::string value) { this->trigger(std::move(value)); }); + ezo->add_slope_callback([this](const std::string &value) { this->trigger(value); }); } }; class DeviceInformationTrigger : public Trigger { public: explicit DeviceInformationTrigger(EZOSensor *ezo) { - ezo->add_device_infomation_callback([this](std::string value) { this->trigger(std::move(value)); }); + ezo->add_device_infomation_callback([this](const std::string &value) { this->trigger(value); }); } }; diff --git a/esphome/components/ezo_pmp/ezo_pmp.h b/esphome/components/ezo_pmp/ezo_pmp.h index 671e124810..d4917e7f4b 100644 --- a/esphome/components/ezo_pmp/ezo_pmp.h +++ b/esphome/components/ezo_pmp/ezo_pmp.h @@ -119,7 +119,7 @@ template class EzoPMPFindAction : public Action { public: EzoPMPFindAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} - void play(Ts... x) override { this->ezopmp_->find(); } + void play(const Ts &...x) override { this->ezopmp_->find(); } protected: EzoPMP *ezopmp_; @@ -129,7 +129,7 @@ template class EzoPMPDoseContinuouslyAction : public Actionezopmp_->dose_continuously(); } + void play(const Ts &...x) override { this->ezopmp_->dose_continuously(); } protected: EzoPMP *ezopmp_; @@ -139,7 +139,7 @@ template class EzoPMPDoseVolumeAction : public Action { public: EzoPMPDoseVolumeAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} - void play(Ts... x) override { this->ezopmp_->dose_volume(this->volume_.value(x...)); } + void play(const Ts &...x) override { this->ezopmp_->dose_volume(this->volume_.value(x...)); } TEMPLATABLE_VALUE(double, volume) protected: @@ -150,7 +150,7 @@ template class EzoPMPDoseVolumeOverTimeAction : public Actionezopmp_->dose_volume_over_time(this->volume_.value(x...), this->duration_.value(x...)); } TEMPLATABLE_VALUE(double, volume) @@ -164,7 +164,7 @@ template class EzoPMPDoseWithConstantFlowRateAction : public Act public: EzoPMPDoseWithConstantFlowRateAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} - void play(Ts... x) override { + void play(const Ts &...x) override { this->ezopmp_->dose_with_constant_flow_rate(this->volume_.value(x...), this->duration_.value(x...)); } TEMPLATABLE_VALUE(double, volume) @@ -178,7 +178,7 @@ template class EzoPMPSetCalibrationVolumeAction : public Action< public: EzoPMPSetCalibrationVolumeAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} - void play(Ts... x) override { this->ezopmp_->set_calibration_volume(this->volume_.value(x...)); } + void play(const Ts &...x) override { this->ezopmp_->set_calibration_volume(this->volume_.value(x...)); } TEMPLATABLE_VALUE(double, volume) protected: @@ -189,7 +189,7 @@ template class EzoPMPClearTotalVolumeDispensedAction : public Ac public: EzoPMPClearTotalVolumeDispensedAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} - void play(Ts... x) override { this->ezopmp_->clear_total_volume_dosed(); } + void play(const Ts &...x) override { this->ezopmp_->clear_total_volume_dosed(); } protected: EzoPMP *ezopmp_; @@ -199,7 +199,7 @@ template class EzoPMPClearCalibrationAction : public Actionezopmp_->clear_calibration(); } + void play(const Ts &...x) override { this->ezopmp_->clear_calibration(); } protected: EzoPMP *ezopmp_; @@ -209,7 +209,7 @@ template class EzoPMPPauseDosingAction : public Action { public: EzoPMPPauseDosingAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} - void play(Ts... x) override { this->ezopmp_->pause_dosing(); } + void play(const Ts &...x) override { this->ezopmp_->pause_dosing(); } protected: EzoPMP *ezopmp_; @@ -219,7 +219,7 @@ template class EzoPMPStopDosingAction : public Action { public: EzoPMPStopDosingAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} - void play(Ts... x) override { this->ezopmp_->stop_dosing(); } + void play(const Ts &...x) override { this->ezopmp_->stop_dosing(); } protected: EzoPMP *ezopmp_; @@ -229,7 +229,7 @@ template class EzoPMPChangeI2CAddressAction : public Actionezopmp_->change_i2c_address(this->address_.value(x...)); } + void play(const Ts &...x) override { this->ezopmp_->change_i2c_address(this->address_.value(x...)); } TEMPLATABLE_VALUE(int, address) protected: @@ -240,7 +240,7 @@ template class EzoPMPArbitraryCommandAction : public Actionezopmp_->exec_arbitrary_command(this->command_.value(x...)); } + void play(const Ts &...x) override { this->ezopmp_->exec_arbitrary_command(this->command_.value(x...)); } TEMPLATABLE_VALUE(std::string, command) protected: diff --git a/esphome/components/factory_reset/button/factory_reset_button.cpp b/esphome/components/factory_reset/button/factory_reset_button.cpp index 585975c043..d582317767 100644 --- a/esphome/components/factory_reset/button/factory_reset_button.cpp +++ b/esphome/components/factory_reset/button/factory_reset_button.cpp @@ -1,7 +1,13 @@ #include "factory_reset_button.h" + +#include "esphome/core/defines.h" + +#ifdef USE_OPENTHREAD +#include "esphome/components/openthread/openthread.h" +#endif +#include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" -#include "esphome/core/application.h" namespace esphome { namespace factory_reset { @@ -13,9 +19,20 @@ void FactoryResetButton::press_action() { ESP_LOGI(TAG, "Resetting"); // Let MQTT settle a bit delay(100); // NOLINT +#ifdef USE_OPENTHREAD + openthread::global_openthread_component->on_factory_reset(FactoryResetButton::factory_reset_callback); +#else + global_preferences->reset(); + App.safe_reboot(); +#endif +} + +#ifdef USE_OPENTHREAD +void FactoryResetButton::factory_reset_callback() { global_preferences->reset(); App.safe_reboot(); } +#endif } // namespace factory_reset } // namespace esphome diff --git a/esphome/components/factory_reset/button/factory_reset_button.h b/esphome/components/factory_reset/button/factory_reset_button.h index 9996a860d9..c68da2ca74 100644 --- a/esphome/components/factory_reset/button/factory_reset_button.h +++ b/esphome/components/factory_reset/button/factory_reset_button.h @@ -1,7 +1,9 @@ #pragma once -#include "esphome/core/component.h" +#include "esphome/core/defines.h" + #include "esphome/components/button/button.h" +#include "esphome/core/component.h" namespace esphome { namespace factory_reset { @@ -9,6 +11,9 @@ namespace factory_reset { class FactoryResetButton : public button::Button, public Component { public: void dump_config() override; +#ifdef USE_OPENTHREAD + static void factory_reset_callback(); +#endif protected: void press_action() override; diff --git a/esphome/components/factory_reset/switch/factory_reset_switch.cpp b/esphome/components/factory_reset/switch/factory_reset_switch.cpp index 1282c73f4e..75449aa526 100644 --- a/esphome/components/factory_reset/switch/factory_reset_switch.cpp +++ b/esphome/components/factory_reset/switch/factory_reset_switch.cpp @@ -1,7 +1,13 @@ #include "factory_reset_switch.h" + +#include "esphome/core/defines.h" + +#ifdef USE_OPENTHREAD +#include "esphome/components/openthread/openthread.h" +#endif +#include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" -#include "esphome/core/application.h" namespace esphome { namespace factory_reset { @@ -17,10 +23,21 @@ void FactoryResetSwitch::write_state(bool state) { ESP_LOGI(TAG, "Resetting"); // Let MQTT settle a bit delay(100); // NOLINT +#ifdef USE_OPENTHREAD + openthread::global_openthread_component->on_factory_reset(FactoryResetSwitch::factory_reset_callback); +#else global_preferences->reset(); App.safe_reboot(); +#endif } } +#ifdef USE_OPENTHREAD +void FactoryResetSwitch::factory_reset_callback() { + global_preferences->reset(); + App.safe_reboot(); +} +#endif + } // namespace factory_reset } // namespace esphome diff --git a/esphome/components/factory_reset/switch/factory_reset_switch.h b/esphome/components/factory_reset/switch/factory_reset_switch.h index 2c914ea76d..8ea0c79108 100644 --- a/esphome/components/factory_reset/switch/factory_reset_switch.h +++ b/esphome/components/factory_reset/switch/factory_reset_switch.h @@ -1,7 +1,8 @@ #pragma once -#include "esphome/core/component.h" #include "esphome/components/switch/switch.h" +#include "esphome/core/component.h" +#include "esphome/core/defines.h" namespace esphome { namespace factory_reset { @@ -9,6 +10,9 @@ namespace factory_reset { class FactoryResetSwitch : public switch_::Switch, public Component { public: void dump_config() override; +#ifdef USE_OPENTHREAD + static void factory_reset_callback(); +#endif protected: void write_state(bool state) override; diff --git a/esphome/components/fan/__init__.py b/esphome/components/fan/__init__.py index 3fb217a24e..35a351e8f1 100644 --- a/esphome/components/fan/__init__.py +++ b/esphome/components/fan/__init__.py @@ -31,14 +31,13 @@ from esphome.const import ( CONF_TRIGGER_ID, CONF_WEB_SERVER, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity IS_PLATFORM_COMPONENT = True fan_ns = cg.esphome_ns.namespace("fan") Fan = fan_ns.class_("Fan", cg.EntityBase) -FanState = fan_ns.class_("Fan", Fan, cg.Component) FanDirection = fan_ns.enum("FanDirection", is_class=True) FAN_DIRECTION_ENUM = { @@ -190,10 +189,6 @@ def fan_schema( return _FAN_SCHEMA.extend(schema) -# Remove before 2025.11.0 -FAN_SCHEMA = fan_schema(Fan) -FAN_SCHEMA.add_extra(cv.deprecated_schema_constant("fan")) - _PRESET_MODES_SCHEMA = cv.All( cv.ensure_list(cv.string_strict), cv.Length(min=1), @@ -398,6 +393,6 @@ async def fan_is_on_off_to_code(config, condition_id, template_arg, args): return cg.new_Pvariable(condition_id, template_arg, paren) -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(fan_ns.using) diff --git a/esphome/components/fan/automation.h b/esphome/components/fan/automation.h index d480a2ef44..ce1db6fc64 100644 --- a/esphome/components/fan/automation.h +++ b/esphome/components/fan/automation.h @@ -1,8 +1,8 @@ #pragma once -#include "esphome/core/component.h" #include "esphome/core/automation.h" -#include "fan_state.h" +#include "esphome/core/component.h" +#include "fan.h" namespace esphome { namespace fan { @@ -15,7 +15,7 @@ template class TurnOnAction : public Action { TEMPLATABLE_VALUE(int, speed) TEMPLATABLE_VALUE(FanDirection, direction) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->state_->turn_on(); if (this->oscillating_.has_value()) { call.set_oscillating(this->oscillating_.value(x...)); @@ -36,7 +36,7 @@ template class TurnOffAction : public Action { public: explicit TurnOffAction(Fan *state) : state_(state) {} - void play(Ts... x) override { this->state_->turn_off().perform(); } + void play(const Ts &...x) override { this->state_->turn_off().perform(); } Fan *state_; }; @@ -45,7 +45,7 @@ template class ToggleAction : public Action { public: explicit ToggleAction(Fan *state) : state_(state) {} - void play(Ts... x) override { this->state_->toggle().perform(); } + void play(const Ts &...x) override { this->state_->toggle().perform(); } Fan *state_; }; @@ -56,7 +56,7 @@ template class CycleSpeedAction : public Action { TEMPLATABLE_VALUE(bool, no_off_cycle) - void play(Ts... x) override { + void play(const Ts &...x) override { // check to see if fan supports speeds and is on if (this->state_->get_traits().supported_speed_count()) { if (this->state_->state) { @@ -97,7 +97,7 @@ template class CycleSpeedAction : public Action { template class FanIsOnCondition : public Condition { public: explicit FanIsOnCondition(Fan *state) : state_(state) {} - bool check(Ts... x) override { return this->state_->state; } + bool check(const Ts &...x) override { return this->state_->state; } protected: Fan *state_; @@ -105,7 +105,7 @@ template class FanIsOnCondition : public Condition { template class FanIsOffCondition : public Condition { public: explicit FanIsOffCondition(Fan *state) : state_(state) {} - bool check(Ts... x) override { return !this->state_->state; } + bool check(const Ts &...x) override { return !this->state_->state; } protected: Fan *state_; @@ -212,18 +212,19 @@ class FanPresetSetTrigger : public Trigger { public: FanPresetSetTrigger(Fan *state) { state->add_on_state_callback([this, state]() { - auto preset_mode = state->preset_mode; + const auto *preset_mode = state->get_preset_mode(); auto should_trigger = preset_mode != this->last_preset_mode_; this->last_preset_mode_ = preset_mode; if (should_trigger) { - this->trigger(preset_mode); + // Trigger with empty string when nullptr to maintain backward compatibility + this->trigger(preset_mode != nullptr ? preset_mode : ""); } }); - this->last_preset_mode_ = state->preset_mode; + this->last_preset_mode_ = state->get_preset_mode(); } protected: - std::string last_preset_mode_; + const char *last_preset_mode_{nullptr}; }; } // namespace fan diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index 82fc5319e0..d37825a651 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -1,4 +1,6 @@ #include "fan.h" +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" namespace esphome { @@ -17,6 +19,27 @@ const LogString *fan_direction_to_string(FanDirection direction) { } } +FanCall &FanCall::set_preset_mode(const std::string &preset_mode) { return this->set_preset_mode(preset_mode.c_str()); } + +FanCall &FanCall::set_preset_mode(const char *preset_mode) { + if (preset_mode == nullptr || strlen(preset_mode) == 0) { + this->preset_mode_ = nullptr; + return *this; + } + + // Find and validate pointer from traits immediately + auto traits = this->parent_.get_traits(); + const char *validated_mode = traits.find_preset_mode(preset_mode); + if (validated_mode != nullptr) { + this->preset_mode_ = validated_mode; // Store pointer from traits + } else { + // Preset mode not found in traits - log warning and don't set + ESP_LOGW(TAG, "%s: Preset mode '%s' not supported", this->parent_.get_name().c_str(), preset_mode); + this->preset_mode_ = nullptr; + } + return *this; +} + void FanCall::perform() { ESP_LOGD(TAG, "'%s' - Setting:", this->parent_.get_name().c_str()); this->validate_(); @@ -32,8 +55,8 @@ void FanCall::perform() { if (this->direction_.has_value()) { ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(*this->direction_))); } - if (!this->preset_mode_.empty()) { - ESP_LOGD(TAG, " Preset Mode: %s", this->preset_mode_.c_str()); + if (this->has_preset_mode()) { + ESP_LOGD(TAG, " Preset Mode: %s", this->preset_mode_); } this->parent_.control(*this); } @@ -46,23 +69,15 @@ void FanCall::validate_() { // https://developers.home-assistant.io/docs/core/entity/fan/#preset-modes // "Manually setting a speed must disable any set preset mode" - this->preset_mode_.clear(); - } - - if (!this->preset_mode_.empty()) { - const auto &preset_modes = traits.supported_preset_modes(); - if (preset_modes.find(this->preset_mode_) == preset_modes.end()) { - ESP_LOGW(TAG, "%s: Preset mode '%s' not supported", this->parent_.get_name().c_str(), this->preset_mode_.c_str()); - this->preset_mode_.clear(); - } + this->preset_mode_ = nullptr; } // when turning on... if (!this->parent_.state && this->binary_state_.has_value() && *this->binary_state_ // ..,and no preset mode will be active... - && this->preset_mode_.empty() && - this->parent_.preset_mode.empty() + && !this->has_preset_mode() && + this->parent_.get_preset_mode() == nullptr // ...and neither current nor new speed is available... && traits.supports_speed() && this->parent_.speed == 0 && !this->speed_.has_value()) { // ...set speed to 100% @@ -92,11 +107,12 @@ FanCall FanRestoreState::to_call(Fan &fan) { call.set_speed(this->speed); call.set_direction(this->direction); - if (fan.get_traits().supports_preset_modes()) { + auto traits = fan.get_traits(); + if (traits.supports_preset_modes()) { // Use stored preset index to get preset name - const auto &preset_modes = fan.get_traits().supported_preset_modes(); + const auto &preset_modes = traits.supported_preset_modes(); if (this->preset_mode < preset_modes.size()) { - call.set_preset_mode(*std::next(preset_modes.begin(), this->preset_mode)); + call.set_preset_mode(preset_modes[this->preset_mode]); } } return call; @@ -107,13 +123,15 @@ void FanRestoreState::apply(Fan &fan) { fan.speed = this->speed; fan.direction = this->direction; - if (fan.get_traits().supports_preset_modes()) { - // Use stored preset index to get preset name - const auto &preset_modes = fan.get_traits().supported_preset_modes(); + auto traits = fan.get_traits(); + if (traits.supports_preset_modes()) { + // Use stored preset index to get preset name from traits + const auto &preset_modes = traits.supported_preset_modes(); if (this->preset_mode < preset_modes.size()) { - fan.preset_mode = *std::next(preset_modes.begin(), this->preset_mode); + fan.set_preset_mode_(preset_modes[this->preset_mode]); } } + fan.publish_state(); } @@ -122,6 +140,29 @@ FanCall Fan::turn_off() { return this->make_call().set_state(false); } FanCall Fan::toggle() { return this->make_call().set_state(!this->state); } FanCall Fan::make_call() { return FanCall(*this); } +const char *Fan::find_preset_mode_(const char *preset_mode) { return this->get_traits().find_preset_mode(preset_mode); } + +bool Fan::set_preset_mode_(const char *preset_mode) { + if (preset_mode == nullptr) { + // Treat nullptr as clearing the preset mode + if (this->preset_mode_ == nullptr) { + return false; // No change + } + this->clear_preset_mode_(); + return true; + } + const char *validated = this->find_preset_mode_(preset_mode); + if (validated == nullptr || this->preset_mode_ == validated) { + return false; // Preset mode not supported or no change + } + this->preset_mode_ = validated; + return true; +} + +bool Fan::set_preset_mode_(const std::string &preset_mode) { return this->set_preset_mode_(preset_mode.c_str()); } + +void Fan::clear_preset_mode_() { this->preset_mode_ = nullptr; } + void Fan::add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } void Fan::publish_state() { auto traits = this->get_traits(); @@ -137,10 +178,14 @@ void Fan::publish_state() { if (traits.supports_direction()) { ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(this->direction))); } - if (traits.supports_preset_modes() && !this->preset_mode.empty()) { - ESP_LOGD(TAG, " Preset Mode: %s", this->preset_mode.c_str()); + const char *preset = this->get_preset_mode(); + if (preset != nullptr) { + ESP_LOGD(TAG, " Preset Mode: %s", preset); } this->state_callback_.call(); +#if defined(USE_FAN) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_fan_update(this); +#endif this->save_state_(); } @@ -148,7 +193,8 @@ void Fan::publish_state() { constexpr uint32_t RESTORE_STATE_VERSION = 0x71700ABA; optional Fan::restore_state_() { FanRestoreState recovered{}; - this->rtc_ = global_preferences->make_preference(this->get_object_id_hash() ^ RESTORE_STATE_VERSION); + this->rtc_ = + global_preferences->make_preference(this->get_preference_hash() ^ RESTORE_STATE_VERSION); bool restored = this->rtc_.load(&recovered); switch (this->restore_mode_) { @@ -181,18 +227,24 @@ void Fan::save_state_() { return; } + auto traits = this->get_traits(); + FanRestoreState state{}; state.state = this->state; state.oscillating = this->oscillating; state.speed = this->speed; state.direction = this->direction; - if (this->get_traits().supports_preset_modes() && !this->preset_mode.empty()) { - const auto &preset_modes = this->get_traits().supported_preset_modes(); - // Store index of current preset mode - auto preset_iterator = preset_modes.find(this->preset_mode); - if (preset_iterator != preset_modes.end()) - state.preset_mode = std::distance(preset_modes.begin(), preset_iterator); + const char *preset = this->get_preset_mode(); + if (preset != nullptr) { + const auto &preset_modes = traits.supported_preset_modes(); + // Find index of current preset mode (pointer comparison is safe since preset is from traits) + for (size_t i = 0; i < preset_modes.size(); i++) { + if (preset_modes[i] == preset) { + state.preset_mode = i; + break; + } + } } this->rtc_.save(&state); @@ -215,8 +267,8 @@ void Fan::dump_traits_(const char *tag, const char *prefix) { } if (traits.supports_preset_modes()) { ESP_LOGCONFIG(tag, "%s Supported presets:", prefix); - for (const std::string &s : traits.supported_preset_modes()) - ESP_LOGCONFIG(tag, "%s - %s", prefix, s.c_str()); + for (const char *s : traits.supported_preset_modes()) + ESP_LOGCONFIG(tag, "%s - %s", prefix, s); } } diff --git a/esphome/components/fan/fan.h b/esphome/components/fan/fan.h index b74187eb4a..e38a80dbbe 100644 --- a/esphome/components/fan/fan.h +++ b/esphome/components/fan/fan.h @@ -60,8 +60,6 @@ class FanCall { this->speed_ = speed; return *this; } - ESPDEPRECATED("set_speed() with string argument is deprecated, use integer argument instead.", "2021.9") - FanCall &set_speed(const char *legacy_speed); optional get_speed() const { return this->speed_; } FanCall &set_direction(FanDirection direction) { this->direction_ = direction; @@ -72,11 +70,10 @@ class FanCall { return *this; } optional get_direction() const { return this->direction_; } - FanCall &set_preset_mode(const std::string &preset_mode) { - this->preset_mode_ = preset_mode; - return *this; - } - std::string get_preset_mode() const { return this->preset_mode_; } + FanCall &set_preset_mode(const std::string &preset_mode); + FanCall &set_preset_mode(const char *preset_mode); + const char *get_preset_mode() const { return this->preset_mode_; } + bool has_preset_mode() const { return this->preset_mode_ != nullptr; } void perform(); @@ -88,7 +85,7 @@ class FanCall { optional oscillating_; optional speed_; optional direction_{}; - std::string preset_mode_{}; + const char *preset_mode_{nullptr}; // Pointer to string in traits (after validation) }; struct FanRestoreState { @@ -114,8 +111,6 @@ class Fan : public EntityBase { int speed{0}; /// The current direction of the fan FanDirection direction{FanDirection::FORWARD}; - // The current preset mode of the fan - std::string preset_mode{}; FanCall turn_on(); FanCall turn_off(); @@ -132,8 +127,15 @@ class Fan : public EntityBase { /// Set the restore mode of this fan. void set_restore_mode(FanRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } + /// Get the current preset mode (returns pointer to string stored in traits, or nullptr if not set) + const char *get_preset_mode() const { return this->preset_mode_; } + + /// Check if a preset mode is currently active + bool has_preset_mode() const { return this->preset_mode_ != nullptr; } + protected: friend FanCall; + friend struct FanRestoreState; virtual void control(const FanCall &call) = 0; @@ -142,9 +144,21 @@ class Fan : public EntityBase { void dump_traits_(const char *tag, const char *prefix); + /// Set the preset mode (finds and stores pointer from traits). Returns true if changed. + bool set_preset_mode_(const char *preset_mode); + /// Set the preset mode (finds and stores pointer from traits). Returns true if changed. + bool set_preset_mode_(const std::string &preset_mode); + /// Clear the preset mode + void clear_preset_mode_(); + /// Find and return the matching preset mode pointer from traits, or nullptr if not found. + const char *find_preset_mode_(const char *preset_mode); + CallbackManager state_callback_{}; ESPPreferenceObject rtc_; FanRestoreMode restore_mode_; + + private: + const char *preset_mode_{nullptr}; }; } // namespace fan diff --git a/esphome/components/fan/fan_state.cpp b/esphome/components/fan/fan_state.cpp deleted file mode 100644 index 7c1658fb2e..0000000000 --- a/esphome/components/fan/fan_state.cpp +++ /dev/null @@ -1,16 +0,0 @@ -#include "fan_state.h" - -namespace esphome { -namespace fan { - -static const char *const TAG = "fan"; - -void FanState::setup() { - auto restore = this->restore_state_(); - if (restore) - restore->to_call(*this).perform(); -} -float FanState::get_setup_priority() const { return setup_priority::DATA - 1.0f; } - -} // namespace fan -} // namespace esphome diff --git a/esphome/components/fan/fan_state.h b/esphome/components/fan/fan_state.h deleted file mode 100644 index 5926e700b0..0000000000 --- a/esphome/components/fan/fan_state.h +++ /dev/null @@ -1,34 +0,0 @@ -#pragma once - -#include "esphome/core/component.h" -#include "fan.h" - -namespace esphome { -namespace fan { - -enum ESPDEPRECATED("LegacyFanDirection members are deprecated, use FanDirection instead.", - "2022.2") LegacyFanDirection { - FAN_DIRECTION_FORWARD = 0, - FAN_DIRECTION_REVERSE = 1 -}; - -class ESPDEPRECATED("FanState is deprecated, use Fan instead.", "2022.2") FanState : public Fan, public Component { - public: - FanState() = default; - - /// Get the traits of this fan. - FanTraits get_traits() override { return this->traits_; } - /// Set the traits of this fan (i.e. what features it supports). - void set_traits(const FanTraits &traits) { this->traits_ = traits; } - - void setup() override; - float get_setup_priority() const override; - - protected: - void control(const FanCall &call) override { this->publish_state(); } - - FanTraits traits_{}; -}; - -} // namespace fan -} // namespace esphome diff --git a/esphome/components/fan/fan_traits.h b/esphome/components/fan/fan_traits.h index 48509e5705..24987fe984 100644 --- a/esphome/components/fan/fan_traits.h +++ b/esphome/components/fan/fan_traits.h @@ -1,15 +1,10 @@ -#include -#include - #pragma once -namespace esphome { +#include +#include +#include -#ifdef USE_API -namespace api { -class APIConnection; -} // namespace api -#endif +namespace esphome { namespace fan { @@ -36,27 +31,38 @@ class FanTraits { /// Set whether this fan supports changing direction void set_direction(bool direction) { this->direction_ = direction; } /// Return the preset modes supported by the fan. - std::set supported_preset_modes() const { return this->preset_modes_; } - /// Set the preset modes supported by the fan. - void set_supported_preset_modes(const std::set &preset_modes) { this->preset_modes_ = preset_modes; } + const std::vector &supported_preset_modes() const { return this->preset_modes_; } + /// Set the preset modes supported by the fan (from initializer list). + void set_supported_preset_modes(std::initializer_list preset_modes) { + this->preset_modes_ = preset_modes; + } + /// Set the preset modes supported by the fan (from vector). + void set_supported_preset_modes(const std::vector &preset_modes) { this->preset_modes_ = preset_modes; } + + // Deleted overloads to catch incorrect std::string usage at compile time with clear error messages + void set_supported_preset_modes(const std::vector &preset_modes) = delete; + void set_supported_preset_modes(std::initializer_list preset_modes) = delete; + /// Return if preset modes are supported bool supports_preset_modes() const { return !this->preset_modes_.empty(); } + /// Find and return the matching preset mode pointer from supported modes, or nullptr if not found. + const char *find_preset_mode(const char *preset_mode) const { + if (preset_mode == nullptr) + return nullptr; + for (const char *mode : this->preset_modes_) { + if (strcmp(mode, preset_mode) == 0) { + return mode; // Return pointer from traits + } + } + return nullptr; + } protected: -#ifdef USE_API - // The API connection is a friend class to access internal methods - friend class api::APIConnection; - // This method returns a reference to the internal preset modes set. - // It is used by the API to avoid copying data when encoding messages. - // Warning: Do not use this method outside of the API connection code. - // It returns a reference to internal data that can be invalidated. - const std::set &supported_preset_modes_for_api_() const { return this->preset_modes_; } -#endif bool oscillation_{false}; bool speed_{false}; bool direction_{false}; int speed_count_{}; - std::set preset_modes_{}; + std::vector preset_modes_{}; }; } // namespace fan diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.cpp b/esphome/components/fingerprint_grow/fingerprint_grow.cpp index 54a267a404..eb7ede8fe9 100644 --- a/esphome/components/fingerprint_grow/fingerprint_grow.cpp +++ b/esphome/components/fingerprint_grow/fingerprint_grow.cpp @@ -80,7 +80,7 @@ void FingerprintGrowComponent::setup() { delay(20); // This delay guarantees the sensor will in fact be powered power. if (this->check_password_()) { - if (this->new_password_ != -1) { + if (this->new_password_ != std::numeric_limits::max()) { if (this->set_password_()) return; } else { diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.h b/esphome/components/fingerprint_grow/fingerprint_grow.h index 1c3098ef14..370b26f56a 100644 --- a/esphome/components/fingerprint_grow/fingerprint_grow.h +++ b/esphome/components/fingerprint_grow/fingerprint_grow.h @@ -6,6 +6,7 @@ #include "esphome/components/binary_sensor/binary_sensor.h" #include "esphome/components/uart/uart.h" +#include #include namespace esphome { @@ -177,7 +178,7 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic uint8_t address_[4] = {0xFF, 0xFF, 0xFF, 0xFF}; uint16_t capacity_ = 64; uint32_t password_ = 0x0; - uint32_t new_password_ = -1; + uint32_t new_password_ = std::numeric_limits::max(); GPIOPin *sensing_pin_{nullptr}; GPIOPin *sensor_power_pin_{nullptr}; uint8_t enrollment_image_ = 0; @@ -272,7 +273,7 @@ template class EnrollmentAction : public Action, public P TEMPLATABLE_VALUE(uint16_t, finger_id) TEMPLATABLE_VALUE(uint8_t, num_scans) - void play(Ts... x) override { + void play(const Ts &...x) override { auto finger_id = this->finger_id_.value(x...); auto num_scans = this->num_scans_.value(x...); if (num_scans) { @@ -286,14 +287,14 @@ template class EnrollmentAction : public Action, public P template class CancelEnrollmentAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->finish_enrollment(1); } + void play(const Ts &...x) override { this->parent_->finish_enrollment(1); } }; template class DeleteAction : public Action, public Parented { public: TEMPLATABLE_VALUE(uint16_t, finger_id) - void play(Ts... x) override { + void play(const Ts &...x) override { auto finger_id = this->finger_id_.value(x...); this->parent_->delete_fingerprint(finger_id); } @@ -301,14 +302,14 @@ template class DeleteAction : public Action, public Paren template class DeleteAllAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->delete_all_fingerprints(); } + void play(const Ts &...x) override { this->parent_->delete_all_fingerprints(); } }; template class LEDControlAction : public Action, public Parented { public: TEMPLATABLE_VALUE(bool, state) - void play(Ts... x) override { + void play(const Ts &...x) override { auto state = this->state_.value(x...); this->parent_->led_control(state); } @@ -321,7 +322,7 @@ template class AuraLEDControlAction : public Action, publ TEMPLATABLE_VALUE(uint8_t, color) TEMPLATABLE_VALUE(uint8_t, count) - void play(Ts... x) override { + void play(const Ts &...x) override { auto state = this->state_.value(x...); auto speed = this->speed_.value(x...); auto color = this->color_.value(x...); diff --git a/esphome/components/font/__init__.py b/esphome/components/font/__init__.py index 4ecc76c561..2667dbdbdf 100644 --- a/esphome/components/font/__init__.py +++ b/esphome/components/font/__init__.py @@ -3,7 +3,6 @@ import functools import hashlib from itertools import accumulate import logging -import os from pathlib import Path import re @@ -37,7 +36,7 @@ from esphome.const import ( CONF_WEIGHT, ) from esphome.core import CORE, HexInt -from esphome.helpers import cpp_string_escape +from esphome.types import ConfigType _LOGGER = logging.getLogger(__name__) @@ -50,7 +49,6 @@ font_ns = cg.esphome_ns.namespace("font") Font = font_ns.class_("Font") Glyph = font_ns.class_("Glyph") -GlyphData = font_ns.struct("GlyphData") CONF_BPP = "bpp" CONF_EXTRAS = "extras" @@ -253,11 +251,11 @@ def validate_truetype_file(value): return CORE.relative_config_path(cv.file_(value)) -def add_local_file(value): +def add_local_file(value: ConfigType) -> ConfigType: if value in FONT_CACHE: return value - path = value[CONF_PATH] - if not os.path.isfile(path): + path = Path(value[CONF_PATH]) + if not path.is_file(): raise cv.Invalid(f"File '{path}' not found.") FONT_CACHE[value] = path return value @@ -318,7 +316,7 @@ def download_gfont(value): external_files.compute_local_file_dir(DOMAIN) / f"{value[CONF_FAMILY]}@{value[CONF_WEIGHT]}@{value[CONF_ITALIC]}@v1.ttf" ) - if not external_files.is_file_recent(str(path), value[CONF_REFRESH]): + if not external_files.is_file_recent(path, value[CONF_REFRESH]): _LOGGER.debug("download_gfont: path=%s", path) try: req = requests.get(url, timeout=external_files.NETWORK_TIMEOUT) @@ -463,7 +461,7 @@ FONT_SCHEMA = cv.Schema( ) ), cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), - cv.GenerateID(CONF_RAW_GLYPH_ID): cv.declare_id(GlyphData), + cv.GenerateID(CONF_RAW_GLYPH_ID): cv.declare_id(Glyph), }, ) @@ -488,6 +486,8 @@ class GlyphInfo: def glyph_to_glyphinfo(glyph, font, size, bpp): + # Convert to 32 bit unicode codepoint + glyph = ord(glyph) scale = 256 // (1 << bpp) if not font.is_scalable: sizes = [pt_to_px(x.size) for x in font.available_sizes] @@ -583,22 +583,15 @@ async def to_code(config): # Create the glyph table that points to data in the above array. glyph_initializer = [ - cg.StructInitializer( - GlyphData, - ( - "a_char", - cg.RawExpression(f"(const uint8_t *){cpp_string_escape(x.glyph)}"), - ), - ( - "data", - cg.RawExpression(f"{str(prog_arr)} + {str(y - len(x.bitmap_data))}"), - ), - ("advance", x.advance), - ("offset_x", x.offset_x), - ("offset_y", x.offset_y), - ("width", x.width), - ("height", x.height), - ) + [ + x.glyph, + prog_arr + (y - len(x.bitmap_data)), + x.advance, + x.offset_x, + x.offset_y, + x.width, + x.height, + ] for (x, y) in zip( glyph_args, list(accumulate([len(x.bitmap_data) for x in glyph_args])) ) diff --git a/esphome/components/font/font.cpp b/esphome/components/font/font.cpp index 8b2420ac07..5e3bf1dd20 100644 --- a/esphome/components/font/font.cpp +++ b/esphome/components/font/font.cpp @@ -6,133 +6,245 @@ namespace esphome { namespace font { - static const char *const TAG = "font"; -const uint8_t *Glyph::get_char() const { return this->glyph_data_->a_char; } -// Compare the char at the string position with this char. -// Return true if this char is less than or equal the other. -bool Glyph::compare_to(const uint8_t *str) const { - // 1 -> this->char_ - // 2 -> str - for (uint32_t i = 0;; i++) { - if (this->glyph_data_->a_char[i] == '\0') - return true; - if (str[i] == '\0') - return false; - if (this->glyph_data_->a_char[i] > str[i]) - return false; - if (this->glyph_data_->a_char[i] < str[i]) - return true; +#ifdef USE_LVGL_FONT +const uint8_t *Font::get_glyph_bitmap(const lv_font_t *font, uint32_t unicode_letter) { + auto *fe = (Font *) font->dsc; + const auto *gd = fe->get_glyph_data_(unicode_letter); + if (gd == nullptr) { + return nullptr; } - // this should not happen - return false; -} -int Glyph::match_length(const uint8_t *str) const { - for (uint32_t i = 0;; i++) { - if (this->glyph_data_->a_char[i] == '\0') - return i; - if (str[i] != this->glyph_data_->a_char[i]) - return 0; - } - // this should not happen - return 0; -} -void Glyph::scan_area(int *x1, int *y1, int *width, int *height) const { - *x1 = this->glyph_data_->offset_x; - *y1 = this->glyph_data_->offset_y; - *width = this->glyph_data_->width; - *height = this->glyph_data_->height; + return gd->data; } -Font::Font(const GlyphData *data, int data_nr, int baseline, int height, int descender, int xheight, int capheight, +bool Font::get_glyph_dsc_cb(const lv_font_t *font, lv_font_glyph_dsc_t *dsc, uint32_t unicode_letter, uint32_t next) { + auto *fe = (Font *) font->dsc; + const auto *gd = fe->get_glyph_data_(unicode_letter); + if (gd == nullptr) { + return false; + } + dsc->adv_w = gd->advance; + dsc->ofs_x = gd->offset_x; + dsc->ofs_y = fe->height_ - gd->height - gd->offset_y - fe->lv_font_.base_line; + dsc->box_w = gd->width; + dsc->box_h = gd->height; + dsc->is_placeholder = 0; + dsc->bpp = fe->get_bpp(); + return true; +} + +const Glyph *Font::get_glyph_data_(uint32_t unicode_letter) { + if (unicode_letter == this->last_letter_ && this->last_letter_ != 0) + return this->last_data_; + auto *glyph = this->find_glyph(unicode_letter); + if (glyph == nullptr) { + return nullptr; + } + this->last_data_ = glyph; + this->last_letter_ = unicode_letter; + return glyph; +} +#endif + +/** + * Attempt to extract a 32 bit Unicode codepoint from a UTF-8 string. + * If successful, return the codepoint and set the length to the number of bytes read. + * If the end of the string has been reached and a valid codepoint has not been found, return 0 and set the length to + * 0. + * + * @param utf8_str The input string + * @param length Pointer to length storage + * @return The extracted code point + */ +static uint32_t extract_unicode_codepoint(const char *utf8_str, size_t *length) { + // Safely cast to uint8_t* for correct bitwise operations on bytes + const uint8_t *current = reinterpret_cast(utf8_str); + uint32_t code_point = 0; + uint8_t c1 = *current++; + + // check for end of string + if (c1 == 0) { + *length = 0; + return 0; + } + + // --- 1-Byte Sequence: 0xxxxxxx (ASCII) --- + if (c1 < 0x80) { + // Valid ASCII byte. + code_point = c1; + // Optimization: No need to check for continuation bytes. + } + // --- 2-Byte Sequence: 110xxxxx 10xxxxxx --- + else if ((c1 & 0xE0) == 0xC0) { + uint8_t c2 = *current++; + + // Error Check 1: Check if c2 is a valid continuation byte (10xxxxxx) + if ((c2 & 0xC0) != 0x80) { + *length = 0; + return 0; + } + + code_point = (c1 & 0x1F) << 6; + code_point |= (c2 & 0x3F); + + // Error Check 2: Overlong check (2-byte must be > 0x7F) + if (code_point <= 0x7F) { + *length = 0; + return 0; + } + } + // --- 3-Byte Sequence: 1110xxxx 10xxxxxx 10xxxxxx --- + else if ((c1 & 0xF0) == 0xE0) { + uint8_t c2 = *current++; + uint8_t c3 = *current++; + + // Error Check 1: Check continuation bytes + if (((c2 & 0xC0) != 0x80) || ((c3 & 0xC0) != 0x80)) { + *length = 0; + return 0; + } + + code_point = (c1 & 0x0F) << 12; + code_point |= (c2 & 0x3F) << 6; + code_point |= (c3 & 0x3F); + + // Error Check 2: Overlong check (3-byte must be > 0x7FF) + // Also check for surrogates (0xD800-0xDFFF) + if (code_point <= 0x7FF || (code_point >= 0xD800 && code_point <= 0xDFFF)) { + *length = 0; + return 0; + } + } + // --- 4-Byte Sequence: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx --- + else if ((c1 & 0xF8) == 0xF0) { + uint8_t c2 = *current++; + uint8_t c3 = *current++; + uint8_t c4 = *current++; + + // Error Check 1: Check continuation bytes + if (((c2 & 0xC0) != 0x80) || ((c3 & 0xC0) != 0x80) || ((c4 & 0xC0) != 0x80)) { + *length = 0; + return 0; + } + + code_point = (c1 & 0x07) << 18; + code_point |= (c2 & 0x3F) << 12; + code_point |= (c3 & 0x3F) << 6; + code_point |= (c4 & 0x3F); + + // Error Check 2: Overlong check (4-byte must be > 0xFFFF) + // Also check for valid Unicode range (must be <= 0x10FFFF) + if (code_point <= 0xFFFF || code_point > 0x10FFFF) { + *length = 0; + return 0; + } + } + // --- Invalid leading byte (e.g., 10xxxxxx or 11111xxx) --- + else { + *length = 0; + return 0; + } + *length = current - reinterpret_cast(utf8_str); + return code_point; +} + +Font::Font(const Glyph *data, int data_nr, int baseline, int height, int descender, int xheight, int capheight, uint8_t bpp) - : baseline_(baseline), + : glyphs_(ConstVector(data, data_nr)), + baseline_(baseline), height_(height), descender_(descender), linegap_(height - baseline - descender), xheight_(xheight), capheight_(capheight), bpp_(bpp) { - glyphs_.reserve(data_nr); - for (int i = 0; i < data_nr; ++i) - glyphs_.emplace_back(&data[i]); +#ifdef USE_LVGL_FONT + this->lv_font_.dsc = this; + this->lv_font_.line_height = this->get_height(); + this->lv_font_.base_line = this->lv_font_.line_height - this->get_baseline(); + this->lv_font_.get_glyph_dsc = get_glyph_dsc_cb; + this->lv_font_.get_glyph_bitmap = get_glyph_bitmap; + this->lv_font_.subpx = LV_FONT_SUBPX_NONE; + this->lv_font_.underline_position = -1; + this->lv_font_.underline_thickness = 1; +#endif } -int Font::match_next_glyph(const uint8_t *str, int *match_length) { + +const Glyph *Font::find_glyph(uint32_t codepoint) const { int lo = 0; int hi = this->glyphs_.size() - 1; while (lo != hi) { int mid = (lo + hi + 1) / 2; - if (this->glyphs_[mid].compare_to(str)) { + if (this->glyphs_[mid].is_less_or_equal(codepoint)) { lo = mid; } else { hi = mid - 1; } } - *match_length = this->glyphs_[lo].match_length(str); - if (*match_length <= 0) - return -1; - return lo; + auto *result = &this->glyphs_[lo]; + if (result->code_point == codepoint) + return result; + return nullptr; } + #ifdef USE_DISPLAY void Font::measure(const char *str, int *width, int *x_offset, int *baseline, int *height) { *baseline = this->baseline_; *height = this->height_; - int i = 0; int min_x = 0; bool has_char = false; int x = 0; - while (str[i] != '\0') { - int match_length; - int glyph_n = this->match_next_glyph((const uint8_t *) str + i, &match_length); - if (glyph_n < 0) { + for (;;) { + size_t length; + auto code_point = extract_unicode_codepoint(str, &length); + if (length == 0) + break; + str += length; + auto *glyph = this->find_glyph(code_point); + if (glyph == nullptr) { // Unknown char, skip - if (!this->get_glyphs().empty()) - x += this->get_glyphs()[0].glyph_data_->advance; - i++; + if (!this->glyphs_.empty()) + x += this->glyphs_[0].advance; continue; } - const Glyph &glyph = this->glyphs_[glyph_n]; if (!has_char) { - min_x = glyph.glyph_data_->offset_x; + min_x = glyph->offset_x; } else { - min_x = std::min(min_x, x + glyph.glyph_data_->offset_x); + min_x = std::min(min_x, x + glyph->offset_x); } - x += glyph.glyph_data_->advance; + x += glyph->advance; - i += match_length; has_char = true; } *x_offset = min_x; *width = x - min_x; } + void Font::print(int x_start, int y_start, display::Display *display, Color color, const char *text, Color background) { - int i = 0; int x_at = x_start; - int scan_x1, scan_y1, scan_width, scan_height; - while (text[i] != '\0') { - int match_length; - int glyph_n = this->match_next_glyph((const uint8_t *) text + i, &match_length); - if (glyph_n < 0) { + for (;;) { + size_t length; + auto code_point = extract_unicode_codepoint(text, &length); + if (length == 0) + break; + text += length; + auto *glyph = this->find_glyph(code_point); + if (glyph == nullptr) { // Unknown char, skip - ESP_LOGW(TAG, "Encountered character without representation in font: '%c'", text[i]); - if (!this->get_glyphs().empty()) { - uint8_t glyph_width = this->get_glyphs()[0].glyph_data_->advance; - display->filled_rectangle(x_at, y_start, glyph_width, this->height_, color); + ESP_LOGW(TAG, "Codepoint 0x%08" PRIx32 " not found in font", code_point); + if (!this->glyphs_.empty()) { + uint8_t glyph_width = this->glyphs_[0].advance; + display->rectangle(x_at, y_start, glyph_width, this->height_, color); x_at += glyph_width; } - - i++; continue; } - const Glyph &glyph = this->get_glyphs()[glyph_n]; - glyph.scan_area(&scan_x1, &scan_y1, &scan_width, &scan_height); - - const uint8_t *data = glyph.glyph_data_->data; - const int max_x = x_at + scan_x1 + scan_width; - const int max_y = y_start + scan_y1 + scan_height; + const uint8_t *data = glyph->data; + const int max_x = x_at + glyph->offset_x + glyph->width; + const int max_y = y_start + glyph->offset_y + glyph->height; uint8_t bitmask = 0; uint8_t pixel_data = 0; @@ -145,10 +257,10 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo auto b_g = (float) background.g; auto b_b = (float) background.b; auto b_w = (float) background.w; - for (int glyph_y = y_start + scan_y1; glyph_y != max_y; glyph_y++) { - for (int glyph_x = x_at + scan_x1; glyph_x != max_x; glyph_x++) { + for (int glyph_y = y_start + glyph->offset_y; glyph_y != max_y; glyph_y++) { + for (int glyph_x = x_at + glyph->offset_x; glyph_x != max_x; glyph_x++) { uint8_t pixel = 0; - for (int bit_num = 0; bit_num != this->bpp_; bit_num++) { + for (uint8_t bit_num = 0; bit_num != this->bpp_; bit_num++) { if (bitmask == 0) { pixel_data = progmem_read_byte(data++); bitmask = 0x80; @@ -168,12 +280,9 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo } } } - x_at += glyph.glyph_data_->advance; - - i += match_length; + x_at += glyph->advance; } } #endif - } // namespace font } // namespace esphome diff --git a/esphome/components/font/font.h b/esphome/components/font/font.h index 28832d647d..262ded3be4 100644 --- a/esphome/components/font/font.h +++ b/esphome/components/font/font.h @@ -6,14 +6,30 @@ #ifdef USE_DISPLAY #include "esphome/components/display/display.h" #endif +#ifdef USE_LVGL_FONT +#include +#endif namespace esphome { namespace font { class Font; -struct GlyphData { - const uint8_t *a_char; +class Glyph { + public: + constexpr Glyph(uint32_t code_point, const uint8_t *data, int advance, int offset_x, int offset_y, int width, + int height) + : code_point(code_point), + data(data), + advance(advance), + offset_x(offset_x), + offset_y(offset_y), + width(width), + height(height) {} + + bool is_less_or_equal(uint32_t other) const { return this->code_point <= other; } + + const uint32_t code_point; const uint8_t *data; int advance; int offset_x; @@ -22,26 +38,6 @@ struct GlyphData { int height; }; -class Glyph { - public: - Glyph(const GlyphData *data) : glyph_data_(data) {} - - const uint8_t *get_char() const; - - bool compare_to(const uint8_t *str) const; - - int match_length(const uint8_t *str) const; - - void scan_area(int *x1, int *y1, int *width, int *height) const; - - const GlyphData *get_glyph_data() const { return this->glyph_data_; } - - protected: - friend Font; - - const GlyphData *glyph_data_; -}; - class Font #ifdef USE_DISPLAY : public display::BaseFont @@ -50,8 +46,8 @@ class Font public: /** Construct the font with the given glyphs. * - * @param data A vector of glyphs, must be sorted lexicographically. - * @param data_nr The number of glyphs in data. + * @param data A list of glyphs, must be sorted lexicographically. + * @param data_nr The number of glyphs * @param baseline The y-offset from the top of the text to the baseline. * @param height The y-offset from the top of the text to the bottom. * @param descender The y-offset from the baseline to the lowest stroke in the font (e.g. from letters like g or p). @@ -59,10 +55,10 @@ class Font * @param capheight The height of capital letters, usually measured at the "X" glyph. * @param bpp The bits per pixel used for this font. Used to read data out of the glyph bitmaps. */ - Font(const GlyphData *data, int data_nr, int baseline, int height, int descender, int xheight, int capheight, + Font(const Glyph *data, int data_nr, int baseline, int height, int descender, int xheight, int capheight, uint8_t bpp = 1); - int match_next_glyph(const uint8_t *str, int *match_length); + const Glyph *find_glyph(uint32_t codepoint) const; #ifdef USE_DISPLAY void print(int x_start, int y_start, display::Display *display, Color color, const char *text, @@ -77,11 +73,14 @@ class Font inline int get_xheight() { return this->xheight_; } inline int get_capheight() { return this->capheight_; } inline int get_bpp() { return this->bpp_; } +#ifdef USE_LVGL_FONT + const lv_font_t *get_lv_font() const { return &this->lv_font_; } +#endif - const std::vector> &get_glyphs() const { return glyphs_; } + const ConstVector &get_glyphs() const { return glyphs_; } protected: - std::vector> glyphs_; + ConstVector glyphs_; int baseline_; int height_; int descender_; @@ -89,6 +88,14 @@ class Font int xheight_; int capheight_; uint8_t bpp_; // bits per pixel +#ifdef USE_LVGL_FONT + lv_font_t lv_font_{}; + static const uint8_t *get_glyph_bitmap(const lv_font_t *font, uint32_t unicode_letter); + static bool get_glyph_dsc_cb(const lv_font_t *font, lv_font_glyph_dsc_t *dsc, uint32_t unicode_letter, uint32_t next); + const Glyph *get_glyph_data_(uint32_t unicode_letter); + uint32_t last_letter_{}; + const Glyph *last_data_{}; +#endif }; } // namespace font diff --git a/esphome/components/gdk101/gdk101.cpp b/esphome/components/gdk101/gdk101.cpp index 096b06917a..617e2138fb 100644 --- a/esphome/components/gdk101/gdk101.cpp +++ b/esphome/components/gdk101/gdk101.cpp @@ -11,22 +11,22 @@ static const uint8_t NUMBER_OF_READ_RETRIES = 5; void GDK101Component::update() { uint8_t data[2]; if (!this->read_dose_1m_(data)) { - this->status_set_warning("Failed to read dose 1m"); + this->status_set_warning(LOG_STR("Failed to read dose 1m")); return; } if (!this->read_dose_10m_(data)) { - this->status_set_warning("Failed to read dose 10m"); + this->status_set_warning(LOG_STR("Failed to read dose 10m")); return; } if (!this->read_status_(data)) { - this->status_set_warning("Failed to read status"); + this->status_set_warning(LOG_STR("Failed to read status")); return; } if (!this->read_measurement_duration_(data)) { - this->status_set_warning("Failed to read measurement duration"); + this->status_set_warning(LOG_STR("Failed to read measurement duration")); return; } this->status_clear_warning(); @@ -36,20 +36,20 @@ void GDK101Component::setup() { uint8_t data[2]; // first, reset the sensor if (!this->reset_sensor_(data)) { - this->status_set_error("Reset failed!"); + this->status_set_error(LOG_STR("Reset failed!")); this->mark_failed(); return; } // sensor should acknowledge success of the reset procedure if (data[0] != 1) { - this->status_set_error("Reset not acknowledged!"); + this->status_set_error(LOG_STR("Reset not acknowledged!")); this->mark_failed(); return; } delay(10); // read firmware version if (!this->read_fw_version_(data)) { - this->status_set_error("Failed to read firmware version"); + this->status_set_error(LOG_STR("Failed to read firmware version")); this->mark_failed(); return; } @@ -62,7 +62,6 @@ void GDK101Component::dump_config() { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); } #ifdef USE_SENSOR - LOG_SENSOR(" ", "Firmware Version", this->fw_version_sensor_); LOG_SENSOR(" ", "Average Radaition Dose per 1 minute", this->rad_1m_sensor_); LOG_SENSOR(" ", "Average Radaition Dose per 10 minutes", this->rad_10m_sensor_); LOG_SENSOR(" ", "Status", this->status_sensor_); @@ -72,6 +71,10 @@ void GDK101Component::dump_config() { #ifdef USE_BINARY_SENSOR LOG_BINARY_SENSOR(" ", "Vibration Status", this->vibration_binary_sensor_); #endif // USE_BINARY_SENSOR + +#ifdef USE_TEXT_SENSOR + LOG_TEXT_SENSOR(" ", "Firmware Version", this->fw_version_text_sensor_); +#endif // USE_TEXT_SENSOR } float GDK101Component::get_setup_priority() const { return setup_priority::DATA; } @@ -153,18 +156,18 @@ bool GDK101Component::read_status_(uint8_t *data) { } bool GDK101Component::read_fw_version_(uint8_t *data) { -#ifdef USE_SENSOR - if (this->fw_version_sensor_ != nullptr) { +#ifdef USE_TEXT_SENSOR + if (this->fw_version_text_sensor_ != nullptr) { if (!this->read_bytes(GDK101_REG_READ_FIRMWARE, data, 2)) { ESP_LOGE(TAG, "Updating GDK101 failed!"); return false; } - const float fw_version = data[0] + (data[1] / 10.0f); + const std::string fw_version_str = str_sprintf("%d.%d", data[0], data[1]); - this->fw_version_sensor_->publish_state(fw_version); + this->fw_version_text_sensor_->publish_state(fw_version_str); } -#endif // USE_SENSOR +#endif // USE_TEXT_SENSOR return true; } diff --git a/esphome/components/gdk101/gdk101.h b/esphome/components/gdk101/gdk101.h index 460e72ac89..f250a42a54 100644 --- a/esphome/components/gdk101/gdk101.h +++ b/esphome/components/gdk101/gdk101.h @@ -8,6 +8,9 @@ #ifdef USE_BINARY_SENSOR #include "esphome/components/binary_sensor/binary_sensor.h" #endif // USE_BINARY_SENSOR +#ifdef USE_TEXT_SENSOR +#include "esphome/components/text_sensor/text_sensor.h" +#endif // USE_TEXT_SENSOR #include "esphome/components/i2c/i2c.h" namespace esphome { @@ -25,12 +28,14 @@ class GDK101Component : public PollingComponent, public i2c::I2CDevice { SUB_SENSOR(rad_1m) SUB_SENSOR(rad_10m) SUB_SENSOR(status) - SUB_SENSOR(fw_version) SUB_SENSOR(measurement_duration) #endif // USE_SENSOR #ifdef USE_BINARY_SENSOR SUB_BINARY_SENSOR(vibration) #endif // USE_BINARY_SENSOR +#ifdef USE_TEXT_SENSOR + SUB_TEXT_SENSOR(fw_version) +#endif // USE_TEXT_SENSOR public: void setup() override; diff --git a/esphome/components/gdk101/sensor.py b/esphome/components/gdk101/sensor.py index d04e0b8367..6cf89e0fd4 100644 --- a/esphome/components/gdk101/sensor.py +++ b/esphome/components/gdk101/sensor.py @@ -40,9 +40,8 @@ CONFIG_SCHEMA = cv.Schema( device_class=DEVICE_CLASS_EMPTY, state_class=STATE_CLASS_MEASUREMENT, ), - cv.Optional(CONF_VERSION): sensor.sensor_schema( - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - accuracy_decimals=1, + cv.Optional(CONF_VERSION): cv.invalid( + "The 'version' option has been moved to the `text_sensor` component." ), cv.Optional(CONF_STATUS): sensor.sensor_schema( entity_category=ENTITY_CATEGORY_DIAGNOSTIC, @@ -71,10 +70,6 @@ async def to_code(config): sens = await sensor.new_sensor(radiation_dose_per_10m) cg.add(hub.set_rad_10m_sensor(sens)) - if version_config := config.get(CONF_VERSION): - sens = await sensor.new_sensor(version_config) - cg.add(hub.set_fw_version_sensor(sens)) - if status_config := config.get(CONF_STATUS): sens = await sensor.new_sensor(status_config) cg.add(hub.set_status_sensor(sens)) diff --git a/esphome/components/gdk101/text_sensor.py b/esphome/components/gdk101/text_sensor.py new file mode 100644 index 0000000000..703e68493a --- /dev/null +++ b/esphome/components/gdk101/text_sensor.py @@ -0,0 +1,23 @@ +import esphome.codegen as cg +from esphome.components import text_sensor +import esphome.config_validation as cv +from esphome.const import CONF_VERSION, ENTITY_CATEGORY_DIAGNOSTIC, ICON_CHIP + +from . import CONF_GDK101_ID, GDK101Component + +DEPENDENCIES = ["gdk101"] + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_GDK101_ID): cv.use_id(GDK101Component), + cv.Required(CONF_VERSION): text_sensor.text_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, icon=ICON_CHIP + ), + } +) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_GDK101_ID]) + var = await text_sensor.new_text_sensor(config[CONF_VERSION]) + cg.add(hub.set_fw_version_text_sensor(var)) diff --git a/esphome/components/globals/__init__.py b/esphome/components/globals/__init__.py index e4bce99b0b..633ccea66b 100644 --- a/esphome/components/globals/__init__.py +++ b/esphome/components/globals/__init__.py @@ -8,7 +8,7 @@ from esphome.const import ( CONF_TYPE, CONF_VALUE, ) -from esphome.core import coroutine_with_priority +from esphome.core import CoroPriority, coroutine_with_priority CODEOWNERS = ["@esphome/core"] globals_ns = cg.esphome_ns.namespace("globals") @@ -35,7 +35,7 @@ CONFIG_SCHEMA = cv.Schema( # Run with low priority so that namespaces are registered first -@coroutine_with_priority(-100.0) +@coroutine_with_priority(CoroPriority.LATE) async def to_code(config): type_ = cg.RawExpression(config[CONF_TYPE]) restore = config[CONF_RESTORE_VALUE] diff --git a/esphome/components/globals/globals_component.h b/esphome/components/globals/globals_component.h index 4c6a12aa72..1d2a08937e 100644 --- a/esphome/components/globals/globals_component.h +++ b/esphome/components/globals/globals_component.h @@ -134,7 +134,7 @@ template class GlobalVarSetAction : public Actionparent_->value() = this->value_.value(x...); } + void play(const Ts &...x) override { this->parent_->value() = this->value_.value(x...); } protected: C *parent_; diff --git a/esphome/components/gp8403/__init__.py b/esphome/components/gp8403/__init__.py index 96f1807688..83859a4030 100644 --- a/esphome/components/gp8403/__init__.py +++ b/esphome/components/gp8403/__init__.py @@ -1,19 +1,25 @@ import esphome.codegen as cg from esphome.components import i2c import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_VOLTAGE +from esphome.const import CONF_ID, CONF_MODEL, CONF_VOLTAGE -CODEOWNERS = ["@jesserockz"] +CODEOWNERS = ["@jesserockz", "@sebydocky"] DEPENDENCIES = ["i2c"] MULTI_CONF = True gp8403_ns = cg.esphome_ns.namespace("gp8403") -GP8403 = gp8403_ns.class_("GP8403", cg.Component, i2c.I2CDevice) +GP8403Component = gp8403_ns.class_("GP8403Component", cg.Component, i2c.I2CDevice) GP8403Voltage = gp8403_ns.enum("GP8403Voltage") +GP8403Model = gp8403_ns.enum("GP8403Model") CONF_GP8403_ID = "gp8403_id" +MODELS = { + "GP8403": GP8403Model.GP8403, + "GP8413": GP8403Model.GP8413, +} + VOLTAGES = { "5V": GP8403Voltage.GP8403_VOLTAGE_5V, "10V": GP8403Voltage.GP8403_VOLTAGE_10V, @@ -22,7 +28,8 @@ VOLTAGES = { CONFIG_SCHEMA = ( cv.Schema( { - cv.GenerateID(): cv.declare_id(GP8403), + cv.GenerateID(): cv.declare_id(GP8403Component), + cv.Optional(CONF_MODEL, default="GP8403"): cv.enum(MODELS, upper=True), cv.Required(CONF_VOLTAGE): cv.enum(VOLTAGES, upper=True), } ) @@ -35,5 +42,5 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) - + cg.add(var.set_model(config[CONF_MODEL])) cg.add(var.set_voltage(config[CONF_VOLTAGE])) diff --git a/esphome/components/gp8403/gp8403.cpp b/esphome/components/gp8403/gp8403.cpp index 5107e96dee..11c2f9a7c0 100644 --- a/esphome/components/gp8403/gp8403.cpp +++ b/esphome/components/gp8403/gp8403.cpp @@ -8,16 +8,48 @@ namespace gp8403 { static const char *const TAG = "gp8403"; static const uint8_t RANGE_REGISTER = 0x01; +static const uint8_t OUTPUT_REGISTER = 0x02; -void GP8403::setup() { this->write_register(RANGE_REGISTER, (uint8_t *) (&this->voltage_), 1); } +const LogString *model_to_string(GP8403Model model) { + switch (model) { + case GP8403Model::GP8403: + return LOG_STR("GP8403"); + case GP8403Model::GP8413: + return LOG_STR("GP8413"); + } + return LOG_STR("Unknown"); +}; -void GP8403::dump_config() { +void GP8403Component::setup() { this->write_register(RANGE_REGISTER, (uint8_t *) (&this->voltage_), 1); } + +void GP8403Component::dump_config() { ESP_LOGCONFIG(TAG, "GP8403:\n" - " Voltage: %dV", - this->voltage_ == GP8403_VOLTAGE_5V ? 5 : 10); + " Voltage: %dV\n" + " Model: %s", + this->voltage_ == GP8403_VOLTAGE_5V ? 5 : 10, LOG_STR_ARG(model_to_string(this->model_))); LOG_I2C_DEVICE(this); } +void GP8403Component::write_state(float state, uint8_t channel) { + uint16_t val = 0; + switch (this->model_) { + case GP8403Model::GP8403: + val = ((uint16_t) (4095 * state)) << 4; + break; + case GP8403Model::GP8413: + val = ((uint16_t) (32767 * state)) << 1; + break; + default: + ESP_LOGE(TAG, "Unknown model %s", LOG_STR_ARG(model_to_string(this->model_))); + return; + } + ESP_LOGV(TAG, "Calculated DAC value: %" PRIu16, val); + i2c::ErrorCode err = this->write_register(OUTPUT_REGISTER + (2 * channel), (uint8_t *) &val, 2); + if (err != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Error writing to %s, code %d", LOG_STR_ARG(model_to_string(this->model_)), err); + } +} + } // namespace gp8403 } // namespace esphome diff --git a/esphome/components/gp8403/gp8403.h b/esphome/components/gp8403/gp8403.h index 9f493d39e3..6613187b20 100644 --- a/esphome/components/gp8403/gp8403.h +++ b/esphome/components/gp8403/gp8403.h @@ -11,15 +11,24 @@ enum GP8403Voltage { GP8403_VOLTAGE_10V = 0x11, }; -class GP8403 : public Component, public i2c::I2CDevice { +enum GP8403Model { + GP8403, + GP8413, +}; + +class GP8403Component : public Component, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - + float get_setup_priority() const override { return setup_priority::DATA; } + void set_model(GP8403Model model) { this->model_ = model; } void set_voltage(gp8403::GP8403Voltage voltage) { this->voltage_ = voltage; } + void write_state(float state, uint8_t channel); + protected: GP8403Voltage voltage_; + GP8403Model model_{GP8403Model::GP8403}; }; } // namespace gp8403 diff --git a/esphome/components/gp8403/output/__init__.py b/esphome/components/gp8403/output/__init__.py index dc57833f4a..5245c405db 100644 --- a/esphome/components/gp8403/output/__init__.py +++ b/esphome/components/gp8403/output/__init__.py @@ -3,7 +3,7 @@ from esphome.components import i2c, output import esphome.config_validation as cv from esphome.const import CONF_CHANNEL, CONF_ID -from .. import CONF_GP8403_ID, GP8403, gp8403_ns +from .. import CONF_GP8403_ID, GP8403Component, gp8403_ns DEPENDENCIES = ["gp8403"] @@ -14,7 +14,7 @@ GP8403Output = gp8403_ns.class_( CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(GP8403Output), - cv.GenerateID(CONF_GP8403_ID): cv.use_id(GP8403), + cv.GenerateID(CONF_GP8403_ID): cv.use_id(GP8403Component), cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=1), } ).extend(cv.COMPONENT_SCHEMA) diff --git a/esphome/components/gp8403/output/gp8403_output.cpp b/esphome/components/gp8403/output/gp8403_output.cpp index edb6972184..dfdc2d6ccb 100644 --- a/esphome/components/gp8403/output/gp8403_output.cpp +++ b/esphome/components/gp8403/output/gp8403_output.cpp @@ -7,8 +7,6 @@ namespace gp8403 { static const char *const TAG = "gp8403.output"; -static const uint8_t OUTPUT_REGISTER = 0x02; - void GP8403Output::dump_config() { ESP_LOGCONFIG(TAG, "GP8403 Output:\n" @@ -16,13 +14,7 @@ void GP8403Output::dump_config() { this->channel_); } -void GP8403Output::write_state(float state) { - uint16_t value = ((uint16_t) (state * 4095)) << 4; - i2c::ErrorCode err = this->parent_->write_register(OUTPUT_REGISTER + (2 * this->channel_), (uint8_t *) &value, 2); - if (err != i2c::ERROR_OK) { - ESP_LOGE(TAG, "Error writing to GP8403, code %d", err); - } -} +void GP8403Output::write_state(float state) { this->parent_->write_state(state, this->channel_); } } // namespace gp8403 } // namespace esphome diff --git a/esphome/components/gp8403/output/gp8403_output.h b/esphome/components/gp8403/output/gp8403_output.h index 71e5efb1cb..c0d6650500 100644 --- a/esphome/components/gp8403/output/gp8403_output.h +++ b/esphome/components/gp8403/output/gp8403_output.h @@ -8,13 +8,11 @@ namespace esphome { namespace gp8403 { -class GP8403Output : public Component, public output::FloatOutput, public Parented { +class GP8403Output : public Component, public output::FloatOutput, public Parented { public: void dump_config() override; float get_setup_priority() const override { return setup_priority::DATA - 1; } - void set_channel(uint8_t channel) { this->channel_ = channel; } - void write_state(float state) override; protected: diff --git a/esphome/components/gpio/binary_sensor/__init__.py b/esphome/components/gpio/binary_sensor/__init__.py index 8372bc7e08..3c2021d40e 100644 --- a/esphome/components/gpio/binary_sensor/__init__.py +++ b/esphome/components/gpio/binary_sensor/__init__.py @@ -39,6 +39,7 @@ CONFIG_SCHEMA = ( # due to hardware limitations or lack of reliable interrupt support. This ensures # stable operation on these platforms. Future maintainers should verify platform # capabilities before changing this default behavior. + # nrf52 has no gpio interrupts implemented yet cv.SplitDefault( CONF_USE_INTERRUPT, bk72xx=False, @@ -46,7 +47,7 @@ CONFIG_SCHEMA = ( esp8266=True, host=True, ln882x=False, - nrf52=True, + nrf52=False, rp2040=True, rtl87xx=False, ): cv.boolean, @@ -94,6 +95,8 @@ async def to_code(config): ) use_interrupt = False - cg.add(var.set_use_interrupt(use_interrupt)) if use_interrupt: cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE])) + else: + # Only generate call when disabling interrupts (default is true) + cg.add(var.set_use_interrupt(use_interrupt)) diff --git a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp index 4b8369cd59..7a35596194 100644 --- a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp +++ b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp @@ -6,6 +6,25 @@ namespace gpio { static const char *const TAG = "gpio.binary_sensor"; +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG +static const LogString *interrupt_type_to_string(gpio::InterruptType type) { + switch (type) { + case gpio::INTERRUPT_RISING_EDGE: + return LOG_STR("RISING_EDGE"); + case gpio::INTERRUPT_FALLING_EDGE: + return LOG_STR("FALLING_EDGE"); + case gpio::INTERRUPT_ANY_EDGE: + return LOG_STR("ANY_EDGE"); + default: + return LOG_STR("UNKNOWN"); + } +} + +static const LogString *gpio_mode_to_string(bool use_interrupt) { + return use_interrupt ? LOG_STR("interrupt") : LOG_STR("polling"); +} +#endif + void IRAM_ATTR GPIOBinarySensorStore::gpio_intr(GPIOBinarySensorStore *arg) { bool new_state = arg->isr_pin_.digital_read(); if (new_state != arg->last_state_) { @@ -51,25 +70,9 @@ void GPIOBinarySensor::setup() { void GPIOBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "GPIO Binary Sensor", this); LOG_PIN(" Pin: ", this->pin_); - const char *mode = this->use_interrupt_ ? "interrupt" : "polling"; - ESP_LOGCONFIG(TAG, " Mode: %s", mode); + ESP_LOGCONFIG(TAG, " Mode: %s", LOG_STR_ARG(gpio_mode_to_string(this->use_interrupt_))); if (this->use_interrupt_) { - const char *interrupt_type; - switch (this->interrupt_type_) { - case gpio::INTERRUPT_RISING_EDGE: - interrupt_type = "RISING_EDGE"; - break; - case gpio::INTERRUPT_FALLING_EDGE: - interrupt_type = "FALLING_EDGE"; - break; - case gpio::INTERRUPT_ANY_EDGE: - interrupt_type = "ANY_EDGE"; - break; - default: - interrupt_type = "UNKNOWN"; - break; - } - ESP_LOGCONFIG(TAG, " Interrupt Type: %s", interrupt_type); + ESP_LOGCONFIG(TAG, " Interrupt Type: %s", LOG_STR_ARG(interrupt_type_to_string(this->interrupt_type_))); } } diff --git a/esphome/components/gpio/switch/gpio_switch.cpp b/esphome/components/gpio/switch/gpio_switch.cpp index b67af5e95d..9043a6a493 100644 --- a/esphome/components/gpio/switch/gpio_switch.cpp +++ b/esphome/components/gpio/switch/gpio_switch.cpp @@ -67,7 +67,7 @@ void GPIOSwitch::write_state(bool state) { this->pin_->digital_write(state); this->publish_state(state); } -void GPIOSwitch::set_interlock(const std::vector &interlock) { this->interlock_ = interlock; } +void GPIOSwitch::set_interlock(const std::initializer_list &interlock) { this->interlock_ = interlock; } } // namespace gpio } // namespace esphome diff --git a/esphome/components/gpio/switch/gpio_switch.h b/esphome/components/gpio/switch/gpio_switch.h index 94d49745b5..080decac08 100644 --- a/esphome/components/gpio/switch/gpio_switch.h +++ b/esphome/components/gpio/switch/gpio_switch.h @@ -2,10 +2,9 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" +#include "esphome/core/helpers.h" #include "esphome/components/switch/switch.h" -#include - namespace esphome { namespace gpio { @@ -19,14 +18,14 @@ class GPIOSwitch : public switch_::Switch, public Component { void setup() override; void dump_config() override; - void set_interlock(const std::vector &interlock); + void set_interlock(const std::initializer_list &interlock); void set_interlock_wait_time(uint32_t interlock_wait_time) { interlock_wait_time_ = interlock_wait_time; } protected: void write_state(bool state) override; GPIOPin *pin_; - std::vector interlock_; + FixedVector interlock_; uint32_t interlock_wait_time_{0}; }; diff --git a/esphome/components/gpio_expander/cached_gpio.h b/esphome/components/gpio_expander/cached_gpio.h index 78c675cdb2..eeff98cb6e 100644 --- a/esphome/components/gpio_expander/cached_gpio.h +++ b/esphome/components/gpio_expander/cached_gpio.h @@ -2,52 +2,81 @@ #include #include +#include +#include +#include #include "esphome/core/hal.h" -namespace esphome { -namespace gpio_expander { +namespace esphome::gpio_expander { /// @brief A class to cache the read state of a GPIO expander. /// This class caches reads between GPIO Pins which are on the same bank. /// This means that for reading whole Port (ex. 8 pins) component needs only one -/// I2C/SPI read per main loop call. It assumes, that one bit in byte identifies one GPIO pin +/// I2C/SPI read per main loop call. It assumes that one bit in byte identifies one GPIO pin. +/// /// Template parameters: -/// T - Type which represents internal register. Could be uint8_t or uint16_t. Adjust to -/// match size of your internal GPIO bank register. -/// N - Number of pins -template class CachedGpioExpander { +/// T - Type which represents internal bank register. Could be uint8_t or uint16_t. +/// Choose based on how your I/O expander reads pins: +/// * uint8_t: For chips that read banks separately (8 pins at a time) +/// Examples: MCP23017 (2x8-bit banks), TCA9555 (2x8-bit banks) +/// * uint16_t: For chips that read all pins at once (up to 16 pins) +/// Examples: PCF8574/8575 (8/16 pins), PCA9554/9555 (8/16 pins) +/// N - Total number of pins (maximum 65535) +/// P - Type for pin number parameters (automatically selected based on N: +/// uint8_t for N<=256, uint16_t for N>256). Can be explicitly specified +/// if needed (e.g., for components like SN74HC165 with >256 pins) +template 256), uint16_t, uint8_t>::type> +class CachedGpioExpander { public: - bool digital_read(T pin) { - uint8_t bank = pin / (sizeof(T) * BITS_PER_BYTE); - if (this->read_cache_invalidated_[bank]) { - this->read_cache_invalidated_[bank] = false; + /// @brief Read the state of the given pin. This will invalidate the cache for the given pin number. + /// @param pin Pin number to read + /// @return Pin state + bool digital_read(P pin) { + const P bank = pin / BANK_SIZE; + const T pin_mask = (1 << (pin % BANK_SIZE)); + // Check if specific pin cache is valid + if (this->read_cache_valid_[bank] & pin_mask) { + // Invalidate pin + this->read_cache_valid_[bank] &= ~pin_mask; + } else { + // Read whole bank from hardware if (!this->digital_read_hw(pin)) return false; + // Mark bank cache as valid except the pin that is being returned now + this->read_cache_valid_[bank] = std::numeric_limits::max() & ~pin_mask; } return this->digital_read_cache(pin); } - void digital_write(T pin, bool value) { this->digital_write_hw(pin, value); } + void digital_write(P pin, bool value) { this->digital_write_hw(pin, value); } protected: - /// @brief Call component low level function to read GPIO state from device - virtual bool digital_read_hw(T pin) = 0; - /// @brief Call component read function from internal cache. - virtual bool digital_read_cache(T pin) = 0; - /// @brief Call component low level function to write GPIO state to device - virtual void digital_write_hw(T pin, bool value) = 0; - const uint8_t cache_byte_size_ = N / (sizeof(T) * BITS_PER_BYTE); + /// @brief Read GPIO bank from hardware into internal state + /// @param pin Pin number (used to determine which bank to read) + /// @return true if read succeeded, false on communication error + /// @note This does NOT return the pin state. It returns whether the read operation succeeded. + /// The actual pin state should be returned by digital_read_cache(). + virtual bool digital_read_hw(P pin) = 0; + + /// @brief Get cached pin value from internal state + /// @param pin Pin number to read + /// @return Pin state (true = HIGH, false = LOW) + virtual bool digital_read_cache(P pin) = 0; + + /// @brief Write GPIO state to hardware + /// @param pin Pin number to write + /// @param value Pin state to write (true = HIGH, false = LOW) + virtual void digital_write_hw(P pin, bool value) = 0; /// @brief Invalidate cache. This function should be called in component loop(). - void reset_pin_cache_() { - for (T i = 0; i < this->cache_byte_size_; i++) { - this->read_cache_invalidated_[i] = true; - } - } + void reset_pin_cache_() { memset(this->read_cache_valid_, 0x00, CACHE_SIZE_BYTES); } - static const uint8_t BITS_PER_BYTE = 8; - std::array read_cache_invalidated_{}; + static constexpr uint16_t BITS_PER_BYTE = 8; + static constexpr uint16_t BANK_SIZE = sizeof(T) * BITS_PER_BYTE; + static constexpr size_t BANKS = N / BANK_SIZE; + static constexpr size_t CACHE_SIZE_BYTES = BANKS * sizeof(T); + + T read_cache_valid_[BANKS]{0}; }; -} // namespace gpio_expander -} // namespace esphome +} // namespace esphome::gpio_expander diff --git a/esphome/components/graph/graph.cpp b/esphome/components/graph/graph.cpp index 5abf2ade0d..e3b9119108 100644 --- a/esphome/components/graph/graph.cpp +++ b/esphome/components/graph/graph.cpp @@ -179,7 +179,7 @@ void Graph::draw(Display *buff, uint16_t x_offset, uint16_t y_offset, Color colo if (b) { int16_t y = (int16_t) roundf((this->height_ - 1) * (1.0 - v)) - thick / 2 + y_offset; auto draw_pixel_at = [&buff, c, y_offset, this](int16_t x, int16_t y) { - if (y >= y_offset && y < y_offset + this->height_) + if (y >= y_offset && static_cast(y) < y_offset + this->height_) buff->draw_pixel_at(x, y, c); }; if (!continuous || !has_prev || !prev_b || (abs(y - prev_y) <= thick)) { @@ -235,7 +235,7 @@ void GraphLegend::init(Graph *g) { std::string valstr = value_accuracy_to_string(trace->sensor_->get_state(), trace->sensor_->get_accuracy_decimals()); if (this->units_) { - valstr += trace->sensor_->get_unit_of_measurement(); + valstr += trace->sensor_->get_unit_of_measurement_ref(); } this->font_value_->measure(valstr.c_str(), &fw, &fos, &fbl, &fh); if (fw > valw) @@ -337,7 +337,7 @@ void Graph::draw_legend(display::Display *buff, uint16_t x_offset, uint16_t y_of return; /// Plot border - if (this->border_) { + if (legend_->border_) { int w = legend_->width_; int h = legend_->height_; buff->horizontal_line(x_offset, y_offset, w, color); @@ -371,7 +371,7 @@ void Graph::draw_legend(display::Display *buff, uint16_t x_offset, uint16_t y_of std::string valstr = value_accuracy_to_string(trace->sensor_->get_state(), trace->sensor_->get_accuracy_decimals()); if (legend_->units_) { - valstr += trace->sensor_->get_unit_of_measurement(); + valstr += trace->sensor_->get_unit_of_measurement_ref(); } buff->printf(xv, yv, legend_->font_value_, trace->get_line_color(), TextAlign::TOP_CENTER, "%s", valstr.c_str()); ESP_LOGV(TAG, " value: %s", valstr.c_str()); diff --git a/esphome/components/graphical_display_menu/graphical_display_menu.cpp b/esphome/components/graphical_display_menu/graphical_display_menu.cpp index 1a29536b46..2b120a746f 100644 --- a/esphome/components/graphical_display_menu/graphical_display_menu.cpp +++ b/esphome/components/graphical_display_menu/graphical_display_menu.cpp @@ -116,7 +116,7 @@ void GraphicalDisplayMenu::draw_menu_internal_(display::Display *display, const int number_items_fit_to_screen = 0; const int max_item_index = this->displayed_item_->items_size() - 1; - for (size_t i = 0; i <= max_item_index; i++) { + for (size_t i = 0; max_item_index >= 0 && i <= static_cast(max_item_index); i++) { const auto *item = this->displayed_item_->get_item(i); const bool selected = i == this->cursor_index_; const display::Rect item_dimensions = this->measure_item(display, item, bounds, selected); @@ -174,7 +174,8 @@ void GraphicalDisplayMenu::draw_menu_internal_(display::Display *display, const display->filled_rectangle(bounds->x, bounds->y, max_width, total_height, this->background_color_); auto y_offset = bounds->y; - for (size_t i = first_item_index; i <= last_item_index; i++) { + for (size_t i = static_cast(first_item_index); + last_item_index >= 0 && i <= static_cast(last_item_index); i++) { const auto *item = this->displayed_item_->get_item(i); const bool selected = i == this->cursor_index_; display::Rect dimensions = menu_dimensions[i]; diff --git a/esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.cpp b/esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.cpp index 4842ee5d06..b0f3429314 100644 --- a/esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.cpp +++ b/esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.cpp @@ -57,11 +57,11 @@ void GroveGasMultichannelV2Component::update() { void GroveGasMultichannelV2Component::dump_config() { ESP_LOGCONFIG(TAG, "Grove Multichannel Gas Sensor V2"); LOG_I2C_DEVICE(this) - LOG_UPDATE_INTERVAL(this) - LOG_SENSOR(" ", "Nitrogen Dioxide", this->nitrogen_dioxide_sensor_) - LOG_SENSOR(" ", "Ethanol", this->ethanol_sensor_) - LOG_SENSOR(" ", "Carbon Monoxide", this->carbon_monoxide_sensor_) - LOG_SENSOR(" ", "TVOC", this->tvoc_sensor_) + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Nitrogen Dioxide", this->nitrogen_dioxide_sensor_); + LOG_SENSOR(" ", "Ethanol", this->ethanol_sensor_); + LOG_SENSOR(" ", "Carbon Monoxide", this->carbon_monoxide_sensor_); + LOG_SENSOR(" ", "TVOC", this->tvoc_sensor_); if (this->is_failed()) { switch (this->error_code_) { diff --git a/esphome/components/grove_tb6612fng/grove_tb6612fng.h b/esphome/components/grove_tb6612fng/grove_tb6612fng.h index 68281117e7..a36cb85cff 100644 --- a/esphome/components/grove_tb6612fng/grove_tb6612fng.h +++ b/esphome/components/grove_tb6612fng/grove_tb6612fng.h @@ -168,7 +168,7 @@ class GROVETB6612FNGMotorRunAction : public Action, public Parentedchannel_.value(x...); auto speed = this->speed_.value(x...); this->parent_->dc_motor_run(channel, speed); @@ -180,7 +180,7 @@ class GROVETB6612FNGMotorBrakeAction : public Action, public Parentedparent_->dc_motor_brake(this->channel_.value(x...)); } + void play(const Ts &...x) override { this->parent_->dc_motor_brake(this->channel_.value(x...)); } }; template @@ -188,19 +188,19 @@ class GROVETB6612FNGMotorStopAction : public Action, public Parentedparent_->dc_motor_stop(this->channel_.value(x...)); } + void play(const Ts &...x) override { this->parent_->dc_motor_stop(this->channel_.value(x...)); } }; template class GROVETB6612FNGMotorStandbyAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->standby(); } + void play(const Ts &...x) override { this->parent_->standby(); } }; template class GROVETB6612FNGMotorNoStandbyAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->not_standby(); } + void play(const Ts &...x) override { this->parent_->not_standby(); } }; template @@ -208,7 +208,7 @@ class GROVETB6612FNGMotorChangeAddressAction : public Action, public Pare public: TEMPLATABLE_VALUE(uint8_t, address) - void play(Ts... x) override { this->parent_->set_i2c_addr(this->address_.value(x...)); } + void play(const Ts &...x) override { this->parent_->set_i2c_addr(this->address_.value(x...)); } }; } // namespace grove_tb6612fng diff --git a/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp index 0319b083ef..b11880a042 100644 --- a/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp +++ b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp @@ -20,12 +20,11 @@ static const size_t MAX_BUTTONS = 4; // max number of buttons scanned #define ERROR_CHECK(err) \ if ((err) != i2c::ERROR_OK) { \ - this->status_set_warning("Communication failure"); \ + this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); \ return; \ } void GT911Touchscreen::setup() { - i2c::ErrorCode err; if (this->reset_pin_ != nullptr) { this->reset_pin_->setup(); this->reset_pin_->digital_write(false); @@ -35,9 +34,14 @@ void GT911Touchscreen::setup() { this->interrupt_pin_->digital_write(false); } delay(2); - this->reset_pin_->digital_write(true); - delay(50); // NOLINT + this->reset_pin_->digital_write(true); // wait at least T3+T4 ms as per the datasheet + this->set_timeout(5 + 50 + 1, [this] { this->setup_internal_(); }); + return; } + this->setup_internal_(); +} + +void GT911Touchscreen::setup_internal_() { if (this->interrupt_pin_ != nullptr) { // set pre-configured input mode this->interrupt_pin_->setup(); @@ -45,7 +49,7 @@ void GT911Touchscreen::setup() { // check the configuration of the int line. uint8_t data[4]; - err = this->write(GET_SWITCHES, sizeof(GET_SWITCHES)); + i2c::ErrorCode err = this->write(GET_SWITCHES, sizeof(GET_SWITCHES)); if (err != i2c::ERROR_OK && this->address_ == PRIMARY_ADDRESS) { this->address_ = SECONDARY_ADDRESS; err = this->write(GET_SWITCHES, sizeof(GET_SWITCHES)); @@ -53,7 +57,7 @@ void GT911Touchscreen::setup() { if (err == i2c::ERROR_OK) { err = this->read(data, 1); if (err == i2c::ERROR_OK) { - ESP_LOGD(TAG, "Read from switches at address 0x%02X: 0x%02X", this->address_, data[0]); + ESP_LOGD(TAG, "Switches ADDR: 0x%02X DATA: 0x%02X", this->address_, data[0]); if (this->interrupt_pin_ != nullptr) { this->attach_interrupt_(this->interrupt_pin_, (data[0] & 1) ? gpio::INTERRUPT_FALLING_EDGE : gpio::INTERRUPT_RISING_EDGE); @@ -75,16 +79,24 @@ void GT911Touchscreen::setup() { } } if (err != i2c::ERROR_OK) { - this->mark_failed("Failed to read calibration"); + this->mark_failed(LOG_STR("Calibration error")); return; } } + if (err != i2c::ERROR_OK) { - this->mark_failed("Failed to communicate"); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); + return; } + this->setup_done_ = true; } void GT911Touchscreen::update_touches() { + this->skip_update_ = true; // skip send touch events by default, set to false after successful error checks + if (!this->setup_done_) { + return; + } + i2c::ErrorCode err; uint8_t touch_state = 0; uint8_t data[MAX_TOUCHES + 1][8]; // 8 bytes each for each point, plus extra space for the key byte @@ -97,7 +109,6 @@ void GT911Touchscreen::update_touches() { uint8_t num_of_touches = touch_state & 0x07; if ((touch_state & 0x80) == 0 || num_of_touches > MAX_TOUCHES) { - this->skip_update_ = true; // skip send touch events, touchscreen is not ready yet. return; } @@ -107,6 +118,7 @@ void GT911Touchscreen::update_touches() { err = this->read(data[0], sizeof(data[0]) * num_of_touches + 1); ERROR_CHECK(err); + this->skip_update_ = false; // All error checks passed, send touch events for (uint8_t i = 0; i != num_of_touches; i++) { uint16_t id = data[i][0]; uint16_t x = encode_uint16(data[i][2], data[i][1]); diff --git a/esphome/components/gt911/touchscreen/gt911_touchscreen.h b/esphome/components/gt911/touchscreen/gt911_touchscreen.h index 17636a2ada..85025b5522 100644 --- a/esphome/components/gt911/touchscreen/gt911_touchscreen.h +++ b/esphome/components/gt911/touchscreen/gt911_touchscreen.h @@ -15,8 +15,20 @@ class GT911ButtonListener { class GT911Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice { public: + /// @brief Initialize the GT911 touchscreen. + /// + /// If @ref reset_pin_ is set, the touchscreen will be hardware reset, + /// and the rest of the setup will be scheduled to run 50ms later using @ref set_timeout() + /// to allow the device to stabilize after reset. + /// + /// If @ref interrupt_pin_ is set, it will be temporarily configured during reset + /// to control I2C address selection. + /// + /// After the timeout, or immediately if no reset is performed, @ref setup_internal_() + /// is called to complete the initialization. void setup() override; void dump_config() override; + bool can_proceed() override { return this->setup_done_; } void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; } @@ -25,8 +37,20 @@ class GT911Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice protected: void update_touches() override; - InternalGPIOPin *interrupt_pin_{}; - GPIOPin *reset_pin_{}; + /// @brief Perform the internal setup routine for the GT911 touchscreen. + /// + /// This function checks the I2C address, configures the interrupt pin (if available), + /// reads the touchscreen mode from the controller, and attempts to read calibration + /// data (maximum X and Y values) if not already set. + /// + /// On success, sets @ref setup_done_ to true. + /// On failure, calls @ref mark_failed() with an appropriate error message. + void setup_internal_(); + /// @brief True if the touchscreen setup has completed successfully. + bool setup_done_{false}; + + InternalGPIOPin *interrupt_pin_{nullptr}; + GPIOPin *reset_pin_{nullptr}; std::vector button_listeners_; uint8_t button_state_{0xFF}; // last button state. Initial FF guarantees first update. }; diff --git a/esphome/components/haier/automation.h b/esphome/components/haier/automation.h index 55df7ecc1d..c1ce7c01ea 100644 --- a/esphome/components/haier/automation.h +++ b/esphome/components/haier/automation.h @@ -10,7 +10,7 @@ namespace haier { template class DisplayOnAction : public Action { public: DisplayOnAction(HaierClimateBase *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->set_display_state(true); } + void play(const Ts &...x) { this->parent_->set_display_state(true); } protected: HaierClimateBase *parent_; @@ -19,7 +19,7 @@ template class DisplayOnAction : public Action { template class DisplayOffAction : public Action { public: DisplayOffAction(HaierClimateBase *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->set_display_state(false); } + void play(const Ts &...x) { this->parent_->set_display_state(false); } protected: HaierClimateBase *parent_; @@ -28,7 +28,7 @@ template class DisplayOffAction : public Action { template class BeeperOnAction : public Action { public: BeeperOnAction(HonClimate *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->set_beeper_state(true); } + void play(const Ts &...x) { this->parent_->set_beeper_state(true); } protected: HonClimate *parent_; @@ -37,7 +37,7 @@ template class BeeperOnAction : public Action { template class BeeperOffAction : public Action { public: BeeperOffAction(HonClimate *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->set_beeper_state(false); } + void play(const Ts &...x) { this->parent_->set_beeper_state(false); } protected: HonClimate *parent_; @@ -47,7 +47,7 @@ template class VerticalAirflowAction : public Action { public: VerticalAirflowAction(HonClimate *parent) : parent_(parent) {} TEMPLATABLE_VALUE(hon_protocol::VerticalSwingMode, direction) - void play(Ts... x) { this->parent_->set_vertical_airflow(this->direction_.value(x...)); } + void play(const Ts &...x) { this->parent_->set_vertical_airflow(this->direction_.value(x...)); } protected: HonClimate *parent_; @@ -57,7 +57,7 @@ template class HorizontalAirflowAction : public Action { public: HorizontalAirflowAction(HonClimate *parent) : parent_(parent) {} TEMPLATABLE_VALUE(hon_protocol::HorizontalSwingMode, direction) - void play(Ts... x) { this->parent_->set_horizontal_airflow(this->direction_.value(x...)); } + void play(const Ts &...x) { this->parent_->set_horizontal_airflow(this->direction_.value(x...)); } protected: HonClimate *parent_; @@ -66,7 +66,7 @@ template class HorizontalAirflowAction : public Action { template class HealthOnAction : public Action { public: HealthOnAction(HaierClimateBase *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->set_health_mode(true); } + void play(const Ts &...x) { this->parent_->set_health_mode(true); } protected: HaierClimateBase *parent_; @@ -75,7 +75,7 @@ template class HealthOnAction : public Action { template class HealthOffAction : public Action { public: HealthOffAction(HaierClimateBase *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->set_health_mode(false); } + void play(const Ts &...x) { this->parent_->set_health_mode(false); } protected: HaierClimateBase *parent_; @@ -84,7 +84,7 @@ template class HealthOffAction : public Action { template class StartSelfCleaningAction : public Action { public: StartSelfCleaningAction(HonClimate *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->start_self_cleaning(); } + void play(const Ts &...x) { this->parent_->start_self_cleaning(); } protected: HonClimate *parent_; @@ -93,7 +93,7 @@ template class StartSelfCleaningAction : public Action { template class StartSteriCleaningAction : public Action { public: StartSteriCleaningAction(HonClimate *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->start_steri_cleaning(); } + void play(const Ts &...x) { this->parent_->start_steri_cleaning(); } protected: HonClimate *parent_; @@ -102,7 +102,7 @@ template class StartSteriCleaningAction : public Action { template class PowerOnAction : public Action { public: PowerOnAction(HaierClimateBase *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->send_power_on_command(); } + void play(const Ts &...x) { this->parent_->send_power_on_command(); } protected: HaierClimateBase *parent_; @@ -111,7 +111,7 @@ template class PowerOnAction : public Action { template class PowerOffAction : public Action { public: PowerOffAction(HaierClimateBase *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->send_power_off_command(); } + void play(const Ts &...x) { this->parent_->send_power_off_command(); } protected: HaierClimateBase *parent_; @@ -120,7 +120,7 @@ template class PowerOffAction : public Action { template class PowerToggleAction : public Action { public: PowerToggleAction(HaierClimateBase *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->toggle_power(); } + void play(const Ts &...x) { this->parent_->toggle_power(); } protected: HaierClimateBase *parent_; diff --git a/esphome/components/haier/haier_base.cpp b/esphome/components/haier/haier_base.cpp index 4f933b08e3..cd2673a272 100644 --- a/esphome/components/haier/haier_base.cpp +++ b/esphome/components/haier/haier_base.cpp @@ -65,7 +65,7 @@ HaierClimateBase::HaierClimateBase() {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH}); this->traits_.set_supported_swing_modes({climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH, climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_HORIZONTAL}); - this->traits_.set_supports_current_temperature(true); + this->traits_.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE); } HaierClimateBase::~HaierClimateBase() {} @@ -171,7 +171,7 @@ void HaierClimateBase::toggle_power() { PendingAction({ActionRequest::TOGGLE_POWER, esphome::optional()}); } -void HaierClimateBase::set_supported_swing_modes(const std::set &modes) { +void HaierClimateBase::set_supported_swing_modes(climate::ClimateSwingModeMask modes) { this->traits_.set_supported_swing_modes(modes); if (!modes.empty()) this->traits_.add_supported_swing_mode(climate::CLIMATE_SWING_OFF); @@ -179,13 +179,13 @@ void HaierClimateBase::set_supported_swing_modes(const std::sethaier_protocol_.set_answer_timeout(timeout); } -void HaierClimateBase::set_supported_modes(const std::set &modes) { +void HaierClimateBase::set_supported_modes(climate::ClimateModeMask modes) { this->traits_.set_supported_modes(modes); this->traits_.add_supported_mode(climate::CLIMATE_MODE_OFF); // Always available this->traits_.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL); // Always available } -void HaierClimateBase::set_supported_presets(const std::set &presets) { +void HaierClimateBase::set_supported_presets(climate::ClimatePresetMask presets) { this->traits_.set_supported_presets(presets); if (!presets.empty()) this->traits_.add_supported_preset(climate::CLIMATE_PRESET_NONE); @@ -351,7 +351,7 @@ ClimateTraits HaierClimateBase::traits() { return traits_; } void HaierClimateBase::initialization() { constexpr uint32_t restore_settings_version = 0xA77D21EF; this->base_rtc_ = - global_preferences->make_preference(this->get_object_id_hash() ^ restore_settings_version); + global_preferences->make_preference(this->get_preference_hash() ^ restore_settings_version); HaierBaseSettings recovered; if (!this->base_rtc_.load(&recovered)) { recovered = {false, true}; diff --git a/esphome/components/haier/haier_base.h b/esphome/components/haier/haier_base.h index f0597c49ff..e24217bfd9 100644 --- a/esphome/components/haier/haier_base.h +++ b/esphome/components/haier/haier_base.h @@ -1,7 +1,6 @@ #pragma once #include -#include #include "esphome/components/climate/climate.h" #include "esphome/components/uart/uart.h" #include "esphome/core/automation.h" @@ -60,9 +59,9 @@ class HaierClimateBase : public esphome::Component, void send_power_off_command(); void toggle_power(); void reset_protocol() { this->reset_protocol_request_ = true; }; - void set_supported_modes(const std::set &modes); - void set_supported_swing_modes(const std::set &modes); - void set_supported_presets(const std::set &presets); + void set_supported_modes(esphome::climate::ClimateModeMask modes); + void set_supported_swing_modes(esphome::climate::ClimateSwingModeMask modes); + void set_supported_presets(esphome::climate::ClimatePresetMask presets); bool valid_connection() const { return this->protocol_phase_ >= ProtocolPhases::IDLE; }; size_t available() noexcept override { return esphome::uart::UARTDevice::available(); }; size_t read_array(uint8_t *data, size_t len) noexcept override { diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp index fd2d6a5800..23d28bfd47 100644 --- a/esphome/components/haier/hon_climate.cpp +++ b/esphome/components/haier/hon_climate.cpp @@ -213,7 +213,7 @@ haier_protocol::HandlerError HonClimate::status_handler_(haier_protocol::FrameTy this->real_control_packet_size_); this->status_message_callback_.call((const char *) data, data_size); } else { - ESP_LOGW(TAG, "Status packet too small: %d (should be >= %d)", data_size, this->real_control_packet_size_); + ESP_LOGW(TAG, "Status packet too small: %zu (should be >= %zu)", data_size, this->real_control_packet_size_); } switch (this->protocol_phase_) { case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST: @@ -516,7 +516,7 @@ void HonClimate::initialization() { HaierClimateBase::initialization(); constexpr uint32_t restore_settings_version = 0x57EB59DDUL; this->hon_rtc_ = - global_preferences->make_preference(this->get_object_id_hash() ^ restore_settings_version); + global_preferences->make_preference(this->get_preference_hash() ^ restore_settings_version); HonSettings recovered; if (this->hon_rtc_.load(&recovered)) { this->settings_ = recovered; @@ -827,7 +827,7 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t * size_t expected_size = 2 + this->status_message_header_size_ + this->real_control_packet_size_ + this->real_sensors_packet_size_; if (size < expected_size) { - ESP_LOGW(TAG, "Unexpected message size %d (expexted >= %d)", size, expected_size); + ESP_LOGW(TAG, "Unexpected message size %u (expexted >= %zu)", size, expected_size); return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE; } uint16_t subtype = (((uint16_t) packet_buffer[0]) << 8) + packet_buffer[1]; @@ -1033,9 +1033,9 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t * { // Swing mode ClimateSwingMode old_swing_mode = this->swing_mode; - const std::set &swing_modes = traits_.get_supported_swing_modes(); - bool vertical_swing_supported = swing_modes.find(CLIMATE_SWING_VERTICAL) != swing_modes.end(); - bool horizontal_swing_supported = swing_modes.find(CLIMATE_SWING_HORIZONTAL) != swing_modes.end(); + const auto &swing_modes = traits_.get_supported_swing_modes(); + bool vertical_swing_supported = swing_modes.count(CLIMATE_SWING_VERTICAL); + bool horizontal_swing_supported = swing_modes.count(CLIMATE_SWING_HORIZONTAL); if (horizontal_swing_supported && (packet.control.horizontal_swing_mode == (uint8_t) hon_protocol::HorizontalSwingMode::AUTO)) { if (vertical_swing_supported && @@ -1218,13 +1218,13 @@ void HonClimate::fill_control_messages_queue_() { (uint8_t) hon_protocol::DataParameters::QUIET_MODE, quiet_mode_buf, 2); } - if ((fast_mode_buf[1] != 0xFF) && ((presets.find(climate::ClimatePreset::CLIMATE_PRESET_BOOST) != presets.end()))) { + if ((fast_mode_buf[1] != 0xFF) && presets.count(climate::ClimatePreset::CLIMATE_PRESET_BOOST)) { this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + (uint8_t) hon_protocol::DataParameters::FAST_MODE, fast_mode_buf, 2); } - if ((away_mode_buf[1] != 0xFF) && ((presets.find(climate::ClimatePreset::CLIMATE_PRESET_AWAY) != presets.end()))) { + if ((away_mode_buf[1] != 0xFF) && presets.count(climate::ClimatePreset::CLIMATE_PRESET_AWAY)) { this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + (uint8_t) hon_protocol::DataParameters::TEN_DEGREE, diff --git a/esphome/components/haier/hon_climate.h b/esphome/components/haier/hon_climate.h index 58173f8154..a567ab1d89 100644 --- a/esphome/components/haier/hon_climate.h +++ b/esphome/components/haier/hon_climate.h @@ -178,7 +178,7 @@ class HonClimate : public HaierClimateBase { int extra_control_packet_bytes_{0}; int extra_sensors_packet_bytes_{4}; int status_message_header_size_{0}; - int real_control_packet_size_{sizeof(hon_protocol::HaierPacketControl)}; + size_t real_control_packet_size_{sizeof(hon_protocol::HaierPacketControl)}; int real_sensors_packet_size_{sizeof(hon_protocol::HaierPacketSensors) + 4}; HonControlMethod control_method_; std::queue control_messages_queue_; diff --git a/esphome/components/hbridge/fan/hbridge_fan.cpp b/esphome/components/hbridge/fan/hbridge_fan.cpp index 605a9d4ef3..488208b725 100644 --- a/esphome/components/hbridge/fan/hbridge_fan.cpp +++ b/esphome/components/hbridge/fan/hbridge_fan.cpp @@ -57,7 +57,7 @@ void HBridgeFan::control(const fan::FanCall &call) { this->oscillating = *call.get_oscillating(); if (call.get_direction().has_value()) this->direction = *call.get_direction(); - this->preset_mode = call.get_preset_mode(); + this->set_preset_mode_(call.get_preset_mode()); this->write_state_(); this->publish_state(); diff --git a/esphome/components/hbridge/fan/hbridge_fan.h b/esphome/components/hbridge/fan/hbridge_fan.h index 4234fccae3..ec1e8ada0e 100644 --- a/esphome/components/hbridge/fan/hbridge_fan.h +++ b/esphome/components/hbridge/fan/hbridge_fan.h @@ -1,7 +1,5 @@ #pragma once -#include - #include "esphome/core/automation.h" #include "esphome/components/output/binary_output.h" #include "esphome/components/output/float_output.h" @@ -22,7 +20,7 @@ class HBridgeFan : public Component, public fan::Fan { void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; } void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; } void set_enable_pin(output::FloatOutput *enable) { enable_ = enable; } - void set_preset_modes(const std::set &presets) { preset_modes_ = presets; } + void set_preset_modes(std::initializer_list presets) { preset_modes_ = presets; } void setup() override; void dump_config() override; @@ -38,7 +36,7 @@ class HBridgeFan : public Component, public fan::Fan { int speed_count_{}; DecayMode decay_mode_{DECAY_MODE_SLOW}; fan::FanTraits traits_; - std::set preset_modes_{}; + std::vector preset_modes_{}; void control(const fan::FanCall &call) override; void write_state_(); @@ -51,7 +49,7 @@ template class BrakeAction : public Action { public: explicit BrakeAction(HBridgeFan *parent) : parent_(parent) {} - void play(Ts... x) override { this->parent_->brake(); } + void play(const Ts &...x) override { this->parent_->brake(); } HBridgeFan *parent_; }; diff --git a/esphome/components/hc8/__init__.py b/esphome/components/hc8/__init__.py new file mode 100644 index 0000000000..e1028456b0 --- /dev/null +++ b/esphome/components/hc8/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@omartijn"] diff --git a/esphome/components/hc8/hc8.cpp b/esphome/components/hc8/hc8.cpp new file mode 100644 index 0000000000..5b649c2735 --- /dev/null +++ b/esphome/components/hc8/hc8.cpp @@ -0,0 +1,99 @@ +#include "hc8.h" +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#include + +namespace esphome::hc8 { + +static const char *const TAG = "hc8"; +static const std::array HC8_COMMAND_GET_PPM{0x64, 0x69, 0x03, 0x5E, 0x4E}; +static const std::array HC8_COMMAND_CALIBRATE_PREAMBLE{0x11, 0x03, 0x03}; + +void HC8Component::setup() { + // send an initial query to the device, this will + // get it out of "active output mode", where it + // generates data every second + this->write_array(HC8_COMMAND_GET_PPM); + this->flush(); + + // ensure the buffer is empty + while (this->available()) + this->read(); +} + +void HC8Component::update() { + uint32_t now_ms = App.get_loop_component_start_time(); + uint32_t warmup_ms = this->warmup_seconds_ * 1000; + if (now_ms < warmup_ms) { + ESP_LOGW(TAG, "HC8 warming up, %" PRIu32 " s left", (warmup_ms - now_ms) / 1000); + this->status_set_warning(); + return; + } + + while (this->available()) + this->read(); + + this->write_array(HC8_COMMAND_GET_PPM); + this->flush(); + + // the sensor is a bit slow in responding, so trying to + // read immediately after sending a query will timeout + this->set_timeout(50, [this]() { + std::array response; + if (!this->read_array(response.data(), response.size())) { + ESP_LOGW(TAG, "Reading data from HC8 failed!"); + this->status_set_warning(); + return; + } + + if (response[0] != 0x64 || response[1] != 0x69) { + ESP_LOGW(TAG, "Invalid preamble from HC8!"); + this->status_set_warning(); + return; + } + + if (crc16(response.data(), 12) != encode_uint16(response[13], response[12])) { + ESP_LOGW(TAG, "HC8 Checksum mismatch"); + this->status_set_warning(); + return; + } + + this->status_clear_warning(); + + const uint16_t ppm = encode_uint16(response[5], response[4]); + ESP_LOGD(TAG, "HC8 Received CO₂=%uppm", ppm); + if (this->co2_sensor_ != nullptr) + this->co2_sensor_->publish_state(ppm); + }); +} + +void HC8Component::calibrate(uint16_t baseline) { + ESP_LOGD(TAG, "HC8 Calibrating baseline to %uppm", baseline); + + std::array command{}; + std::copy(begin(HC8_COMMAND_CALIBRATE_PREAMBLE), end(HC8_COMMAND_CALIBRATE_PREAMBLE), begin(command)); + command[3] = baseline >> 8; + command[4] = baseline; + command[5] = 0; + + // the last byte is a checksum over the data + for (uint8_t i = 0; i < 5; ++i) + command[5] -= command[i]; + + this->write_array(command); + this->flush(); +} + +float HC8Component::get_setup_priority() const { return setup_priority::DATA; } + +void HC8Component::dump_config() { + ESP_LOGCONFIG(TAG, "HC8:"); + LOG_SENSOR(" ", "CO2", this->co2_sensor_); + this->check_uart_settings(9600); + + ESP_LOGCONFIG(TAG, " Warmup time: %" PRIu32 " s", this->warmup_seconds_); +} + +} // namespace esphome::hc8 diff --git a/esphome/components/hc8/hc8.h b/esphome/components/hc8/hc8.h new file mode 100644 index 0000000000..7711fb8c97 --- /dev/null +++ b/esphome/components/hc8/hc8.h @@ -0,0 +1,37 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" + +#include + +namespace esphome::hc8 { + +class HC8Component : public PollingComponent, public uart::UARTDevice { + public: + float get_setup_priority() const override; + + void setup() override; + void update() override; + void dump_config() override; + + void calibrate(uint16_t baseline); + + void set_co2_sensor(sensor::Sensor *co2_sensor) { co2_sensor_ = co2_sensor; } + void set_warmup_seconds(uint32_t seconds) { warmup_seconds_ = seconds; } + + protected: + sensor::Sensor *co2_sensor_{nullptr}; + uint32_t warmup_seconds_{0}; +}; + +template class HC8CalibrateAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint16_t, baseline) + + void play(const Ts &...x) override { this->parent_->calibrate(this->baseline_.value(x...)); } +}; + +} // namespace esphome::hc8 diff --git a/esphome/components/hc8/sensor.py b/esphome/components/hc8/sensor.py new file mode 100644 index 0000000000..90698b2661 --- /dev/null +++ b/esphome/components/hc8/sensor.py @@ -0,0 +1,79 @@ +from esphome import automation +import esphome.codegen as cg +from esphome.components import sensor, uart +import esphome.config_validation as cv +from esphome.const import ( + CONF_BASELINE, + CONF_CO2, + CONF_ID, + DEVICE_CLASS_CARBON_DIOXIDE, + ICON_MOLECULE_CO2, + STATE_CLASS_MEASUREMENT, + UNIT_PARTS_PER_MILLION, +) + +DEPENDENCIES = ["uart"] + +CONF_WARMUP_TIME = "warmup_time" + +hc8_ns = cg.esphome_ns.namespace("hc8") +HC8Component = hc8_ns.class_("HC8Component", cg.PollingComponent, uart.UARTDevice) +HC8CalibrateAction = hc8_ns.class_("HC8CalibrateAction", automation.Action) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(HC8Component), + cv.Optional(CONF_CO2): sensor.sensor_schema( + unit_of_measurement=UNIT_PARTS_PER_MILLION, + icon=ICON_MOLECULE_CO2, + accuracy_decimals=0, + device_class=DEVICE_CLASS_CARBON_DIOXIDE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional( + CONF_WARMUP_TIME, default="75s" + ): cv.positive_time_period_seconds, + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(uart.UART_DEVICE_SCHEMA) +) + +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "hc8", + baud_rate=9600, + require_rx=True, + require_tx=True, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) + + if co2 := config.get(CONF_CO2): + sens = await sensor.new_sensor(co2) + cg.add(var.set_co2_sensor(sens)) + + cg.add(var.set_warmup_seconds(config[CONF_WARMUP_TIME])) + + +CALIBRATION_ACTION_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(HC8Component), + cv.Required(CONF_BASELINE): cv.templatable(cv.uint16_t), + } +) + + +@automation.register_action( + "hc8.calibrate", HC8CalibrateAction, CALIBRATION_ACTION_SCHEMA +) +async def hc8_calibration_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + template_ = await cg.templatable(config[CONF_BASELINE], args, cg.uint16) + cg.add(var.set_baseline(template_)) + return var diff --git a/esphome/components/hdc1080/hdc1080.cpp b/esphome/components/hdc1080/hdc1080.cpp index 6d16133c36..fa293f6fc5 100644 --- a/esphome/components/hdc1080/hdc1080.cpp +++ b/esphome/components/hdc1080/hdc1080.cpp @@ -7,24 +7,21 @@ namespace hdc1080 { static const char *const TAG = "hdc1080"; -static const uint8_t HDC1080_ADDRESS = 0x40; // 0b1000000 from datasheet static const uint8_t HDC1080_CMD_CONFIGURATION = 0x02; static const uint8_t HDC1080_CMD_TEMPERATURE = 0x00; static const uint8_t HDC1080_CMD_HUMIDITY = 0x01; void HDC1080Component::setup() { - const uint8_t data[2] = { - 0b00000000, // resolution 14bit for both humidity and temperature - 0b00000000 // reserved - }; + const uint8_t config[2] = {0x00, 0x00}; // resolution 14bit for both humidity and temperature - if (!this->write_bytes(HDC1080_CMD_CONFIGURATION, data, 2)) { - // as instruction is same as powerup defaults (for now), interpret as warning if this fails - ESP_LOGW(TAG, "HDC1080 initial config instruction error"); + // if configuration fails - there is a problem + if (this->write_register(HDC1080_CMD_CONFIGURATION, config, 2) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Failed to configure HDC1080"); this->status_set_warning(); return; } } + void HDC1080Component::dump_config() { ESP_LOGCONFIG(TAG, "HDC1080:"); LOG_I2C_DEVICE(this); @@ -35,39 +32,51 @@ void HDC1080Component::dump_config() { LOG_SENSOR(" ", "Temperature", this->temperature_); LOG_SENSOR(" ", "Humidity", this->humidity_); } + void HDC1080Component::update() { - uint16_t raw_temp; + // regardless of what sensor/s are defined in yaml configuration + // the hdc1080 setup configuration used, requires both temperature and humidity to be read + + this->status_clear_warning(); + if (this->write(&HDC1080_CMD_TEMPERATURE, 1) != i2c::ERROR_OK) { this->status_set_warning(); return; } - delay(20); - if (this->read(reinterpret_cast(&raw_temp), 2) != i2c::ERROR_OK) { - this->status_set_warning(); - return; - } - raw_temp = i2c::i2ctohs(raw_temp); - float temp = raw_temp * 0.0025177f - 40.0f; // raw * 2^-16 * 165 - 40 - this->temperature_->publish_state(temp); - uint16_t raw_humidity; - if (this->write(&HDC1080_CMD_HUMIDITY, 1) != i2c::ERROR_OK) { - this->status_set_warning(); - return; - } - delay(20); - if (this->read(reinterpret_cast(&raw_humidity), 2) != i2c::ERROR_OK) { - this->status_set_warning(); - return; - } - raw_humidity = i2c::i2ctohs(raw_humidity); - float humidity = raw_humidity * 0.001525879f; // raw * 2^-16 * 100 - this->humidity_->publish_state(humidity); + this->set_timeout(20, [this]() { + uint16_t raw_temperature; + if (this->read(reinterpret_cast(&raw_temperature), 2) != i2c::ERROR_OK) { + this->status_set_warning(); + return; + } - ESP_LOGD(TAG, "Got temperature=%.1f°C humidity=%.1f%%", temp, humidity); - this->status_clear_warning(); + if (this->temperature_ != nullptr) { + raw_temperature = i2c::i2ctohs(raw_temperature); + float temperature = raw_temperature * 0.0025177f - 40.0f; // raw * 2^-16 * 165 - 40 + this->temperature_->publish_state(temperature); + } + + if (this->write(&HDC1080_CMD_HUMIDITY, 1) != i2c::ERROR_OK) { + this->status_set_warning(); + return; + } + + this->set_timeout(20, [this]() { + uint16_t raw_humidity; + if (this->read(reinterpret_cast(&raw_humidity), 2) != i2c::ERROR_OK) { + this->status_set_warning(); + return; + } + + if (this->humidity_ != nullptr) { + raw_humidity = i2c::i2ctohs(raw_humidity); + float humidity = raw_humidity * 0.001525879f; // raw * 2^-16 * 100 + this->humidity_->publish_state(humidity); + } + }); + }); } -float HDC1080Component::get_setup_priority() const { return setup_priority::DATA; } } // namespace hdc1080 } // namespace esphome diff --git a/esphome/components/hdc1080/hdc1080.h b/esphome/components/hdc1080/hdc1080.h index 2ff7b6dc33..7ad0764f1f 100644 --- a/esphome/components/hdc1080/hdc1080.h +++ b/esphome/components/hdc1080/hdc1080.h @@ -12,13 +12,11 @@ class HDC1080Component : public PollingComponent, public i2c::I2CDevice { void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } - /// Setup the sensor and check for connection. void setup() override; void dump_config() override; - /// Retrieve the latest sensor values. This operation takes approximately 16ms. void update() override; - float get_setup_priority() const override; + float get_setup_priority() const override { return setup_priority::DATA; } protected: sensor::Sensor *temperature_{nullptr}; diff --git a/esphome/components/hdc2010/__init__.py b/esphome/components/hdc2010/__init__.py new file mode 100644 index 0000000000..badf9dbb0c --- /dev/null +++ b/esphome/components/hdc2010/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@optimusprimespace", "@ssieb"] diff --git a/esphome/components/hdc2010/hdc2010.cpp b/esphome/components/hdc2010/hdc2010.cpp new file mode 100644 index 0000000000..c53fdb3f5b --- /dev/null +++ b/esphome/components/hdc2010/hdc2010.cpp @@ -0,0 +1,111 @@ +#include "esphome/core/hal.h" +#include "hdc2010.h" +// https://github.com/vigsterkr/homebridge-hdc2010/blob/main/src/hdc2010.js +// https://github.com/lime-labs/HDC2080-Arduino/blob/master/src/HDC2080.cpp +namespace esphome { +namespace hdc2010 { + +static const char *const TAG = "hdc2010"; + +static const uint8_t HDC2010_ADDRESS = 0x40; // 0b1000000 or 0b1000001 from datasheet +static const uint8_t HDC2010_CMD_CONFIGURATION_MEASUREMENT = 0x8F; +static const uint8_t HDC2010_CMD_START_MEASUREMENT = 0xF9; +static const uint8_t HDC2010_CMD_TEMPERATURE_LOW = 0x00; +static const uint8_t HDC2010_CMD_TEMPERATURE_HIGH = 0x01; +static const uint8_t HDC2010_CMD_HUMIDITY_LOW = 0x02; +static const uint8_t HDC2010_CMD_HUMIDITY_HIGH = 0x03; +static const uint8_t CONFIG = 0x0E; +static const uint8_t MEASUREMENT_CONFIG = 0x0F; + +void HDC2010Component::setup() { + ESP_LOGCONFIG(TAG, "Running setup"); + + const uint8_t data[2] = { + 0b00000000, // resolution 14bit for both humidity and temperature + 0b00000000 // reserved + }; + + if (!this->write_bytes(HDC2010_CMD_CONFIGURATION_MEASUREMENT, data, 2)) { + ESP_LOGW(TAG, "Initial config instruction error"); + this->status_set_warning(); + return; + } + + // Set measurement mode to temperature and humidity + uint8_t config_contents; + this->read_register(MEASUREMENT_CONFIG, &config_contents, 1); + config_contents = (config_contents & 0xF9); // Always set to TEMP_AND_HUMID mode + this->write_bytes(MEASUREMENT_CONFIG, &config_contents, 1); + + // Set rate to manual + this->read_register(CONFIG, &config_contents, 1); + config_contents &= 0x8F; + this->write_bytes(CONFIG, &config_contents, 1); + + // Set temperature resolution to 14bit + this->read_register(CONFIG, &config_contents, 1); + config_contents &= 0x3F; + this->write_bytes(CONFIG, &config_contents, 1); + + // Set humidity resolution to 14bit + this->read_register(CONFIG, &config_contents, 1); + config_contents &= 0xCF; + this->write_bytes(CONFIG, &config_contents, 1); +} + +void HDC2010Component::dump_config() { + ESP_LOGCONFIG(TAG, "HDC2010:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); + } + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); +} + +void HDC2010Component::update() { + // Trigger measurement + uint8_t config_contents; + this->read_register(CONFIG, &config_contents, 1); + config_contents |= 0x01; + this->write_bytes(MEASUREMENT_CONFIG, &config_contents, 1); + + // 1ms delay after triggering the sample + set_timeout(1, [this]() { + if (this->temperature_sensor_ != nullptr) { + float temp = this->read_temp(); + this->temperature_sensor_->publish_state(temp); + ESP_LOGD(TAG, "Temp=%.1f°C", temp); + } + + if (this->humidity_sensor_ != nullptr) { + float humidity = this->read_humidity(); + this->humidity_sensor_->publish_state(humidity); + ESP_LOGD(TAG, "Humidity=%.1f%%", humidity); + } + }); +} + +float HDC2010Component::read_temp() { + uint8_t byte[2]; + + this->read_register(HDC2010_CMD_TEMPERATURE_LOW, &byte[0], 1); + this->read_register(HDC2010_CMD_TEMPERATURE_HIGH, &byte[1], 1); + + uint16_t temp = encode_uint16(byte[1], byte[0]); + return (float) temp * 0.0025177f - 40.0f; +} + +float HDC2010Component::read_humidity() { + uint8_t byte[2]; + + this->read_register(HDC2010_CMD_HUMIDITY_LOW, &byte[0], 1); + this->read_register(HDC2010_CMD_HUMIDITY_HIGH, &byte[1], 1); + + uint16_t humidity = encode_uint16(byte[1], byte[0]); + return (float) humidity * 0.001525879f; +} + +} // namespace hdc2010 +} // namespace esphome diff --git a/esphome/components/hdc2010/hdc2010.h b/esphome/components/hdc2010/hdc2010.h new file mode 100644 index 0000000000..52c00686e6 --- /dev/null +++ b/esphome/components/hdc2010/hdc2010.h @@ -0,0 +1,32 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace hdc2010 { + +class HDC2010Component : public PollingComponent, public i2c::I2CDevice { + public: + void set_temperature_sensor(sensor::Sensor *temperature) { this->temperature_sensor_ = temperature; } + + void set_humidity_sensor(sensor::Sensor *humidity) { this->humidity_sensor_ = humidity; } + + /// Setup the sensor and check for connection. + void setup() override; + void dump_config() override; + /// Retrieve the latest sensor values. This operation takes approximately 16ms. + void update() override; + + float read_temp(); + + float read_humidity(); + + protected: + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; +}; + +} // namespace hdc2010 +} // namespace esphome diff --git a/esphome/components/hdc2010/sensor.py b/esphome/components/hdc2010/sensor.py new file mode 100644 index 0000000000..15e19f2cc8 --- /dev/null +++ b/esphome/components/hdc2010/sensor.py @@ -0,0 +1,56 @@ +import esphome.codegen as cg +from esphome.components import i2c, sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_HUMIDITY, + CONF_ID, + CONF_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, +) + +DEPENDENCIES = ["i2c"] + +hdc2010_ns = cg.esphome_ns.namespace("hdc2010") +HDC2010Component = hdc2010_ns.class_( + "HDC2010Component", cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(HDC2010Component), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x40)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + if temperature_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature_config) + cg.add(var.set_temperature_sensor(sens)) + + if humidity_config := config.get(CONF_HUMIDITY): + sens = await sensor.new_sensor(humidity_config) + cg.add(var.set_humidity_sensor(sens)) diff --git a/esphome/components/heatpumpir/climate.py b/esphome/components/heatpumpir/climate.py index 4f83bf2435..ec6eac670f 100644 --- a/esphome/components/heatpumpir/climate.py +++ b/esphome/components/heatpumpir/climate.py @@ -128,4 +128,4 @@ async def to_code(config): cg.add_library("tonia/HeatpumpIR", "1.0.37") if CORE.is_libretiny or CORE.is_esp32: - CORE.add_platformio_option("lib_ignore", "IRremoteESP8266") + CORE.add_platformio_option("lib_ignore", ["IRremoteESP8266"]) diff --git a/esphome/components/heatpumpir/heatpumpir.h b/esphome/components/heatpumpir/heatpumpir.h index 3e14c11861..ed43ffdc83 100644 --- a/esphome/components/heatpumpir/heatpumpir.h +++ b/esphome/components/heatpumpir/heatpumpir.h @@ -97,12 +97,11 @@ const float TEMP_MAX = 100; // Celsius class HeatpumpIRClimate : public climate_ir::ClimateIR { public: HeatpumpIRClimate() - : climate_ir::ClimateIR( - TEMP_MIN, TEMP_MAX, 1.0f, true, true, - std::set{climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, - climate::CLIMATE_FAN_HIGH, climate::CLIMATE_FAN_AUTO}, - std::set{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL, - climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_BOTH}) {} + : climate_ir::ClimateIR(TEMP_MIN, TEMP_MAX, 1.0f, true, true, + {climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH, + climate::CLIMATE_FAN_AUTO}, + {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL, + climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_BOTH}) {} void setup() override; void set_protocol(Protocol protocol) { this->protocol_ = protocol; } void set_horizontal_default(HorizontalDirection horizontal_direction) { diff --git a/esphome/components/hlk_fm22x/__init__.py b/esphome/components/hlk_fm22x/__init__.py new file mode 100644 index 0000000000..efd64b6513 --- /dev/null +++ b/esphome/components/hlk_fm22x/__init__.py @@ -0,0 +1,247 @@ +from esphome import automation +import esphome.codegen as cg +from esphome.components import uart +import esphome.config_validation as cv +from esphome.const import ( + CONF_DIRECTION, + CONF_ID, + CONF_NAME, + CONF_ON_ENROLLMENT_DONE, + CONF_ON_ENROLLMENT_FAILED, + CONF_TRIGGER_ID, +) + +CODEOWNERS = ["@OnFreund"] +DEPENDENCIES = ["uart"] +AUTO_LOAD = ["binary_sensor", "sensor", "text_sensor"] +MULTI_CONF = True + +CONF_HLK_FM22X_ID = "hlk_fm22x_id" +CONF_FACE_ID = "face_id" +CONF_ON_FACE_SCAN_MATCHED = "on_face_scan_matched" +CONF_ON_FACE_SCAN_UNMATCHED = "on_face_scan_unmatched" +CONF_ON_FACE_SCAN_INVALID = "on_face_scan_invalid" +CONF_ON_FACE_INFO = "on_face_info" + +hlk_fm22x_ns = cg.esphome_ns.namespace("hlk_fm22x") +HlkFm22xComponent = hlk_fm22x_ns.class_( + "HlkFm22xComponent", cg.PollingComponent, uart.UARTDevice +) + +FaceScanMatchedTrigger = hlk_fm22x_ns.class_( + "FaceScanMatchedTrigger", automation.Trigger.template(cg.int16, cg.std_string) +) + +FaceScanUnmatchedTrigger = hlk_fm22x_ns.class_( + "FaceScanUnmatchedTrigger", automation.Trigger.template() +) + +FaceScanInvalidTrigger = hlk_fm22x_ns.class_( + "FaceScanInvalidTrigger", automation.Trigger.template(cg.uint8) +) + +FaceInfoTrigger = hlk_fm22x_ns.class_( + "FaceInfoTrigger", + automation.Trigger.template( + cg.int16, cg.int16, cg.int16, cg.int16, cg.int16, cg.int16, cg.int16, cg.int16 + ), +) + +EnrollmentDoneTrigger = hlk_fm22x_ns.class_( + "EnrollmentDoneTrigger", automation.Trigger.template(cg.int16, cg.uint8) +) + +EnrollmentFailedTrigger = hlk_fm22x_ns.class_( + "EnrollmentFailedTrigger", automation.Trigger.template(cg.uint8) +) + +EnrollmentAction = hlk_fm22x_ns.class_("EnrollmentAction", automation.Action) +DeleteAction = hlk_fm22x_ns.class_("DeleteAction", automation.Action) +DeleteAllAction = hlk_fm22x_ns.class_("DeleteAllAction", automation.Action) +ScanAction = hlk_fm22x_ns.class_("ScanAction", automation.Action) +ResetAction = hlk_fm22x_ns.class_("ResetAction", automation.Action) + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(HlkFm22xComponent), + cv.Optional(CONF_ON_FACE_SCAN_MATCHED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + FaceScanMatchedTrigger + ), + } + ), + cv.Optional(CONF_ON_FACE_SCAN_UNMATCHED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + FaceScanUnmatchedTrigger + ), + } + ), + cv.Optional(CONF_ON_FACE_SCAN_INVALID): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + FaceScanInvalidTrigger + ), + } + ), + cv.Optional(CONF_ON_FACE_INFO): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FaceInfoTrigger), + } + ), + cv.Optional(CONF_ON_ENROLLMENT_DONE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + EnrollmentDoneTrigger + ), + } + ), + cv.Optional(CONF_ON_ENROLLMENT_FAILED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + EnrollmentFailedTrigger + ), + } + ), + } + ) + .extend(cv.polling_component_schema("50ms")) + .extend(uart.UART_DEVICE_SCHEMA), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) + + for conf in config.get(CONF_ON_FACE_SCAN_MATCHED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation( + trigger, [(cg.int16, "face_id"), (cg.std_string, "name")], conf + ) + + for conf in config.get(CONF_ON_FACE_SCAN_UNMATCHED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + + for conf in config.get(CONF_ON_FACE_SCAN_INVALID, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(cg.uint8, "error")], conf) + + for conf in config.get(CONF_ON_FACE_INFO, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation( + trigger, + [ + (cg.int16, "status"), + (cg.int16, "left"), + (cg.int16, "top"), + (cg.int16, "right"), + (cg.int16, "bottom"), + (cg.int16, "yaw"), + (cg.int16, "pitch"), + (cg.int16, "roll"), + ], + conf, + ) + + for conf in config.get(CONF_ON_ENROLLMENT_DONE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation( + trigger, [(cg.int16, "face_id"), (cg.uint8, "direction")], conf + ) + + for conf in config.get(CONF_ON_ENROLLMENT_FAILED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(cg.uint8, "error")], conf) + + +@automation.register_action( + "hlk_fm22x.enroll", + EnrollmentAction, + cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(HlkFm22xComponent), + cv.Required(CONF_NAME): cv.templatable(cv.string), + cv.Required(CONF_DIRECTION): cv.templatable(cv.uint8_t), + }, + key=CONF_NAME, + ), +) +async def hlk_fm22x_enroll_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + + template_ = await cg.templatable(config[CONF_NAME], args, cg.std_string) + cg.add(var.set_name(template_)) + template_ = await cg.templatable(config[CONF_DIRECTION], args, cg.uint8) + cg.add(var.set_direction(template_)) + return var + + +@automation.register_action( + "hlk_fm22x.delete", + DeleteAction, + cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(HlkFm22xComponent), + cv.Required(CONF_FACE_ID): cv.templatable(cv.uint16_t), + }, + key=CONF_FACE_ID, + ), +) +async def hlk_fm22x_delete_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + + template_ = await cg.templatable(config[CONF_FACE_ID], args, cg.int16) + cg.add(var.set_face_id(template_)) + return var + + +@automation.register_action( + "hlk_fm22x.delete_all", + DeleteAllAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(HlkFm22xComponent), + } + ), +) +async def hlk_fm22x_delete_all_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +@automation.register_action( + "hlk_fm22x.scan", + ScanAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(HlkFm22xComponent), + } + ), +) +async def hlk_fm22x_scan_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +@automation.register_action( + "hlk_fm22x.reset", + ResetAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(HlkFm22xComponent), + } + ), +) +async def hlk_fm22x_reset_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var diff --git a/esphome/components/hlk_fm22x/binary_sensor.py b/esphome/components/hlk_fm22x/binary_sensor.py new file mode 100644 index 0000000000..3620f33ac0 --- /dev/null +++ b/esphome/components/hlk_fm22x/binary_sensor.py @@ -0,0 +1,21 @@ +import esphome.codegen as cg +from esphome.components import binary_sensor +import esphome.config_validation as cv +from esphome.const import CONF_ICON, ICON_KEY_PLUS + +from . import CONF_HLK_FM22X_ID, HlkFm22xComponent + +DEPENDENCIES = ["hlk_fm22x"] + +CONFIG_SCHEMA = binary_sensor.binary_sensor_schema().extend( + { + cv.GenerateID(CONF_HLK_FM22X_ID): cv.use_id(HlkFm22xComponent), + cv.Optional(CONF_ICON, default=ICON_KEY_PLUS): cv.icon, + } +) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_HLK_FM22X_ID]) + var = await binary_sensor.new_binary_sensor(config) + cg.add(hub.set_enrolling_binary_sensor(var)) diff --git a/esphome/components/hlk_fm22x/hlk_fm22x.cpp b/esphome/components/hlk_fm22x/hlk_fm22x.cpp new file mode 100644 index 0000000000..ab15a2340d --- /dev/null +++ b/esphome/components/hlk_fm22x/hlk_fm22x.cpp @@ -0,0 +1,325 @@ +#include "hlk_fm22x.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" +#include +#include + +namespace esphome::hlk_fm22x { + +static const char *const TAG = "hlk_fm22x"; + +void HlkFm22xComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up HLK-FM22X..."); + this->set_enrolling_(false); + while (this->available()) { + this->read(); + } + this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_STATUS); }); +} + +void HlkFm22xComponent::update() { + if (this->active_command_ != HlkFm22xCommand::NONE) { + if (this->wait_cycles_ > 600) { + ESP_LOGE(TAG, "Command 0x%.2X timed out", this->active_command_); + if (HlkFm22xCommand::RESET == this->active_command_) { + this->mark_failed(); + } else { + this->reset(); + } + } + } + this->recv_command_(); +} + +void HlkFm22xComponent::enroll_face(const std::string &name, HlkFm22xFaceDirection direction) { + if (name.length() > 31) { + ESP_LOGE(TAG, "enroll_face(): name too long '%s'", name.c_str()); + return; + } + ESP_LOGI(TAG, "Starting enrollment for %s", name.c_str()); + std::array data{}; + data[0] = 0; // admin + std::copy(name.begin(), name.end(), data.begin() + 1); + // Remaining bytes are already zero-initialized + data[33] = (uint8_t) direction; + data[34] = 10; // timeout + this->send_command_(HlkFm22xCommand::ENROLL, data.data(), data.size()); + this->set_enrolling_(true); +} + +void HlkFm22xComponent::scan_face() { + ESP_LOGI(TAG, "Verify face"); + static const uint8_t DATA[] = {0, 0}; + this->send_command_(HlkFm22xCommand::VERIFY, DATA, sizeof(DATA)); +} + +void HlkFm22xComponent::delete_face(int16_t face_id) { + ESP_LOGI(TAG, "Deleting face in slot %d", face_id); + const uint8_t data[] = {(uint8_t) (face_id >> 8), (uint8_t) (face_id & 0xFF)}; + this->send_command_(HlkFm22xCommand::DELETE_FACE, data, sizeof(data)); +} + +void HlkFm22xComponent::delete_all_faces() { + ESP_LOGI(TAG, "Deleting all stored faces"); + this->send_command_(HlkFm22xCommand::DELETE_ALL_FACES); +} + +void HlkFm22xComponent::get_face_count_() { + ESP_LOGD(TAG, "Getting face count"); + this->send_command_(HlkFm22xCommand::GET_ALL_FACE_IDS); +} + +void HlkFm22xComponent::reset() { + ESP_LOGI(TAG, "Resetting module"); + this->active_command_ = HlkFm22xCommand::NONE; + this->wait_cycles_ = 0; + this->set_enrolling_(false); + this->send_command_(HlkFm22xCommand::RESET); +} + +void HlkFm22xComponent::send_command_(HlkFm22xCommand command, const uint8_t *data, size_t size) { + ESP_LOGV(TAG, "Send command: 0x%.2X", command); + if (this->active_command_ != HlkFm22xCommand::NONE) { + ESP_LOGW(TAG, "Command 0x%.2X already active", this->active_command_); + return; + } + this->wait_cycles_ = 0; + this->active_command_ = command; + while (this->available()) + this->read(); + this->write((uint8_t) (START_CODE >> 8)); + this->write((uint8_t) (START_CODE & 0xFF)); + this->write((uint8_t) command); + uint16_t data_size = size; + this->write((uint8_t) (data_size >> 8)); + this->write((uint8_t) (data_size & 0xFF)); + + uint8_t checksum = 0; + checksum ^= (uint8_t) command; + checksum ^= (data_size >> 8); + checksum ^= (data_size & 0xFF); + for (size_t i = 0; i < size; i++) { + this->write(data[i]); + checksum ^= data[i]; + } + + this->write(checksum); + this->active_command_ = command; + this->wait_cycles_ = 0; +} + +void HlkFm22xComponent::recv_command_() { + uint8_t byte, checksum = 0; + uint16_t length = 0; + + if (this->available() < 7) { + ++this->wait_cycles_; + return; + } + this->wait_cycles_ = 0; + + if ((this->read() != (uint8_t) (START_CODE >> 8)) || (this->read() != (uint8_t) (START_CODE & 0xFF))) { + ESP_LOGE(TAG, "Invalid start code"); + return; + } + + byte = this->read(); + checksum ^= byte; + HlkFm22xResponseType response_type = (HlkFm22xResponseType) byte; + + byte = this->read(); + checksum ^= byte; + length = byte << 8; + byte = this->read(); + checksum ^= byte; + length |= byte; + + std::vector data; + data.reserve(length); + for (uint16_t idx = 0; idx < length; ++idx) { + byte = this->read(); + checksum ^= byte; + data.push_back(byte); + } + + ESP_LOGV(TAG, "Recv type: 0x%.2X, data: %s", response_type, format_hex_pretty(data).c_str()); + + byte = this->read(); + if (byte != checksum) { + ESP_LOGE(TAG, "Invalid checksum for data. Calculated: 0x%.2X, Received: 0x%.2X", checksum, byte); + return; + } + switch (response_type) { + case HlkFm22xResponseType::NOTE: + this->handle_note_(data); + break; + case HlkFm22xResponseType::REPLY: + this->handle_reply_(data); + break; + default: + ESP_LOGW(TAG, "Unexpected response type: 0x%.2X", response_type); + break; + } +} + +void HlkFm22xComponent::handle_note_(const std::vector &data) { + switch (data[0]) { + case HlkFm22xNoteType::FACE_STATE: + if (data.size() < 17) { + ESP_LOGE(TAG, "Invalid face note data size: %u", data.size()); + break; + } + { + int16_t info[8]; + uint8_t offset = 1; + for (int16_t &i : info) { + i = ((int16_t) data[offset + 1] << 8) | data[offset]; + offset += 2; + } + ESP_LOGV(TAG, "Face state: status: %d, left: %d, top: %d, right: %d, bottom: %d, yaw: %d, pitch: %d, roll: %d", + info[0], info[1], info[2], info[3], info[4], info[5], info[6], info[7]); + this->face_info_callback_.call(info[0], info[1], info[2], info[3], info[4], info[5], info[6], info[7]); + } + break; + case HlkFm22xNoteType::READY: + ESP_LOGE(TAG, "Command 0x%.2X timed out", this->active_command_); + switch (this->active_command_) { + case HlkFm22xCommand::ENROLL: + this->set_enrolling_(false); + this->enrollment_failed_callback_.call(HlkFm22xResult::FAILED4_TIMEOUT); + break; + case HlkFm22xCommand::VERIFY: + this->face_scan_invalid_callback_.call(HlkFm22xResult::FAILED4_TIMEOUT); + break; + default: + break; + } + this->active_command_ = HlkFm22xCommand::NONE; + this->wait_cycles_ = 0; + break; + default: + ESP_LOGW(TAG, "Unhandled note: 0x%.2X", data[0]); + break; + } +} + +void HlkFm22xComponent::handle_reply_(const std::vector &data) { + auto expected = this->active_command_; + this->active_command_ = HlkFm22xCommand::NONE; + if (data[0] != (uint8_t) expected) { + ESP_LOGE(TAG, "Unexpected response command. Expected: 0x%.2X, Received: 0x%.2X", expected, data[0]); + return; + } + + if (data[1] != HlkFm22xResult::SUCCESS) { + ESP_LOGE(TAG, "Command <0x%.2X> failed. Error: 0x%.2X", data[0], data[1]); + switch (expected) { + case HlkFm22xCommand::ENROLL: + this->set_enrolling_(false); + this->enrollment_failed_callback_.call(data[1]); + break; + case HlkFm22xCommand::VERIFY: + if (data[1] == HlkFm22xResult::REJECTED) { + this->face_scan_unmatched_callback_.call(); + } else { + this->face_scan_invalid_callback_.call(data[1]); + } + break; + default: + break; + } + return; + } + switch (expected) { + case HlkFm22xCommand::VERIFY: { + int16_t face_id = ((int16_t) data[2] << 8) | data[3]; + std::string name(data.begin() + 4, data.begin() + 36); + ESP_LOGD(TAG, "Face verified. ID: %d, name: %s", face_id, name.c_str()); + if (this->last_face_id_sensor_ != nullptr) { + this->last_face_id_sensor_->publish_state(face_id); + } + if (this->last_face_name_text_sensor_ != nullptr) { + this->last_face_name_text_sensor_->publish_state(name); + } + this->face_scan_matched_callback_.call(face_id, name); + break; + } + case HlkFm22xCommand::ENROLL: { + int16_t face_id = ((int16_t) data[2] << 8) | data[3]; + HlkFm22xFaceDirection direction = (HlkFm22xFaceDirection) data[4]; + ESP_LOGI(TAG, "Face enrolled. ID: %d, Direction: 0x%.2X", face_id, direction); + this->enrollment_done_callback_.call(face_id, (uint8_t) direction); + this->set_enrolling_(false); + this->defer([this]() { this->get_face_count_(); }); + break; + } + case HlkFm22xCommand::GET_STATUS: + if (this->status_sensor_ != nullptr) { + this->status_sensor_->publish_state(data[2]); + } + this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_VERSION); }); + break; + case HlkFm22xCommand::GET_VERSION: + if (this->version_text_sensor_ != nullptr) { + std::string version(data.begin() + 2, data.end()); + this->version_text_sensor_->publish_state(version); + } + this->defer([this]() { this->get_face_count_(); }); + break; + case HlkFm22xCommand::GET_ALL_FACE_IDS: + if (this->face_count_sensor_ != nullptr) { + this->face_count_sensor_->publish_state(data[2]); + } + break; + case HlkFm22xCommand::DELETE_FACE: + ESP_LOGI(TAG, "Deleted face"); + break; + case HlkFm22xCommand::DELETE_ALL_FACES: + ESP_LOGI(TAG, "Deleted all faces"); + break; + case HlkFm22xCommand::RESET: + ESP_LOGI(TAG, "Module reset"); + this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_STATUS); }); + break; + default: + ESP_LOGW(TAG, "Unhandled command: 0x%.2X", this->active_command_); + break; + } +} + +void HlkFm22xComponent::set_enrolling_(bool enrolling) { + if (this->enrolling_binary_sensor_ != nullptr) { + this->enrolling_binary_sensor_->publish_state(enrolling); + } +} + +void HlkFm22xComponent::dump_config() { + ESP_LOGCONFIG(TAG, "HLK_FM22X:"); + LOG_UPDATE_INTERVAL(this); + if (this->version_text_sensor_) { + LOG_TEXT_SENSOR(" ", "Version", this->version_text_sensor_); + ESP_LOGCONFIG(TAG, " Current Value: %s", this->version_text_sensor_->get_state().c_str()); + } + if (this->enrolling_binary_sensor_) { + LOG_BINARY_SENSOR(" ", "Enrolling", this->enrolling_binary_sensor_); + ESP_LOGCONFIG(TAG, " Current Value: %s", this->enrolling_binary_sensor_->state ? "ON" : "OFF"); + } + if (this->face_count_sensor_) { + LOG_SENSOR(" ", "Face Count", this->face_count_sensor_); + ESP_LOGCONFIG(TAG, " Current Value: %u", (uint16_t) this->face_count_sensor_->get_state()); + } + if (this->status_sensor_) { + LOG_SENSOR(" ", "Status", this->status_sensor_); + ESP_LOGCONFIG(TAG, " Current Value: %u", (uint8_t) this->status_sensor_->get_state()); + } + if (this->last_face_id_sensor_) { + LOG_SENSOR(" ", "Last Face ID", this->last_face_id_sensor_); + ESP_LOGCONFIG(TAG, " Current Value: %u", (int16_t) this->last_face_id_sensor_->get_state()); + } + if (this->last_face_name_text_sensor_) { + LOG_TEXT_SENSOR(" ", "Last Face Name", this->last_face_name_text_sensor_); + ESP_LOGCONFIG(TAG, " Current Value: %s", this->last_face_name_text_sensor_->get_state().c_str()); + } +} + +} // namespace esphome::hlk_fm22x diff --git a/esphome/components/hlk_fm22x/hlk_fm22x.h b/esphome/components/hlk_fm22x/hlk_fm22x.h new file mode 100644 index 0000000000..5ecc715ea1 --- /dev/null +++ b/esphome/components/hlk_fm22x/hlk_fm22x.h @@ -0,0 +1,224 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/text_sensor/text_sensor.h" +#include "esphome/components/uart/uart.h" + +#include +#include + +namespace esphome::hlk_fm22x { + +static const uint16_t START_CODE = 0xEFAA; +enum HlkFm22xCommand { + NONE = 0x00, + RESET = 0x10, + GET_STATUS = 0x11, + VERIFY = 0x12, + ENROLL = 0x13, + DELETE_FACE = 0x20, + DELETE_ALL_FACES = 0x21, + GET_ALL_FACE_IDS = 0x24, + GET_VERSION = 0x30, + GET_SERIAL_NUMBER = 0x93, +}; + +enum HlkFm22xResponseType { + REPLY = 0x00, + NOTE = 0x01, + IMAGE = 0x02, +}; + +enum HlkFm22xNoteType { + READY = 0x00, + FACE_STATE = 0x01, +}; + +enum HlkFm22xResult { + SUCCESS = 0x00, + REJECTED = 0x01, + ABORTED = 0x02, + FAILED4_CAMERA = 0x04, + FAILED4_UNKNOWNREASON = 0x05, + FAILED4_INVALIDPARAM = 0x06, + FAILED4_NOMEMORY = 0x07, + FAILED4_UNKNOWNUSER = 0x08, + FAILED4_MAXUSER = 0x09, + FAILED4_FACEENROLLED = 0x0A, + FAILED4_LIVENESSCHECK = 0x0C, + FAILED4_TIMEOUT = 0x0D, + FAILED4_AUTHORIZATION = 0x0E, + FAILED4_READ_FILE = 0x13, + FAILED4_WRITE_FILE = 0x14, + FAILED4_NO_ENCRYPT = 0x15, + FAILED4_NO_RGBIMAGE = 0x17, + FAILED4_JPGPHOTO_LARGE = 0x18, + FAILED4_JPGPHOTO_SMALL = 0x19, +}; + +enum HlkFm22xFaceDirection { + FACE_DIRECTION_UNDEFINED = 0x00, + FACE_DIRECTION_MIDDLE = 0x01, + FACE_DIRECTION_RIGHT = 0x02, + FACE_DIRECTION_LEFT = 0x04, + FACE_DIRECTION_DOWN = 0x08, + FACE_DIRECTION_UP = 0x10, +}; + +class HlkFm22xComponent : public PollingComponent, public uart::UARTDevice { + public: + void setup() override; + void update() override; + void dump_config() override; + + void set_face_count_sensor(sensor::Sensor *face_count_sensor) { this->face_count_sensor_ = face_count_sensor; } + void set_status_sensor(sensor::Sensor *status_sensor) { this->status_sensor_ = status_sensor; } + void set_last_face_id_sensor(sensor::Sensor *last_face_id_sensor) { + this->last_face_id_sensor_ = last_face_id_sensor; + } + void set_last_face_name_text_sensor(text_sensor::TextSensor *last_face_name_text_sensor) { + this->last_face_name_text_sensor_ = last_face_name_text_sensor; + } + void set_enrolling_binary_sensor(binary_sensor::BinarySensor *enrolling_binary_sensor) { + this->enrolling_binary_sensor_ = enrolling_binary_sensor; + } + void set_version_text_sensor(text_sensor::TextSensor *version_text_sensor) { + this->version_text_sensor_ = version_text_sensor; + } + void add_on_face_scan_matched_callback(std::function callback) { + this->face_scan_matched_callback_.add(std::move(callback)); + } + void add_on_face_scan_unmatched_callback(std::function callback) { + this->face_scan_unmatched_callback_.add(std::move(callback)); + } + void add_on_face_scan_invalid_callback(std::function callback) { + this->face_scan_invalid_callback_.add(std::move(callback)); + } + void add_on_face_info_callback( + std::function callback) { + this->face_info_callback_.add(std::move(callback)); + } + void add_on_enrollment_done_callback(std::function callback) { + this->enrollment_done_callback_.add(std::move(callback)); + } + void add_on_enrollment_failed_callback(std::function callback) { + this->enrollment_failed_callback_.add(std::move(callback)); + } + + void enroll_face(const std::string &name, HlkFm22xFaceDirection direction); + void scan_face(); + void delete_face(int16_t face_id); + void delete_all_faces(); + void reset(); + + protected: + void get_face_count_(); + void send_command_(HlkFm22xCommand command, const uint8_t *data = nullptr, size_t size = 0); + void recv_command_(); + void handle_note_(const std::vector &data); + void handle_reply_(const std::vector &data); + void set_enrolling_(bool enrolling); + + HlkFm22xCommand active_command_ = HlkFm22xCommand::NONE; + uint16_t wait_cycles_ = 0; + sensor::Sensor *face_count_sensor_{nullptr}; + sensor::Sensor *status_sensor_{nullptr}; + sensor::Sensor *last_face_id_sensor_{nullptr}; + binary_sensor::BinarySensor *enrolling_binary_sensor_{nullptr}; + text_sensor::TextSensor *last_face_name_text_sensor_{nullptr}; + text_sensor::TextSensor *version_text_sensor_{nullptr}; + CallbackManager face_scan_invalid_callback_; + CallbackManager face_scan_matched_callback_; + CallbackManager face_scan_unmatched_callback_; + CallbackManager face_info_callback_; + CallbackManager enrollment_done_callback_; + CallbackManager enrollment_failed_callback_; +}; + +class FaceScanMatchedTrigger : public Trigger { + public: + explicit FaceScanMatchedTrigger(HlkFm22xComponent *parent) { + parent->add_on_face_scan_matched_callback( + [this](int16_t face_id, const std::string &name) { this->trigger(face_id, name); }); + } +}; + +class FaceScanUnmatchedTrigger : public Trigger<> { + public: + explicit FaceScanUnmatchedTrigger(HlkFm22xComponent *parent) { + parent->add_on_face_scan_unmatched_callback([this]() { this->trigger(); }); + } +}; + +class FaceScanInvalidTrigger : public Trigger { + public: + explicit FaceScanInvalidTrigger(HlkFm22xComponent *parent) { + parent->add_on_face_scan_invalid_callback([this](uint8_t error) { this->trigger(error); }); + } +}; + +class FaceInfoTrigger : public Trigger { + public: + explicit FaceInfoTrigger(HlkFm22xComponent *parent) { + parent->add_on_face_info_callback( + [this](int16_t status, int16_t left, int16_t top, int16_t right, int16_t bottom, int16_t yaw, int16_t pitch, + int16_t roll) { this->trigger(status, left, top, right, bottom, yaw, pitch, roll); }); + } +}; + +class EnrollmentDoneTrigger : public Trigger { + public: + explicit EnrollmentDoneTrigger(HlkFm22xComponent *parent) { + parent->add_on_enrollment_done_callback( + [this](int16_t face_id, uint8_t direction) { this->trigger(face_id, direction); }); + } +}; + +class EnrollmentFailedTrigger : public Trigger { + public: + explicit EnrollmentFailedTrigger(HlkFm22xComponent *parent) { + parent->add_on_enrollment_failed_callback([this](uint8_t error) { this->trigger(error); }); + } +}; + +template class EnrollmentAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(std::string, name) + TEMPLATABLE_VALUE(uint8_t, direction) + + void play(Ts... x) override { + auto name = this->name_.value(x...); + auto direction = (HlkFm22xFaceDirection) this->direction_.value(x...); + this->parent_->enroll_face(name, direction); + } +}; + +template class DeleteAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(int16_t, face_id) + + void play(Ts... x) override { + auto face_id = this->face_id_.value(x...); + this->parent_->delete_face(face_id); + } +}; + +template class DeleteAllAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->delete_all_faces(); } +}; + +template class ScanAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->scan_face(); } +}; + +template class ResetAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->reset(); } +}; + +} // namespace esphome::hlk_fm22x diff --git a/esphome/components/hlk_fm22x/sensor.py b/esphome/components/hlk_fm22x/sensor.py new file mode 100644 index 0000000000..e14b45599f --- /dev/null +++ b/esphome/components/hlk_fm22x/sensor.py @@ -0,0 +1,47 @@ +import esphome.codegen as cg +from esphome.components import sensor +import esphome.config_validation as cv +from esphome.const import CONF_STATUS, ENTITY_CATEGORY_DIAGNOSTIC, ICON_ACCOUNT + +from . import CONF_HLK_FM22X_ID, HlkFm22xComponent + +DEPENDENCIES = ["hlk_fm22x"] + +CONF_FACE_COUNT = "face_count" +CONF_LAST_FACE_ID = "last_face_id" +ICON_FACE = "mdi:face-recognition" + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_HLK_FM22X_ID): cv.use_id(HlkFm22xComponent), + cv.Optional(CONF_FACE_COUNT): sensor.sensor_schema( + icon=ICON_FACE, + accuracy_decimals=0, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_STATUS): sensor.sensor_schema( + accuracy_decimals=0, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_LAST_FACE_ID): sensor.sensor_schema( + icon=ICON_ACCOUNT, + accuracy_decimals=0, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } +) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_HLK_FM22X_ID]) + + for key in [ + CONF_FACE_COUNT, + CONF_STATUS, + CONF_LAST_FACE_ID, + ]: + if key not in config: + continue + conf = config[key] + sens = await sensor.new_sensor(conf) + cg.add(getattr(hub, f"set_{key}_sensor")(sens)) diff --git a/esphome/components/hlk_fm22x/text_sensor.py b/esphome/components/hlk_fm22x/text_sensor.py new file mode 100644 index 0000000000..06da61c8b3 --- /dev/null +++ b/esphome/components/hlk_fm22x/text_sensor.py @@ -0,0 +1,42 @@ +import esphome.codegen as cg +from esphome.components import text_sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_VERSION, + ENTITY_CATEGORY_DIAGNOSTIC, + ICON_ACCOUNT, + ICON_RESTART, +) + +from . import CONF_HLK_FM22X_ID, HlkFm22xComponent + +DEPENDENCIES = ["hlk_fm22x"] + +CONF_LAST_FACE_NAME = "last_face_name" + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_HLK_FM22X_ID): cv.use_id(HlkFm22xComponent), + cv.Optional(CONF_VERSION): text_sensor.text_sensor_schema( + icon=ICON_RESTART, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_LAST_FACE_NAME): text_sensor.text_sensor_schema( + icon=ICON_ACCOUNT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } +) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_HLK_FM22X_ID]) + for key in [ + CONF_VERSION, + CONF_LAST_FACE_NAME, + ]: + if key not in config: + continue + conf = config[key] + sens = await text_sensor.new_text_sensor(conf) + cg.add(getattr(hub, f"set_{key}_text_sensor")(sens)) diff --git a/esphome/components/hlw8012/hlw8012.cpp b/esphome/components/hlw8012/hlw8012.cpp index a28678e630..73696bd2a5 100644 --- a/esphome/components/hlw8012/hlw8012.cpp +++ b/esphome/components/hlw8012/hlw8012.cpp @@ -42,11 +42,11 @@ void HLW8012Component::dump_config() { " Current resistor: %.1f mΩ\n" " Voltage Divider: %.1f", this->change_mode_every_, this->current_resistor_ * 1000.0f, this->voltage_divider_); - LOG_UPDATE_INTERVAL(this) - LOG_SENSOR(" ", "Voltage", this->voltage_sensor_) - LOG_SENSOR(" ", "Current", this->current_sensor_) - LOG_SENSOR(" ", "Power", this->power_sensor_) - LOG_SENSOR(" ", "Energy", this->energy_sensor_) + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Voltage", this->voltage_sensor_); + LOG_SENSOR(" ", "Current", this->current_sensor_); + LOG_SENSOR(" ", "Power", this->power_sensor_); + LOG_SENSOR(" ", "Energy", this->energy_sensor_); } float HLW8012Component::get_setup_priority() const { return setup_priority::DATA; } void HLW8012Component::update() { diff --git a/esphome/components/hm3301/aqi_calculator.h b/esphome/components/hm3301/aqi_calculator.h index c1b47826a2..aa01060d2c 100644 --- a/esphome/components/hm3301/aqi_calculator.h +++ b/esphome/components/hm3301/aqi_calculator.h @@ -1,7 +1,7 @@ #pragma once - +#include #include "abstract_aqi_calculator.h" -// https://www.airnow.gov/sites/default/files/2020-05/aqi-technical-assistance-document-sept2018.pdf +// https://document.airnow.gov/technical-assistance-document-for-the-reporting-of-daily-air-quailty.pdf namespace esphome { namespace hm3301 { @@ -16,16 +16,15 @@ class AQICalculator : public AbstractAQICalculator { } protected: - static const int AMOUNT_OF_LEVELS = 7; + static const int AMOUNT_OF_LEVELS = 6; - int index_grid_[AMOUNT_OF_LEVELS][2] = {{0, 50}, {51, 100}, {101, 150}, {151, 200}, - {201, 300}, {301, 400}, {401, 500}}; + int index_grid_[AMOUNT_OF_LEVELS][2] = {{0, 50}, {51, 100}, {101, 150}, {151, 200}, {201, 300}, {301, 500}}; - int pm2_5_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 12}, {13, 35}, {36, 55}, {56, 150}, - {151, 250}, {251, 350}, {351, 500}}; + int pm2_5_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 9}, {10, 35}, {36, 55}, + {56, 125}, {126, 225}, {226, INT_MAX}}; - int pm10_0_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 54}, {55, 154}, {155, 254}, {255, 354}, - {355, 424}, {425, 504}, {505, 604}}; + int pm10_0_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 54}, {55, 154}, {155, 254}, + {255, 354}, {355, 424}, {425, INT_MAX}}; int calculate_index_(uint16_t value, int array[AMOUNT_OF_LEVELS][2]) { int grid_index = get_grid_index_(value, array); diff --git a/esphome/components/homeassistant/number/homeassistant_number.cpp b/esphome/components/homeassistant/number/homeassistant_number.cpp index 87bf6727f2..9963f3431d 100644 --- a/esphome/components/homeassistant/number/homeassistant_number.cpp +++ b/esphome/components/homeassistant/number/homeassistant_number.cpp @@ -87,20 +87,19 @@ void HomeassistantNumber::control(float value) { static constexpr auto ENTITY_ID_KEY = StringRef::from_lit("entity_id"); static constexpr auto VALUE_KEY = StringRef::from_lit("value"); - api::HomeassistantServiceResponse resp; + api::HomeassistantActionRequest resp; resp.set_service(SERVICE_NAME); - resp.data.emplace_back(); - auto &entity_id = resp.data.back(); + resp.data.init(2); + auto &entity_id = resp.data.emplace_back(); entity_id.set_key(ENTITY_ID_KEY); entity_id.value = this->entity_id_; - resp.data.emplace_back(); - auto &entity_value = resp.data.back(); + auto &entity_value = resp.data.emplace_back(); entity_value.set_key(VALUE_KEY); entity_value.value = to_string(value); - api::global_api_server->send_homeassistant_service_call(resp); + api::global_api_server->send_homeassistant_action(resp); } } // namespace homeassistant diff --git a/esphome/components/homeassistant/switch/homeassistant_switch.cpp b/esphome/components/homeassistant/switch/homeassistant_switch.cpp index b3300335b9..27d3705fc2 100644 --- a/esphome/components/homeassistant/switch/homeassistant_switch.cpp +++ b/esphome/components/homeassistant/switch/homeassistant_switch.cpp @@ -44,19 +44,19 @@ void HomeassistantSwitch::write_state(bool state) { static constexpr auto SERVICE_OFF = StringRef::from_lit("homeassistant.turn_off"); static constexpr auto ENTITY_ID_KEY = StringRef::from_lit("entity_id"); - api::HomeassistantServiceResponse resp; + api::HomeassistantActionRequest resp; if (state) { resp.set_service(SERVICE_ON); } else { resp.set_service(SERVICE_OFF); } - resp.data.emplace_back(); - auto &entity_id_kv = resp.data.back(); + resp.data.init(1); + auto &entity_id_kv = resp.data.emplace_back(); entity_id_kv.set_key(ENTITY_ID_KEY); entity_id_kv.value = this->entity_id_; - api::global_api_server->send_homeassistant_service_call(resp); + api::global_api_server->send_homeassistant_action(resp); } } // namespace homeassistant diff --git a/esphome/components/homeassistant/time/homeassistant_time.cpp b/esphome/components/homeassistant/time/homeassistant_time.cpp index 0a91a2f63d..e72c5a21f5 100644 --- a/esphome/components/homeassistant/time/homeassistant_time.cpp +++ b/esphome/components/homeassistant/time/homeassistant_time.cpp @@ -7,10 +7,8 @@ namespace homeassistant { static const char *const TAG = "homeassistant.time"; void HomeassistantTime::dump_config() { - ESP_LOGCONFIG(TAG, - "Home Assistant Time:\n" - " Timezone: '%s'", - this->timezone_.c_str()); + ESP_LOGCONFIG(TAG, "Home Assistant Time"); + RealTimeClock::dump_config(); } float HomeassistantTime::get_setup_priority() const { return setup_priority::DATA; } diff --git a/esphome/components/honeywellabp2_i2c/honeywellabp2.cpp b/esphome/components/honeywellabp2_i2c/honeywellabp2.cpp index 11f5dbc314..f173a1afbd 100644 --- a/esphome/components/honeywellabp2_i2c/honeywellabp2.cpp +++ b/esphome/components/honeywellabp2_i2c/honeywellabp2.cpp @@ -15,7 +15,7 @@ static const char *const TAG = "honeywellabp2"; void HONEYWELLABP2Sensor::read_sensor_data() { if (this->read(raw_data_, 7) != i2c::ERROR_OK) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); - this->status_set_warning("couldn't read sensor data"); + this->status_set_warning(LOG_STR("couldn't read sensor data")); return; } float press_counts = encode_uint24(raw_data_[1], raw_data_[2], raw_data_[3]); // calculate digital pressure counts @@ -31,7 +31,7 @@ void HONEYWELLABP2Sensor::read_sensor_data() { void HONEYWELLABP2Sensor::start_measurement() { if (this->write(i2c_cmd_, 3) != i2c::ERROR_OK) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); - this->status_set_warning("couldn't start measurement"); + this->status_set_warning(LOG_STR("couldn't start measurement")); return; } this->measurement_running_ = true; @@ -40,7 +40,7 @@ void HONEYWELLABP2Sensor::start_measurement() { bool HONEYWELLABP2Sensor::is_measurement_ready() { if (this->read(raw_data_, 1) != i2c::ERROR_OK) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); - this->status_set_warning("couldn't check measurement"); + this->status_set_warning(LOG_STR("couldn't check measurement")); return false; } if ((raw_data_[0] & (0x1 << STATUS_BIT_BUSY)) > 0) { @@ -53,7 +53,7 @@ bool HONEYWELLABP2Sensor::is_measurement_ready() { void HONEYWELLABP2Sensor::measurement_timeout() { ESP_LOGE(TAG, "Timeout!"); this->measurement_running_ = false; - this->status_set_warning("measurement timed out"); + this->status_set_warning(LOG_STR("measurement timed out")); } float HONEYWELLABP2Sensor::get_pressure() { return this->last_pressure_; } diff --git a/esphome/components/host/gpio.h b/esphome/components/host/gpio.h index a60d535912..ae677291b9 100644 --- a/esphome/components/host/gpio.h +++ b/esphome/components/host/gpio.h @@ -28,8 +28,8 @@ class HostGPIOPin : public InternalGPIOPin { void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; uint8_t pin_; - bool inverted_; - gpio::Flags flags_; + bool inverted_{}; + gpio::Flags flags_{}; }; } // namespace host diff --git a/esphome/components/host/gpio.py b/esphome/components/host/gpio.py index 0f22a790bd..fcfb0b6c54 100644 --- a/esphome/components/host/gpio.py +++ b/esphome/components/host/gpio.py @@ -57,6 +57,9 @@ async def host_pin_to_code(config): var = cg.new_Pvariable(config[CONF_ID]) num = config[CONF_NUMBER] cg.add(var.set_pin(num)) - cg.add(var.set_inverted(config[CONF_INVERTED])) + # Only set if true to avoid bloating setup() function + # (inverted bit in pin_flags_ bitfield is zero-initialized to false) + if config[CONF_INVERTED]: + cg.add(var.set_inverted(True)) cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) return var diff --git a/esphome/components/host/preferences.h b/esphome/components/host/preferences.h index 6707366517..6b2e7eb8f9 100644 --- a/esphome/components/host/preferences.h +++ b/esphome/components/host/preferences.h @@ -42,9 +42,10 @@ class HostPreferences : public ESPPreferences { if (len > 255) return false; this->setup_(); - if (this->data.count(key) == 0) + auto it = this->data.find(key); + if (it == this->data.end()) return false; - auto vec = this->data[key]; + const auto &vec = it->second; if (vec.size() != len) return false; memcpy(data, vec.data(), len); diff --git a/esphome/components/hte501/hte501.cpp b/esphome/components/hte501/hte501.cpp index 75770ceffe..b7d3be63fe 100644 --- a/esphome/components/hte501/hte501.cpp +++ b/esphome/components/hte501/hte501.cpp @@ -9,10 +9,9 @@ static const char *const TAG = "hte501"; void HTE501Component::setup() { uint8_t address[] = {0x70, 0x29}; - this->write(address, 2, false); uint8_t identification[9]; - this->read(identification, 9); - if (identification[8] != calc_crc8_(identification, 0, 7)) { + this->write_read(address, sizeof address, identification, sizeof identification); + if (identification[8] != crc8(identification, 8, 0xFF, 0x31, true)) { this->error_code_ = CRC_CHECK_FAILED; this->mark_failed(); return; @@ -42,11 +41,12 @@ void HTE501Component::dump_config() { float HTE501Component::get_setup_priority() const { return setup_priority::DATA; } void HTE501Component::update() { uint8_t address_1[] = {0x2C, 0x1B}; - this->write(address_1, 2, true); + this->write(address_1, 2); this->set_timeout(50, [this]() { uint8_t i2c_response[6]; this->read(i2c_response, 6); - if (i2c_response[2] != calc_crc8_(i2c_response, 0, 1) && i2c_response[5] != calc_crc8_(i2c_response, 3, 4)) { + if (i2c_response[2] != crc8(i2c_response, 2, 0xFF, 0x31, true) && + i2c_response[5] != crc8(i2c_response + 3, 2, 0xFF, 0x31, true)) { this->error_code_ = CRC_CHECK_FAILED; this->status_set_warning(); return; @@ -67,24 +67,5 @@ void HTE501Component::update() { this->status_clear_warning(); }); } - -unsigned char HTE501Component::calc_crc8_(const unsigned char buf[], unsigned char from, unsigned char to) { - unsigned char crc_val = 0xFF; - unsigned char i = 0; - unsigned char j = 0; - for (i = from; i <= to; i++) { - int cur_val = buf[i]; - for (j = 0; j < 8; j++) { - if (((crc_val ^ cur_val) & 0x80) != 0) // If MSBs are not equal - { - crc_val = ((crc_val << 1) ^ 0x31); - } else { - crc_val = (crc_val << 1); - } - cur_val = cur_val << 1; - } - } - return crc_val; -} } // namespace hte501 } // namespace esphome diff --git a/esphome/components/hte501/hte501.h b/esphome/components/hte501/hte501.h index 0d2c952e81..a7072d5bdb 100644 --- a/esphome/components/hte501/hte501.h +++ b/esphome/components/hte501/hte501.h @@ -1,8 +1,8 @@ #pragma once -#include "esphome/core/component.h" -#include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/core/component.h" namespace esphome { namespace hte501 { @@ -19,7 +19,6 @@ class HTE501Component : public PollingComponent, public i2c::I2CDevice { void update() override; protected: - unsigned char calc_crc8_(const unsigned char buf[], unsigned char from, unsigned char to); sensor::Sensor *temperature_sensor_; sensor::Sensor *humidity_sensor_; diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index 146458f53b..f4fa448c5b 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -5,12 +5,13 @@ from esphome.components.const import CONF_REQUEST_HEADERS from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( + CONF_CAPTURE_RESPONSE, CONF_ESP8266_DISABLE_SSL_SUPPORT, CONF_ID, CONF_METHOD, CONF_ON_ERROR, + CONF_ON_RESPONSE, CONF_TIMEOUT, - CONF_TRIGGER_ID, CONF_URL, CONF_WATCHDOG_TIMEOUT, PLATFORM_HOST, @@ -52,12 +53,10 @@ CONF_BUFFER_SIZE_TX = "buffer_size_tx" CONF_CA_CERTIFICATE_PATH = "ca_certificate_path" CONF_MAX_RESPONSE_BUFFER_SIZE = "max_response_buffer_size" -CONF_ON_RESPONSE = "on_response" CONF_HEADERS = "headers" CONF_COLLECT_HEADERS = "collect_headers" CONF_BODY = "body" CONF_JSON = "json" -CONF_CAPTURE_RESPONSE = "capture_response" def validate_url(value): @@ -194,7 +193,7 @@ async def to_code(config): cg.add_define("CPPHTTPLIB_OPENSSL_SUPPORT") elif path := config.get(CONF_CA_CERTIFICATE_PATH): cg.add_define("CPPHTTPLIB_OPENSSL_SUPPORT") - cg.add(var.set_ca_path(path)) + cg.add(var.set_ca_path(str(path))) cg.add_build_flag("-lssl") cg.add_build_flag("-lcrypto") @@ -216,16 +215,8 @@ HTTP_REQUEST_ACTION_SCHEMA = cv.Schema( f"{CONF_VERIFY_SSL} has moved to the base component configuration." ), cv.Optional(CONF_CAPTURE_RESPONSE, default=False): cv.boolean, - cv.Optional(CONF_ON_RESPONSE): automation.validate_automation( - {cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(HttpRequestResponseTrigger)} - ), - cv.Optional(CONF_ON_ERROR): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - automation.Trigger.template() - ) - } - ), + cv.Optional(CONF_ON_RESPONSE): automation.validate_automation(single=True), + cv.Optional(CONF_ON_ERROR): automation.validate_automation(single=True), cv.Optional(CONF_MAX_RESPONSE_BUFFER_SIZE, default="1kB"): cv.validate_bytes, } ) @@ -280,7 +271,12 @@ async def http_request_action_to_code(config, action_id, template_arg, args): template_ = await cg.templatable(config[CONF_URL], args, cg.std_string) cg.add(var.set_url(template_)) cg.add(var.set_method(config[CONF_METHOD])) - cg.add(var.set_capture_response(config[CONF_CAPTURE_RESPONSE])) + + capture_response = config[CONF_CAPTURE_RESPONSE] + if capture_response: + cg.add(var.set_capture_response(capture_response)) + cg.add_define("USE_HTTP_REQUEST_RESPONSE") + cg.add(var.set_max_response_buffer_size(config[CONF_MAX_RESPONSE_BUFFER_SIZE])) if CONF_BODY in config: @@ -303,21 +299,26 @@ async def http_request_action_to_code(config, action_id, template_arg, args): for value in config.get(CONF_COLLECT_HEADERS, []): cg.add(var.add_collect_header(value)) - for conf in config.get(CONF_ON_RESPONSE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) - cg.add(var.register_response_trigger(trigger)) - await automation.build_automation( - trigger, - [ - (cg.std_shared_ptr.template(HttpContainer), "response"), - (cg.std_string_ref, "body"), - ], - conf, - ) - for conf in config.get(CONF_ON_ERROR, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) - cg.add(var.register_error_trigger(trigger)) - await automation.build_automation(trigger, [], conf) + if response_conf := config.get(CONF_ON_RESPONSE): + if capture_response: + await automation.build_automation( + var.get_success_trigger_with_response(), + [ + (cg.std_shared_ptr.template(HttpContainer), "response"), + (cg.std_string_ref, "body"), + *args, + ], + response_conf, + ) + else: + await automation.build_automation( + var.get_success_trigger(), + [(cg.std_shared_ptr.template(HttpContainer), "response"), *args], + response_conf, + ) + + if error_conf := config.get(CONF_ON_ERROR): + await automation.build_automation(var.get_error_trigger(), args, error_conf) return var diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index 95515f731a..8a82a44d7d 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -113,8 +113,8 @@ class HttpContainer : public Parented { class HttpRequestResponseTrigger : public Trigger, std::string &> { public: - void process(std::shared_ptr container, std::string &response_body) { - this->trigger(std::move(container), response_body); + void process(const std::shared_ptr &container, std::string &response_body) { + this->trigger(container, response_body); } }; @@ -124,7 +124,7 @@ class HttpRequestComponent : public Component { float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } void set_useragent(const char *useragent) { this->useragent_ = useragent; } - void set_timeout(uint16_t timeout) { this->timeout_ = timeout; } + void set_timeout(uint32_t timeout) { this->timeout_ = timeout; } void set_watchdog_timeout(uint32_t watchdog_timeout) { this->watchdog_timeout_ = watchdog_timeout; } uint32_t get_watchdog_timeout() const { return this->watchdog_timeout_; } void set_follow_redirects(bool follow_redirects) { this->follow_redirects_ = follow_redirects; } @@ -167,13 +167,13 @@ class HttpRequestComponent : public Component { } protected: - virtual std::shared_ptr perform(std::string url, std::string method, std::string body, - std::list
request_headers, - std::set collect_headers) = 0; + virtual std::shared_ptr perform(const std::string &url, const std::string &method, + const std::string &body, const std::list
&request_headers, + const std::set &collect_headers) = 0; const char *useragent_{nullptr}; bool follow_redirects_{}; uint16_t redirect_limit_{}; - uint16_t timeout_{4500}; + uint32_t timeout_{4500}; uint32_t watchdog_timeout_{0}; }; @@ -183,7 +183,9 @@ template class HttpRequestSendAction : public Action { TEMPLATABLE_VALUE(std::string, url) TEMPLATABLE_VALUE(const char *, method) TEMPLATABLE_VALUE(std::string, body) +#ifdef USE_HTTP_REQUEST_RESPONSE TEMPLATABLE_VALUE(bool, capture_response) +#endif void add_request_header(const char *key, TemplatableValue value) { this->request_headers_.insert({key, value}); @@ -195,15 +197,20 @@ template class HttpRequestSendAction : public Action { void set_json(std::function json_func) { this->json_func_ = json_func; } - void register_response_trigger(HttpRequestResponseTrigger *trigger) { this->response_triggers_.push_back(trigger); } +#ifdef USE_HTTP_REQUEST_RESPONSE + Trigger, std::string &, Ts...> *get_success_trigger_with_response() const { + return this->success_trigger_with_response_; + } +#endif + Trigger, Ts...> *get_success_trigger() const { return this->success_trigger_; } - void register_error_trigger(Trigger<> *trigger) { this->error_triggers_.push_back(trigger); } + Trigger *get_error_trigger() const { return this->error_trigger_; } void set_max_response_buffer_size(size_t max_response_buffer_size) { this->max_response_buffer_size_ = max_response_buffer_size; } - void play(Ts... x) override { + void play(const Ts &...x) override { std::string body; if (this->body_.has_value()) { body = this->body_.value(x...); @@ -228,17 +235,20 @@ template class HttpRequestSendAction : public Action { auto container = this->parent_->start(this->url_.value(x...), this->method_.value(x...), body, request_headers, this->collect_headers_); + auto captured_args = std::make_tuple(x...); + if (container == nullptr) { - for (auto *trigger : this->error_triggers_) - trigger->trigger(); + std::apply([this](Ts... captured_args_inner) { this->error_trigger_->trigger(captured_args_inner...); }, + captured_args); return; } size_t content_length = container->content_length; size_t max_length = std::min(content_length, this->max_response_buffer_size_); - std::string response_body; +#ifdef USE_HTTP_REQUEST_RESPONSE if (this->capture_response_.value(x...)) { + std::string response_body; RAMAllocator allocator; uint8_t *buf = allocator.allocate(max_length); if (buf != nullptr) { @@ -253,19 +263,17 @@ template class HttpRequestSendAction : public Action { response_body.assign((char *) buf, read_index); allocator.deallocate(buf, max_length); } - } - - if (this->response_triggers_.size() == 1) { - // if there is only one trigger, no need to copy the response body - this->response_triggers_[0]->process(container, response_body); - } else { - for (auto *trigger : this->response_triggers_) { - // with multiple triggers, pass a copy of the response body to each - // one so that modifications made in one trigger are not visible to - // the others - auto response_body_copy = std::string(response_body); - trigger->process(container, response_body_copy); - } + std::apply( + [this, &container, &response_body](Ts... captured_args_inner) { + this->success_trigger_with_response_->trigger(container, response_body, captured_args_inner...); + }, + captured_args); + } else +#endif + { + std::apply([this, &container]( + Ts... captured_args_inner) { this->success_trigger_->trigger(container, captured_args_inner...); }, + captured_args); } container->end(); } @@ -283,8 +291,13 @@ template class HttpRequestSendAction : public Action { std::set collect_headers_{"content-type", "content-length"}; std::map> json_{}; std::function json_func_{nullptr}; - std::vector response_triggers_{}; - std::vector *> error_triggers_{}; +#ifdef USE_HTTP_REQUEST_RESPONSE + Trigger, std::string &, Ts...> *success_trigger_with_response_ = + new Trigger, std::string &, Ts...>(); +#endif + Trigger, Ts...> *success_trigger_ = + new Trigger, Ts...>(); + Trigger *error_trigger_ = new Trigger(); size_t max_response_buffer_size_{SIZE_MAX}; }; diff --git a/esphome/components/http_request/http_request_arduino.cpp b/esphome/components/http_request/http_request_arduino.cpp index c009b33c2d..c64a7be554 100644 --- a/esphome/components/http_request/http_request_arduino.cpp +++ b/esphome/components/http_request/http_request_arduino.cpp @@ -14,9 +14,10 @@ namespace http_request { static const char *const TAG = "http_request.arduino"; -std::shared_ptr HttpRequestArduino::perform(std::string url, std::string method, std::string body, - std::list
request_headers, - std::set collect_headers) { +std::shared_ptr HttpRequestArduino::perform(const std::string &url, const std::string &method, + const std::string &body, + const std::list
&request_headers, + const std::set &collect_headers) { if (!network::is_connected()) { this->status_momentary_error("failed", 1000); ESP_LOGW(TAG, "HTTP Request failed; Not connected to network"); diff --git a/esphome/components/http_request/http_request_arduino.h b/esphome/components/http_request/http_request_arduino.h index 44744f8c78..b736bb56d1 100644 --- a/esphome/components/http_request/http_request_arduino.h +++ b/esphome/components/http_request/http_request_arduino.h @@ -31,9 +31,9 @@ class HttpContainerArduino : public HttpContainer { class HttpRequestArduino : public HttpRequestComponent { protected: - std::shared_ptr perform(std::string url, std::string method, std::string body, - std::list
request_headers, - std::set collect_headers) override; + std::shared_ptr perform(const std::string &url, const std::string &method, const std::string &body, + const std::list
&request_headers, + const std::set &collect_headers) override; }; } // namespace http_request diff --git a/esphome/components/http_request/http_request_host.cpp b/esphome/components/http_request/http_request_host.cpp index 192032c1ac..402affc1d1 100644 --- a/esphome/components/http_request/http_request_host.cpp +++ b/esphome/components/http_request/http_request_host.cpp @@ -1,7 +1,10 @@ -#include "http_request_host.h" - #ifdef USE_HOST +#define USE_HTTP_REQUEST_HOST_H +#define CPPHTTPLIB_NO_EXCEPTIONS +#include "httplib.h" +#include "http_request_host.h" + #include #include "esphome/components/network/util.h" #include "esphome/components/watchdog/watchdog.h" @@ -14,9 +17,10 @@ namespace http_request { static const char *const TAG = "http_request.host"; -std::shared_ptr HttpRequestHost::perform(std::string url, std::string method, std::string body, - std::list
request_headers, - std::set response_headers) { +std::shared_ptr HttpRequestHost::perform(const std::string &url, const std::string &method, + const std::string &body, + const std::list
&request_headers, + const std::set &response_headers) { if (!network::is_connected()) { this->status_momentary_error("failed", 1000); ESP_LOGW(TAG, "HTTP Request failed; Not connected to network"); diff --git a/esphome/components/http_request/http_request_host.h b/esphome/components/http_request/http_request_host.h index 49fd3b43fe..886ba94938 100644 --- a/esphome/components/http_request/http_request_host.h +++ b/esphome/components/http_request/http_request_host.h @@ -1,11 +1,7 @@ #pragma once -#include "http_request.h" - #ifdef USE_HOST - -#define CPPHTTPLIB_NO_EXCEPTIONS -#include "httplib.h" +#include "http_request.h" namespace esphome { namespace http_request { @@ -22,9 +18,9 @@ class HttpContainerHost : public HttpContainer { class HttpRequestHost : public HttpRequestComponent { public: - std::shared_ptr perform(std::string url, std::string method, std::string body, - std::list
request_headers, - std::set response_headers) override; + std::shared_ptr perform(const std::string &url, const std::string &method, const std::string &body, + const std::list
&request_headers, + const std::set &response_headers) override; void set_ca_path(const char *ca_path) { this->ca_path_ = ca_path; } protected: diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp index 89a0891b03..34a3fb87eb 100644 --- a/esphome/components/http_request/http_request_idf.cpp +++ b/esphome/components/http_request/http_request_idf.cpp @@ -52,9 +52,10 @@ esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) { return ESP_OK; } -std::shared_ptr HttpRequestIDF::perform(std::string url, std::string method, std::string body, - std::list
request_headers, - std::set collect_headers) { +std::shared_ptr HttpRequestIDF::perform(const std::string &url, const std::string &method, + const std::string &body, + const std::list
&request_headers, + const std::set &collect_headers) { if (!network::is_connected()) { this->status_momentary_error("failed", 1000); ESP_LOGE(TAG, "HTTP Request failed; Not connected to network"); diff --git a/esphome/components/http_request/http_request_idf.h b/esphome/components/http_request/http_request_idf.h index 5c5b784853..e51b3aaebc 100644 --- a/esphome/components/http_request/http_request_idf.h +++ b/esphome/components/http_request/http_request_idf.h @@ -37,9 +37,9 @@ class HttpRequestIDF : public HttpRequestComponent { void set_buffer_size_tx(uint16_t buffer_size_tx) { this->buffer_size_tx_ = buffer_size_tx; } protected: - std::shared_ptr perform(std::string url, std::string method, std::string body, - std::list
request_headers, - std::set collect_headers) override; + std::shared_ptr perform(const std::string &url, const std::string &method, const std::string &body, + const std::list
&request_headers, + const std::set &collect_headers) override; // if zero ESP-IDF will use DEFAULT_HTTP_BUF_SIZE uint16_t buffer_size_rx_{}; uint16_t buffer_size_tx_{}; diff --git a/esphome/components/http_request/httplib.h b/esphome/components/http_request/httplib.h index a2f4436ec7..8b08699702 100644 --- a/esphome/components/http_request/httplib.h +++ b/esphome/components/http_request/httplib.h @@ -3,12 +3,10 @@ /** * NOTE: This is a copy of httplib.h from https://github.com/yhirose/cpp-httplib * - * It has been modified only to add ifdefs for USE_HOST. While it contains many functions unused in ESPHome, + * It has been modified to add ifdefs for USE_HOST. While it contains many functions unused in ESPHome, * it was considered preferable to use it with as few changes as possible, to facilitate future updates. */ -#include "esphome/core/defines.h" - // // httplib.h // @@ -17,6 +15,11 @@ // #ifdef USE_HOST +// Prevent this code being included in main.cpp +#ifdef USE_HTTP_REQUEST_HOST_H + +#include "esphome/core/defines.h" + #ifndef CPPHTTPLIB_HTTPLIB_H #define CPPHTTPLIB_HTTPLIB_H @@ -9687,5 +9690,6 @@ inline SSL_CTX *Client::ssl_context() const { #endif #endif // CPPHTTPLIB_HTTPLIB_H +#endif // USE_HTTP_REQUEST_HOST_H #endif diff --git a/esphome/components/http_request/ota/__init__.py b/esphome/components/http_request/ota/__init__.py index a3f6d5840c..d2c574d8c6 100644 --- a/esphome/components/http_request/ota/__init__.py +++ b/esphome/components/http_request/ota/__init__.py @@ -4,6 +4,7 @@ from esphome.components.ota import BASE_OTA_SCHEMA, OTAComponent, ota_to_code import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_PASSWORD, CONF_URL, CONF_USERNAME from esphome.core import coroutine_with_priority +from esphome.coroutine import CoroPriority from .. import CONF_HTTP_REQUEST_ID, HttpRequestComponent, http_request_ns @@ -40,7 +41,7 @@ CONFIG_SCHEMA = cv.All( ) -@coroutine_with_priority(52.0) +@coroutine_with_priority(CoroPriority.OTA_UPDATES) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await ota_to_code(var, config) diff --git a/esphome/components/http_request/ota/automation.h b/esphome/components/http_request/ota/automation.h index d4c21f1c72..6c50bb9b0d 100644 --- a/esphome/components/http_request/ota/automation.h +++ b/esphome/components/http_request/ota/automation.h @@ -15,7 +15,7 @@ template class OtaHttpRequestComponentFlashAction : public Actio TEMPLATABLE_VALUE(std::string, url) TEMPLATABLE_VALUE(std::string, username) - void play(Ts... x) override { + void play(const Ts &...x) override { if (this->md5_url_.has_value()) { this->parent_->set_md5_url(this->md5_url_.value(x...)); } diff --git a/esphome/components/http_request/update/http_request_update.cpp b/esphome/components/http_request/update/http_request_update.cpp index 06aa6da6a4..c91b0eba73 100644 --- a/esphome/components/http_request/update/http_request_update.cpp +++ b/esphome/components/http_request/update/http_request_update.cpp @@ -29,7 +29,7 @@ void HttpRequestUpdate::setup() { this->publish_state(); } else if (state == ota::OTAState::OTA_ABORT || state == ota::OTAState::OTA_ERROR) { this->state_ = update::UPDATE_STATE_AVAILABLE; - this->status_set_error("Failed to install firmware"); + this->status_set_error(LOG_STR("Failed to install firmware")); this->publish_state(); } }); @@ -49,18 +49,19 @@ void HttpRequestUpdate::update_task(void *params) { auto container = this_update->request_parent_->get(this_update->source_url_); if (container == nullptr || container->status_code != HTTP_STATUS_OK) { - std::string msg = str_sprintf("Failed to fetch manifest from %s", this_update->source_url_.c_str()); + ESP_LOGE(TAG, "Failed to fetch manifest from %s", this_update->source_url_.c_str()); // Defer to main loop to avoid race condition on component_state_ read-modify-write - this_update->defer([this_update, msg]() { this_update->status_set_error(msg.c_str()); }); + this_update->defer([this_update]() { this_update->status_set_error(LOG_STR("Failed to fetch manifest")); }); UPDATE_RETURN; } RAMAllocator allocator; uint8_t *data = allocator.allocate(container->content_length); if (data == nullptr) { - std::string msg = str_sprintf("Failed to allocate %zu bytes for manifest", container->content_length); + ESP_LOGE(TAG, "Failed to allocate %zu bytes for manifest", container->content_length); // Defer to main loop to avoid race condition on component_state_ read-modify-write - this_update->defer([this_update, msg]() { this_update->status_set_error(msg.c_str()); }); + this_update->defer( + [this_update]() { this_update->status_set_error(LOG_STR("Failed to allocate memory for manifest")); }); container->end(); UPDATE_RETURN; } @@ -121,9 +122,9 @@ void HttpRequestUpdate::update_task(void *params) { } if (!valid) { - std::string msg = str_sprintf("Failed to parse JSON from %s", this_update->source_url_.c_str()); + ESP_LOGE(TAG, "Failed to parse JSON from %s", this_update->source_url_.c_str()); // Defer to main loop to avoid race condition on component_state_ read-modify-write - this_update->defer([this_update, msg]() { this_update->status_set_error(msg.c_str()); }); + this_update->defer([this_update]() { this_update->status_set_error(LOG_STR("Failed to parse manifest JSON")); }); UPDATE_RETURN; } diff --git a/esphome/components/htu21d/htu21d.cpp b/esphome/components/htu21d/htu21d.cpp index f2e7ae93cb..c5d91d3dd0 100644 --- a/esphome/components/htu21d/htu21d.cpp +++ b/esphome/components/htu21d/htu21d.cpp @@ -57,7 +57,6 @@ void HTU21DComponent::update() { if (this->temperature_ != nullptr) this->temperature_->publish_state(temperature); - this->status_clear_warning(); if (this->write(&HTU21D_REGISTER_HUMIDITY, 1) != i2c::ERROR_OK) { this->status_set_warning(); @@ -79,10 +78,11 @@ void HTU21DComponent::update() { if (this->humidity_ != nullptr) this->humidity_->publish_state(humidity); - int8_t heater_level; + this->status_clear_warning(); // HTU21D does have a heater module but does not have heater level // Setting heater level to 1 in case the heater is ON + uint8_t heater_level = 0; if (this->sensor_model_ == HTU21D_SENSOR_MODEL_HTU21D) { if (this->is_heater_enabled()) { heater_level = 1; @@ -97,34 +97,30 @@ void HTU21DComponent::update() { if (this->heater_ != nullptr) this->heater_->publish_state(heater_level); - this->status_clear_warning(); }); }); } bool HTU21DComponent::is_heater_enabled() { uint8_t raw_heater; - if (this->read_register(HTU21D_REGISTER_STATUS, reinterpret_cast(&raw_heater), 2) != i2c::ERROR_OK) { + if (this->read_register(HTU21D_REGISTER_STATUS, &raw_heater, 1) != i2c::ERROR_OK) { this->status_set_warning(); return false; } - raw_heater = i2c::i2ctohs(raw_heater); - return (bool) (((raw_heater) >> (HTU21D_REG_HTRE_BIT)) & 0x01); + return (bool) ((raw_heater >> HTU21D_REG_HTRE_BIT) & 0x01); } void HTU21DComponent::set_heater(bool status) { uint8_t raw_heater; - if (this->read_register(HTU21D_REGISTER_STATUS, reinterpret_cast(&raw_heater), 2) != i2c::ERROR_OK) { + if (this->read_register(HTU21D_REGISTER_STATUS, &raw_heater, 1) != i2c::ERROR_OK) { this->status_set_warning(); return; } - raw_heater = i2c::i2ctohs(raw_heater); if (status) { - raw_heater |= (1 << (HTU21D_REG_HTRE_BIT)); + raw_heater |= (1 << HTU21D_REG_HTRE_BIT); } else { - raw_heater &= ~(1 << (HTU21D_REG_HTRE_BIT)); + raw_heater &= ~(1 << HTU21D_REG_HTRE_BIT); } - if (this->write_register(HTU21D_WRITERHT_REG_CMD, &raw_heater, 1) != i2c::ERROR_OK) { this->status_set_warning(); return; @@ -138,14 +134,13 @@ void HTU21DComponent::set_heater_level(uint8_t level) { } } -int8_t HTU21DComponent::get_heater_level() { - int8_t raw_heater; - if (this->read_register(HTU21D_READHEATER_REG_CMD, reinterpret_cast(&raw_heater), 2) != i2c::ERROR_OK) { +uint8_t HTU21DComponent::get_heater_level() { + uint8_t raw_heater; + if (this->read_register(HTU21D_READHEATER_REG_CMD, &raw_heater, 1) != i2c::ERROR_OK) { this->status_set_warning(); return 0; } - raw_heater = i2c::i2ctohs(raw_heater); - return raw_heater; + return raw_heater & 0xF; } float HTU21DComponent::get_setup_priority() const { return setup_priority::DATA; } diff --git a/esphome/components/htu21d/htu21d.h b/esphome/components/htu21d/htu21d.h index 8533875d43..277c6ca3e5 100644 --- a/esphome/components/htu21d/htu21d.h +++ b/esphome/components/htu21d/htu21d.h @@ -26,7 +26,7 @@ class HTU21DComponent : public PollingComponent, public i2c::I2CDevice { bool is_heater_enabled(); void set_heater(bool status); void set_heater_level(uint8_t level); - int8_t get_heater_level(); + uint8_t get_heater_level(); float get_setup_priority() const override; @@ -41,7 +41,7 @@ template class SetHeaterLevelAction : public Action, publ public: TEMPLATABLE_VALUE(uint8_t, level) - void play(Ts... x) override { + void play(const Ts &...x) override { auto level = this->level_.value(x...); this->parent_->set_heater_level(level); @@ -52,7 +52,7 @@ template class SetHeaterAction : public Action, public Pa public: TEMPLATABLE_VALUE(bool, status) - void play(Ts... x) override { + void play(const Ts &...x) override { auto status = this->status_.value(x...); this->parent_->set_heater(status); diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index 4172b23845..738568cd3c 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -2,26 +2,32 @@ import logging from esphome import pins import esphome.codegen as cg -from esphome.components import esp32 +from esphome.components.zephyr import ( + zephyr_add_overlay, + zephyr_add_prj_conf, + zephyr_data, +) +from esphome.components.zephyr.const import KEY_BOARD from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( CONF_ADDRESS, CONF_FREQUENCY, + CONF_I2C, CONF_I2C_ID, CONF_ID, CONF_SCAN, CONF_SCL, CONF_SDA, CONF_TIMEOUT, - KEY_CORE, - KEY_FRAMEWORK_VERSION, PLATFORM_ESP32, PLATFORM_ESP8266, + PLATFORM_NRF52, PLATFORM_RP2040, PlatformFramework, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority +from esphome.cpp_generator import MockObj import esphome.final_validate as fv LOGGER = logging.getLogger(__name__) @@ -31,6 +37,7 @@ I2CBus = i2c_ns.class_("I2CBus") InternalI2CBus = i2c_ns.class_("InternalI2CBus", I2CBus) ArduinoI2CBus = i2c_ns.class_("ArduinoI2CBus", InternalI2CBus, cg.Component) IDFI2CBus = i2c_ns.class_("IDFI2CBus", InternalI2CBus, cg.Component) +ZephyrI2CBus = i2c_ns.class_("ZephyrI2CBus", I2CBus, cg.Component) I2CDevice = i2c_ns.class_("I2CDevice") @@ -40,36 +47,20 @@ MULTI_CONF = True def _bus_declare_type(value): + if CORE.is_esp32: + return cv.declare_id(IDFI2CBus)(value) if CORE.using_arduino: return cv.declare_id(ArduinoI2CBus)(value) - if CORE.using_esp_idf: - return cv.declare_id(IDFI2CBus)(value) + if CORE.using_zephyr: + return cv.declare_id(ZephyrI2CBus)(value) raise NotImplementedError def validate_config(config): - if ( - config[CONF_SCAN] - and CORE.is_esp32 - and CORE.using_esp_idf - and esp32.get_esp32_variant() - in [ - esp32.const.VARIANT_ESP32C5, - esp32.const.VARIANT_ESP32C6, - esp32.const.VARIANT_ESP32P4, - ] - ): - version: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] - if version.major == 5 and ( - (version.minor == 3 and version.patch <= 3) - or (version.minor == 4 and version.patch <= 1) - ): - LOGGER.warning( - "There is a bug in esp-idf version %s that breaks I2C scan, I2C scan " - "has been disabled, see https://github.com/esphome/issues/issues/7128", - str(version), - ) - config[CONF_SCAN] = False + if CORE.is_esp32: + return cv.require_framework_version( + esp_idf=cv.Version(5, 4, 2), esp32_arduino=cv.Version(3, 2, 1) + )(config) return config @@ -78,30 +69,77 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): _bus_declare_type, cv.Optional(CONF_SDA, default="SDA"): pins.internal_gpio_pin_number, - cv.SplitDefault(CONF_SDA_PULLUP_ENABLED, esp32_idf=True): cv.All( - cv.only_with_esp_idf, cv.boolean + cv.SplitDefault(CONF_SDA_PULLUP_ENABLED, esp32=True): cv.All( + cv.only_on_esp32, cv.boolean ), cv.Optional(CONF_SCL, default="SCL"): pins.internal_gpio_pin_number, - cv.SplitDefault(CONF_SCL_PULLUP_ENABLED, esp32_idf=True): cv.All( - cv.only_with_esp_idf, cv.boolean + cv.SplitDefault(CONF_SCL_PULLUP_ENABLED, esp32=True): cv.All( + cv.only_on_esp32, cv.boolean ), - cv.Optional(CONF_FREQUENCY, default="50kHz"): cv.All( - cv.frequency, cv.Range(min=0, min_included=False) + cv.SplitDefault( + CONF_FREQUENCY, + esp32="50kHz", + esp8266="50kHz", + rp2040="50kHz", + nrf52="100kHz", + ): cv.All( + cv.frequency, + cv.Range(min=0, min_included=False), + ), + cv.Optional(CONF_TIMEOUT): cv.All( + cv.only_with_framework(["arduino", "esp-idf"]), + cv.positive_time_period, ), - cv.Optional(CONF_TIMEOUT): cv.positive_time_period, cv.Optional(CONF_SCAN, default=True): cv.boolean, } ).extend(cv.COMPONENT_SCHEMA), - cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040]), + cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040, PLATFORM_NRF52]), validate_config, ) -@coroutine_with_priority(1.0) +def _final_validate(config): + full_config = fv.full_config.get()[CONF_I2C] + if CORE.using_zephyr and len(full_config) > 1: + raise cv.Invalid("Second i2c is not implemented on Zephyr yet") + + +FINAL_VALIDATE_SCHEMA = _final_validate + + +@coroutine_with_priority(CoroPriority.BUS) async def to_code(config): cg.add_global(i2c_ns.using) cg.add_define("USE_I2C") - var = cg.new_Pvariable(config[CONF_ID]) + if CORE.using_zephyr: + zephyr_add_prj_conf("I2C", True) + i2c = "i2c0" + if zephyr_data()[KEY_BOARD] in ["xiao_ble"]: + i2c = "i2c1" + zephyr_add_overlay( + f""" + &pinctrl {{ + {i2c}_default: {i2c}_default {{ + group1 {{ + psels = , + ; + }}; + }}; + {i2c}_sleep: {i2c}_sleep {{ + group1 {{ + psels = , + ; + low-power-enable; + }}; + }}; + }}; + """ + ) + var = cg.new_Pvariable( + config[CONF_ID], MockObj(f"DEVICE_DT_GET(DT_NODELABEL({i2c}))") + ) + else: + var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) cg.add(var.set_sda_pin(config[CONF_SDA])) @@ -115,7 +153,7 @@ async def to_code(config): cg.add(var.set_scan(config[CONF_SCAN])) if CONF_TIMEOUT in config: cg.add(var.set_timeout(int(config[CONF_TIMEOUT].total_microseconds))) - if CORE.using_arduino: + if CORE.using_arduino and not CORE.is_esp32: cg.add_library("Wire", None) @@ -212,13 +250,16 @@ def final_validate_device_schema( FILTER_SOURCE_FILES = filter_source_files_from_platform( { "i2c_bus_arduino.cpp": { - PlatformFramework.ESP32_ARDUINO, PlatformFramework.ESP8266_ARDUINO, PlatformFramework.RP2040_ARDUINO, PlatformFramework.BK72XX_ARDUINO, PlatformFramework.RTL87XX_ARDUINO, PlatformFramework.LN882X_ARDUINO, }, - "i2c_bus_esp_idf.cpp": {PlatformFramework.ESP32_IDF}, + "i2c_bus_esp_idf.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + }, + "i2c_bus_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR}, } ) diff --git a/esphome/components/i2c/i2c.cpp b/esphome/components/i2c/i2c.cpp index 2b2190d28b..f8c7a1b40b 100644 --- a/esphome/components/i2c/i2c.cpp +++ b/esphome/components/i2c/i2c.cpp @@ -1,4 +1,7 @@ #include "i2c.h" + +#include "esphome/core/defines.h" +#include "esphome/core/hal.h" #include "esphome/core/log.h" #include @@ -7,38 +10,54 @@ namespace i2c { static const char *const TAG = "i2c"; -ErrorCode I2CDevice::read_register(uint8_t a_register, uint8_t *data, size_t len, bool stop) { - ErrorCode err = this->write(&a_register, 1, stop); - if (err != ERROR_OK) - return err; - return bus_->read(address_, data, len); +void I2CBus::i2c_scan_() { + // suppress logs from the IDF I2C library during the scan +#if defined(USE_ESP32) && defined(USE_LOGGER) + auto previous = esp_log_level_get("*"); + esp_log_level_set("*", ESP_LOG_NONE); +#endif + + for (uint8_t address = 8; address != 120; address++) { + auto err = write_readv(address, nullptr, 0, nullptr, 0); + if (err == ERROR_OK) { + scan_results_.emplace_back(address, true); + } else if (err == ERROR_UNKNOWN) { + scan_results_.emplace_back(address, false); + } + // it takes 16sec to scan on nrf52. It prevents board reset. + arch_feed_wdt(); + } +#if defined(USE_ESP32) && defined(USE_LOGGER) + esp_log_level_set("*", previous); +#endif } -ErrorCode I2CDevice::read_register16(uint16_t a_register, uint8_t *data, size_t len, bool stop) { +ErrorCode I2CDevice::read_register(uint8_t a_register, uint8_t *data, size_t len) { + return bus_->write_readv(this->address_, &a_register, 1, data, len); +} + +ErrorCode I2CDevice::read_register16(uint16_t a_register, uint8_t *data, size_t len) { a_register = convert_big_endian(a_register); - ErrorCode const err = this->write(reinterpret_cast(&a_register), 2, stop); - if (err != ERROR_OK) - return err; - return bus_->read(address_, data, len); + return bus_->write_readv(this->address_, reinterpret_cast(&a_register), 2, data, len); } -ErrorCode I2CDevice::write_register(uint8_t a_register, const uint8_t *data, size_t len, bool stop) { - WriteBuffer buffers[2]; - buffers[0].data = &a_register; - buffers[0].len = 1; - buffers[1].data = data; - buffers[1].len = len; - return bus_->writev(address_, buffers, 2, stop); +ErrorCode I2CDevice::write_register(uint8_t a_register, const uint8_t *data, size_t len) const { + SmallBufferWithHeapFallback<17> buffer_alloc; // Most I2C writes are <= 16 bytes + uint8_t *buffer = buffer_alloc.get(len + 1); + + buffer[0] = a_register; + std::copy(data, data + len, buffer + 1); + return this->bus_->write_readv(this->address_, buffer, len + 1, nullptr, 0); } -ErrorCode I2CDevice::write_register16(uint16_t a_register, const uint8_t *data, size_t len, bool stop) { - a_register = convert_big_endian(a_register); - WriteBuffer buffers[2]; - buffers[0].data = reinterpret_cast(&a_register); - buffers[0].len = 2; - buffers[1].data = data; - buffers[1].len = len; - return bus_->writev(address_, buffers, 2, stop); +ErrorCode I2CDevice::write_register16(uint16_t a_register, const uint8_t *data, size_t len) const { + SmallBufferWithHeapFallback<18> buffer_alloc; // Most I2C writes are <= 16 bytes + 2 for register + uint8_t *buffer = buffer_alloc.get(len + 2); + + buffer[0] = a_register >> 8; + buffer[1] = a_register; + std::copy(data, data + len, buffer + 2); + return this->bus_->write_readv(this->address_, buffer, len + 2, nullptr, 0); } bool I2CDevice::read_bytes_16(uint8_t a_register, uint16_t *data, uint8_t len) { @@ -49,7 +68,7 @@ bool I2CDevice::read_bytes_16(uint8_t a_register, uint16_t *data, uint8_t len) { return true; } -bool I2CDevice::write_bytes_16(uint8_t a_register, const uint16_t *data, uint8_t len) { +bool I2CDevice::write_bytes_16(uint8_t a_register, const uint16_t *data, uint8_t len) const { // we have to copy in order to be able to change byte order std::unique_ptr temp{new uint16_t[len]}; for (size_t i = 0; i < len; i++) diff --git a/esphome/components/i2c/i2c.h b/esphome/components/i2c/i2c.h index 15f786245b..48a6e751cf 100644 --- a/esphome/components/i2c/i2c.h +++ b/esphome/components/i2c/i2c.h @@ -1,10 +1,10 @@ #pragma once -#include "i2c_bus.h" -#include "esphome/core/helpers.h" -#include "esphome/core/optional.h" #include #include +#include "esphome/core/helpers.h" +#include "esphome/core/optional.h" +#include "i2c_bus.h" namespace esphome { namespace i2c { @@ -161,51 +161,53 @@ class I2CDevice { /// @param data pointer to an array to store the bytes /// @param len length of the buffer = number of bytes to read /// @return an i2c::ErrorCode - ErrorCode read(uint8_t *data, size_t len) { return bus_->read(address_, data, len); } + ErrorCode read(uint8_t *data, size_t len) const { return bus_->write_readv(this->address_, nullptr, 0, data, len); } /// @brief reads an array of bytes from a specific register in the I²C device /// @param a_register an 8 bits internal address of the I²C register to read from /// @param data pointer to an array to store the bytes /// @param len length of the buffer = number of bytes to read - /// @param stop (true/false): True will send a stop message, releasing the bus after - /// transmission. False will send a restart, keeping the connection active. /// @return an i2c::ErrorCode - ErrorCode read_register(uint8_t a_register, uint8_t *data, size_t len, bool stop = true); + ErrorCode read_register(uint8_t a_register, uint8_t *data, size_t len); /// @brief reads an array of bytes from a specific register in the I²C device /// @param a_register the 16 bits internal address of the I²C register to read from /// @param data pointer to an array of bytes to store the information /// @param len length of the buffer = number of bytes to read - /// @param stop (true/false): True will send a stop message, releasing the bus after - /// transmission. False will send a restart, keeping the connection active. /// @return an i2c::ErrorCode - ErrorCode read_register16(uint16_t a_register, uint8_t *data, size_t len, bool stop = true); + ErrorCode read_register16(uint16_t a_register, uint8_t *data, size_t len); /// @brief writes an array of bytes to a device using an I2CBus /// @param data pointer to an array that contains the bytes to send /// @param len length of the buffer = number of bytes to write - /// @param stop (true/false): True will send a stop message, releasing the bus after - /// transmission. False will send a restart, keeping the connection active. /// @return an i2c::ErrorCode - ErrorCode write(const uint8_t *data, size_t len, bool stop = true) { return bus_->write(address_, data, len, stop); } + ErrorCode write(const uint8_t *data, size_t len) const { + return bus_->write_readv(this->address_, data, len, nullptr, 0); + } + + /// @brief writes an array of bytes to a device, then reads an array, as a single transaction + /// @param write_data pointer to an array that contains the bytes to send + /// @param write_len length of the buffer = number of bytes to write + /// @param read_data pointer to an array to store the bytes read + /// @param read_len length of the buffer = number of bytes to read + /// @return an i2c::ErrorCode + ErrorCode write_read(const uint8_t *write_data, size_t write_len, uint8_t *read_data, size_t read_len) const { + return bus_->write_readv(this->address_, write_data, write_len, read_data, read_len); + } /// @brief writes an array of bytes to a specific register in the I²C device /// @param a_register the internal address of the register to read from /// @param data pointer to an array to store the bytes /// @param len length of the buffer = number of bytes to read - /// @param stop (true/false): True will send a stop message, releasing the bus after - /// transmission. False will send a restart, keeping the connection active. /// @return an i2c::ErrorCode - ErrorCode write_register(uint8_t a_register, const uint8_t *data, size_t len, bool stop = true); + ErrorCode write_register(uint8_t a_register, const uint8_t *data, size_t len) const; /// @brief write an array of bytes to a specific register in the I²C device /// @param a_register the 16 bits internal address of the register to read from /// @param data pointer to an array to store the bytes /// @param len length of the buffer = number of bytes to read - /// @param stop (true/false): True will send a stop message, releasing the bus after - /// transmission. False will send a restart, keeping the connection active. /// @return an i2c::ErrorCode - ErrorCode write_register16(uint16_t a_register, const uint8_t *data, size_t len, bool stop = true); + ErrorCode write_register16(uint16_t a_register, const uint8_t *data, size_t len) const; /// /// Compat APIs @@ -217,7 +219,7 @@ class I2CDevice { return read_register(a_register, data, len) == ERROR_OK; } - bool read_bytes_raw(uint8_t *data, uint8_t len) { return read(data, len) == ERROR_OK; } + bool read_bytes_raw(uint8_t *data, uint8_t len) const { return read(data, len) == ERROR_OK; } template optional> read_bytes(uint8_t a_register) { std::array res; @@ -236,9 +238,7 @@ class I2CDevice { bool read_bytes_16(uint8_t a_register, uint16_t *data, uint8_t len); - bool read_byte(uint8_t a_register, uint8_t *data, bool stop = true) { - return read_register(a_register, data, 1, stop) == ERROR_OK; - } + bool read_byte(uint8_t a_register, uint8_t *data) { return read_register(a_register, data, 1) == ERROR_OK; } optional read_byte(uint8_t a_register) { uint8_t data; @@ -249,11 +249,11 @@ class I2CDevice { bool read_byte_16(uint8_t a_register, uint16_t *data) { return read_bytes_16(a_register, data, 1); } - bool write_bytes(uint8_t a_register, const uint8_t *data, uint8_t len, bool stop = true) { - return write_register(a_register, data, len, stop) == ERROR_OK; + bool write_bytes(uint8_t a_register, const uint8_t *data, uint8_t len) const { + return write_register(a_register, data, len) == ERROR_OK; } - bool write_bytes(uint8_t a_register, const std::vector &data) { + bool write_bytes(uint8_t a_register, const std::vector &data) const { return write_bytes(a_register, data.data(), data.size()); } @@ -261,13 +261,42 @@ class I2CDevice { return write_bytes(a_register, data.data(), data.size()); } - bool write_bytes_16(uint8_t a_register, const uint16_t *data, uint8_t len); + bool write_bytes_16(uint8_t a_register, const uint16_t *data, uint8_t len) const; - bool write_byte(uint8_t a_register, uint8_t data, bool stop = true) { - return write_bytes(a_register, &data, 1, stop); + bool write_byte(uint8_t a_register, uint8_t data) const { return write_bytes(a_register, &data, 1); } + + bool write_byte_16(uint8_t a_register, uint16_t data) const { return write_bytes_16(a_register, &data, 1); } + + // Deprecated functions + + ESPDEPRECATED("The stop argument is no longer used. This will be removed from ESPHome 2026.3.0", "2025.9.0") + ErrorCode read_register(uint8_t a_register, uint8_t *data, size_t len, bool stop) { + return this->read_register(a_register, data, len); } - bool write_byte_16(uint8_t a_register, uint16_t data) { return write_bytes_16(a_register, &data, 1); } + ESPDEPRECATED("The stop argument is no longer used. This will be removed from ESPHome 2026.3.0", "2025.9.0") + ErrorCode read_register16(uint16_t a_register, uint8_t *data, size_t len, bool stop) { + return this->read_register16(a_register, data, len); + } + + ESPDEPRECATED("The stop argument is no longer used; use write_read() for consecutive write and read. This will be " + "removed from ESPHome 2026.3.0", + "2025.9.0") + ErrorCode write(const uint8_t *data, size_t len, bool stop) const { return this->write(data, len); } + + ESPDEPRECATED("The stop argument is no longer used; use write_read() for consecutive write and read. This will be " + "removed from ESPHome 2026.3.0", + "2025.9.0") + ErrorCode write_register(uint8_t a_register, const uint8_t *data, size_t len, bool stop) const { + return this->write_register(a_register, data, len); + } + + ESPDEPRECATED("The stop argument is no longer used; use write_read() for consecutive write and read. This will be " + "removed from ESPHome 2026.3.0", + "2025.9.0") + ErrorCode write_register16(uint16_t a_register, const uint8_t *data, size_t len, bool stop) const { + return this->write_register16(a_register, data, len); + } protected: uint8_t address_{0x00}; ///< store the address of the device on the bus diff --git a/esphome/components/i2c/i2c_bus.h b/esphome/components/i2c/i2c_bus.h index da94aa940d..1acbe506a3 100644 --- a/esphome/components/i2c/i2c_bus.h +++ b/esphome/components/i2c/i2c_bus.h @@ -1,12 +1,32 @@ #pragma once #include #include +#include +#include #include #include +#include "esphome/core/helpers.h" + namespace esphome { namespace i2c { +/// @brief Helper class for efficient buffer allocation - uses stack for small sizes, heap for large +template class SmallBufferWithHeapFallback { + public: + uint8_t *get(size_t size) { + if (size <= STACK_SIZE) { + return this->stack_buffer_; + } + this->heap_buffer_ = std::unique_ptr(new uint8_t[size]); + return this->heap_buffer_.get(); + } + + private: + uint8_t stack_buffer_[STACK_SIZE]; + std::unique_ptr heap_buffer_; +}; + /// @brief Error codes returned by I2CBus and I2CDevice methods enum ErrorCode { NO_ERROR = 0, ///< No error found during execution of method @@ -39,71 +59,79 @@ struct WriteBuffer { /// note https://www.nxp.com/docs/en/application-note/AN10216.pdf class I2CBus { public: - /// @brief Creates a ReadBuffer and calls the virtual readv() method to read bytes into this buffer - /// @param address address of the I²C component on the i2c bus - /// @param buffer pointer to an array of bytes that will be used to store the data received - /// @param len length of the buffer = number of bytes to read - /// @return an i2c::ErrorCode - virtual ErrorCode read(uint8_t address, uint8_t *buffer, size_t len) { - ReadBuffer buf; - buf.data = buffer; - buf.len = len; - return readv(address, &buf, 1); - } + virtual ~I2CBus() = default; - /// @brief This virtual method reads bytes from an I2CBus into an array of ReadBuffer. - /// @param address address of the I²C component on the i2c bus - /// @param buffers pointer to an array of ReadBuffer - /// @param count number of ReadBuffer to read - /// @return an i2c::ErrorCode - /// @details This is a pure virtual method that must be implemented in a subclass. - virtual ErrorCode readv(uint8_t address, ReadBuffer *buffers, size_t count) = 0; - - virtual ErrorCode write(uint8_t address, const uint8_t *buffer, size_t len) { - return write(address, buffer, len, true); - } - - /// @brief Creates a WriteBuffer and calls the writev() method to send the bytes from this buffer - /// @param address address of the I²C component on the i2c bus - /// @param buffer pointer to an array of bytes that contains the data to be sent - /// @param len length of the buffer = number of bytes to write - /// @param stop true or false: True will send a stop message, releasing the bus after - /// transmission. False will send a restart, keeping the connection active. - /// @return an i2c::ErrorCode - virtual ErrorCode write(uint8_t address, const uint8_t *buffer, size_t len, bool stop) { - WriteBuffer buf; - buf.data = buffer; - buf.len = len; - return writev(address, &buf, 1, stop); - } - - virtual ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t cnt) { - return writev(address, buffers, cnt, true); - } - - /// @brief This virtual method writes bytes to an I2CBus from an array of WriteBuffer. - /// @param address address of the I²C component on the i2c bus - /// @param buffers pointer to an array of WriteBuffer - /// @param count number of WriteBuffer to write - /// @param stop true or false: True will send a stop message, releasing the bus after + /// @brief This virtual method writes bytes to an I2CBus from an array, + /// then reads bytes into an array of ReadBuffer. + /// @param address address of the I²C device on the i2c bus + /// @param write_buffer pointer to data + /// @param write_count number of bytes to write + /// @param read_buffer pointer to an array to receive data + /// @param read_count number of bytes to read /// transmission. False will send a restart, keeping the connection active. /// @return an i2c::ErrorCode /// @details This is a pure virtual method that must be implemented in the subclass. - virtual ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t count, bool stop) = 0; + virtual ErrorCode write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count, uint8_t *read_buffer, + size_t read_count) = 0; + + // Legacy functions for compatibility + + ErrorCode read(uint8_t address, uint8_t *buffer, size_t len) { + return this->write_readv(address, nullptr, 0, buffer, len); + } + + ErrorCode write(uint8_t address, const uint8_t *buffer, size_t len, bool stop = true) { + return this->write_readv(address, buffer, len, nullptr, 0); + } + + ESPDEPRECATED("This method is deprecated and will be removed in ESPHome 2026.3.0. Use write_readv() instead.", + "2025.9.0") + ErrorCode readv(uint8_t address, ReadBuffer *read_buffers, size_t count) { + size_t total_len = 0; + for (size_t i = 0; i != count; i++) { + total_len += read_buffers[i].len; + } + + SmallBufferWithHeapFallback<128> buffer_alloc; // Most I2C reads are small + uint8_t *buffer = buffer_alloc.get(total_len); + + auto err = this->write_readv(address, nullptr, 0, buffer, total_len); + if (err != ERROR_OK) + return err; + size_t pos = 0; + for (size_t i = 0; i != count; i++) { + if (read_buffers[i].len != 0) { + std::memcpy(read_buffers[i].data, buffer + pos, read_buffers[i].len); + pos += read_buffers[i].len; + } + } + return ERROR_OK; + } + + ESPDEPRECATED("This method is deprecated and will be removed in ESPHome 2026.3.0. Use write_readv() instead.", + "2025.9.0") + ErrorCode writev(uint8_t address, const WriteBuffer *write_buffers, size_t count, bool stop = true) { + size_t total_len = 0; + for (size_t i = 0; i != count; i++) { + total_len += write_buffers[i].len; + } + + SmallBufferWithHeapFallback<128> buffer_alloc; // Most I2C writes are small + uint8_t *buffer = buffer_alloc.get(total_len); + + size_t pos = 0; + for (size_t i = 0; i != count; i++) { + std::memcpy(buffer + pos, write_buffers[i].data, write_buffers[i].len); + pos += write_buffers[i].len; + } + + return this->write_readv(address, buffer, total_len, nullptr, 0); + } protected: /// @brief Scans the I2C bus for devices. Devices presence is kept in an array of std::pair /// that contains the address and the corresponding bool presence flag. - virtual void i2c_scan() { - for (uint8_t address = 8; address < 120; address++) { - auto err = writev(address, nullptr, 0); - if (err == ERROR_OK) { - scan_results_.emplace_back(address, true); - } else if (err == ERROR_UNKNOWN) { - scan_results_.emplace_back(address, false); - } - } - } + void i2c_scan_(); std::vector> scan_results_; ///< array containing scan results bool scan_{false}; ///< Should we scan ? Can be set in the yaml }; diff --git a/esphome/components/i2c/i2c_bus_arduino.cpp b/esphome/components/i2c/i2c_bus_arduino.cpp index 24385745eb..1579020c9b 100644 --- a/esphome/components/i2c/i2c_bus_arduino.cpp +++ b/esphome/components/i2c/i2c_bus_arduino.cpp @@ -1,4 +1,4 @@ -#ifdef USE_ARDUINO +#if defined(USE_ARDUINO) && !defined(USE_ESP32) #include "i2c_bus_arduino.h" #include @@ -15,16 +15,7 @@ static const char *const TAG = "i2c.arduino"; void ArduinoI2CBus::setup() { recover_(); -#if defined(USE_ESP32) - static uint8_t next_bus_num = 0; - if (next_bus_num == 0) { - wire_ = &Wire; - } else { - wire_ = new TwoWire(next_bus_num); // NOLINT(cppcoreguidelines-owning-memory) - } - this->port_ = next_bus_num; - next_bus_num++; -#elif defined(USE_ESP8266) +#if defined(USE_ESP8266) wire_ = new TwoWire(); // NOLINT(cppcoreguidelines-owning-memory) #elif defined(USE_RP2040) static bool first = true; @@ -41,7 +32,7 @@ void ArduinoI2CBus::setup() { this->initialized_ = true; if (this->scan_) { ESP_LOGV(TAG, "Scanning bus for active devices"); - this->i2c_scan(); + this->i2c_scan_(); } } @@ -54,10 +45,7 @@ void ArduinoI2CBus::set_pins_and_clock_() { wire_->begin(static_cast(sda_pin_), static_cast(scl_pin_)); #endif if (timeout_ > 0) { // if timeout specified in yaml -#if defined(USE_ESP32) - // https://github.com/espressif/arduino-esp32/blob/master/libraries/Wire/src/Wire.cpp - wire_->setTimeOut(timeout_ / 1000); // unit: ms -#elif defined(USE_ESP8266) +#if defined(USE_ESP8266) // https://github.com/esp8266/Arduino/blob/master/libraries/Wire/Wire.h wire_->setClockStretchLimit(timeout_); // unit: us #elif defined(USE_RP2040) @@ -76,9 +64,7 @@ void ArduinoI2CBus::dump_config() { " Frequency: %u Hz", this->sda_pin_, this->scl_pin_, this->frequency_); if (timeout_ > 0) { -#if defined(USE_ESP32) - ESP_LOGCONFIG(TAG, " Timeout: %u ms", this->timeout_ / 1000); -#elif defined(USE_ESP8266) +#if defined(USE_ESP8266) ESP_LOGCONFIG(TAG, " Timeout: %u us", this->timeout_); #elif defined(USE_RP2040) ESP_LOGCONFIG(TAG, " Timeout: %u ms", this->timeout_ / 1000); @@ -111,88 +97,37 @@ void ArduinoI2CBus::dump_config() { } } -ErrorCode ArduinoI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt) { +ErrorCode ArduinoI2CBus::write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count, + uint8_t *read_buffer, size_t read_count) { #if defined(USE_ESP8266) this->set_pins_and_clock_(); // reconfigure Wire global state in case there are multiple instances #endif - - // logging is only enabled with vv level, if warnings are shown the caller - // should log them if (!initialized_) { - ESP_LOGVV(TAG, "i2c bus not initialized!"); - return ERROR_NOT_INITIALIZED; - } - size_t to_request = 0; - for (size_t i = 0; i < cnt; i++) - to_request += buffers[i].len; - size_t ret = wire_->requestFrom(address, to_request, true); - if (ret != to_request) { - ESP_LOGVV(TAG, "RX %u from %02X failed with error %u", to_request, address, ret); - return ERROR_TIMEOUT; - } - - for (size_t i = 0; i < cnt; i++) { - const auto &buf = buffers[i]; - for (size_t j = 0; j < buf.len; j++) - buf.data[j] = wire_->read(); - } - -#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE - char debug_buf[4]; - std::string debug_hex; - - for (size_t i = 0; i < cnt; i++) { - const auto &buf = buffers[i]; - for (size_t j = 0; j < buf.len; j++) { - snprintf(debug_buf, sizeof(debug_buf), "%02X", buf.data[j]); - debug_hex += debug_buf; - } - } - ESP_LOGVV(TAG, "0x%02X RX %s", address, debug_hex.c_str()); -#endif - - return ERROR_OK; -} -ErrorCode ArduinoI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cnt, bool stop) { -#if defined(USE_ESP8266) - this->set_pins_and_clock_(); // reconfigure Wire global state in case there are multiple instances -#endif - - // logging is only enabled with vv level, if warnings are shown the caller - // should log them - if (!initialized_) { - ESP_LOGVV(TAG, "i2c bus not initialized!"); + ESP_LOGD(TAG, "i2c bus not initialized!"); return ERROR_NOT_INITIALIZED; } -#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE - char debug_buf[4]; - std::string debug_hex; + ESP_LOGV(TAG, "0x%02X TX %s", address, format_hex_pretty(write_buffer, write_count).c_str()); - for (size_t i = 0; i < cnt; i++) { - const auto &buf = buffers[i]; - for (size_t j = 0; j < buf.len; j++) { - snprintf(debug_buf, sizeof(debug_buf), "%02X", buf.data[j]); - debug_hex += debug_buf; - } - } - ESP_LOGVV(TAG, "0x%02X TX %s", address, debug_hex.c_str()); -#endif - - wire_->beginTransmission(address); - size_t written = 0; - for (size_t i = 0; i < cnt; i++) { - const auto &buf = buffers[i]; - if (buf.len == 0) - continue; - size_t ret = wire_->write(buf.data, buf.len); - written += ret; - if (ret != buf.len) { - ESP_LOGVV(TAG, "TX failed at %u", written); + uint8_t status = 0; + if (write_count != 0 || read_count == 0) { + wire_->beginTransmission(address); + size_t ret = wire_->write(write_buffer, write_count); + if (ret != write_count) { + ESP_LOGV(TAG, "TX failed"); return ERROR_UNKNOWN; } + status = wire_->endTransmission(read_count == 0); + } + if (status == 0 && read_count != 0) { + size_t ret2 = wire_->requestFrom(address, read_count, true); + if (ret2 != read_count) { + ESP_LOGVV(TAG, "RX %u from %02X failed with error %u", read_count, address, ret2); + return ERROR_TIMEOUT; + } + for (size_t j = 0; j != read_count; j++) + read_buffer[j] = wire_->read(); } - uint8_t status = wire_->endTransmission(stop); switch (status) { case 0: return ERROR_OK; @@ -326,4 +261,4 @@ void ArduinoI2CBus::recover_() { } // namespace i2c } // namespace esphome -#endif // USE_ESP_IDF +#endif // defined(USE_ARDUINO) && !defined(USE_ESP32) diff --git a/esphome/components/i2c/i2c_bus_arduino.h b/esphome/components/i2c/i2c_bus_arduino.h index 7e6616cbce..2d69e7684c 100644 --- a/esphome/components/i2c/i2c_bus_arduino.h +++ b/esphome/components/i2c/i2c_bus_arduino.h @@ -1,6 +1,6 @@ #pragma once -#ifdef USE_ARDUINO +#if defined(USE_ARDUINO) && !defined(USE_ESP32) #include #include "esphome/core/component.h" @@ -19,8 +19,8 @@ class ArduinoI2CBus : public InternalI2CBus, public Component { public: void setup() override; void dump_config() override; - ErrorCode readv(uint8_t address, ReadBuffer *buffers, size_t cnt) override; - ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t cnt, bool stop) override; + ErrorCode write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count, uint8_t *read_buffer, + size_t read_count) override; float get_setup_priority() const override { return setup_priority::BUS; } void set_scan(bool scan) { scan_ = scan; } @@ -29,7 +29,7 @@ class ArduinoI2CBus : public InternalI2CBus, public Component { void set_frequency(uint32_t frequency) { frequency_ = frequency; } void set_timeout(uint32_t timeout) { timeout_ = timeout; } - int get_port() const override { return this->port_; } + int get_port() const override { return 0; } private: void recover_(); @@ -37,7 +37,6 @@ class ArduinoI2CBus : public InternalI2CBus, public Component { RecoveryCode recovery_result_; protected: - int8_t port_{-1}; TwoWire *wire_; uint8_t sda_pin_; uint8_t scl_pin_; @@ -49,4 +48,4 @@ class ArduinoI2CBus : public InternalI2CBus, public Component { } // namespace i2c } // namespace esphome -#endif // USE_ARDUINO +#endif // defined(USE_ARDUINO) && !defined(USE_ESP32) diff --git a/esphome/components/i2c/i2c_bus_esp_idf.cpp b/esphome/components/i2c/i2c_bus_esp_idf.cpp index cf31ba1c0d..c22db51c68 100644 --- a/esphome/components/i2c/i2c_bus_esp_idf.cpp +++ b/esphome/components/i2c/i2c_bus_esp_idf.cpp @@ -1,6 +1,7 @@ -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include "i2c_bus_esp_idf.h" + #include #include #include @@ -9,10 +10,6 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 3, 0) -#define SOC_HP_I2C_NUM SOC_I2C_NUM -#endif - namespace esphome { namespace i2c { @@ -34,7 +31,6 @@ void IDFI2CBus::setup() { this->recover_(); -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) next_port = (i2c_port_t) (next_port + 1); i2c_master_bus_config_t bus_conf{}; @@ -77,56 +73,8 @@ void IDFI2CBus::setup() { if (this->scan_) { ESP_LOGV(TAG, "Scanning for devices"); - this->i2c_scan(); + this->i2c_scan_(); } -#else -#if SOC_HP_I2C_NUM > 1 - next_port = (next_port == I2C_NUM_0) ? I2C_NUM_1 : I2C_NUM_MAX; -#else - next_port = I2C_NUM_MAX; -#endif - - i2c_config_t conf{}; - memset(&conf, 0, sizeof(conf)); - conf.mode = I2C_MODE_MASTER; - conf.sda_io_num = sda_pin_; - conf.sda_pullup_en = sda_pullup_enabled_; - conf.scl_io_num = scl_pin_; - conf.scl_pullup_en = scl_pullup_enabled_; - conf.master.clk_speed = frequency_; -#ifdef USE_ESP32_VARIANT_ESP32S2 - // workaround for https://github.com/esphome/issues/issues/6718 - conf.clk_flags = I2C_SCLK_SRC_FLAG_AWARE_DFS; -#endif - esp_err_t err = i2c_param_config(port_, &conf); - if (err != ESP_OK) { - ESP_LOGW(TAG, "i2c_param_config failed: %s", esp_err_to_name(err)); - this->mark_failed(); - return; - } - if (timeout_ > 0) { - err = i2c_set_timeout(port_, timeout_ * 80); // unit: APB 80MHz clock cycle - if (err != ESP_OK) { - ESP_LOGW(TAG, "i2c_set_timeout failed: %s", esp_err_to_name(err)); - this->mark_failed(); - return; - } else { - ESP_LOGV(TAG, "i2c_timeout set to %" PRIu32 " ticks (%" PRIu32 " us)", timeout_ * 80, timeout_); - } - } - err = i2c_driver_install(port_, I2C_MODE_MASTER, 0, 0, 0); - if (err != ESP_OK) { - ESP_LOGW(TAG, "i2c_driver_install failed: %s", esp_err_to_name(err)); - this->mark_failed(); - return; - } - - initialized_ = true; - if (this->scan_) { - ESP_LOGV(TAG, "Scanning bus for active devices"); - this->i2c_scan(); - } -#endif } void IDFI2CBus::dump_config() { @@ -166,267 +114,73 @@ void IDFI2CBus::dump_config() { } } -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) -void IDFI2CBus::i2c_scan() { - for (uint8_t address = 8; address < 120; address++) { - auto err = i2c_master_probe(this->bus_, address, 20); - if (err == ESP_OK) { - this->scan_results_.emplace_back(address, true); - } - } -} -#endif - -ErrorCode IDFI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt) { - // logging is only enabled with vv level, if warnings are shown the caller +ErrorCode IDFI2CBus::write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count, uint8_t *read_buffer, + size_t read_count) { + // logging is only enabled with v level, if warnings are shown the caller // should log them if (!initialized_) { - ESP_LOGVV(TAG, "i2c bus not initialized!"); + ESP_LOGW(TAG, "i2c bus not initialized!"); return ERROR_NOT_INITIALIZED; } -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) - i2c_operation_job_t jobs[cnt + 4]; - uint8_t read = (address << 1) | I2C_MASTER_READ; - size_t last = 0, num = 0; - - jobs[num].command = I2C_MASTER_CMD_START; - num++; - - jobs[num].command = I2C_MASTER_CMD_WRITE; - jobs[num].write.ack_check = true; - jobs[num].write.data = &read; - jobs[num].write.total_bytes = 1; - num++; - - // find the last valid index - for (size_t i = 0; i < cnt; i++) { - const auto &buf = buffers[i]; - if (buf.len == 0) { - continue; + i2c_operation_job_t jobs[8]{}; + size_t num_jobs = 0; + uint8_t write_addr = (address << 1) | I2C_MASTER_WRITE; + uint8_t read_addr = (address << 1) | I2C_MASTER_READ; + ESP_LOGV(TAG, "Writing %zu bytes, reading %zu bytes", write_count, read_count); + if (read_count == 0 && write_count == 0) { + // basically just a bus probe. Send a start, address and stop + ESP_LOGV(TAG, "0x%02X BUS PROBE", address); + jobs[num_jobs++].command = I2C_MASTER_CMD_START; + jobs[num_jobs].command = I2C_MASTER_CMD_WRITE; + jobs[num_jobs].write.ack_check = true; + jobs[num_jobs].write.data = &write_addr; + jobs[num_jobs++].write.total_bytes = 1; + } else { + if (write_count != 0) { + ESP_LOGV(TAG, "0x%02X TX %s", address, format_hex_pretty(write_buffer, write_count).c_str()); + jobs[num_jobs++].command = I2C_MASTER_CMD_START; + jobs[num_jobs].command = I2C_MASTER_CMD_WRITE; + jobs[num_jobs].write.ack_check = true; + jobs[num_jobs].write.data = &write_addr; + jobs[num_jobs++].write.total_bytes = 1; + jobs[num_jobs].command = I2C_MASTER_CMD_WRITE; + jobs[num_jobs].write.ack_check = true; + jobs[num_jobs].write.data = (uint8_t *) write_buffer; + jobs[num_jobs++].write.total_bytes = write_count; } - last = i; - } - - for (size_t i = 0; i < cnt; i++) { - const auto &buf = buffers[i]; - if (buf.len == 0) { - continue; - } - if (i == last) { - // the last byte read before stop should always be a nack, - // split the last read if len is larger than 1 - if (buf.len > 1) { - jobs[num].command = I2C_MASTER_CMD_READ; - jobs[num].read.ack_value = I2C_ACK_VAL; - jobs[num].read.data = (uint8_t *) buf.data; - jobs[num].read.total_bytes = buf.len - 1; - num++; + if (read_count != 0) { + ESP_LOGV(TAG, "0x%02X RX bytes %zu", address, read_count); + jobs[num_jobs++].command = I2C_MASTER_CMD_START; + jobs[num_jobs].command = I2C_MASTER_CMD_WRITE; + jobs[num_jobs].write.ack_check = true; + jobs[num_jobs].write.data = &read_addr; + jobs[num_jobs++].write.total_bytes = 1; + if (read_count > 1) { + jobs[num_jobs].command = I2C_MASTER_CMD_READ; + jobs[num_jobs].read.ack_value = I2C_ACK_VAL; + jobs[num_jobs].read.data = read_buffer; + jobs[num_jobs++].read.total_bytes = read_count - 1; } - jobs[num].command = I2C_MASTER_CMD_READ; - jobs[num].read.ack_value = I2C_NACK_VAL; - jobs[num].read.data = (uint8_t *) buf.data + buf.len - 1; - jobs[num].read.total_bytes = 1; - num++; - } else { - jobs[num].command = I2C_MASTER_CMD_READ; - jobs[num].read.ack_value = I2C_ACK_VAL; - jobs[num].read.data = (uint8_t *) buf.data; - jobs[num].read.total_bytes = buf.len; - num++; + jobs[num_jobs].command = I2C_MASTER_CMD_READ; + jobs[num_jobs].read.ack_value = I2C_NACK_VAL; + jobs[num_jobs].read.data = read_buffer + read_count - 1; + jobs[num_jobs++].read.total_bytes = 1; } } - - jobs[num].command = I2C_MASTER_CMD_STOP; - num++; - - esp_err_t err = i2c_master_execute_defined_operations(this->dev_, jobs, num, 20); + jobs[num_jobs++].command = I2C_MASTER_CMD_STOP; + ESP_LOGV(TAG, "Sending %zu jobs", num_jobs); + esp_err_t err = i2c_master_execute_defined_operations(this->dev_, jobs, num_jobs, 20); if (err == ESP_ERR_INVALID_STATE) { - ESP_LOGVV(TAG, "RX from %02X failed: not acked", address); + ESP_LOGV(TAG, "TX to %02X failed: not acked", address); return ERROR_NOT_ACKNOWLEDGED; } else if (err == ESP_ERR_TIMEOUT) { - ESP_LOGVV(TAG, "RX from %02X failed: timeout", address); + ESP_LOGV(TAG, "TX to %02X failed: timeout", address); return ERROR_TIMEOUT; } else if (err != ESP_OK) { - ESP_LOGVV(TAG, "RX from %02X failed: %s", address, esp_err_to_name(err)); + ESP_LOGV(TAG, "TX to %02X failed: %s", address, esp_err_to_name(err)); return ERROR_UNKNOWN; } -#else - i2c_cmd_handle_t cmd = i2c_cmd_link_create(); - esp_err_t err = i2c_master_start(cmd); - if (err != ESP_OK) { - ESP_LOGVV(TAG, "RX from %02X master start failed: %s", address, esp_err_to_name(err)); - i2c_cmd_link_delete(cmd); - return ERROR_UNKNOWN; - } - err = i2c_master_write_byte(cmd, (address << 1) | I2C_MASTER_READ, true); - if (err != ESP_OK) { - ESP_LOGVV(TAG, "RX from %02X address write failed: %s", address, esp_err_to_name(err)); - i2c_cmd_link_delete(cmd); - return ERROR_UNKNOWN; - } - for (size_t i = 0; i < cnt; i++) { - const auto &buf = buffers[i]; - if (buf.len == 0) - continue; - err = i2c_master_read(cmd, buf.data, buf.len, i == cnt - 1 ? I2C_MASTER_LAST_NACK : I2C_MASTER_ACK); - if (err != ESP_OK) { - ESP_LOGVV(TAG, "RX from %02X data read failed: %s", address, esp_err_to_name(err)); - i2c_cmd_link_delete(cmd); - return ERROR_UNKNOWN; - } - } - err = i2c_master_stop(cmd); - if (err != ESP_OK) { - ESP_LOGVV(TAG, "RX from %02X stop failed: %s", address, esp_err_to_name(err)); - i2c_cmd_link_delete(cmd); - return ERROR_UNKNOWN; - } - err = i2c_master_cmd_begin(port_, cmd, 20 / portTICK_PERIOD_MS); - // i2c_master_cmd_begin() will block for a whole second if no ack: - // https://github.com/espressif/esp-idf/issues/4999 - i2c_cmd_link_delete(cmd); - if (err == ESP_FAIL) { - // transfer not acked - ESP_LOGVV(TAG, "RX from %02X failed: not acked", address); - return ERROR_NOT_ACKNOWLEDGED; - } else if (err == ESP_ERR_TIMEOUT) { - ESP_LOGVV(TAG, "RX from %02X failed: timeout", address); - return ERROR_TIMEOUT; - } else if (err != ESP_OK) { - ESP_LOGVV(TAG, "RX from %02X failed: %s", address, esp_err_to_name(err)); - return ERROR_UNKNOWN; - } -#endif - -#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE - char debug_buf[4]; - std::string debug_hex; - - for (size_t i = 0; i < cnt; i++) { - const auto &buf = buffers[i]; - for (size_t j = 0; j < buf.len; j++) { - snprintf(debug_buf, sizeof(debug_buf), "%02X", buf.data[j]); - debug_hex += debug_buf; - } - } - ESP_LOGVV(TAG, "0x%02X RX %s", address, debug_hex.c_str()); -#endif - - return ERROR_OK; -} - -ErrorCode IDFI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cnt, bool stop) { - // logging is only enabled with vv level, if warnings are shown the caller - // should log them - if (!initialized_) { - ESP_LOGVV(TAG, "i2c bus not initialized!"); - return ERROR_NOT_INITIALIZED; - } - -#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE - char debug_buf[4]; - std::string debug_hex; - - for (size_t i = 0; i < cnt; i++) { - const auto &buf = buffers[i]; - for (size_t j = 0; j < buf.len; j++) { - snprintf(debug_buf, sizeof(debug_buf), "%02X", buf.data[j]); - debug_hex += debug_buf; - } - } - ESP_LOGVV(TAG, "0x%02X TX %s", address, debug_hex.c_str()); -#endif - -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) - i2c_operation_job_t jobs[cnt + 3]; - uint8_t write = (address << 1) | I2C_MASTER_WRITE; - size_t num = 0; - - jobs[num].command = I2C_MASTER_CMD_START; - num++; - - jobs[num].command = I2C_MASTER_CMD_WRITE; - jobs[num].write.ack_check = true; - jobs[num].write.data = &write; - jobs[num].write.total_bytes = 1; - num++; - - for (size_t i = 0; i < cnt; i++) { - const auto &buf = buffers[i]; - if (buf.len == 0) { - continue; - } - jobs[num].command = I2C_MASTER_CMD_WRITE; - jobs[num].write.ack_check = true; - jobs[num].write.data = (uint8_t *) buf.data; - jobs[num].write.total_bytes = buf.len; - num++; - } - - if (stop) { - jobs[num].command = I2C_MASTER_CMD_STOP; - num++; - } - - esp_err_t err = i2c_master_execute_defined_operations(this->dev_, jobs, num, 20); - if (err == ESP_ERR_INVALID_STATE) { - ESP_LOGVV(TAG, "TX to %02X failed: not acked", address); - return ERROR_NOT_ACKNOWLEDGED; - } else if (err == ESP_ERR_TIMEOUT) { - ESP_LOGVV(TAG, "TX to %02X failed: timeout", address); - return ERROR_TIMEOUT; - } else if (err != ESP_OK) { - ESP_LOGVV(TAG, "TX to %02X failed: %s", address, esp_err_to_name(err)); - return ERROR_UNKNOWN; - } -#else - i2c_cmd_handle_t cmd = i2c_cmd_link_create(); - esp_err_t err = i2c_master_start(cmd); - if (err != ESP_OK) { - ESP_LOGVV(TAG, "TX to %02X master start failed: %s", address, esp_err_to_name(err)); - i2c_cmd_link_delete(cmd); - return ERROR_UNKNOWN; - } - err = i2c_master_write_byte(cmd, (address << 1) | I2C_MASTER_WRITE, true); - if (err != ESP_OK) { - ESP_LOGVV(TAG, "TX to %02X address write failed: %s", address, esp_err_to_name(err)); - i2c_cmd_link_delete(cmd); - return ERROR_UNKNOWN; - } - for (size_t i = 0; i < cnt; i++) { - const auto &buf = buffers[i]; - if (buf.len == 0) - continue; - err = i2c_master_write(cmd, buf.data, buf.len, true); - if (err != ESP_OK) { - ESP_LOGVV(TAG, "TX to %02X data write failed: %s", address, esp_err_to_name(err)); - i2c_cmd_link_delete(cmd); - return ERROR_UNKNOWN; - } - } - if (stop) { - err = i2c_master_stop(cmd); - if (err != ESP_OK) { - ESP_LOGVV(TAG, "TX to %02X master stop failed: %s", address, esp_err_to_name(err)); - i2c_cmd_link_delete(cmd); - return ERROR_UNKNOWN; - } - } - err = i2c_master_cmd_begin(port_, cmd, 20 / portTICK_PERIOD_MS); - i2c_cmd_link_delete(cmd); - if (err == ESP_FAIL) { - // transfer not acked - ESP_LOGVV(TAG, "TX to %02X failed: not acked", address); - return ERROR_NOT_ACKNOWLEDGED; - } else if (err == ESP_ERR_TIMEOUT) { - ESP_LOGVV(TAG, "TX to %02X failed: timeout", address); - return ERROR_TIMEOUT; - } else if (err != ESP_OK) { - ESP_LOGVV(TAG, "TX to %02X failed: %s", address, esp_err_to_name(err)); - return ERROR_UNKNOWN; - } -#endif return ERROR_OK; } @@ -436,8 +190,8 @@ ErrorCode IDFI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cnt, b void IDFI2CBus::recover_() { ESP_LOGI(TAG, "Performing bus recovery"); - const gpio_num_t scl_pin = static_cast(scl_pin_); - const gpio_num_t sda_pin = static_cast(sda_pin_); + const auto scl_pin = static_cast(scl_pin_); + const auto sda_pin = static_cast(sda_pin_); // For the upcoming operations, target for a 60kHz toggle frequency. // 1000kHz is the maximum frequency for I2C running in standard-mode, @@ -545,5 +299,4 @@ void IDFI2CBus::recover_() { } // namespace i2c } // namespace esphome - -#endif // USE_ESP_IDF +#endif // USE_ESP32 diff --git a/esphome/components/i2c/i2c_bus_esp_idf.h b/esphome/components/i2c/i2c_bus_esp_idf.h index 4e8f86fd0c..63fe8b701c 100644 --- a/esphome/components/i2c/i2c_bus_esp_idf.h +++ b/esphome/components/i2c/i2c_bus_esp_idf.h @@ -1,15 +1,10 @@ #pragma once -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 -#include "esp_idf_version.h" #include "esphome/core/component.h" #include "i2c_bus.h" -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) #include -#else -#include -#endif namespace esphome { namespace i2c { @@ -24,36 +19,33 @@ class IDFI2CBus : public InternalI2CBus, public Component { public: void setup() override; void dump_config() override; - ErrorCode readv(uint8_t address, ReadBuffer *buffers, size_t cnt) override; - ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t cnt, bool stop) override; + ErrorCode write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count, uint8_t *read_buffer, + size_t read_count) override; float get_setup_priority() const override { return setup_priority::BUS; } - void set_scan(bool scan) { scan_ = scan; } - void set_sda_pin(uint8_t sda_pin) { sda_pin_ = sda_pin; } - void set_sda_pullup_enabled(bool sda_pullup_enabled) { sda_pullup_enabled_ = sda_pullup_enabled; } - void set_scl_pin(uint8_t scl_pin) { scl_pin_ = scl_pin; } - void set_scl_pullup_enabled(bool scl_pullup_enabled) { scl_pullup_enabled_ = scl_pullup_enabled; } - void set_frequency(uint32_t frequency) { frequency_ = frequency; } - void set_timeout(uint32_t timeout) { timeout_ = timeout; } + void set_scan(bool scan) { this->scan_ = scan; } + void set_sda_pin(uint8_t sda_pin) { this->sda_pin_ = sda_pin; } + void set_sda_pullup_enabled(bool sda_pullup_enabled) { this->sda_pullup_enabled_ = sda_pullup_enabled; } + void set_scl_pin(uint8_t scl_pin) { this->scl_pin_ = scl_pin; } + void set_scl_pullup_enabled(bool scl_pullup_enabled) { this->scl_pullup_enabled_ = scl_pullup_enabled; } + void set_frequency(uint32_t frequency) { this->frequency_ = frequency; } + void set_timeout(uint32_t timeout) { this->timeout_ = timeout; } - int get_port() const override { return static_cast(this->port_); } + int get_port() const override { return this->port_; } private: void recover_(); - RecoveryCode recovery_result_; + RecoveryCode recovery_result_{}; protected: -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) - i2c_master_dev_handle_t dev_; - i2c_master_bus_handle_t bus_; - void i2c_scan() override; -#endif - i2c_port_t port_; - uint8_t sda_pin_; - bool sda_pullup_enabled_; - uint8_t scl_pin_; - bool scl_pullup_enabled_; - uint32_t frequency_; + i2c_master_dev_handle_t dev_{}; + i2c_master_bus_handle_t bus_{}; + i2c_port_t port_{}; + uint8_t sda_pin_{}; + bool sda_pullup_enabled_{}; + uint8_t scl_pin_{}; + bool scl_pullup_enabled_{}; + uint32_t frequency_{}; uint32_t timeout_ = 0; bool initialized_ = false; }; @@ -61,4 +53,4 @@ class IDFI2CBus : public InternalI2CBus, public Component { } // namespace i2c } // namespace esphome -#endif // USE_ESP_IDF +#endif // USE_ESP32 diff --git a/esphome/components/i2c/i2c_bus_zephyr.cpp b/esphome/components/i2c/i2c_bus_zephyr.cpp new file mode 100644 index 0000000000..1eb9944dcb --- /dev/null +++ b/esphome/components/i2c/i2c_bus_zephyr.cpp @@ -0,0 +1,134 @@ +#ifdef USE_ZEPHYR + +#include "i2c_bus_zephyr.h" +#include +#include "esphome/core/log.h" + +namespace esphome::i2c { + +static const char *const TAG = "i2c.zephyr"; + +static const char *get_speed(uint32_t dev_config) { + switch (I2C_SPEED_GET(dev_config)) { + case I2C_SPEED_STANDARD: + return "100 kHz"; + case I2C_SPEED_FAST: + return "400 kHz"; + case I2C_SPEED_FAST_PLUS: + return "1 MHz"; + case I2C_SPEED_HIGH: + return "3.4 MHz"; + case I2C_SPEED_ULTRA: + return "5 MHz"; + } + return "unknown"; +} + +void ZephyrI2CBus::setup() { + if (!device_is_ready(this->i2c_dev_)) { + ESP_LOGE(TAG, "I2C dev is not ready."); + mark_failed(); + return; + } + + int ret = i2c_configure(this->i2c_dev_, this->dev_config_); + if (ret < 0) { + ESP_LOGE(TAG, "I2C: Failed to configure device"); + } + + this->recovery_result_ = i2c_recover_bus(this->i2c_dev_); + if (this->recovery_result_ != 0) { + ESP_LOGE(TAG, "I2C recover bus failed, err %d", this->recovery_result_); + } + if (this->scan_) { + ESP_LOGV(TAG, "Scanning I2C bus for active devices..."); + this->i2c_scan_(); + } +} + +void ZephyrI2CBus::dump_config() { + ESP_LOGCONFIG(TAG, + "I2C Bus:\n" + " SDA Pin: GPIO%u\n" + " SCL Pin: GPIO%u\n" + " Frequency: %s\n" + " Name: %s", + this->sda_pin_, this->scl_pin_, get_speed(this->dev_config_), this->i2c_dev_->name); + + if (this->recovery_result_ != 0) { + ESP_LOGCONFIG(TAG, " Recovery: failed, err %d", this->recovery_result_); + } else { + ESP_LOGCONFIG(TAG, " Recovery: bus successfully recovered"); + } + if (this->scan_) { + ESP_LOGI(TAG, "Results from I2C bus scan:"); + if (scan_results_.empty()) { + ESP_LOGI(TAG, "Found no I2C devices!"); + } else { + for (const auto &s : scan_results_) { + if (s.second) { + ESP_LOGI(TAG, "Found I2C device at address 0x%02X", s.first); + } else { + ESP_LOGE(TAG, "Unknown error at address 0x%02X", s.first); + } + } + } + } +} + +ErrorCode ZephyrI2CBus::write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count, + uint8_t *read_buffer, size_t read_count) { + if (!device_is_ready(this->i2c_dev_)) { + return ERROR_NOT_INITIALIZED; + } + + i2c_msg msgs[2]{}; + size_t cnt = 0; + uint8_t dst = 0x00; // dummy data to not use random value + + if (read_count == 0 && write_count == 0) { + msgs[cnt].buf = &dst; + msgs[cnt].len = 0U; + msgs[cnt++].flags = I2C_MSG_WRITE; + } else { + if (write_count) { + // the same struct is used for read/write — const cast is fine; data isn't modified + msgs[cnt].buf = const_cast(write_buffer); + msgs[cnt].len = write_count; + msgs[cnt++].flags = I2C_MSG_WRITE; + } + if (read_count) { + msgs[cnt].buf = const_cast(read_buffer); + msgs[cnt].len = read_count; + msgs[cnt++].flags = I2C_MSG_READ | I2C_MSG_RESTART; + } + } + + msgs[cnt - 1].flags |= I2C_MSG_STOP; + + auto err = i2c_transfer(this->i2c_dev_, msgs, cnt, address); + + if (err == -EIO) { + return ERROR_NOT_ACKNOWLEDGED; + } + + if (err != 0) { + ESP_LOGE(TAG, "i2c transfer error %d", err); + return ERROR_UNKNOWN; + } + + return ERROR_OK; +} + +void ZephyrI2CBus::set_frequency(uint32_t frequency) { + this->dev_config_ &= ~I2C_SPEED_MASK; + if (frequency >= 400000) { + this->dev_config_ |= I2C_SPEED_SET(I2C_SPEED_FAST); + } else { + this->dev_config_ |= I2C_SPEED_SET(I2C_SPEED_STANDARD); + } +} + +} // namespace esphome::i2c + +#endif diff --git a/esphome/components/i2c/i2c_bus_zephyr.h b/esphome/components/i2c/i2c_bus_zephyr.h new file mode 100644 index 0000000000..49cac5b992 --- /dev/null +++ b/esphome/components/i2c/i2c_bus_zephyr.h @@ -0,0 +1,38 @@ +#pragma once + +#ifdef USE_ZEPHYR + +#include "i2c_bus.h" +#include "esphome/core/component.h" + +struct device; + +namespace esphome::i2c { + +class ZephyrI2CBus : public InternalI2CBus, public Component { + public: + explicit ZephyrI2CBus(const device *i2c_dev) : i2c_dev_(i2c_dev) {} + void setup() override; + void dump_config() override; + ErrorCode write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count, uint8_t *read_buffer, + size_t read_count) override; + float get_setup_priority() const override { return setup_priority::BUS; } + + void set_scan(bool scan) { scan_ = scan; } + void set_sda_pin(uint8_t sda_pin) { this->sda_pin_ = sda_pin; } + void set_scl_pin(uint8_t scl_pin) { this->scl_pin_ = scl_pin; } + void set_frequency(uint32_t frequency); + + int get_port() const override { return 0; } + + protected: + const device *i2c_dev_; + int recovery_result_ = 0; + uint8_t sda_pin_{}; + uint8_t scl_pin_{}; + uint32_t dev_config_{}; +}; + +} // namespace esphome::i2c + +#endif diff --git a/esphome/components/i2s_audio/__init__.py b/esphome/components/i2s_audio/__init__.py index aa0a688fa0..907429ee0e 100644 --- a/esphome/components/i2s_audio/__init__.py +++ b/esphome/components/i2s_audio/__init__.py @@ -4,6 +4,9 @@ from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant from esphome.components.esp32.const import ( VARIANT_ESP32, VARIANT_ESP32C3, + VARIANT_ESP32C5, + VARIANT_ESP32C6, + VARIANT_ESP32H2, VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3, @@ -62,12 +65,15 @@ I2S_ROLE_OPTIONS = { CONF_SECONDARY: i2s_role_t.I2S_ROLE_SLAVE, # NOLINT } -# https://github.com/espressif/esp-idf/blob/master/components/soc/{variant}/include/soc/soc_caps.h +# https://github.com/espressif/esp-idf/blob/master/components/soc/{variant}/include/soc/soc_caps.h (SOC_I2S_NUM) I2S_PORTS = { VARIANT_ESP32: 2, VARIANT_ESP32S2: 1, VARIANT_ESP32S3: 2, VARIANT_ESP32C3: 1, + VARIANT_ESP32C5: 1, + VARIANT_ESP32C6: 1, + VARIANT_ESP32H2: 1, VARIANT_ESP32P4: 3, } @@ -137,7 +143,18 @@ def validate_mclk_divisible_by_3(config): return config -_use_legacy_driver = None +# Key for storing legacy driver setting in CORE.data +I2S_USE_LEGACY_DRIVER_KEY = "i2s_use_legacy_driver" + + +def _get_use_legacy_driver(): + """Get the legacy driver setting from CORE.data.""" + return CORE.data.get(I2S_USE_LEGACY_DRIVER_KEY) + + +def _set_use_legacy_driver(value: bool) -> None: + """Set the legacy driver setting in CORE.data.""" + CORE.data[I2S_USE_LEGACY_DRIVER_KEY] = value def i2s_audio_component_schema( @@ -203,17 +220,15 @@ async def register_i2s_audio_component(var, config): def validate_use_legacy(value): - global _use_legacy_driver # noqa: PLW0603 if CONF_USE_LEGACY in value: - if (_use_legacy_driver is not None) and ( - _use_legacy_driver != value[CONF_USE_LEGACY] - ): + existing_value = _get_use_legacy_driver() + if (existing_value is not None) and (existing_value != value[CONF_USE_LEGACY]): raise cv.Invalid( f"All i2s_audio components must set {CONF_USE_LEGACY} to the same value." ) if (not value[CONF_USE_LEGACY]) and (CORE.using_arduino): - raise cv.Invalid("Arduino supports only the legacy i2s driver.") - _use_legacy_driver = value[CONF_USE_LEGACY] + raise cv.Invalid("Arduino supports only the legacy i2s driver") + _set_use_legacy_driver(value[CONF_USE_LEGACY]) return value @@ -243,7 +258,8 @@ def _final_validate(_): def use_legacy(): - return not (CORE.using_esp_idf and not _use_legacy_driver) + legacy_driver = _get_use_legacy_driver() + return not (CORE.using_esp_idf and not legacy_driver) FINAL_VALIDATE_SCHEMA = _final_validate @@ -256,8 +272,7 @@ async def to_code(config): cg.add_define("USE_I2S_LEGACY") # Helps avoid callbacks being skipped due to processor load - if CORE.using_esp_idf: - add_idf_sdkconfig_option("CONFIG_I2S_ISR_IRAM_SAFE", True) + add_idf_sdkconfig_option("CONFIG_I2S_ISR_IRAM_SAFE", True) cg.add(var.set_lrclk_pin(config[CONF_I2S_LRCLK_PIN])) if CONF_I2S_BCLK_PIN in config: diff --git a/esphome/components/i2s_audio/media_player/__init__.py b/esphome/components/i2s_audio/media_player/__init__.py index ad6665a5f5..316ce7c48b 100644 --- a/esphome/components/i2s_audio/media_player/__init__.py +++ b/esphome/components/i2s_audio/media_player/__init__.py @@ -92,7 +92,7 @@ CONFIG_SCHEMA = cv.All( def _final_validate(_): if not use_legacy(): - raise cv.Invalid("I2S media player is only compatible with legacy i2s driver.") + raise cv.Invalid("I2S media player is only compatible with legacy i2s driver") FINAL_VALIDATE_SCHEMA = _final_validate diff --git a/esphome/components/i2s_audio/microphone/__init__.py b/esphome/components/i2s_audio/microphone/__init__.py index 0f02ba6c3a..f919199c60 100644 --- a/esphome/components/i2s_audio/microphone/__init__.py +++ b/esphome/components/i2s_audio/microphone/__init__.py @@ -122,7 +122,7 @@ CONFIG_SCHEMA = cv.All( def _final_validate(config): if not use_legacy() and config[CONF_ADC_TYPE] == "internal": - raise cv.Invalid("Internal ADC is only compatible with legacy i2s driver.") + raise cv.Invalid("Internal ADC is only compatible with legacy i2s driver") FINAL_VALIDATE_SCHEMA = _final_validate diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp index 5ca33b3493..cdebc214e2 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp @@ -24,9 +24,6 @@ static const uint32_t READ_DURATION_MS = 16; static const size_t TASK_STACK_SIZE = 4096; static const ssize_t TASK_PRIORITY = 23; -// Use an exponential moving average to correct a DC offset with weight factor 1/1000 -static const int32_t DC_OFFSET_MOVING_AVERAGE_COEFFICIENT_DENOMINATOR = 1000; - static const char *const TAG = "i2s_audio.microphone"; enum MicrophoneEventGroupBits : uint32_t { @@ -381,26 +378,57 @@ void I2SAudioMicrophone::mic_task(void *params) { } void I2SAudioMicrophone::fix_dc_offset_(std::vector &data) { + /** + * From https://www.musicdsp.org/en/latest/Filters/135-dc-filter.html: + * + * y(n) = x(n) - x(n-1) + R * y(n-1) + * R = 1 - (pi * 2 * frequency / samplerate) + * + * From https://en.wikipedia.org/wiki/Hearing_range: + * The human range is commonly given as 20Hz up. + * + * From https://en.wikipedia.org/wiki/High-resolution_audio: + * A reasonable upper bound for sample rate seems to be 96kHz. + * + * Calculate R value for 20Hz on a 96kHz sample rate: + * R = 1 - (pi * 2 * 20 / 96000) + * R = 0.9986910031 + * + * Transform floating point to bit-shifting approximation: + * output = input - prev_input + R * prev_output + * output = input - prev_input + (prev_output - (prev_output >> S)) + * + * Approximate bit-shift value S from R: + * R = 1 - (1 >> S) + * R = 1 - (1 / 2^S) + * R = 1 - 2^-S + * 0.9986910031 = 1 - 2^-S + * S = 9.57732 ~= 10 + * + * Actual R from S: + * R = 1 - 2^-10 = 0.9990234375 + * + * Confirm this has effect outside human hearing on 96000kHz sample: + * 0.9990234375 = 1 - (pi * 2 * f / 96000) + * f = 14.9208Hz + * + * Confirm this has effect outside human hearing on PDM 16kHz sample: + * 0.9990234375 = 1 - (pi * 2 * f / 16000) + * f = 2.4868Hz + * + */ + const uint8_t dc_filter_shift = 10; const size_t bytes_per_sample = this->audio_stream_info_.samples_to_bytes(1); const uint32_t total_samples = this->audio_stream_info_.bytes_to_samples(data.size()); - - if (total_samples == 0) { - return; - } - - int64_t offset_accumulator = 0; for (uint32_t sample_index = 0; sample_index < total_samples; ++sample_index) { const uint32_t byte_index = sample_index * bytes_per_sample; - int32_t sample = audio::unpack_audio_sample_to_q31(&data[byte_index], bytes_per_sample); - offset_accumulator += sample; - sample -= this->dc_offset_; - audio::pack_q31_as_audio_sample(sample, &data[byte_index], bytes_per_sample); + int32_t input = audio::unpack_audio_sample_to_q31(&data[byte_index], bytes_per_sample); + int32_t output = input - this->dc_offset_prev_input_ + + (this->dc_offset_prev_output_ - (this->dc_offset_prev_output_ >> dc_filter_shift)); + this->dc_offset_prev_input_ = input; + this->dc_offset_prev_output_ = output; + audio::pack_q31_as_audio_sample(output, &data[byte_index], bytes_per_sample); } - - const int32_t new_offset = offset_accumulator / total_samples; - this->dc_offset_ = new_offset / DC_OFFSET_MOVING_AVERAGE_COEFFICIENT_DENOMINATOR + - (DC_OFFSET_MOVING_AVERAGE_COEFFICIENT_DENOMINATOR - 1) * this->dc_offset_ / - DC_OFFSET_MOVING_AVERAGE_COEFFICIENT_DENOMINATOR; } size_t I2SAudioMicrophone::read_(uint8_t *buf, size_t len, TickType_t ticks_to_wait) { diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h index 633bd0e7dd..de272ba23d 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h @@ -82,7 +82,8 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub bool correct_dc_offset_; bool locked_driver_{false}; - int32_t dc_offset_{0}; + int32_t dc_offset_prev_input_{0}; + int32_t dc_offset_prev_output_{0}; }; } // namespace i2s_audio diff --git a/esphome/components/i2s_audio/speaker/__init__.py b/esphome/components/i2s_audio/speaker/__init__.py index cb7b876a40..98322d3a18 100644 --- a/esphome/components/i2s_audio/speaker/__init__.py +++ b/esphome/components/i2s_audio/speaker/__init__.py @@ -163,7 +163,7 @@ CONFIG_SCHEMA = cv.All( def _final_validate(config): if not use_legacy(): if config[CONF_DAC_TYPE] == "internal": - raise cv.Invalid("Internal DAC is only compatible with legacy i2s driver.") + raise cv.Invalid("Internal DAC is only compatible with legacy i2s driver") if config[CONF_I2S_COMM_FMT] == "stand_max": raise cv.Invalid( "I2S standard max format only implemented with legacy i2s driver." diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index 7ae3ec8b3b..53e378c41e 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -377,7 +377,7 @@ void I2SAudioSpeaker::speaker_task(void *params) { this_speaker->current_stream_info_.get_bits_per_sample() <= 16) { size_t len = bytes_read / sizeof(int16_t); int16_t *tmp_buf = (int16_t *) new_data; - for (int i = 0; i < len; i += 2) { + for (size_t i = 0; i < len; i += 2) { int16_t tmp = tmp_buf[i]; tmp_buf[i] = tmp_buf[i + 1]; tmp_buf[i + 1] = tmp; diff --git a/esphome/components/iaqcore/iaqcore.cpp b/esphome/components/iaqcore/iaqcore.cpp index 2a84eabf75..274f9086b6 100644 --- a/esphome/components/iaqcore/iaqcore.cpp +++ b/esphome/components/iaqcore/iaqcore.cpp @@ -35,7 +35,7 @@ void IAQCore::setup() { void IAQCore::update() { uint8_t buffer[sizeof(SensorData)]; - if (this->read_register(0xB5, buffer, sizeof(buffer), false) != i2c::ERROR_OK) { + if (this->read_register(0xB5, buffer, sizeof(buffer)) != i2c::ERROR_OK) { ESP_LOGD(TAG, "Read failed"); this->status_set_warning(); this->publish_nans_(); diff --git a/esphome/components/ili9xxx/ili9xxx_display.cpp b/esphome/components/ili9xxx/ili9xxx_display.cpp index ec0a860aa8..2a3d0edca7 100644 --- a/esphome/components/ili9xxx/ili9xxx_display.cpp +++ b/esphome/components/ili9xxx/ili9xxx_display.cpp @@ -325,7 +325,7 @@ void ILI9XXXDisplay::draw_pixels_at(int x_start, int y_start, int w, int h, cons // we could deal here with a non-zero y_offset, but if x_offset is zero, y_offset probably will be so don't bother this->write_array(ptr, w * h * 2); } else { - for (size_t y = 0; y != h; y++) { + for (size_t y = 0; y != static_cast(h); y++) { this->write_array(ptr + (y + y_offset) * stride + x_offset, w * 2); } } @@ -349,7 +349,7 @@ void ILI9XXXDisplay::draw_pixels_at(int x_start, int y_start, int w, int h, cons App.feed_wdt(); } // end of line? Skip to the next. - if (++pixel == w) { + if (++pixel == static_cast(w)) { pixel = 0; ptr += (x_pad + x_offset) * 2; } diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index f880b5f736..bf25a7cd92 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -671,18 +671,33 @@ async def write_image(config, all_frames=False): resize = config.get(CONF_RESIZE) if is_svg_file(path): # Local import so use of non-SVG files needn't require cairosvg installed + from pyexpat import ExpatError + from xml.etree.ElementTree import ParseError + from cairosvg import svg2png + from cairosvg.helpers import PointError if not resize: resize = (None, None) - with open(path, "rb") as file: - image = svg2png( - file_obj=file, - output_width=resize[0], - output_height=resize[1], - ) - image = Image.open(io.BytesIO(image)) - width, height = image.size + try: + with open(path, "rb") as file: + image = svg2png( + file_obj=file, + output_width=resize[0], + output_height=resize[1], + ) + image = Image.open(io.BytesIO(image)) + width, height = image.size + except ( + ValueError, + ParseError, + IndexError, + ExpatError, + AttributeError, + TypeError, + PointError, + ) as e: + raise core.EsphomeError(f"Could not load SVG image {path}: {e}") from e else: image = Image.open(path) width, height = image.size diff --git a/esphome/components/image/image.cpp b/esphome/components/image/image.cpp index 7b65c4d0cb..90e021467f 100644 --- a/esphome/components/image/image.cpp +++ b/esphome/components/image/image.cpp @@ -125,7 +125,7 @@ lv_img_dsc_t *Image::get_lv_img_dsc() { case IMAGE_TYPE_RGB: #if LV_COLOR_DEPTH == 32 - switch (this->transparent_) { + switch (this->transparency_) { case TRANSPARENCY_ALPHA_CHANNEL: this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_ALPHA; break; @@ -156,7 +156,8 @@ lv_img_dsc_t *Image::get_lv_img_dsc() { break; } #else - this->dsc_.header.cf = this->transparent_ == TRANSPARENCY_ALPHA_CHANNEL ? LV_IMG_CF_RGB565A8 : LV_IMG_CF_RGB565; + this->dsc_.header.cf = + this->transparency_ == TRANSPARENCY_ALPHA_CHANNEL ? LV_IMG_CF_RGB565A8 : LV_IMG_CF_RGB565; #endif break; } diff --git a/esphome/components/improv_base/__init__.py b/esphome/components/improv_base/__init__.py index aa75f4d89c..e175aa2220 100644 --- a/esphome/components/improv_base/__init__.py +++ b/esphome/components/improv_base/__init__.py @@ -3,6 +3,8 @@ import re import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import __version__ +from esphome.cpp_generator import MockObj +from esphome.types import ConfigType CODEOWNERS = ["@esphome/core"] @@ -35,7 +37,9 @@ def _process_next_url(url: str): return url -async def setup_improv_core(var, config): - if CONF_NEXT_URL in config: - cg.add(var.set_next_url(_process_next_url(config[CONF_NEXT_URL]))) +async def setup_improv_core(var: MockObj, config: ConfigType, component: str): + if next_url := config.get(CONF_NEXT_URL): + cg.add(var.set_next_url(_process_next_url(next_url))) + cg.add_define(f"USE_{component.upper()}_NEXT_URL") + cg.add_library("improv/Improv", "1.2.4") diff --git a/esphome/components/improv_base/improv_base.cpp b/esphome/components/improv_base/improv_base.cpp index e890187d1a..2091390f95 100644 --- a/esphome/components/improv_base/improv_base.cpp +++ b/esphome/components/improv_base/improv_base.cpp @@ -2,36 +2,50 @@ #include "esphome/components/network/util.h" #include "esphome/core/application.h" +#include "esphome/core/defines.h" namespace esphome { namespace improv_base { +#if defined(USE_ESP32_IMPROV_NEXT_URL) || defined(USE_IMPROV_SERIAL_NEXT_URL) +static constexpr const char DEVICE_NAME_PLACEHOLDER[] = "{{device_name}}"; +static constexpr size_t DEVICE_NAME_PLACEHOLDER_LEN = sizeof(DEVICE_NAME_PLACEHOLDER) - 1; +static constexpr const char IP_ADDRESS_PLACEHOLDER[] = "{{ip_address}}"; +static constexpr size_t IP_ADDRESS_PLACEHOLDER_LEN = sizeof(IP_ADDRESS_PLACEHOLDER) - 1; + +static void replace_all_in_place(std::string &str, const char *placeholder, size_t placeholder_len, + const std::string &replacement) { + size_t pos = 0; + const size_t replacement_len = replacement.length(); + while ((pos = str.find(placeholder, pos)) != std::string::npos) { + str.replace(pos, placeholder_len, replacement); + pos += replacement_len; + } +} + std::string ImprovBase::get_formatted_next_url_() { if (this->next_url_.empty()) { return ""; } - std::string copy = this->next_url_; - // Device name - std::size_t pos = this->next_url_.find("{{device_name}}"); - if (pos != std::string::npos) { - const std::string &device_name = App.get_name(); - copy.replace(pos, 15, device_name); - } - // Ip address - pos = this->next_url_.find("{{ip_address}}"); - if (pos != std::string::npos) { - for (auto &ip : network::get_ip_addresses()) { - if (ip.is_ip4()) { - std::string ipa = ip.str(); - copy.replace(pos, 14, ipa); - break; - } + std::string formatted_url = this->next_url_; + + // Replace all occurrences of {{device_name}} + replace_all_in_place(formatted_url, DEVICE_NAME_PLACEHOLDER, DEVICE_NAME_PLACEHOLDER_LEN, App.get_name()); + + // Replace all occurrences of {{ip_address}} + for (auto &ip : network::get_ip_addresses()) { + if (ip.is_ip4()) { + replace_all_in_place(formatted_url, IP_ADDRESS_PLACEHOLDER, IP_ADDRESS_PLACEHOLDER_LEN, ip.str()); + break; } } - return copy; + // Note: {{esphome_version}} is replaced at code generation time in Python + + return formatted_url; } +#endif } // namespace improv_base } // namespace esphome diff --git a/esphome/components/improv_base/improv_base.h b/esphome/components/improv_base/improv_base.h index 90cd02a4ab..e4138479df 100644 --- a/esphome/components/improv_base/improv_base.h +++ b/esphome/components/improv_base/improv_base.h @@ -1,17 +1,22 @@ #pragma once #include +#include "esphome/core/defines.h" namespace esphome { namespace improv_base { class ImprovBase { public: +#if defined(USE_ESP32_IMPROV_NEXT_URL) || defined(USE_IMPROV_SERIAL_NEXT_URL) void set_next_url(const std::string &next_url) { this->next_url_ = next_url; } +#endif protected: +#if defined(USE_ESP32_IMPROV_NEXT_URL) || defined(USE_IMPROV_SERIAL_NEXT_URL) std::string get_formatted_next_url_(); std::string next_url_; +#endif }; } // namespace improv_base diff --git a/esphome/components/improv_serial/__init__.py b/esphome/components/improv_serial/__init__.py index 568b200a85..fb2b541707 100644 --- a/esphome/components/improv_serial/__init__.py +++ b/esphome/components/improv_serial/__init__.py @@ -43,4 +43,4 @@ FINAL_VALIDATE_SCHEMA = validate_logger async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - await improv_base.setup_improv_core(var, config) + await improv_base.setup_improv_core(var, config, "improv_serial") diff --git a/esphome/components/improv_serial/improv_serial_component.cpp b/esphome/components/improv_serial/improv_serial_component.cpp index ae4927828b..70260eeab3 100644 --- a/esphome/components/improv_serial/improv_serial_component.cpp +++ b/esphome/components/improv_serial/improv_serial_component.cpp @@ -15,11 +15,10 @@ static const char *const TAG = "improv_serial"; void ImprovSerialComponent::setup() { global_improv_serial_component = this; -#ifdef USE_ARDUINO - this->hw_serial_ = logger::global_logger->get_hw_serial(); -#endif -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 this->uart_num_ = logger::global_logger->get_uart_num(); +#elif defined(USE_ARDUINO) + this->hw_serial_ = logger::global_logger->get_hw_serial(); #endif if (wifi::global_wifi_component->has_sta()) { @@ -29,18 +28,44 @@ void ImprovSerialComponent::setup() { } } +void ImprovSerialComponent::loop() { + if (this->last_read_byte_ && (millis() - this->last_read_byte_ > IMPROV_SERIAL_TIMEOUT)) { + this->last_read_byte_ = 0; + this->rx_buffer_.clear(); + ESP_LOGV(TAG, "Timeout"); + } + + auto byte = this->read_byte_(); + while (byte.has_value()) { + if (this->parse_improv_serial_byte_(byte.value())) { + this->last_read_byte_ = millis(); + } else { + this->last_read_byte_ = 0; + this->rx_buffer_.clear(); + } + byte = this->read_byte_(); + } + + if (this->state_ == improv::STATE_PROVISIONING) { + if (wifi::global_wifi_component->is_connected()) { + wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(), + this->connecting_sta_.get_password()); + this->connecting_sta_ = {}; + this->cancel_timeout("wifi-connect-timeout"); + this->set_state_(improv::STATE_PROVISIONED); + + std::vector url = this->build_rpc_settings_response_(improv::WIFI_SETTINGS); + this->send_response_(url); + } + } +} + void ImprovSerialComponent::dump_config() { ESP_LOGCONFIG(TAG, "Improv Serial:"); } optional ImprovSerialComponent::read_byte_() { optional byte; uint8_t data = 0; -#ifdef USE_ARDUINO - if (this->hw_serial_->available()) { - this->hw_serial_->readBytes(&data, 1); - byte = data; - } -#endif -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 switch (logger::global_logger->get_uart()) { case logger::UART_SELECTION_UART0: case logger::UART_SELECTION_UART1: @@ -76,82 +101,90 @@ optional ImprovSerialComponent::read_byte_() { default: break; } +#elif defined(USE_ARDUINO) + if (this->hw_serial_->available()) { + this->hw_serial_->readBytes(&data, 1); + byte = data; + } #endif return byte; } -void ImprovSerialComponent::write_data_(std::vector &data) { - data.push_back('\n'); -#ifdef USE_ARDUINO - this->hw_serial_->write(data.data(), data.size()); -#endif -#ifdef USE_ESP_IDF +void ImprovSerialComponent::write_data_(const uint8_t *data, const size_t size) { + // First, set length field + this->tx_header_[TX_LENGTH_IDX] = this->tx_header_[TX_TYPE_IDX] == TYPE_RPC_RESPONSE ? size : 1; + + const bool there_is_data = data != nullptr && size > 0; + // If there_is_data, checksum must not include our optional data byte + const uint8_t header_checksum_len = there_is_data ? TX_BUFFER_SIZE - 3 : TX_BUFFER_SIZE - 2; + // Only transmit the full buffer length if there is no data (only state/error byte is provided in this case) + const uint8_t header_tx_len = there_is_data ? TX_BUFFER_SIZE - 3 : TX_BUFFER_SIZE; + // Calculate checksum for message + uint8_t checksum = 0; + for (uint8_t i = 0; i < header_checksum_len; i++) { + checksum += this->tx_header_[i]; + } + if (there_is_data) { + // Include data in checksum + for (size_t i = 0; i < size; i++) { + checksum += data[i]; + } + } + this->tx_header_[TX_CHECKSUM_IDX] = checksum; + +#ifdef USE_ESP32 switch (logger::global_logger->get_uart()) { case logger::UART_SELECTION_UART0: case logger::UART_SELECTION_UART1: #if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32C6) && \ !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32S3) case logger::UART_SELECTION_UART2: -#endif // !USE_ESP32_VARIANT_ESP32C3 && !USE_ESP32_VARIANT_ESP32S2 && !USE_ESP32_VARIANT_ESP32S3 - uart_write_bytes(this->uart_num_, data.data(), data.size()); +#endif + uart_write_bytes(this->uart_num_, this->tx_header_, header_tx_len); + if (there_is_data) { + uart_write_bytes(this->uart_num_, data, size); + uart_write_bytes(this->uart_num_, &this->tx_header_[TX_CHECKSUM_IDX], 2); // Footer: checksum and newline + } break; #if defined(USE_LOGGER_USB_CDC) && defined(CONFIG_ESP_CONSOLE_USB_CDC) - case logger::UART_SELECTION_USB_CDC: { - const char *msg = (char *) data.data(); - esp_usb_console_write_buf(msg, data.size()); + case logger::UART_SELECTION_USB_CDC: + esp_usb_console_write_buf((const char *) this->tx_header_, header_tx_len); + if (there_is_data) { + esp_usb_console_write_buf((const char *) data, size); + esp_usb_console_write_buf((const char *) &this->tx_header_[TX_CHECKSUM_IDX], + 2); // Footer: checksum and newline + } break; - } -#endif // USE_LOGGER_USB_CDC +#endif #ifdef USE_LOGGER_USB_SERIAL_JTAG case logger::UART_SELECTION_USB_SERIAL_JTAG: - usb_serial_jtag_write_bytes((char *) data.data(), data.size(), 20 / portTICK_PERIOD_MS); - delay(10); - usb_serial_jtag_ll_txfifo_flush(); // fixes for issue in IDF 4.4.7 + usb_serial_jtag_write_bytes((const char *) this->tx_header_, header_tx_len, 20 / portTICK_PERIOD_MS); + if (there_is_data) { + usb_serial_jtag_write_bytes((const char *) data, size, 20 / portTICK_PERIOD_MS); + usb_serial_jtag_write_bytes((const char *) &this->tx_header_[TX_CHECKSUM_IDX], 2, + 20 / portTICK_PERIOD_MS); // Footer: checksum and newline + } break; -#endif // USE_LOGGER_USB_SERIAL_JTAG +#endif default: break; } +#elif defined(USE_ARDUINO) + this->hw_serial_->write(this->tx_header_, header_tx_len); + if (there_is_data) { + this->hw_serial_->write(data, size); + this->hw_serial_->write(&this->tx_header_[TX_CHECKSUM_IDX], 2); // Footer: checksum and newline + } #endif } -void ImprovSerialComponent::loop() { - if (this->last_read_byte_ && (millis() - this->last_read_byte_ > IMPROV_SERIAL_TIMEOUT)) { - this->last_read_byte_ = 0; - this->rx_buffer_.clear(); - ESP_LOGV(TAG, "Improv Serial timeout"); - } - - auto byte = this->read_byte_(); - while (byte.has_value()) { - if (this->parse_improv_serial_byte_(byte.value())) { - this->last_read_byte_ = millis(); - } else { - this->last_read_byte_ = 0; - this->rx_buffer_.clear(); - } - byte = this->read_byte_(); - } - - if (this->state_ == improv::STATE_PROVISIONING) { - if (wifi::global_wifi_component->is_connected()) { - wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(), - this->connecting_sta_.get_password()); - this->connecting_sta_ = {}; - this->cancel_timeout("wifi-connect-timeout"); - this->set_state_(improv::STATE_PROVISIONED); - - std::vector url = this->build_rpc_settings_response_(improv::WIFI_SETTINGS); - this->send_response_(url); - } - } -} - std::vector ImprovSerialComponent::build_rpc_settings_response_(improv::Command command) { std::vector urls; +#ifdef USE_IMPROV_SERIAL_NEXT_URL if (!this->next_url_.empty()) { urls.push_back(this->get_formatted_next_url_()); } +#endif #ifdef USE_WEBSERVER for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) { if (ip.is_ip4()) { @@ -178,13 +211,13 @@ std::vector ImprovSerialComponent::build_version_info_() { bool ImprovSerialComponent::parse_improv_serial_byte_(uint8_t byte) { size_t at = this->rx_buffer_.size(); this->rx_buffer_.push_back(byte); - ESP_LOGV(TAG, "Improv Serial byte: 0x%02X", byte); + ESP_LOGV(TAG, "Byte: 0x%02X", byte); const uint8_t *raw = &this->rx_buffer_[0]; return improv::parse_improv_serial_byte( at, byte, raw, [this](improv::ImprovCommand command) -> bool { return this->parse_improv_payload_(command); }, [this](improv::Error error) -> void { - ESP_LOGW(TAG, "Error decoding Improv payload"); + ESP_LOGW(TAG, "Error decoding payload"); this->set_error_(error); }); } @@ -198,9 +231,9 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command this->connecting_sta_ = sta; wifi::global_wifi_component->set_sta(sta); - wifi::global_wifi_component->start_connecting(sta, false); + wifi::global_wifi_component->start_connecting(sta); this->set_state_(improv::STATE_PROVISIONING); - ESP_LOGD(TAG, "Received Improv wifi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(), + ESP_LOGD(TAG, "Received settings: SSID=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(), command.password.c_str()); auto f = std::bind(&ImprovSerialComponent::on_wifi_connect_timeout_, this); @@ -221,7 +254,7 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command } case improv::GET_WIFI_NETWORKS: { std::vector networks; - auto results = wifi::global_wifi_component->get_scan_result(); + const auto &results = wifi::global_wifi_component->get_scan_result(); for (auto &scan : results) { if (scan.get_is_hidden()) continue; @@ -241,7 +274,7 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command return true; } default: { - ESP_LOGW(TAG, "Unknown Improv payload"); + ESP_LOGW(TAG, "Unknown payload"); this->set_error_(improv::ERROR_UNKNOWN_RPC); return false; } @@ -250,57 +283,26 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command void ImprovSerialComponent::set_state_(improv::State state) { this->state_ = state; - - std::vector data = {'I', 'M', 'P', 'R', 'O', 'V'}; - data.resize(11); - data[6] = IMPROV_SERIAL_VERSION; - data[7] = TYPE_CURRENT_STATE; - data[8] = 1; - data[9] = state; - - uint8_t checksum = 0x00; - for (uint8_t d : data) - checksum += d; - data[10] = checksum; - - this->write_data_(data); + this->tx_header_[TX_TYPE_IDX] = TYPE_CURRENT_STATE; + this->tx_header_[TX_DATA_IDX] = state; + this->write_data_(); } void ImprovSerialComponent::set_error_(improv::Error error) { - std::vector data = {'I', 'M', 'P', 'R', 'O', 'V'}; - data.resize(11); - data[6] = IMPROV_SERIAL_VERSION; - data[7] = TYPE_ERROR_STATE; - data[8] = 1; - data[9] = error; - - uint8_t checksum = 0x00; - for (uint8_t d : data) - checksum += d; - data[10] = checksum; - this->write_data_(data); + this->tx_header_[TX_TYPE_IDX] = TYPE_ERROR_STATE; + this->tx_header_[TX_DATA_IDX] = error; + this->write_data_(); } void ImprovSerialComponent::send_response_(std::vector &response) { - std::vector data = {'I', 'M', 'P', 'R', 'O', 'V'}; - data.resize(9); - data[6] = IMPROV_SERIAL_VERSION; - data[7] = TYPE_RPC_RESPONSE; - data[8] = response.size(); - data.insert(data.end(), response.begin(), response.end()); - - uint8_t checksum = 0x00; - for (uint8_t d : data) - checksum += d; - data.push_back(checksum); - - this->write_data_(data); + this->tx_header_[TX_TYPE_IDX] = TYPE_RPC_RESPONSE; + this->write_data_(response.data(), response.size()); } void ImprovSerialComponent::on_wifi_connect_timeout_() { this->set_error_(improv::ERROR_UNABLE_TO_CONNECT); this->set_state_(improv::STATE_AUTHORIZED); - ESP_LOGW(TAG, "Timed out trying to connect to given WiFi network"); + ESP_LOGW(TAG, "Timed out while connecting to Wi-Fi network"); wifi::global_wifi_component->clear_sta(); } diff --git a/esphome/components/improv_serial/improv_serial_component.h b/esphome/components/improv_serial/improv_serial_component.h index 5d2534c2fc..057247f376 100644 --- a/esphome/components/improv_serial/improv_serial_component.h +++ b/esphome/components/improv_serial/improv_serial_component.h @@ -9,10 +9,7 @@ #include #include -#ifdef USE_ARDUINO -#include -#endif -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include #if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32S3) || \ defined(USE_ESP32_VARIANT_ESP32H2) @@ -22,11 +19,23 @@ #if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) #include #endif +#elif defined(USE_ARDUINO) +#include #endif namespace esphome { namespace improv_serial { +// TX buffer layout constants +static constexpr uint8_t TX_HEADER_SIZE = 6; // Bytes 0-5 = "IMPROV" +static constexpr uint8_t TX_VERSION_IDX = 6; +static constexpr uint8_t TX_TYPE_IDX = 7; +static constexpr uint8_t TX_LENGTH_IDX = 8; +static constexpr uint8_t TX_DATA_IDX = 9; // For state/error messages only +static constexpr uint8_t TX_CHECKSUM_IDX = 10; +static constexpr uint8_t TX_NEWLINE_IDX = 11; +static constexpr uint8_t TX_BUFFER_SIZE = 12; + enum ImprovSerialType : uint8_t { TYPE_CURRENT_STATE = 0x01, TYPE_ERROR_STATE = 0x02, @@ -58,13 +67,27 @@ class ImprovSerialComponent : public Component, public improv_base::ImprovBase { std::vector build_version_info_(); optional read_byte_(); - void write_data_(std::vector &data); + void write_data_(const uint8_t *data = nullptr, size_t size = 0); -#ifdef USE_ARDUINO - Stream *hw_serial_{nullptr}; -#endif -#ifdef USE_ESP_IDF + uint8_t tx_header_[TX_BUFFER_SIZE] = { + 'I', // 0: Header + 'M', // 1: Header + 'P', // 2: Header + 'R', // 3: Header + 'O', // 4: Header + 'V', // 5: Header + IMPROV_SERIAL_VERSION, // 6: Version + 0, // 7: ImprovSerialType + 0, // 8: Length + 0, // 9...X: Data (here, one byte reserved for state/error) + 0, // X + 10: Checksum + '\n', + }; + +#ifdef USE_ESP32 uart_port_t uart_num_; +#elif defined(USE_ARDUINO) + Stream *hw_serial_{nullptr}; #endif std::vector rx_buffer_; diff --git a/esphome/components/ina2xx_base/__init__.py b/esphome/components/ina2xx_base/__init__.py index ff70f217ec..ce68ad2726 100644 --- a/esphome/components/ina2xx_base/__init__.py +++ b/esphome/components/ina2xx_base/__init__.py @@ -18,6 +18,7 @@ from esphome.const import ( DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, UNIT_AMPERE, UNIT_CELSIUS, UNIT_VOLT, @@ -34,6 +35,7 @@ CONF_CHARGE = "charge" CONF_CHARGE_COULOMBS = "charge_coulombs" CONF_ENERGY_JOULES = "energy_joules" CONF_TEMPERATURE_COEFFICIENT = "temperature_coefficient" +CONF_RESET_ON_BOOT = "reset_on_boot" UNIT_AMPERE_HOURS = "Ah" UNIT_COULOMB = "C" UNIT_JOULE = "J" @@ -112,6 +114,7 @@ INA2XX_SCHEMA = cv.Schema( cv.Optional(CONF_TEMPERATURE_COEFFICIENT, default=0): cv.int_range( min=0, max=16383 ), + cv.Optional(CONF_RESET_ON_BOOT, default=True): cv.boolean, cv.Optional(CONF_SHUNT_VOLTAGE): cv.maybe_simple_value( sensor.sensor_schema( unit_of_measurement=UNIT_MILLIVOLT, @@ -162,7 +165,7 @@ INA2XX_SCHEMA = cv.Schema( unit_of_measurement=UNIT_WATT_HOURS, accuracy_decimals=8, device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_MEASUREMENT, + state_class=STATE_CLASS_TOTAL_INCREASING, ), key=CONF_NAME, ), @@ -170,7 +173,8 @@ INA2XX_SCHEMA = cv.Schema( sensor.sensor_schema( unit_of_measurement=UNIT_JOULE, accuracy_decimals=8, - state_class=STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, ), key=CONF_NAME, ), @@ -204,6 +208,7 @@ async def setup_ina2xx(var, config): cg.add(var.set_adc_range(config[CONF_ADC_RANGE])) cg.add(var.set_adc_avg_samples(config[CONF_ADC_AVERAGING])) cg.add(var.set_shunt_tempco(config[CONF_TEMPERATURE_COEFFICIENT])) + cg.add(var.set_reset_on_boot(config[CONF_RESET_ON_BOOT])) adc_time_config = config[CONF_ADC_TIME] if isinstance(adc_time_config, dict): diff --git a/esphome/components/ina2xx_base/ina2xx_base.cpp b/esphome/components/ina2xx_base/ina2xx_base.cpp index 35a94e3989..4ab02703e8 100644 --- a/esphome/components/ina2xx_base/ina2xx_base.cpp +++ b/esphome/components/ina2xx_base/ina2xx_base.cpp @@ -257,7 +257,12 @@ bool INA2XX::reset_energy_counters() { bool INA2XX::reset_config_() { ESP_LOGV(TAG, "Reset"); ConfigurationRegister cfg{0}; - cfg.RST = true; + if (!this->reset_on_boot_) { + ESP_LOGI(TAG, "Skipping on-boot device reset"); + cfg.RST = false; + } else { + cfg.RST = true; + } return this->write_unsigned_16_(RegisterMap::REG_CONFIG, cfg.raw_u16); } diff --git a/esphome/components/ina2xx_base/ina2xx_base.h b/esphome/components/ina2xx_base/ina2xx_base.h index 261c5321bf..ba0999b28e 100644 --- a/esphome/components/ina2xx_base/ina2xx_base.h +++ b/esphome/components/ina2xx_base/ina2xx_base.h @@ -127,6 +127,7 @@ class INA2XX : public PollingComponent { void set_adc_time_die_temperature(AdcTime time) { this->adc_time_die_temperature_ = time; } void set_adc_avg_samples(AdcAvgSamples samples) { this->adc_avg_samples_ = samples; } void set_shunt_tempco(uint16_t coeff) { this->shunt_tempco_ppm_c_ = coeff; } + void set_reset_on_boot(bool reset) { this->reset_on_boot_ = reset; } void set_shunt_voltage_sensor(sensor::Sensor *sensor) { this->shunt_voltage_sensor_ = sensor; } void set_bus_voltage_sensor(sensor::Sensor *sensor) { this->bus_voltage_sensor_ = sensor; } @@ -172,6 +173,7 @@ class INA2XX : public PollingComponent { AdcTime adc_time_die_temperature_{AdcTime::ADC_TIME_4120US}; AdcAvgSamples adc_avg_samples_{AdcAvgSamples::ADC_AVG_SAMPLES_128}; uint16_t shunt_tempco_ppm_c_{0}; + bool reset_on_boot_{true}; // // Calculated coefficients diff --git a/esphome/components/ina2xx_i2c/ina2xx_i2c.cpp b/esphome/components/ina2xx_i2c/ina2xx_i2c.cpp index d28525635d..a363a9c12f 100644 --- a/esphome/components/ina2xx_i2c/ina2xx_i2c.cpp +++ b/esphome/components/ina2xx_i2c/ina2xx_i2c.cpp @@ -21,7 +21,7 @@ void INA2XXI2C::dump_config() { } bool INA2XXI2C::read_ina_register(uint8_t reg, uint8_t *data, size_t len) { - auto ret = this->read_register(reg, data, len, false); + auto ret = this->read_register(reg, data, len); if (ret != i2c::ERROR_OK) { ESP_LOGE(TAG, "read_ina_register_ failed. Reg=0x%02X Err=%d", reg, ret); } diff --git a/esphome/components/inkplate/__init__.py b/esphome/components/inkplate/__init__.py new file mode 100644 index 0000000000..1c6013793a --- /dev/null +++ b/esphome/components/inkplate/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@jesserockz", "@JosipKuci"] diff --git a/esphome/components/inkplate/const.py b/esphome/components/inkplate/const.py new file mode 100644 index 0000000000..77bf933320 --- /dev/null +++ b/esphome/components/inkplate/const.py @@ -0,0 +1,105 @@ +WAVEFORMS = { + "inkplate_6": ( + (0, 1, 1, 0, 0, 1, 1, 0, 0), + (0, 1, 2, 1, 1, 2, 1, 0, 0), + (1, 1, 1, 2, 2, 1, 0, 0, 0), + (0, 0, 0, 1, 1, 1, 2, 0, 0), + (2, 1, 1, 1, 2, 1, 2, 0, 0), + (2, 2, 1, 1, 2, 1, 2, 0, 0), + (1, 1, 1, 2, 1, 2, 2, 0, 0), + (0, 0, 0, 0, 0, 0, 2, 0, 0), + ), + "inkplate_10": ( + (0, 0, 0, 0, 0, 0, 0, 1, 0), + (0, 0, 0, 2, 2, 2, 1, 1, 0), + (0, 0, 2, 1, 1, 2, 2, 1, 0), + (0, 1, 2, 2, 1, 2, 2, 1, 0), + (0, 0, 2, 1, 2, 2, 2, 1, 0), + (0, 2, 2, 2, 2, 2, 2, 1, 0), + (0, 0, 0, 0, 0, 2, 1, 2, 0), + (0, 0, 0, 2, 2, 2, 2, 2, 0), + ), + "inkplate_6_plus": ( + (0, 0, 0, 0, 0, 2, 1, 1, 0), + (0, 0, 2, 1, 1, 1, 2, 1, 0), + (0, 2, 2, 2, 1, 1, 2, 1, 0), + (0, 0, 2, 2, 2, 1, 2, 1, 0), + (0, 0, 0, 0, 2, 2, 2, 1, 0), + (0, 0, 2, 1, 2, 1, 1, 2, 0), + (0, 0, 2, 2, 2, 1, 1, 2, 0), + (0, 0, 0, 0, 2, 2, 2, 2, 0), + ), + "inkplate_6_v2": ( + (1, 0, 1, 0, 1, 1, 1, 0, 0), + (0, 0, 0, 1, 1, 1, 1, 0, 0), + (1, 1, 1, 1, 0, 2, 1, 0, 0), + (1, 1, 1, 2, 2, 1, 1, 0, 0), + (1, 1, 1, 1, 2, 2, 1, 0, 0), + (0, 1, 1, 1, 2, 2, 1, 0, 0), + (0, 0, 0, 0, 1, 1, 2, 0, 0), + (0, 0, 0, 0, 0, 1, 2, 0, 0), + ), + "inkplate_5": ( + (0, 0, 1, 1, 0, 1, 1, 1, 0), + (0, 1, 1, 1, 1, 2, 0, 1, 0), + (1, 2, 2, 0, 2, 1, 1, 1, 0), + (1, 1, 1, 2, 0, 1, 1, 2, 0), + (0, 1, 1, 1, 2, 0, 1, 2, 0), + (0, 0, 0, 1, 1, 2, 1, 2, 0), + (1, 1, 1, 2, 0, 2, 1, 2, 0), + (0, 0, 0, 0, 0, 0, 0, 0, 0), + ), + "inkplate_5_v2": ( + (0, 0, 1, 1, 2, 1, 1, 1, 0), + (1, 1, 2, 2, 1, 2, 1, 1, 0), + (0, 1, 2, 2, 1, 1, 2, 1, 0), + (0, 0, 1, 1, 1, 1, 1, 2, 0), + (1, 2, 1, 2, 1, 1, 1, 2, 0), + (0, 1, 1, 1, 2, 0, 1, 2, 0), + (1, 1, 1, 2, 2, 2, 1, 2, 0), + (0, 0, 0, 0, 0, 0, 0, 0, 0), + ), +} + +INKPLATE_10_CUSTOM_WAVEFORMS = ( + ( + (0, 0, 0, 0, 0, 0, 0, 0, 0), + (0, 0, 0, 2, 1, 2, 1, 1, 0), + (0, 0, 0, 2, 2, 1, 2, 1, 0), + (0, 0, 2, 2, 1, 2, 2, 1, 0), + (0, 0, 0, 2, 1, 1, 1, 2, 0), + (0, 0, 2, 2, 2, 1, 1, 2, 0), + (0, 0, 0, 0, 0, 1, 2, 2, 0), + (0, 0, 0, 0, 2, 2, 2, 2, 0), + ), + ( + (0, 3, 3, 3, 3, 3, 3, 3, 0), + (0, 1, 2, 1, 1, 2, 2, 1, 0), + (0, 2, 2, 2, 1, 2, 2, 1, 0), + (0, 0, 2, 2, 2, 2, 2, 1, 0), + (0, 3, 3, 2, 1, 1, 1, 2, 0), + (0, 3, 3, 2, 2, 1, 1, 2, 0), + (0, 2, 1, 2, 1, 2, 1, 2, 0), + (0, 3, 3, 3, 2, 2, 2, 2, 0), + ), + ( + (0, 0, 0, 0, 0, 0, 0, 1, 0), + (0, 0, 0, 2, 2, 2, 1, 1, 0), + (0, 0, 2, 1, 1, 2, 2, 1, 0), + (1, 1, 2, 2, 1, 2, 2, 1, 0), + (0, 0, 2, 1, 2, 2, 2, 1, 0), + (0, 1, 2, 2, 2, 2, 2, 1, 0), + (0, 0, 0, 2, 2, 2, 1, 2, 0), + (0, 0, 0, 2, 2, 2, 2, 2, 0), + ), + ( + (0, 0, 0, 0, 0, 0, 0, 1, 0), + (0, 0, 0, 2, 2, 2, 1, 1, 0), + (2, 2, 2, 1, 0, 2, 1, 0, 0), + (2, 1, 1, 2, 1, 1, 1, 2, 0), + (2, 2, 2, 1, 1, 1, 0, 2, 0), + (2, 2, 2, 1, 1, 2, 1, 2, 0), + (0, 0, 0, 0, 2, 1, 2, 2, 0), + (0, 0, 0, 0, 2, 2, 2, 2, 0), + ), +) diff --git a/esphome/components/inkplate/display.py b/esphome/components/inkplate/display.py new file mode 100644 index 0000000000..47c8c898e5 --- /dev/null +++ b/esphome/components/inkplate/display.py @@ -0,0 +1,246 @@ +from esphome import pins +import esphome.codegen as cg +from esphome.components import display, i2c +from esphome.components.esp32 import CONF_CPU_FREQUENCY +import esphome.config_validation as cv +from esphome.const import ( + CONF_FULL_UPDATE_EVERY, + CONF_ID, + CONF_IGNORE_STRAPPING_WARNING, + CONF_LAMBDA, + CONF_MIRROR_X, + CONF_MIRROR_Y, + CONF_MODEL, + CONF_NUMBER, + CONF_OE_PIN, + CONF_PAGES, + CONF_TRANSFORM, + CONF_WAKEUP_PIN, + PLATFORM_ESP32, +) +import esphome.final_validate as fv + +from .const import INKPLATE_10_CUSTOM_WAVEFORMS, WAVEFORMS + +DEPENDENCIES = ["i2c", "esp32", "psram"] + +CONF_DISPLAY_DATA_0_PIN = "display_data_0_pin" +CONF_DISPLAY_DATA_1_PIN = "display_data_1_pin" +CONF_DISPLAY_DATA_2_PIN = "display_data_2_pin" +CONF_DISPLAY_DATA_3_PIN = "display_data_3_pin" +CONF_DISPLAY_DATA_4_PIN = "display_data_4_pin" +CONF_DISPLAY_DATA_5_PIN = "display_data_5_pin" +CONF_DISPLAY_DATA_6_PIN = "display_data_6_pin" +CONF_DISPLAY_DATA_7_PIN = "display_data_7_pin" + +CONF_CL_PIN = "cl_pin" +CONF_CKV_PIN = "ckv_pin" +CONF_GREYSCALE = "greyscale" +CONF_GMOD_PIN = "gmod_pin" +CONF_GPIO0_ENABLE_PIN = "gpio0_enable_pin" +CONF_LE_PIN = "le_pin" +CONF_PARTIAL_UPDATING = "partial_updating" +CONF_POWERUP_PIN = "powerup_pin" +CONF_SPH_PIN = "sph_pin" +CONF_SPV_PIN = "spv_pin" +CONF_VCOM_PIN = "vcom_pin" + +inkplate_ns = cg.esphome_ns.namespace("inkplate") +Inkplate = inkplate_ns.class_( + "Inkplate", + cg.PollingComponent, + i2c.I2CDevice, + display.Display, + display.DisplayBuffer, +) + +InkplateModel = inkplate_ns.enum("InkplateModel") + +MODELS = { + "inkplate_6": InkplateModel.INKPLATE_6, + "inkplate_10": InkplateModel.INKPLATE_10, + "inkplate_6_plus": InkplateModel.INKPLATE_6_PLUS, + "inkplate_6_v2": InkplateModel.INKPLATE_6_V2, + "inkplate_5": InkplateModel.INKPLATE_5, + "inkplate_5_v2": InkplateModel.INKPLATE_5_V2, +} + +CONF_CUSTOM_WAVEFORM = "custom_waveform" + + +def _validate_custom_waveform(config): + if CONF_CUSTOM_WAVEFORM in config and config[CONF_MODEL] != "inkplate_10": + raise cv.Invalid("Custom waveforms are only supported on the Inkplate 10") + return config + + +CONFIG_SCHEMA = cv.All( + display.FULL_DISPLAY_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(Inkplate), + cv.Optional(CONF_GREYSCALE, default=False): cv.boolean, + cv.Optional(CONF_CUSTOM_WAVEFORM): cv.All( + cv.uint8_t, cv.Range(min=1, max=len(INKPLATE_10_CUSTOM_WAVEFORMS)) + ), + cv.Optional(CONF_TRANSFORM): cv.Schema( + { + cv.Optional(CONF_MIRROR_X, default=False): cv.boolean, + cv.Optional(CONF_MIRROR_Y, default=False): cv.boolean, + } + ), + cv.Optional(CONF_PARTIAL_UPDATING, default=True): cv.boolean, + cv.Optional(CONF_FULL_UPDATE_EVERY, default=10): cv.uint32_t, + cv.Optional(CONF_MODEL, default="inkplate_6"): cv.enum( + MODELS, lower=True, space="_" + ), + # Control pins + cv.Required(CONF_CKV_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_GMOD_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_GPIO0_ENABLE_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_OE_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_POWERUP_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_SPH_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_SPV_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_VCOM_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_WAKEUP_PIN): pins.gpio_output_pin_schema, + cv.Optional( + CONF_CL_PIN, + default={CONF_NUMBER: 0, CONF_IGNORE_STRAPPING_WARNING: True}, + ): pins.internal_gpio_output_pin_schema, + cv.Optional( + CONF_LE_PIN, + default={CONF_NUMBER: 2, CONF_IGNORE_STRAPPING_WARNING: True}, + ): pins.internal_gpio_output_pin_schema, + # Data pins + cv.Optional( + CONF_DISPLAY_DATA_0_PIN, default=4 + ): pins.internal_gpio_output_pin_schema, + cv.Optional( + CONF_DISPLAY_DATA_1_PIN, + default={CONF_NUMBER: 5, CONF_IGNORE_STRAPPING_WARNING: True}, + ): pins.internal_gpio_output_pin_schema, + cv.Optional( + CONF_DISPLAY_DATA_2_PIN, default=18 + ): pins.internal_gpio_output_pin_schema, + cv.Optional( + CONF_DISPLAY_DATA_3_PIN, default=19 + ): pins.internal_gpio_output_pin_schema, + cv.Optional( + CONF_DISPLAY_DATA_4_PIN, default=23 + ): pins.internal_gpio_output_pin_schema, + cv.Optional( + CONF_DISPLAY_DATA_5_PIN, default=25 + ): pins.internal_gpio_output_pin_schema, + cv.Optional( + CONF_DISPLAY_DATA_6_PIN, default=26 + ): pins.internal_gpio_output_pin_schema, + cv.Optional( + CONF_DISPLAY_DATA_7_PIN, default=27 + ): pins.internal_gpio_output_pin_schema, + } + ) + .extend(cv.polling_component_schema("5s")) + .extend(i2c.i2c_device_schema(0x48)), + cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), + _validate_custom_waveform, +) + + +def _validate_cpu_frequency(config): + esp32_config = fv.full_config.get()[PLATFORM_ESP32] + if esp32_config[CONF_CPU_FREQUENCY] != "240MHZ": + raise cv.Invalid( + "Inkplate requires 240MHz CPU frequency (set in esp32 component)" + ) + return config + + +FINAL_VALIDATE_SCHEMA = _validate_cpu_frequency + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + + await display.register_display(var, config) + await i2c.register_i2c_device(var, config) + + if CONF_LAMBDA in config: + lambda_ = await cg.process_lambda( + config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void + ) + cg.add(var.set_writer(lambda_)) + + cg.add(var.set_greyscale(config[CONF_GREYSCALE])) + if transform := config.get(CONF_TRANSFORM): + cg.add(var.set_mirror_x(transform[CONF_MIRROR_X])) + cg.add(var.set_mirror_y(transform[CONF_MIRROR_Y])) + cg.add(var.set_partial_updating(config[CONF_PARTIAL_UPDATING])) + cg.add(var.set_full_update_every(config[CONF_FULL_UPDATE_EVERY])) + + cg.add(var.set_model(config[CONF_MODEL])) + + if custom_waveform := config.get(CONF_CUSTOM_WAVEFORM): + waveform = INKPLATE_10_CUSTOM_WAVEFORMS[custom_waveform - 1] + waveform = [element for tupl in waveform for element in tupl] + cg.add(var.set_waveform(waveform, True)) + else: + waveform = WAVEFORMS[config[CONF_MODEL]] + waveform = [element for tupl in waveform for element in tupl] + cg.add(var.set_waveform(waveform, False)) + + ckv = await cg.gpio_pin_expression(config[CONF_CKV_PIN]) + cg.add(var.set_ckv_pin(ckv)) + + gmod = await cg.gpio_pin_expression(config[CONF_GMOD_PIN]) + cg.add(var.set_gmod_pin(gmod)) + + gpio0_enable = await cg.gpio_pin_expression(config[CONF_GPIO0_ENABLE_PIN]) + cg.add(var.set_gpio0_enable_pin(gpio0_enable)) + + oe = await cg.gpio_pin_expression(config[CONF_OE_PIN]) + cg.add(var.set_oe_pin(oe)) + + powerup = await cg.gpio_pin_expression(config[CONF_POWERUP_PIN]) + cg.add(var.set_powerup_pin(powerup)) + + sph = await cg.gpio_pin_expression(config[CONF_SPH_PIN]) + cg.add(var.set_sph_pin(sph)) + + spv = await cg.gpio_pin_expression(config[CONF_SPV_PIN]) + cg.add(var.set_spv_pin(spv)) + + vcom = await cg.gpio_pin_expression(config[CONF_VCOM_PIN]) + cg.add(var.set_vcom_pin(vcom)) + + wakeup = await cg.gpio_pin_expression(config[CONF_WAKEUP_PIN]) + cg.add(var.set_wakeup_pin(wakeup)) + + cl = await cg.gpio_pin_expression(config[CONF_CL_PIN]) + cg.add(var.set_cl_pin(cl)) + + le = await cg.gpio_pin_expression(config[CONF_LE_PIN]) + cg.add(var.set_le_pin(le)) + + display_data_0 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_0_PIN]) + cg.add(var.set_display_data_0_pin(display_data_0)) + + display_data_1 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_1_PIN]) + cg.add(var.set_display_data_1_pin(display_data_1)) + + display_data_2 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_2_PIN]) + cg.add(var.set_display_data_2_pin(display_data_2)) + + display_data_3 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_3_PIN]) + cg.add(var.set_display_data_3_pin(display_data_3)) + + display_data_4 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_4_PIN]) + cg.add(var.set_display_data_4_pin(display_data_4)) + + display_data_5 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_5_PIN]) + cg.add(var.set_display_data_5_pin(display_data_5)) + + display_data_6 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_6_PIN]) + cg.add(var.set_display_data_6_pin(display_data_6)) + + display_data_7 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_7_PIN]) + cg.add(var.set_display_data_7_pin(display_data_7)) diff --git a/esphome/components/inkplate6/inkplate.cpp b/esphome/components/inkplate/inkplate.cpp similarity index 82% rename from esphome/components/inkplate6/inkplate.cpp rename to esphome/components/inkplate/inkplate.cpp index b3d0b87e83..f96fb6905e 100644 --- a/esphome/components/inkplate6/inkplate.cpp +++ b/esphome/components/inkplate/inkplate.cpp @@ -6,11 +6,11 @@ #include namespace esphome { -namespace inkplate6 { +namespace inkplate { static const char *const TAG = "inkplate"; -void Inkplate6::setup() { +void Inkplate::setup() { for (uint32_t i = 0; i < 256; i++) { this->pin_lut_[i] = ((i & 0b00000011) << 4) | (((i & 0b00001100) >> 2) << 18) | (((i & 0b00010000) >> 4) << 23) | (((i & 0b11100000) >> 5) << 25); @@ -56,7 +56,7 @@ void Inkplate6::setup() { /** * Allocate buffers. May be called after setup to re-initialise if e.g. greyscale is changed. */ -void Inkplate6::initialize_() { +void Inkplate::initialize_() { RAMAllocator allocator; RAMAllocator allocator32; uint32_t buffer_size = this->get_buffer_length_(); @@ -81,29 +81,25 @@ void Inkplate6::initialize_() { return; } if (this->greyscale_) { - uint8_t glut_size = 9; - - this->glut_ = allocator32.allocate(256 * glut_size); + this->glut_ = allocator32.allocate(256 * GLUT_SIZE); if (this->glut_ == nullptr) { ESP_LOGE(TAG, "Could not allocate glut!"); this->mark_failed(); return; } - this->glut2_ = allocator32.allocate(256 * glut_size); + this->glut2_ = allocator32.allocate(256 * GLUT_SIZE); if (this->glut2_ == nullptr) { ESP_LOGE(TAG, "Could not allocate glut2!"); this->mark_failed(); return; } - const auto *const waveform3_bit = waveform3BitAll[this->model_]; - - for (int i = 0; i < glut_size; i++) { + for (uint8_t i = 0; i < GLUT_SIZE; i++) { for (uint32_t j = 0; j < 256; j++) { - uint8_t z = (waveform3_bit[j & 0x07][i] << 2) | (waveform3_bit[(j >> 4) & 0x07][i]); + uint8_t z = (this->waveform_[j & 0x07][i] << 2) | (this->waveform_[(j >> 4) & 0x07][i]); this->glut_[i * 256 + j] = ((z & 0b00000011) << 4) | (((z & 0b00001100) >> 2) << 18) | (((z & 0b00010000) >> 4) << 23) | (((z & 0b11100000) >> 5) << 25); - z = ((waveform3_bit[j & 0x07][i] << 2) | (waveform3_bit[(j >> 4) & 0x07][i])) << 4; + z = ((this->waveform_[j & 0x07][i] << 2) | (this->waveform_[(j >> 4) & 0x07][i])) << 4; this->glut2_[i * 256 + j] = ((z & 0b00000011) << 4) | (((z & 0b00001100) >> 2) << 18) | (((z & 0b00010000) >> 4) << 23) | (((z & 0b11100000) >> 5) << 25); } @@ -130,9 +126,9 @@ void Inkplate6::initialize_() { memset(this->buffer_, 0, buffer_size); } -float Inkplate6::get_setup_priority() const { return setup_priority::PROCESSOR; } +float Inkplate::get_setup_priority() const { return setup_priority::PROCESSOR; } -size_t Inkplate6::get_buffer_length_() { +size_t Inkplate::get_buffer_length_() { if (this->greyscale_) { return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) / 2u; } else { @@ -140,7 +136,7 @@ size_t Inkplate6::get_buffer_length_() { } } -void Inkplate6::update() { +void Inkplate::update() { this->do_update_(); if (this->full_update_every_ > 0 && this->partial_updates_ >= this->full_update_every_) { @@ -150,7 +146,7 @@ void Inkplate6::update() { this->display(); } -void HOT Inkplate6::draw_absolute_pixel_internal(int x, int y, Color color) { +void HOT Inkplate::draw_absolute_pixel_internal(int x, int y, Color color) { if (x >= this->get_width_internal() || y >= this->get_height_internal() || x < 0 || y < 0) return; @@ -171,18 +167,18 @@ void HOT Inkplate6::draw_absolute_pixel_internal(int x, int y, Color color) { // uint8_t gs = (uint8_t)(px*7); uint8_t gs = ((color.red * 2126 / 10000) + (color.green * 7152 / 10000) + (color.blue * 722 / 10000)) >> 5; - this->buffer_[pos] = (pixelMaskGLUT[x_sub] & current) | (x_sub ? gs : gs << 4); + this->buffer_[pos] = (PIXEL_MASK_GLUT[x_sub] & current) | (x_sub ? gs : gs << 4); } else { int x1 = x / 8; int x_sub = x % 8; uint32_t pos = (x1 + y * (this->get_width_internal() / 8)); uint8_t current = this->partial_buffer_[pos]; - this->partial_buffer_[pos] = (~pixelMaskLUT[x_sub] & current) | (color.is_on() ? 0 : pixelMaskLUT[x_sub]); + this->partial_buffer_[pos] = (~PIXEL_MASK_LUT[x_sub] & current) | (color.is_on() ? 0 : PIXEL_MASK_LUT[x_sub]); } } -void Inkplate6::dump_config() { +void Inkplate::dump_config() { LOG_DISPLAY("", "Inkplate", this); ESP_LOGCONFIG(TAG, " Greyscale: %s\n" @@ -214,7 +210,7 @@ void Inkplate6::dump_config() { LOG_UPDATE_INTERVAL(this); } -void Inkplate6::eink_off_() { +void Inkplate::eink_off_() { ESP_LOGV(TAG, "Eink off called"); if (!panel_on_) return; @@ -242,7 +238,7 @@ void Inkplate6::eink_off_() { pins_z_state_(); } -void Inkplate6::eink_on_() { +void Inkplate::eink_on_() { ESP_LOGV(TAG, "Eink on called"); if (panel_on_) return; @@ -284,7 +280,7 @@ void Inkplate6::eink_on_() { this->oe_pin_->digital_write(true); } -bool Inkplate6::read_power_status_() { +bool Inkplate::read_power_status_() { uint8_t data; auto err = this->read_register(0x0F, &data, 1); if (err == i2c::ERROR_OK) { @@ -293,7 +289,7 @@ bool Inkplate6::read_power_status_() { return false; } -void Inkplate6::fill(Color color) { +void Inkplate::fill(Color color) { ESP_LOGV(TAG, "Fill called"); uint32_t start_time = millis(); @@ -308,7 +304,7 @@ void Inkplate6::fill(Color color) { ESP_LOGV(TAG, "Fill finished (%ums)", millis() - start_time); } -void Inkplate6::display() { +void Inkplate::display() { ESP_LOGV(TAG, "Display called"); uint32_t start_time = millis(); @@ -324,7 +320,7 @@ void Inkplate6::display() { ESP_LOGV(TAG, "Display finished (full) (%ums)", millis() - start_time); } -void Inkplate6::display1b_() { +void Inkplate::display1b_() { ESP_LOGV(TAG, "Display1b called"); uint32_t start_time = millis(); @@ -334,32 +330,71 @@ void Inkplate6::display1b_() { uint8_t buffer_value; const uint8_t *buffer_ptr; eink_on_(); - if (this->model_ == INKPLATE_6_PLUS) { - clean_fast_(0, 1); - clean_fast_(1, 15); - clean_fast_(2, 1); - clean_fast_(0, 5); - clean_fast_(2, 1); - clean_fast_(1, 15); - } else { - clean_fast_(0, 1); - clean_fast_(1, 21); - clean_fast_(2, 1); - clean_fast_(0, 12); - clean_fast_(2, 1); - clean_fast_(1, 21); - clean_fast_(2, 1); - clean_fast_(0, 12); - clean_fast_(2, 1); + uint8_t rep = 4; + switch (this->model_) { + case INKPLATE_10: + clean_fast_(0, 1); + clean_fast_(1, 10); + clean_fast_(2, 1); + clean_fast_(0, 10); + clean_fast_(2, 1); + clean_fast_(1, 10); + clean_fast_(2, 1); + clean_fast_(0, 10); + rep = 5; + break; + case INKPLATE_6_PLUS: + clean_fast_(0, 1); + clean_fast_(1, 15); + clean_fast_(2, 1); + clean_fast_(0, 5); + clean_fast_(2, 1); + clean_fast_(1, 15); + break; + case INKPLATE_6: + case INKPLATE_6_V2: + clean_fast_(0, 1); + clean_fast_(1, 18); + clean_fast_(2, 1); + clean_fast_(0, 18); + clean_fast_(2, 1); + clean_fast_(1, 18); + clean_fast_(2, 1); + clean_fast_(0, 18); + clean_fast_(2, 1); + if (this->model_ == INKPLATE_6_V2) + rep = 5; + break; + case INKPLATE_5: + clean_fast_(0, 1); + clean_fast_(1, 14); + clean_fast_(2, 1); + clean_fast_(0, 14); + clean_fast_(2, 1); + clean_fast_(1, 14); + clean_fast_(2, 1); + clean_fast_(0, 14); + clean_fast_(2, 1); + rep = 5; + break; + case INKPLATE_5_V2: + clean_fast_(0, 1); + clean_fast_(1, 11); + clean_fast_(2, 1); + clean_fast_(0, 11); + clean_fast_(2, 1); + clean_fast_(1, 11); + clean_fast_(2, 1); + clean_fast_(0, 11); + rep = 3; + break; } uint32_t clock = (1 << this->cl_pin_->get_pin()); uint32_t data_mask = this->get_data_pin_mask_(); ESP_LOGV(TAG, "Display1b start loops (%ums)", millis() - start_time); - int rep = (this->model_ == INKPLATE_6_V2) ? 5 : 4; - - for (int k = 0; k < rep; k++) { + for (uint8_t k = 0; k < rep; k++) { buffer_ptr = &this->buffer_[this->get_buffer_length_() - 1]; vscan_start_(); for (int i = 0, im = this->get_height_internal(); i < im; i++) { @@ -452,28 +487,75 @@ void Inkplate6::display1b_() { ESP_LOGV(TAG, "Display1b finished (%ums)", millis() - start_time); } -void Inkplate6::display3b_() { +void Inkplate::display3b_() { ESP_LOGV(TAG, "Display3b called"); uint32_t start_time = millis(); eink_on_(); - if (this->model_ == INKPLATE_6_PLUS) { - clean_fast_(0, 1); - clean_fast_(1, 15); - clean_fast_(2, 1); - clean_fast_(0, 5); - clean_fast_(2, 1); - clean_fast_(1, 15); - } else { - clean_fast_(0, 1); - clean_fast_(1, 21); - clean_fast_(2, 1); - clean_fast_(0, 12); - clean_fast_(2, 1); - clean_fast_(1, 21); - clean_fast_(2, 1); - clean_fast_(0, 12); - clean_fast_(2, 1); + + switch (this->model_) { + case INKPLATE_10: + if (this->custom_waveform_) { + clean_fast_(1, 1); + clean_fast_(0, 7); + clean_fast_(2, 1); + clean_fast_(1, 12); + clean_fast_(2, 1); + clean_fast_(0, 7); + clean_fast_(2, 1); + clean_fast_(1, 12); + } else { + clean_fast_(1, 1); + clean_fast_(0, 10); + clean_fast_(2, 1); + clean_fast_(1, 10); + clean_fast_(2, 1); + clean_fast_(0, 10); + clean_fast_(2, 1); + clean_fast_(1, 10); + } + break; + case INKPLATE_6_PLUS: + clean_fast_(0, 1); + clean_fast_(1, 15); + clean_fast_(2, 1); + clean_fast_(0, 5); + clean_fast_(2, 1); + clean_fast_(1, 15); + break; + case INKPLATE_6: + case INKPLATE_6_V2: + clean_fast_(0, 1); + clean_fast_(1, 18); + clean_fast_(2, 1); + clean_fast_(0, 18); + clean_fast_(2, 1); + clean_fast_(1, 18); + clean_fast_(2, 1); + clean_fast_(0, 18); + clean_fast_(2, 1); + break; + case INKPLATE_5: + clean_fast_(0, 1); + clean_fast_(1, 14); + clean_fast_(2, 1); + clean_fast_(0, 14); + clean_fast_(2, 1); + clean_fast_(1, 14); + clean_fast_(2, 1); + clean_fast_(0, 14); + clean_fast_(2, 1); + break; + case INKPLATE_5_V2: + clean_fast_(0, 1); + clean_fast_(1, 11); + clean_fast_(2, 1); + clean_fast_(0, 11); + clean_fast_(2, 1); + clean_fast_(1, 11); + clean_fast_(2, 1); + clean_fast_(0, 11); + break; } uint32_t clock = (1 << this->cl_pin_->get_pin()); @@ -518,7 +600,7 @@ void Inkplate6::display3b_() { ESP_LOGV(TAG, "Display3b finished (%ums)", millis() - start_time); } -bool Inkplate6::partial_update_() { +bool Inkplate::partial_update_() { ESP_LOGV(TAG, "Partial update called"); uint32_t start_time = millis(); if (this->greyscale_) @@ -560,7 +642,7 @@ bool Inkplate6::partial_update_() { GPIO.out_w1ts = this->pin_lut_[data] | clock; GPIO.out_w1tc = data_mask | clock; } - // New Inkplate6 panel doesn't need last clock + // New Inkplate panel doesn't need last clock if (this->model_ != INKPLATE_6_V2) { GPIO.out_w1ts = clock; GPIO.out_w1tc = data_mask | clock; @@ -580,7 +662,7 @@ bool Inkplate6::partial_update_() { return true; } -void Inkplate6::vscan_start_() { +void Inkplate::vscan_start_() { this->ckv_pin_->digital_write(true); delayMicroseconds(7); this->spv_pin_->digital_write(false); @@ -604,7 +686,7 @@ void Inkplate6::vscan_start_() { this->ckv_pin_->digital_write(true); } -void Inkplate6::hscan_start_(uint32_t d) { +void Inkplate::hscan_start_(uint32_t d) { uint8_t clock = (1 << this->cl_pin_->get_pin()); this->sph_pin_->digital_write(false); GPIO.out_w1ts = d | clock; @@ -613,14 +695,14 @@ void Inkplate6::hscan_start_(uint32_t d) { this->ckv_pin_->digital_write(true); } -void Inkplate6::vscan_end_() { +void Inkplate::vscan_end_() { this->ckv_pin_->digital_write(false); this->le_pin_->digital_write(true); this->le_pin_->digital_write(false); delayMicroseconds(0); } -void Inkplate6::clean() { +void Inkplate::clean() { ESP_LOGV(TAG, "Clean called"); uint32_t start_time = millis(); @@ -634,7 +716,7 @@ void Inkplate6::clean() { ESP_LOGV(TAG, "Clean finished (%ums)", millis() - start_time); } -void Inkplate6::clean_fast_(uint8_t c, uint8_t rep) { +void Inkplate::clean_fast_(uint8_t c, uint8_t rep) { ESP_LOGV(TAG, "Clean fast called with: (%d, %d)", c, rep); uint32_t start_time = millis(); @@ -666,7 +748,7 @@ void Inkplate6::clean_fast_(uint8_t c, uint8_t rep) { GPIO.out_w1ts = clock; GPIO.out_w1tc = clock; } - // New Inkplate6 panel doesn't need last clock + // New Inkplate panel doesn't need last clock if (this->model_ != INKPLATE_6_V2) { GPIO.out_w1ts = send | clock; GPIO.out_w1tc = clock; @@ -679,7 +761,7 @@ void Inkplate6::clean_fast_(uint8_t c, uint8_t rep) { ESP_LOGV(TAG, "Clean fast finished (%ums)", millis() - start_time); } -void Inkplate6::pins_z_state_() { +void Inkplate::pins_z_state_() { this->cl_pin_->pin_mode(gpio::FLAG_INPUT); this->le_pin_->pin_mode(gpio::FLAG_INPUT); this->ckv_pin_->pin_mode(gpio::FLAG_INPUT); @@ -699,7 +781,7 @@ void Inkplate6::pins_z_state_() { this->display_data_7_pin_->pin_mode(gpio::FLAG_INPUT); } -void Inkplate6::pins_as_outputs_() { +void Inkplate::pins_as_outputs_() { this->cl_pin_->pin_mode(gpio::FLAG_OUTPUT); this->le_pin_->pin_mode(gpio::FLAG_OUTPUT); this->ckv_pin_->pin_mode(gpio::FLAG_OUTPUT); @@ -719,5 +801,5 @@ void Inkplate6::pins_as_outputs_() { this->display_data_7_pin_->pin_mode(gpio::FLAG_OUTPUT); } -} // namespace inkplate6 +} // namespace inkplate } // namespace esphome diff --git a/esphome/components/inkplate6/inkplate.h b/esphome/components/inkplate/inkplate.h similarity index 56% rename from esphome/components/inkplate6/inkplate.h rename to esphome/components/inkplate/inkplate.h index d8918bdf2a..fb4674b522 100644 --- a/esphome/components/inkplate6/inkplate.h +++ b/esphome/components/inkplate/inkplate.h @@ -5,8 +5,10 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" +#include + namespace esphome { -namespace inkplate6 { +namespace inkplate { enum InkplateModel : uint8_t { INKPLATE_6 = 0, @@ -17,79 +19,35 @@ enum InkplateModel : uint8_t { INKPLATE_5_V2 = 5, }; -class Inkplate6 : public display::DisplayBuffer, public i2c::I2CDevice { +static constexpr uint8_t GLUT_SIZE = 9; +static constexpr uint8_t GLUT_COUNT = 8; + +static constexpr uint8_t LUT2[16] = {0xAA, 0xA9, 0xA6, 0xA5, 0x9A, 0x99, 0x96, 0x95, + 0x6A, 0x69, 0x66, 0x65, 0x5A, 0x59, 0x56, 0x55}; +static constexpr uint8_t LUTW[16] = {0xFF, 0xFE, 0xFB, 0xFA, 0xEF, 0xEE, 0xEB, 0xEA, + 0xBF, 0xBE, 0xBB, 0xBA, 0xAF, 0xAE, 0xAB, 0xAA}; +static constexpr uint8_t LUTB[16] = {0xFF, 0xFD, 0xF7, 0xF5, 0xDF, 0xDD, 0xD7, 0xD5, + 0x7F, 0x7D, 0x77, 0x75, 0x5F, 0x5D, 0x57, 0x55}; + +static constexpr uint8_t PIXEL_MASK_LUT[8] = {0x1, 0x2, 0x4, 0x8, 0x10, 0x20, 0x40, 0x80}; +static constexpr uint8_t PIXEL_MASK_GLUT[2] = {0x0F, 0xF0}; + +class Inkplate : public display::DisplayBuffer, public i2c::I2CDevice { public: - const uint8_t LUT2[16] = {0xAA, 0xA9, 0xA6, 0xA5, 0x9A, 0x99, 0x96, 0x95, - 0x6A, 0x69, 0x66, 0x65, 0x5A, 0x59, 0x56, 0x55}; - const uint8_t LUTW[16] = {0xFF, 0xFE, 0xFB, 0xFA, 0xEF, 0xEE, 0xEB, 0xEA, - 0xBF, 0xBE, 0xBB, 0xBA, 0xAF, 0xAE, 0xAB, 0xAA}; - const uint8_t LUTB[16] = {0xFF, 0xFD, 0xF7, 0xF5, 0xDF, 0xDD, 0xD7, 0xD5, - 0x7F, 0x7D, 0x77, 0x75, 0x5F, 0x5D, 0x57, 0x55}; - - const uint8_t pixelMaskLUT[8] = {0x1, 0x2, 0x4, 0x8, 0x10, 0x20, 0x40, 0x80}; - const uint8_t pixelMaskGLUT[2] = {0x0F, 0xF0}; - - const uint8_t waveform3BitAll[6][8][9] = {// INKPLATE_6 - {{0, 1, 1, 0, 0, 1, 1, 0, 0}, - {0, 1, 2, 1, 1, 2, 1, 0, 0}, - {1, 1, 1, 2, 2, 1, 0, 0, 0}, - {0, 0, 0, 1, 1, 1, 2, 0, 0}, - {2, 1, 1, 1, 2, 1, 2, 0, 0}, - {2, 2, 1, 1, 2, 1, 2, 0, 0}, - {1, 1, 1, 2, 1, 2, 2, 0, 0}, - {0, 0, 0, 0, 0, 0, 2, 0, 0}}, - // INKPLATE_10 - {{0, 0, 0, 0, 0, 0, 0, 1, 0}, - {0, 0, 0, 2, 2, 2, 1, 1, 0}, - {0, 0, 2, 1, 1, 2, 2, 1, 0}, - {0, 1, 2, 2, 1, 2, 2, 1, 0}, - {0, 0, 2, 1, 2, 2, 2, 1, 0}, - {0, 2, 2, 2, 2, 2, 2, 1, 0}, - {0, 0, 0, 0, 0, 2, 1, 2, 0}, - {0, 0, 0, 2, 2, 2, 2, 2, 0}}, - // INKPLATE_6_PLUS - {{0, 0, 0, 0, 0, 2, 1, 1, 0}, - {0, 0, 2, 1, 1, 1, 2, 1, 0}, - {0, 2, 2, 2, 1, 1, 2, 1, 0}, - {0, 0, 2, 2, 2, 1, 2, 1, 0}, - {0, 0, 0, 0, 2, 2, 2, 1, 0}, - {0, 0, 2, 1, 2, 1, 1, 2, 0}, - {0, 0, 2, 2, 2, 1, 1, 2, 0}, - {0, 0, 0, 0, 2, 2, 2, 2, 0}}, - // INKPLATE_6_V2 - {{1, 0, 1, 0, 1, 1, 1, 0, 0}, - {0, 0, 0, 1, 1, 1, 1, 0, 0}, - {1, 1, 1, 1, 0, 2, 1, 0, 0}, - {1, 1, 1, 2, 2, 1, 1, 0, 0}, - {1, 1, 1, 1, 2, 2, 1, 0, 0}, - {0, 1, 1, 1, 2, 2, 1, 0, 0}, - {0, 0, 0, 0, 1, 1, 2, 0, 0}, - {0, 0, 0, 0, 0, 1, 2, 0, 0}}, - // INKPLATE_5 - {{0, 0, 1, 1, 0, 1, 1, 1, 0}, - {0, 1, 1, 1, 1, 2, 0, 1, 0}, - {1, 2, 2, 0, 2, 1, 1, 1, 0}, - {1, 1, 1, 2, 0, 1, 1, 2, 0}, - {0, 1, 1, 1, 2, 0, 1, 2, 0}, - {0, 0, 0, 1, 1, 2, 1, 2, 0}, - {1, 1, 1, 2, 0, 2, 1, 2, 0}, - {0, 0, 0, 0, 0, 0, 0, 0, 0}}, - // INKPLATE_5_V2 - {{0, 0, 1, 1, 2, 1, 1, 1, 0}, - {1, 1, 2, 2, 1, 2, 1, 1, 0}, - {0, 1, 2, 2, 1, 1, 2, 1, 0}, - {0, 0, 1, 1, 1, 1, 1, 2, 0}, - {1, 2, 1, 2, 1, 1, 1, 2, 0}, - {0, 1, 1, 1, 2, 0, 1, 2, 0}, - {1, 1, 1, 2, 2, 2, 1, 2, 0}, - {0, 0, 0, 0, 0, 0, 0, 0, 0}}}; - void set_greyscale(bool greyscale) { this->greyscale_ = greyscale; this->block_partial_ = true; if (this->is_ready()) this->initialize_(); } + + void set_waveform(const std::array &waveform, bool is_custom) { + static_assert(sizeof(this->waveform_) == sizeof(uint8_t) * GLUT_COUNT * GLUT_SIZE, + "waveform_ buffer size must match input waveform array size"); + memmove(this->waveform_, waveform.data(), sizeof(this->waveform_)); + this->custom_waveform_ = is_custom; + } + void set_mirror_y(bool mirror_y) { this->mirror_y_ = mirror_y; } void set_mirror_x(bool mirror_x) { this->mirror_x_ = mirror_x; } @@ -225,6 +183,8 @@ class Inkplate6 : public display::DisplayBuffer, public i2c::I2CDevice { bool mirror_y_{false}; bool mirror_x_{false}; bool partial_updating_; + bool custom_waveform_{false}; + uint8_t waveform_[GLUT_COUNT][GLUT_SIZE]; InkplateModel model_; @@ -250,5 +210,5 @@ class Inkplate6 : public display::DisplayBuffer, public i2c::I2CDevice { GPIOPin *wakeup_pin_; }; -} // namespace inkplate6 +} // namespace inkplate } // namespace esphome diff --git a/esphome/components/inkplate6/__init__.py b/esphome/components/inkplate6/__init__.py index b1de57df8f..e69de29bb2 100644 --- a/esphome/components/inkplate6/__init__.py +++ b/esphome/components/inkplate6/__init__.py @@ -1 +0,0 @@ -CODEOWNERS = ["@jesserockz"] diff --git a/esphome/components/inkplate6/display.py b/esphome/components/inkplate6/display.py index 063fc8b0aa..ff14be5491 100644 --- a/esphome/components/inkplate6/display.py +++ b/esphome/components/inkplate6/display.py @@ -1,214 +1,5 @@ -from esphome import pins -import esphome.codegen as cg -from esphome.components import display, i2c -from esphome.components.esp32 import CONF_CPU_FREQUENCY import esphome.config_validation as cv -from esphome.const import ( - CONF_FULL_UPDATE_EVERY, - CONF_ID, - CONF_LAMBDA, - CONF_MIRROR_X, - CONF_MIRROR_Y, - CONF_MODEL, - CONF_OE_PIN, - CONF_PAGES, - CONF_TRANSFORM, - CONF_WAKEUP_PIN, - PLATFORM_ESP32, + +CONFIG_SCHEMA = cv.invalid( + "The inkplate6 display component has been renamed to inkplate." ) -import esphome.final_validate as fv - -DEPENDENCIES = ["i2c", "esp32"] -AUTO_LOAD = ["psram"] - -CONF_DISPLAY_DATA_0_PIN = "display_data_0_pin" -CONF_DISPLAY_DATA_1_PIN = "display_data_1_pin" -CONF_DISPLAY_DATA_2_PIN = "display_data_2_pin" -CONF_DISPLAY_DATA_3_PIN = "display_data_3_pin" -CONF_DISPLAY_DATA_4_PIN = "display_data_4_pin" -CONF_DISPLAY_DATA_5_PIN = "display_data_5_pin" -CONF_DISPLAY_DATA_6_PIN = "display_data_6_pin" -CONF_DISPLAY_DATA_7_PIN = "display_data_7_pin" - -CONF_CL_PIN = "cl_pin" -CONF_CKV_PIN = "ckv_pin" -CONF_GREYSCALE = "greyscale" -CONF_GMOD_PIN = "gmod_pin" -CONF_GPIO0_ENABLE_PIN = "gpio0_enable_pin" -CONF_LE_PIN = "le_pin" -CONF_PARTIAL_UPDATING = "partial_updating" -CONF_POWERUP_PIN = "powerup_pin" -CONF_SPH_PIN = "sph_pin" -CONF_SPV_PIN = "spv_pin" -CONF_VCOM_PIN = "vcom_pin" - -inkplate6_ns = cg.esphome_ns.namespace("inkplate6") -Inkplate6 = inkplate6_ns.class_( - "Inkplate6", - cg.PollingComponent, - i2c.I2CDevice, - display.Display, - display.DisplayBuffer, -) - -InkplateModel = inkplate6_ns.enum("InkplateModel") - -MODELS = { - "inkplate_6": InkplateModel.INKPLATE_6, - "inkplate_10": InkplateModel.INKPLATE_10, - "inkplate_6_plus": InkplateModel.INKPLATE_6_PLUS, - "inkplate_6_v2": InkplateModel.INKPLATE_6_V2, - "inkplate_5": InkplateModel.INKPLATE_5, - "inkplate_5_v2": InkplateModel.INKPLATE_5_V2, -} - -CONFIG_SCHEMA = cv.All( - display.FULL_DISPLAY_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(Inkplate6), - cv.Optional(CONF_GREYSCALE, default=False): cv.boolean, - cv.Optional(CONF_TRANSFORM): cv.Schema( - { - cv.Optional(CONF_MIRROR_X, default=False): cv.boolean, - cv.Optional(CONF_MIRROR_Y, default=False): cv.boolean, - } - ), - cv.Optional(CONF_PARTIAL_UPDATING, default=True): cv.boolean, - cv.Optional(CONF_FULL_UPDATE_EVERY, default=10): cv.uint32_t, - cv.Optional(CONF_MODEL, default="inkplate_6"): cv.enum( - MODELS, lower=True, space="_" - ), - # Control pins - cv.Required(CONF_CKV_PIN): pins.gpio_output_pin_schema, - cv.Required(CONF_GMOD_PIN): pins.gpio_output_pin_schema, - cv.Required(CONF_GPIO0_ENABLE_PIN): pins.gpio_output_pin_schema, - cv.Required(CONF_OE_PIN): pins.gpio_output_pin_schema, - cv.Required(CONF_POWERUP_PIN): pins.gpio_output_pin_schema, - cv.Required(CONF_SPH_PIN): pins.gpio_output_pin_schema, - cv.Required(CONF_SPV_PIN): pins.gpio_output_pin_schema, - cv.Required(CONF_VCOM_PIN): pins.gpio_output_pin_schema, - cv.Required(CONF_WAKEUP_PIN): pins.gpio_output_pin_schema, - cv.Optional(CONF_CL_PIN, default=0): pins.internal_gpio_output_pin_schema, - cv.Optional(CONF_LE_PIN, default=2): pins.internal_gpio_output_pin_schema, - # Data pins - cv.Optional( - CONF_DISPLAY_DATA_0_PIN, default=4 - ): pins.internal_gpio_output_pin_schema, - cv.Optional( - CONF_DISPLAY_DATA_1_PIN, default=5 - ): pins.internal_gpio_output_pin_schema, - cv.Optional( - CONF_DISPLAY_DATA_2_PIN, default=18 - ): pins.internal_gpio_output_pin_schema, - cv.Optional( - CONF_DISPLAY_DATA_3_PIN, default=19 - ): pins.internal_gpio_output_pin_schema, - cv.Optional( - CONF_DISPLAY_DATA_4_PIN, default=23 - ): pins.internal_gpio_output_pin_schema, - cv.Optional( - CONF_DISPLAY_DATA_5_PIN, default=25 - ): pins.internal_gpio_output_pin_schema, - cv.Optional( - CONF_DISPLAY_DATA_6_PIN, default=26 - ): pins.internal_gpio_output_pin_schema, - cv.Optional( - CONF_DISPLAY_DATA_7_PIN, default=27 - ): pins.internal_gpio_output_pin_schema, - } - ) - .extend(cv.polling_component_schema("5s")) - .extend(i2c.i2c_device_schema(0x48)), - cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), -) - - -def _validate_cpu_frequency(config): - esp32_config = fv.full_config.get()[PLATFORM_ESP32] - if esp32_config[CONF_CPU_FREQUENCY] != "240MHZ": - raise cv.Invalid( - "Inkplate requires 240MHz CPU frequency (set in esp32 component)" - ) - return config - - -FINAL_VALIDATE_SCHEMA = _validate_cpu_frequency - - -async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - - await display.register_display(var, config) - await i2c.register_i2c_device(var, config) - - if CONF_LAMBDA in config: - lambda_ = await cg.process_lambda( - config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void - ) - cg.add(var.set_writer(lambda_)) - - cg.add(var.set_greyscale(config[CONF_GREYSCALE])) - if transform := config.get(CONF_TRANSFORM): - cg.add(var.set_mirror_x(transform[CONF_MIRROR_X])) - cg.add(var.set_mirror_y(transform[CONF_MIRROR_Y])) - cg.add(var.set_partial_updating(config[CONF_PARTIAL_UPDATING])) - cg.add(var.set_full_update_every(config[CONF_FULL_UPDATE_EVERY])) - - cg.add(var.set_model(config[CONF_MODEL])) - - ckv = await cg.gpio_pin_expression(config[CONF_CKV_PIN]) - cg.add(var.set_ckv_pin(ckv)) - - gmod = await cg.gpio_pin_expression(config[CONF_GMOD_PIN]) - cg.add(var.set_gmod_pin(gmod)) - - gpio0_enable = await cg.gpio_pin_expression(config[CONF_GPIO0_ENABLE_PIN]) - cg.add(var.set_gpio0_enable_pin(gpio0_enable)) - - oe = await cg.gpio_pin_expression(config[CONF_OE_PIN]) - cg.add(var.set_oe_pin(oe)) - - powerup = await cg.gpio_pin_expression(config[CONF_POWERUP_PIN]) - cg.add(var.set_powerup_pin(powerup)) - - sph = await cg.gpio_pin_expression(config[CONF_SPH_PIN]) - cg.add(var.set_sph_pin(sph)) - - spv = await cg.gpio_pin_expression(config[CONF_SPV_PIN]) - cg.add(var.set_spv_pin(spv)) - - vcom = await cg.gpio_pin_expression(config[CONF_VCOM_PIN]) - cg.add(var.set_vcom_pin(vcom)) - - wakeup = await cg.gpio_pin_expression(config[CONF_WAKEUP_PIN]) - cg.add(var.set_wakeup_pin(wakeup)) - - cl = await cg.gpio_pin_expression(config[CONF_CL_PIN]) - cg.add(var.set_cl_pin(cl)) - - le = await cg.gpio_pin_expression(config[CONF_LE_PIN]) - cg.add(var.set_le_pin(le)) - - display_data_0 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_0_PIN]) - cg.add(var.set_display_data_0_pin(display_data_0)) - - display_data_1 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_1_PIN]) - cg.add(var.set_display_data_1_pin(display_data_1)) - - display_data_2 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_2_PIN]) - cg.add(var.set_display_data_2_pin(display_data_2)) - - display_data_3 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_3_PIN]) - cg.add(var.set_display_data_3_pin(display_data_3)) - - display_data_4 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_4_PIN]) - cg.add(var.set_display_data_4_pin(display_data_4)) - - display_data_5 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_5_PIN]) - cg.add(var.set_display_data_5_pin(display_data_5)) - - display_data_6 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_6_PIN]) - cg.add(var.set_display_data_6_pin(display_data_6)) - - display_data_7 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_7_PIN]) - cg.add(var.set_display_data_7_pin(display_data_7)) diff --git a/esphome/components/integration/integration_sensor.cpp b/esphome/components/integration/integration_sensor.cpp index c09778e79e..80c718dc8d 100644 --- a/esphome/components/integration/integration_sensor.cpp +++ b/esphome/components/integration/integration_sensor.cpp @@ -10,7 +10,7 @@ static const char *const TAG = "integration"; void IntegrationSensor::setup() { if (this->restore_) { - this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); + this->pref_ = global_preferences->make_preference(this->get_preference_hash()); float preference_value = 0; this->pref_.load(&preference_value); this->result_ = preference_value; diff --git a/esphome/components/integration/integration_sensor.h b/esphome/components/integration/integration_sensor.h index d9f2f5e50f..f075d163fe 100644 --- a/esphome/components/integration/integration_sensor.h +++ b/esphome/components/integration/integration_sensor.h @@ -75,7 +75,7 @@ template class ResetAction : public Action { public: explicit ResetAction(IntegrationSensor *parent) : parent_(parent) {} - void play(Ts... x) override { this->parent_->reset(); } + void play(const Ts &...x) override { this->parent_->reset(); } protected: IntegrationSensor *parent_; diff --git a/esphome/components/jsn_sr04t/jsn_sr04t.cpp b/esphome/components/jsn_sr04t/jsn_sr04t.cpp index 077d4e58ea..84181dac48 100644 --- a/esphome/components/jsn_sr04t/jsn_sr04t.cpp +++ b/esphome/components/jsn_sr04t/jsn_sr04t.cpp @@ -10,7 +10,7 @@ namespace jsn_sr04t { static const char *const TAG = "jsn_sr04t.sensor"; void Jsnsr04tComponent::update() { - this->write_byte(0x55); + this->write_byte((this->model_ == AJ_SR04M) ? 0x01 : 0x55); ESP_LOGV(TAG, "Request read out from sensor"); } @@ -31,19 +31,10 @@ void Jsnsr04tComponent::loop() { } void Jsnsr04tComponent::check_buffer_() { - uint8_t checksum = 0; - switch (this->model_) { - case JSN_SR04T: - checksum = this->buffer_[0] + this->buffer_[1] + this->buffer_[2]; - break; - case AJ_SR04M: - checksum = this->buffer_[1] + this->buffer_[2]; - break; - } - + uint8_t checksum = this->buffer_[0] + this->buffer_[1] + this->buffer_[2]; if (this->buffer_[3] == checksum) { uint16_t distance = encode_uint16(this->buffer_[1], this->buffer_[2]); - if (distance > 250) { + if (distance > ((this->model_ == AJ_SR04M) ? 200 : 250)) { float meters = distance / 1000.0f; ESP_LOGV(TAG, "Distance from sensor: %umm, %.3fm", distance, meters); this->publish_state(meters); diff --git a/esphome/components/json/__init__.py b/esphome/components/json/__init__.py index 9773bf67ce..4cd737c60d 100644 --- a/esphome/components/json/__init__.py +++ b/esphome/components/json/__init__.py @@ -1,8 +1,8 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.core import coroutine_with_priority +from esphome.core import CoroPriority, coroutine_with_priority -CODEOWNERS = ["@OttoWinter"] +CODEOWNERS = ["@esphome/core"] json_ns = cg.esphome_ns.namespace("json") CONFIG_SCHEMA = cv.All( @@ -10,7 +10,7 @@ CONFIG_SCHEMA = cv.All( ) -@coroutine_with_priority(1.0) +@coroutine_with_priority(CoroPriority.BUS) async def to_code(config): cg.add_library("bblanchon/ArduinoJson", "7.4.2") cg.add_define("USE_JSON") diff --git a/esphome/components/json/json_util.cpp b/esphome/components/json/json_util.cpp index 94c531222a..869d29f92e 100644 --- a/esphome/components/json/json_util.cpp +++ b/esphome/components/json/json_util.cpp @@ -8,70 +8,68 @@ namespace json { static const char *const TAG = "json"; -// Build an allocator for the JSON Library using the RAMAllocator class -struct SpiRamAllocator : ArduinoJson::Allocator { - void *allocate(size_t size) override { return this->allocator_.allocate(size); } - - void deallocate(void *pointer) override { - // ArduinoJson's Allocator interface doesn't provide the size parameter in deallocate. - // RAMAllocator::deallocate() requires the size, which we don't have access to here. - // RAMAllocator::deallocate implementation just calls free() regardless of whether - // the memory was allocated with heap_caps_malloc or malloc. - // This is safe because ESP-IDF's heap implementation internally tracks the memory region - // and routes free() to the appropriate heap. - free(pointer); // NOLINT(cppcoreguidelines-owning-memory,cppcoreguidelines-no-malloc) - } - - void *reallocate(void *ptr, size_t new_size) override { - return this->allocator_.reallocate(static_cast(ptr), new_size); - } - - protected: - RAMAllocator allocator_{RAMAllocator(RAMAllocator::NONE)}; -}; +#ifdef USE_PSRAM +// Global allocator that outlives all JsonDocuments returned by parse_json() +// This prevents dangling pointer issues when JsonDocuments are returned from functions +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) - Must be mutable for ArduinoJson::Allocator +static SpiRamAllocator global_json_allocator; +#endif std::string build_json(const json_build_t &f) { // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - auto doc_allocator = SpiRamAllocator(); - JsonDocument json_document(&doc_allocator); - if (json_document.overflowed()) { - ESP_LOGE(TAG, "Could not allocate memory for JSON document!"); - return "{}"; - } - JsonObject root = json_document.to(); + JsonBuilder builder; + JsonObject root = builder.root(); f(root); - if (json_document.overflowed()) { - ESP_LOGE(TAG, "Could not allocate memory for JSON document!"); - return "{}"; - } - std::string output; - serializeJson(json_document, output); - return output; + return builder.serialize(); // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) } bool parse_json(const std::string &data, const json_parse_t &f) { // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - auto doc_allocator = SpiRamAllocator(); - JsonDocument json_document(&doc_allocator); + JsonDocument doc = parse_json(reinterpret_cast(data.c_str()), data.size()); + if (doc.overflowed() || doc.isNull()) + return false; + return f(doc.as()); + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) +} + +JsonDocument parse_json(const uint8_t *data, size_t len) { + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + if (data == nullptr || len == 0) { + ESP_LOGE(TAG, "No data to parse"); + return JsonObject(); // return unbound object + } +#ifdef USE_PSRAM + JsonDocument json_document(&global_json_allocator); +#else + JsonDocument json_document; +#endif if (json_document.overflowed()) { ESP_LOGE(TAG, "Could not allocate memory for JSON document!"); - return false; + return JsonObject(); // return unbound object } - DeserializationError err = deserializeJson(json_document, data); - - JsonObject root = json_document.as(); + DeserializationError err = deserializeJson(json_document, data, len); if (err == DeserializationError::Ok) { - return f(root); + return json_document; } else if (err == DeserializationError::NoMemory) { ESP_LOGE(TAG, "Can not allocate more memory for deserialization. Consider making source string smaller"); - return false; + return JsonObject(); // return unbound object } ESP_LOGE(TAG, "Parse error: %s", err.c_str()); - return false; + return JsonObject(); // return unbound object // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) } +std::string JsonBuilder::serialize() { + if (doc_.overflowed()) { + ESP_LOGE(TAG, "JSON document overflow"); + return "{}"; + } + std::string output; + serializeJson(doc_, output); + return output; +} + } // namespace json } // namespace esphome diff --git a/esphome/components/json/json_util.h b/esphome/components/json/json_util.h index 72d31c8afe..91cc84dc14 100644 --- a/esphome/components/json/json_util.h +++ b/esphome/components/json/json_util.h @@ -2,6 +2,7 @@ #include +#include "esphome/core/defines.h" #include "esphome/core/helpers.h" #define ARDUINOJSON_ENABLE_STD_STRING 1 // NOLINT @@ -13,6 +14,31 @@ namespace esphome { namespace json { +#ifdef USE_PSRAM +// Build an allocator for the JSON Library using the RAMAllocator class +// This is only compiled when PSRAM is enabled +struct SpiRamAllocator : ArduinoJson::Allocator { + void *allocate(size_t size) override { return allocator_.allocate(size); } + + void deallocate(void *ptr) override { + // ArduinoJson's Allocator interface doesn't provide the size parameter in deallocate. + // RAMAllocator::deallocate() requires the size, which we don't have access to here. + // RAMAllocator::deallocate implementation just calls free() regardless of whether + // the memory was allocated with heap_caps_malloc or malloc. + // This is safe because ESP-IDF's heap implementation internally tracks the memory region + // and routes free() to the appropriate heap. + free(ptr); // NOLINT(cppcoreguidelines-owning-memory,cppcoreguidelines-no-malloc) + } + + void *reallocate(void *ptr, size_t new_size) override { + return allocator_.reallocate(static_cast(ptr), new_size); + } + + protected: + RAMAllocator allocator_{RAMAllocator::NONE}; +}; +#endif + /// Callback function typedef for parsing JsonObjects. using json_parse_t = std::function; @@ -25,5 +51,36 @@ std::string build_json(const json_build_t &f); /// Parse a JSON string and run the provided json parse function if it's valid. bool parse_json(const std::string &data, const json_parse_t &f); +/// Parse a JSON string and return the root JsonDocument (or an unbound object on error) +JsonDocument parse_json(const uint8_t *data, size_t len); +/// Parse a JSON string and return the root JsonDocument (or an unbound object on error) +inline JsonDocument parse_json(const std::string &data) { + return parse_json(reinterpret_cast(data.c_str()), data.size()); +} + +/// Builder class for creating JSON documents without lambdas +class JsonBuilder { + public: + JsonObject root() { + if (!root_created_) { + root_ = doc_.to(); + root_created_ = true; + } + return root_; + } + + std::string serialize(); + + private: +#ifdef USE_PSRAM + SpiRamAllocator allocator_; + JsonDocument doc_{&allocator_}; +#else + JsonDocument doc_; +#endif + JsonObject root_; + bool root_created_{false}; +}; + } // namespace json } // namespace esphome diff --git a/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp b/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp index c058c7b3aa..e5fa035682 100644 --- a/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp +++ b/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp @@ -22,7 +22,7 @@ void KamstrupKMPComponent::dump_config() { LOG_SENSOR(" ", "Flow", this->flow_sensor_); LOG_SENSOR(" ", "Volume", this->volume_sensor_); - for (int i = 0; i < this->custom_sensors_.size(); i++) { + for (size_t i = 0; i < this->custom_sensors_.size(); i++) { LOG_SENSOR(" ", "Custom Sensor", this->custom_sensors_[i]); ESP_LOGCONFIG(TAG, " Command: 0x%04X", this->custom_commands_[i]); } @@ -268,7 +268,7 @@ void KamstrupKMPComponent::set_sensor_value_(uint16_t command, float value, uint } // Custom sensors - for (int i = 0; i < this->custom_commands_.size(); i++) { + for (size_t i = 0; i < this->custom_commands_.size(); i++) { if (command == this->custom_commands_[i]) { this->custom_sensors_[i]->publish_state(value); } diff --git a/esphome/components/key_collector/key_collector.h b/esphome/components/key_collector/key_collector.h index 6e585ddd8e..735f396809 100644 --- a/esphome/components/key_collector/key_collector.h +++ b/esphome/components/key_collector/key_collector.h @@ -13,8 +13,8 @@ class KeyCollector : public Component { void loop() override; void dump_config() override; void set_provider(key_provider::KeyProvider *provider); - void set_min_length(int min_length) { this->min_length_ = min_length; }; - void set_max_length(int max_length) { this->max_length_ = max_length; }; + void set_min_length(uint32_t min_length) { this->min_length_ = min_length; }; + void set_max_length(uint32_t max_length) { this->max_length_ = max_length; }; void set_start_keys(std::string start_keys) { this->start_keys_ = std::move(start_keys); }; void set_end_keys(std::string end_keys) { this->end_keys_ = std::move(end_keys); }; void set_end_key_required(bool end_key_required) { this->end_key_required_ = end_key_required; }; @@ -33,8 +33,8 @@ class KeyCollector : public Component { protected: void key_pressed_(uint8_t key); - int min_length_{0}; - int max_length_{0}; + uint32_t min_length_{0}; + uint32_t max_length_{0}; std::string start_keys_; std::string end_keys_; bool end_key_required_{false}; @@ -52,11 +52,11 @@ class KeyCollector : public Component { }; template class EnableAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->set_enabled(true); } + void play(const Ts &...x) override { this->parent_->set_enabled(true); } }; template class DisableAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->set_enabled(false); } + void play(const Ts &...x) override { this->parent_->set_enabled(false); } }; } // namespace key_collector diff --git a/esphome/components/kmeteriso/kmeteriso.cpp b/esphome/components/kmeteriso/kmeteriso.cpp index 3aedac3f5f..36f6d74ba0 100644 --- a/esphome/components/kmeteriso/kmeteriso.cpp +++ b/esphome/components/kmeteriso/kmeteriso.cpp @@ -22,7 +22,7 @@ void KMeterISOComponent::setup() { this->reset_to_construction_state(); } - auto err = this->bus_->writev(this->address_, nullptr, 0); + auto err = this->bus_->write_readv(this->address_, nullptr, 0, nullptr, 0); if (err == esphome::i2c::ERROR_OK) { ESP_LOGCONFIG(TAG, "Could write to the address %d.", this->address_); } else { diff --git a/esphome/components/kuntze/kuntze.cpp b/esphome/components/kuntze/kuntze.cpp index 42545d9d54..30f98aaa99 100644 --- a/esphome/components/kuntze/kuntze.cpp +++ b/esphome/components/kuntze/kuntze.cpp @@ -14,7 +14,7 @@ void Kuntze::on_modbus_data(const std::vector &data) { auto get_16bit = [&](int i) -> uint16_t { return (uint16_t(data[i * 2]) << 8) | uint16_t(data[i * 2 + 1]); }; this->waiting_ = false; - ESP_LOGV(TAG, "Data: %s", hexencode(data).c_str()); + ESP_LOGV(TAG, "Data: %s", format_hex_pretty(data).c_str()); float value = (float) get_16bit(0); for (int i = 0; i < data[3]; i++) diff --git a/esphome/components/lc709203f/lc709203f.cpp b/esphome/components/lc709203f/lc709203f.cpp index e5d12a75d4..7e6ac878f8 100644 --- a/esphome/components/lc709203f/lc709203f.cpp +++ b/esphome/components/lc709203f/lc709203f.cpp @@ -1,5 +1,6 @@ -#include "esphome/core/log.h" #include "lc709203f.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" namespace esphome { namespace lc709203f { @@ -184,12 +185,12 @@ uint8_t Lc709203f::get_register_(uint8_t register_to_read, uint16_t *register_va // function will send a stop between the read and the write portion of the I2C // transaction. This is bad in this case and will result in reading nothing but 0xFFFF // from the registers. - return_code = this->read_register(register_to_read, &read_buffer[3], 3, false); + return_code = this->read_register(register_to_read, &read_buffer[3], 3); if (return_code != i2c::NO_ERROR) { // Error on the i2c bus this->status_set_warning( str_sprintf("Error code %d when reading from register 0x%02X", return_code, register_to_read).c_str()); - } else if (this->crc8_(read_buffer, 5) != read_buffer[5]) { + } else if (crc8(read_buffer, 5, 0x00, 0x07, true) != read_buffer[5]) { // I2C indicated OK, but the CRC of the data does not matcth. this->status_set_warning(str_sprintf("CRC error reading from register 0x%02X", register_to_read).c_str()); } else { @@ -220,12 +221,12 @@ uint8_t Lc709203f::set_register_(uint8_t register_to_set, uint16_t value_to_set) write_buffer[1] = register_to_set; write_buffer[2] = value_to_set & 0xFF; // Low byte write_buffer[3] = (value_to_set >> 8) & 0xFF; // High byte - write_buffer[4] = this->crc8_(write_buffer, 4); + write_buffer[4] = crc8(write_buffer, 4, 0x00, 0x07, true); for (uint8_t i = 0; i <= LC709203F_I2C_RETRY_COUNT; i++) { // Note: we don't write the first byte of the write buffer to the device. // This is done automatically by the write() function. - return_code = this->write(&write_buffer[1], 4, true); + return_code = this->write(&write_buffer[1], 4); if (return_code == i2c::NO_ERROR) { return return_code; } else { @@ -239,20 +240,6 @@ uint8_t Lc709203f::set_register_(uint8_t register_to_set, uint16_t value_to_set) return return_code; } -uint8_t Lc709203f::crc8_(uint8_t *byte_buffer, uint8_t length_of_crc) { - uint8_t crc = 0x00; - const uint8_t polynomial(0x07); - - for (uint8_t j = length_of_crc; j; --j) { - crc ^= *byte_buffer++; - - for (uint8_t i = 8; i; --i) { - crc = (crc & 0x80) ? (crc << 1) ^ polynomial : (crc << 1); - } - } - return crc; -} - void Lc709203f::set_pack_size(uint16_t pack_size) { static const uint16_t PACK_SIZE_ARRAY[6] = {100, 200, 500, 1000, 2000, 3000}; static const uint16_t APA_ARRAY[6] = {0x08, 0x0B, 0x10, 0x19, 0x2D, 0x36}; diff --git a/esphome/components/lc709203f/lc709203f.h b/esphome/components/lc709203f/lc709203f.h index 3b5b04775f..59988a0079 100644 --- a/esphome/components/lc709203f/lc709203f.h +++ b/esphome/components/lc709203f/lc709203f.h @@ -1,8 +1,8 @@ #pragma once -#include "esphome/core/component.h" -#include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/core/component.h" namespace esphome { namespace lc709203f { @@ -38,7 +38,6 @@ class Lc709203f : public sensor::Sensor, public PollingComponent, public i2c::I2 private: uint8_t get_register_(uint8_t register_to_read, uint16_t *register_value); uint8_t set_register_(uint8_t register_to_set, uint16_t value_to_set); - uint8_t crc8_(uint8_t *byte_buffer, uint8_t length_of_crc); protected: sensor::Sensor *voltage_sensor_{nullptr}; diff --git a/esphome/components/lcd_gpio/gpio_lcd_display.h b/esphome/components/lcd_gpio/gpio_lcd_display.h index aba254a90a..81e4dc51a0 100644 --- a/esphome/components/lcd_gpio/gpio_lcd_display.h +++ b/esphome/components/lcd_gpio/gpio_lcd_display.h @@ -2,13 +2,18 @@ #include "esphome/core/hal.h" #include "esphome/components/lcd_base/lcd_display.h" +#include "esphome/components/display/display.h" namespace esphome { namespace lcd_gpio { +class GPIOLCDDisplay; + +using gpio_lcd_writer_t = display::DisplayWriter; + class GPIOLCDDisplay : public lcd_base::LCDDisplay { public: - void set_writer(std::function &&writer) { this->writer_ = std::move(writer); } + void set_writer(gpio_lcd_writer_t &&writer) { this->writer_ = std::move(writer); } void setup() override; void set_data_pins(GPIOPin *d0, GPIOPin *d1, GPIOPin *d2, GPIOPin *d3) { this->data_pins_[0] = d0; @@ -43,7 +48,7 @@ class GPIOLCDDisplay : public lcd_base::LCDDisplay { GPIOPin *rw_pin_{nullptr}; GPIOPin *enable_pin_{nullptr}; GPIOPin *data_pins_[8]{nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr}; - std::function writer_; + gpio_lcd_writer_t writer_; }; } // namespace lcd_gpio diff --git a/esphome/components/lcd_pcf8574/pcf8574_display.h b/esphome/components/lcd_pcf8574/pcf8574_display.h index 4db3afb9b0..672b609036 100644 --- a/esphome/components/lcd_pcf8574/pcf8574_display.h +++ b/esphome/components/lcd_pcf8574/pcf8574_display.h @@ -3,13 +3,18 @@ #include "esphome/core/component.h" #include "esphome/components/lcd_base/lcd_display.h" #include "esphome/components/i2c/i2c.h" +#include "esphome/components/display/display.h" namespace esphome { namespace lcd_pcf8574 { +class PCF8574LCDDisplay; + +using pcf8574_lcd_writer_t = display::DisplayWriter; + class PCF8574LCDDisplay : public lcd_base::LCDDisplay, public i2c::I2CDevice { public: - void set_writer(std::function &&writer) { this->writer_ = std::move(writer); } + void set_writer(pcf8574_lcd_writer_t &&writer) { this->writer_ = std::move(writer); } void setup() override; void dump_config() override; void backlight(); @@ -24,7 +29,7 @@ class PCF8574LCDDisplay : public lcd_base::LCDDisplay, public i2c::I2CDevice { // Stores the current state of the backlight. uint8_t backlight_value_; - std::function writer_; + pcf8574_lcd_writer_t writer_; }; } // namespace lcd_pcf8574 diff --git a/esphome/components/ld2410/__init__.py b/esphome/components/ld2410/__init__.py index 4918190179..b492bbcd14 100644 --- a/esphome/components/ld2410/__init__.py +++ b/esphome/components/ld2410/__init__.py @@ -14,18 +14,16 @@ ld2410_ns = cg.esphome_ns.namespace("ld2410") LD2410Component = ld2410_ns.class_("LD2410Component", cg.Component, uart.UARTDevice) CONF_LD2410_ID = "ld2410_id" - CONF_MAX_MOVE_DISTANCE = "max_move_distance" CONF_MAX_STILL_DISTANCE = "max_still_distance" -CONF_STILL_THRESHOLDS = [f"g{x}_still_threshold" for x in range(9)] CONF_MOVE_THRESHOLDS = [f"g{x}_move_threshold" for x in range(9)] +CONF_STILL_THRESHOLDS = [f"g{x}_still_threshold" for x in range(9)] CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(LD2410Component), - cv.Optional(CONF_THROTTLE, default="1000ms"): cv.All( - cv.positive_time_period_milliseconds, - cv.Range(min=cv.TimePeriod(milliseconds=1)), + cv.Optional(CONF_THROTTLE): cv.invalid( + f"{CONF_THROTTLE} has been removed; use per-sensor filters, instead" ), cv.Optional(CONF_MAX_MOVE_DISTANCE): cv.invalid( f"The '{CONF_MAX_MOVE_DISTANCE}' option has been moved to the '{CONF_MAX_MOVE_DISTANCE}'" @@ -75,7 +73,6 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await uart.register_uart_device(var, config) - cg.add(var.set_throttle(config[CONF_THROTTLE])) CALIBRATION_ACTION_SCHEMA = maybe_simple_id( diff --git a/esphome/components/ld2410/automation.h b/esphome/components/ld2410/automation.h index 7cb9855f84..614453b575 100644 --- a/esphome/components/ld2410/automation.h +++ b/esphome/components/ld2410/automation.h @@ -4,19 +4,17 @@ #include "esphome/core/component.h" #include "ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { template class BluetoothPasswordSetAction : public Action { public: explicit BluetoothPasswordSetAction(LD2410Component *ld2410_comp) : ld2410_comp_(ld2410_comp) {} TEMPLATABLE_VALUE(std::string, password) - void play(Ts... x) override { this->ld2410_comp_->set_bluetooth_password(this->password_.value(x...)); } + void play(const Ts &...x) override { this->ld2410_comp_->set_bluetooth_password(this->password_.value(x...)); } protected: LD2410Component *ld2410_comp_; }; -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/binary_sensor.py b/esphome/components/ld2410/binary_sensor.py index d2938754e9..4e35f67fbe 100644 --- a/esphome/components/ld2410/binary_sensor.py +++ b/esphome/components/ld2410/binary_sensor.py @@ -22,19 +22,23 @@ CONFIG_SCHEMA = { cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component), cv.Optional(CONF_HAS_TARGET): binary_sensor.binary_sensor_schema( device_class=DEVICE_CLASS_OCCUPANCY, + filters=[{"settle": cv.TimePeriod(milliseconds=1000)}], icon=ICON_ACCOUNT, ), cv.Optional(CONF_HAS_MOVING_TARGET): binary_sensor.binary_sensor_schema( device_class=DEVICE_CLASS_MOTION, + filters=[{"settle": cv.TimePeriod(milliseconds=1000)}], icon=ICON_MOTION_SENSOR, ), cv.Optional(CONF_HAS_STILL_TARGET): binary_sensor.binary_sensor_schema( device_class=DEVICE_CLASS_OCCUPANCY, + filters=[{"settle": cv.TimePeriod(milliseconds=1000)}], icon=ICON_MOTION_SENSOR, ), cv.Optional(CONF_OUT_PIN_PRESENCE_STATUS): binary_sensor.binary_sensor_schema( device_class=DEVICE_CLASS_PRESENCE, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + filters=[{"settle": cv.TimePeriod(milliseconds=1000)}], icon=ICON_ACCOUNT, ), } diff --git a/esphome/components/ld2410/button/factory_reset_button.cpp b/esphome/components/ld2410/button/factory_reset_button.cpp index a848b02a9d..0223df7086 100644 --- a/esphome/components/ld2410/button/factory_reset_button.cpp +++ b/esphome/components/ld2410/button/factory_reset_button.cpp @@ -1,9 +1,7 @@ #include "factory_reset_button.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { void FactoryResetButton::press_action() { this->parent_->factory_reset(); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/button/factory_reset_button.h b/esphome/components/ld2410/button/factory_reset_button.h index 45bf979033..715a8c4056 100644 --- a/esphome/components/ld2410/button/factory_reset_button.h +++ b/esphome/components/ld2410/button/factory_reset_button.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class FactoryResetButton : public button::Button, public Parented { public: @@ -14,5 +13,4 @@ class FactoryResetButton : public button::Button, public Parentedparent_->read_all_info(); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/button/query_button.h b/esphome/components/ld2410/button/query_button.h index c7a47e32d8..7a786901ae 100644 --- a/esphome/components/ld2410/button/query_button.h +++ b/esphome/components/ld2410/button/query_button.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class QueryButton : public button::Button, public Parented { public: @@ -14,5 +13,4 @@ class QueryButton : public button::Button, public Parented { void press_action() override; }; -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/button/restart_button.cpp b/esphome/components/ld2410/button/restart_button.cpp index de0d36c1ef..0d5002d3c6 100644 --- a/esphome/components/ld2410/button/restart_button.cpp +++ b/esphome/components/ld2410/button/restart_button.cpp @@ -1,9 +1,7 @@ #include "restart_button.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { void RestartButton::press_action() { this->parent_->restart_and_read_all_info(); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/button/restart_button.h b/esphome/components/ld2410/button/restart_button.h index d00dc05a53..9bf8639a8c 100644 --- a/esphome/components/ld2410/button/restart_button.h +++ b/esphome/components/ld2410/button/restart_button.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class RestartButton : public button::Button, public Parented { public: @@ -14,5 +13,4 @@ class RestartButton : public button::Button, public Parented { void press_action() override; }; -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index e0287465f8..bb2e4e2f4c 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -9,12 +9,9 @@ #include "esphome/core/application.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { static const char *const TAG = "ld2410"; -static const char *const UNKNOWN_MAC = "unknown"; -static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X"; enum BaudRate : uint8_t { BAUD_RATE_9600 = 1, @@ -121,9 +118,9 @@ constexpr Uint8ToString OUT_PIN_LEVELS_BY_UINT[] = { }; // Helper functions for lookups -template uint8_t find_uint8(const StringToUint8 (&arr)[N], const std::string &str) { +template uint8_t find_uint8(const StringToUint8 (&arr)[N], const char *str) { for (const auto &entry : arr) { - if (str == entry.str) + if (strcmp(str, entry.str) == 0) return entry.value; } return 0xFF; // Not found @@ -181,16 +178,15 @@ static inline bool validate_header_footer(const uint8_t *header_footer, const ui } void LD2410Component::dump_config() { - std::string mac_str = - mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC; - std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5], - this->version_[4], this->version_[3], this->version_[2]); + char mac_s[18]; + char version_s[20]; + const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s); + ld24xx::format_version_str(this->version_, version_s); ESP_LOGCONFIG(TAG, "LD2410:\n" " Firmware version: %s\n" - " MAC address: %s\n" - " Throttle: %u ms", - version.c_str(), mac_str.c_str(), this->throttle_); + " MAC address: %s", + version_s, mac_str); #ifdef USE_BINARY_SENSOR ESP_LOGCONFIG(TAG, "Binary Sensors:"); LOG_BINARY_SENSOR(" ", "Target", this->target_binary_sensor_); @@ -306,11 +302,6 @@ void LD2410Component::send_command_(uint8_t command, const uint8_t *command_valu } void LD2410Component::handle_periodic_data_() { - // Reduce data update rate to reduce home assistant database growth - // Check this first to prevent unnecessary processing done in later checks/parsing - if (App.get_loop_component_start_time() - this->last_periodic_millis_ < this->throttle_) { - return; - } // 4 frame header bytes + 2 length bytes + 1 data end byte + 1 crc byte + 4 frame footer bytes // data header=0xAA, data footer=0x55, crc=0x00 if (this->buffer_pos_ < 12 || !ld2410::validate_header_footer(DATA_FRAME_HEADER, this->buffer_data_) || @@ -318,9 +309,6 @@ void LD2410Component::handle_periodic_data_() { this->buffer_data_[this->buffer_pos_ - 5] != CHECK) { return; } - // Save the timestamp after validating the frame so, if invalid, we'll take the next frame immediately - this->last_periodic_millis_ = App.get_loop_component_start_time(); - /* Data Type: 7th 0x01: Engineering mode @@ -450,19 +438,19 @@ bool LD2410Component::handle_ack_data_() { ESP_LOGV(TAG, "Baud rate change"); #ifdef USE_SELECT if (this->baud_rate_select_ != nullptr) { - ESP_LOGE(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->state.c_str()); + ESP_LOGE(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->current_option()); } #endif break; case CMD_QUERY_VERSION: { std::memcpy(this->version_, &this->buffer_data_[12], sizeof(this->version_)); - std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5], - this->version_[4], this->version_[3], this->version_[2]); - ESP_LOGV(TAG, "Firmware version: %s", version.c_str()); + char version_s[20]; + ld24xx::format_version_str(this->version_, version_s); + ESP_LOGV(TAG, "Firmware version: %s", version_s); #ifdef USE_TEXT_SENSOR if (this->version_text_sensor_ != nullptr) { - this->version_text_sensor_->publish_state(version); + this->version_text_sensor_->publish_state(version_s); } #endif break; @@ -515,9 +503,9 @@ bool LD2410Component::handle_ack_data_() { std::memcpy(this->mac_address_, &this->buffer_data_[10], sizeof(this->mac_address_)); } - std::string mac_str = - mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC; - ESP_LOGV(TAG, "MAC address: %s", mac_str.c_str()); + char mac_s[18]; + const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s); + ESP_LOGV(TAG, "MAC address: %s", mac_str); #ifdef USE_TEXT_SENSOR if (this->mac_text_sensor_ != nullptr) { this->mac_text_sensor_->publish_state(mac_str); @@ -635,14 +623,14 @@ void LD2410Component::set_bluetooth(bool enable) { this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); } -void LD2410Component::set_distance_resolution(const std::string &state) { +void LD2410Component::set_distance_resolution(const char *state) { this->set_config_mode_(true); const uint8_t cmd_value[2] = {find_uint8(DISTANCE_RESOLUTIONS_BY_STR, state), 0x00}; this->send_command_(CMD_SET_DISTANCE_RESOLUTION, cmd_value, sizeof(cmd_value)); this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); } -void LD2410Component::set_baud_rate(const std::string &state) { +void LD2410Component::set_baud_rate(const char *state) { this->set_config_mode_(true); const uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00}; this->send_command_(CMD_SET_BAUD_RATE, cmd_value, sizeof(cmd_value)); @@ -768,10 +756,10 @@ void LD2410Component::set_light_out_control() { #endif #ifdef USE_SELECT if (this->light_function_select_ != nullptr && this->light_function_select_->has_state()) { - this->light_function_ = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_select_->state); + this->light_function_ = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_select_->current_option()); } if (this->out_pin_level_select_ != nullptr && this->out_pin_level_select_->has_state()) { - this->out_pin_level_ = find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->state); + this->out_pin_level_ = find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->current_option()); } #endif this->set_config_mode_(true); @@ -793,5 +781,4 @@ void LD2410Component::set_gate_still_sensor(uint8_t gate, sensor::Sensor *s) { } #endif -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/ld2410.h b/esphome/components/ld2410/ld2410.h index e9225ccfe4..efe585fb76 100644 --- a/esphome/components/ld2410/ld2410.h +++ b/esphome/components/ld2410/ld2410.h @@ -29,8 +29,7 @@ #include -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { using namespace ld24xx; @@ -93,14 +92,13 @@ class LD2410Component : public Component, public uart::UARTDevice { void set_gate_move_sensor(uint8_t gate, sensor::Sensor *s); void set_gate_still_sensor(uint8_t gate, sensor::Sensor *s); #endif - void set_throttle(uint16_t value) { this->throttle_ = value; }; void set_bluetooth_password(const std::string &password); void set_engineering_mode(bool enable); void read_all_info(); void restart_and_read_all_info(); void set_bluetooth(bool enable); - void set_distance_resolution(const std::string &state); - void set_baud_rate(const std::string &state); + void set_distance_resolution(const char *state); + void set_baud_rate(const char *state); void factory_reset(); protected: @@ -116,8 +114,6 @@ class LD2410Component : public Component, public uart::UARTDevice { void query_light_control_(); void restart_(); - uint32_t last_periodic_millis_ = 0; - uint16_t throttle_ = 0; uint8_t light_function_ = 0; uint8_t light_threshold_ = 0; uint8_t out_pin_level_ = 0; @@ -136,5 +132,4 @@ class LD2410Component : public Component, public uart::UARTDevice { #endif }; -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/number/gate_threshold_number.cpp b/esphome/components/ld2410/number/gate_threshold_number.cpp index 5d040554d7..65e864a4d7 100644 --- a/esphome/components/ld2410/number/gate_threshold_number.cpp +++ b/esphome/components/ld2410/number/gate_threshold_number.cpp @@ -1,7 +1,6 @@ #include "gate_threshold_number.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { GateThresholdNumber::GateThresholdNumber(uint8_t gate) : gate_(gate) {} @@ -10,5 +9,4 @@ void GateThresholdNumber::control(float value) { this->parent_->set_gate_threshold(this->gate_); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/number/gate_threshold_number.h b/esphome/components/ld2410/number/gate_threshold_number.h index 2806ecce63..63491f18d3 100644 --- a/esphome/components/ld2410/number/gate_threshold_number.h +++ b/esphome/components/ld2410/number/gate_threshold_number.h @@ -3,8 +3,7 @@ #include "esphome/components/number/number.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class GateThresholdNumber : public number::Number, public Parented { public: @@ -15,5 +14,4 @@ class GateThresholdNumber : public number::Number, public Parentedpublish_state(value); this->parent_->set_light_out_control(); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/number/light_threshold_number.h b/esphome/components/ld2410/number/light_threshold_number.h index 8f014373c0..3c5e433416 100644 --- a/esphome/components/ld2410/number/light_threshold_number.h +++ b/esphome/components/ld2410/number/light_threshold_number.h @@ -3,8 +3,7 @@ #include "esphome/components/number/number.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class LightThresholdNumber : public number::Number, public Parented { public: @@ -14,5 +13,4 @@ class LightThresholdNumber : public number::Number, public Parentedpublish_state(value); this->parent_->set_max_distances_timeout(); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/number/max_distance_timeout_number.h b/esphome/components/ld2410/number/max_distance_timeout_number.h index 7d91b4b5fe..35f4cbbfae 100644 --- a/esphome/components/ld2410/number/max_distance_timeout_number.h +++ b/esphome/components/ld2410/number/max_distance_timeout_number.h @@ -3,8 +3,7 @@ #include "esphome/components/number/number.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class MaxDistanceTimeoutNumber : public number::Number, public Parented { public: @@ -14,5 +13,4 @@ class MaxDistanceTimeoutNumber : public number::Number, public Parentedpublish_state(value); - this->parent_->set_baud_rate(state); +void BaudRateSelect::control(size_t index) { + this->publish_state(index); + this->parent_->set_baud_rate(this->option_at(index)); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/select/baud_rate_select.h b/esphome/components/ld2410/select/baud_rate_select.h index 3827b6a48a..fb1d016b1f 100644 --- a/esphome/components/ld2410/select/baud_rate_select.h +++ b/esphome/components/ld2410/select/baud_rate_select.h @@ -3,16 +3,14 @@ #include "esphome/components/select/select.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class BaudRateSelect : public select::Select, public Parented { public: BaudRateSelect() = default; protected: - void control(const std::string &value) override; + void control(size_t index) override; }; -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/select/distance_resolution_select.cpp b/esphome/components/ld2410/select/distance_resolution_select.cpp index eef34bda63..635bf206d3 100644 --- a/esphome/components/ld2410/select/distance_resolution_select.cpp +++ b/esphome/components/ld2410/select/distance_resolution_select.cpp @@ -1,12 +1,10 @@ #include "distance_resolution_select.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { -void DistanceResolutionSelect::control(const std::string &value) { - this->publish_state(value); - this->parent_->set_distance_resolution(state); +void DistanceResolutionSelect::control(size_t index) { + this->publish_state(index); + this->parent_->set_distance_resolution(this->option_at(index)); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/select/distance_resolution_select.h b/esphome/components/ld2410/select/distance_resolution_select.h index d6affb1020..be2389d36e 100644 --- a/esphome/components/ld2410/select/distance_resolution_select.h +++ b/esphome/components/ld2410/select/distance_resolution_select.h @@ -3,16 +3,14 @@ #include "esphome/components/select/select.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class DistanceResolutionSelect : public select::Select, public Parented { public: DistanceResolutionSelect() = default; protected: - void control(const std::string &value) override; + void control(size_t index) override; }; -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/select/light_out_control_select.cpp b/esphome/components/ld2410/select/light_out_control_select.cpp index ac23248a64..1c55721d52 100644 --- a/esphome/components/ld2410/select/light_out_control_select.cpp +++ b/esphome/components/ld2410/select/light_out_control_select.cpp @@ -1,12 +1,10 @@ #include "light_out_control_select.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { -void LightOutControlSelect::control(const std::string &value) { - this->publish_state(value); +void LightOutControlSelect::control(size_t index) { + this->publish_state(index); this->parent_->set_light_out_control(); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/select/light_out_control_select.h b/esphome/components/ld2410/select/light_out_control_select.h index 5d72e1774e..608c311af4 100644 --- a/esphome/components/ld2410/select/light_out_control_select.h +++ b/esphome/components/ld2410/select/light_out_control_select.h @@ -3,16 +3,14 @@ #include "esphome/components/select/select.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class LightOutControlSelect : public select::Select, public Parented { public: LightOutControlSelect() = default; protected: - void control(const std::string &value) override; + void control(size_t index) override; }; -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/sensor.py b/esphome/components/ld2410/sensor.py index 92245ea9a6..3bd34963bc 100644 --- a/esphome/components/ld2410/sensor.py +++ b/esphome/components/ld2410/sensor.py @@ -18,42 +18,98 @@ from esphome.const import ( from . import CONF_LD2410_ID, LD2410Component DEPENDENCIES = ["ld2410"] -CONF_STILL_DISTANCE = "still_distance" -CONF_MOVING_ENERGY = "moving_energy" -CONF_STILL_ENERGY = "still_energy" + CONF_DETECTION_DISTANCE = "detection_distance" CONF_MOVE_ENERGY = "move_energy" +CONF_MOVING_ENERGY = "moving_energy" +CONF_STILL_DISTANCE = "still_distance" +CONF_STILL_ENERGY = "still_energy" + CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component), cv.Optional(CONF_MOVING_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - unit_of_measurement=UNIT_CENTIMETER, + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_SIGNAL, + unit_of_measurement=UNIT_CENTIMETER, ), cv.Optional(CONF_STILL_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - unit_of_measurement=UNIT_CENTIMETER, + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_SIGNAL, + unit_of_measurement=UNIT_CENTIMETER, ), cv.Optional(CONF_MOVING_ENERGY): sensor.sensor_schema( - unit_of_measurement=UNIT_PERCENT, + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_MOTION_SENSOR, + unit_of_measurement=UNIT_PERCENT, ), cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema( - unit_of_measurement=UNIT_PERCENT, + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_FLASH, + unit_of_measurement=UNIT_PERCENT, ), cv.Optional(CONF_LIGHT): sensor.sensor_schema( device_class=DEVICE_CLASS_ILLUMINANCE, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_LIGHTBULB, ), cv.Optional(CONF_DETECTION_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - unit_of_measurement=UNIT_CENTIMETER, + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_SIGNAL, + unit_of_measurement=UNIT_CENTIMETER, ), } ) @@ -63,14 +119,32 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( cv.Optional(f"g{x}"): cv.Schema( { cv.Optional(CONF_MOVE_ENERGY): sensor.sensor_schema( - unit_of_measurement=UNIT_PERCENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_MOTION_SENSOR, + unit_of_measurement=UNIT_PERCENT, ), cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema( - unit_of_measurement=UNIT_PERCENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_FLASH, + unit_of_measurement=UNIT_PERCENT, ), } ) diff --git a/esphome/components/ld2410/switch/bluetooth_switch.cpp b/esphome/components/ld2410/switch/bluetooth_switch.cpp index 9bcee9b049..c85dd70ec9 100644 --- a/esphome/components/ld2410/switch/bluetooth_switch.cpp +++ b/esphome/components/ld2410/switch/bluetooth_switch.cpp @@ -1,12 +1,10 @@ #include "bluetooth_switch.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { void BluetoothSwitch::write_state(bool state) { this->publish_state(state); this->parent_->set_bluetooth(state); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/switch/bluetooth_switch.h b/esphome/components/ld2410/switch/bluetooth_switch.h index 35ae1ec0c9..07804e2292 100644 --- a/esphome/components/ld2410/switch/bluetooth_switch.h +++ b/esphome/components/ld2410/switch/bluetooth_switch.h @@ -3,8 +3,7 @@ #include "esphome/components/switch/switch.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class BluetoothSwitch : public switch_::Switch, public Parented { public: @@ -14,5 +13,4 @@ class BluetoothSwitch : public switch_::Switch, public Parented void write_state(bool state) override; }; -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/switch/engineering_mode_switch.cpp b/esphome/components/ld2410/switch/engineering_mode_switch.cpp index 967c87c887..4f2f08b03e 100644 --- a/esphome/components/ld2410/switch/engineering_mode_switch.cpp +++ b/esphome/components/ld2410/switch/engineering_mode_switch.cpp @@ -1,12 +1,10 @@ #include "engineering_mode_switch.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { void EngineeringModeSwitch::write_state(bool state) { this->publish_state(state); this->parent_->set_engineering_mode(state); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/switch/engineering_mode_switch.h b/esphome/components/ld2410/switch/engineering_mode_switch.h index e521200cd6..4dd8e16653 100644 --- a/esphome/components/ld2410/switch/engineering_mode_switch.h +++ b/esphome/components/ld2410/switch/engineering_mode_switch.h @@ -3,8 +3,7 @@ #include "esphome/components/switch/switch.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class EngineeringModeSwitch : public switch_::Switch, public Parented { public: @@ -14,5 +13,4 @@ class EngineeringModeSwitch : public switch_::Switch, public Parentedparent_->factory_reset(); } + +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/button/factory_reset_button.h b/esphome/components/ld2412/button/factory_reset_button.h new file mode 100644 index 0000000000..1ef6b23b80 --- /dev/null +++ b/esphome/components/ld2412/button/factory_reset_button.h @@ -0,0 +1,16 @@ +#pragma once + +#include "esphome/components/button/button.h" +#include "../ld2412.h" + +namespace esphome::ld2412 { + +class FactoryResetButton : public button::Button, public Parented { + public: + FactoryResetButton() = default; + + protected: + void press_action() override; +}; + +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/button/query_button.cpp b/esphome/components/ld2412/button/query_button.cpp new file mode 100644 index 0000000000..9affb26556 --- /dev/null +++ b/esphome/components/ld2412/button/query_button.cpp @@ -0,0 +1,7 @@ +#include "query_button.h" + +namespace esphome::ld2412 { + +void QueryButton::press_action() { this->parent_->read_all_info(); } + +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/button/query_button.h b/esphome/components/ld2412/button/query_button.h new file mode 100644 index 0000000000..373e135802 --- /dev/null +++ b/esphome/components/ld2412/button/query_button.h @@ -0,0 +1,16 @@ +#pragma once + +#include "esphome/components/button/button.h" +#include "../ld2412.h" + +namespace esphome::ld2412 { + +class QueryButton : public button::Button, public Parented { + public: + QueryButton() = default; + + protected: + void press_action() override; +}; + +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/button/restart_button.cpp b/esphome/components/ld2412/button/restart_button.cpp new file mode 100644 index 0000000000..430f6c998f --- /dev/null +++ b/esphome/components/ld2412/button/restart_button.cpp @@ -0,0 +1,7 @@ +#include "restart_button.h" + +namespace esphome::ld2412 { + +void RestartButton::press_action() { this->parent_->restart_and_read_all_info(); } + +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/button/restart_button.h b/esphome/components/ld2412/button/restart_button.h new file mode 100644 index 0000000000..80c79f5e7d --- /dev/null +++ b/esphome/components/ld2412/button/restart_button.h @@ -0,0 +1,16 @@ +#pragma once + +#include "esphome/components/button/button.h" +#include "../ld2412.h" + +namespace esphome::ld2412 { + +class RestartButton : public button::Button, public Parented { + public: + RestartButton() = default; + + protected: + void press_action() override; +}; + +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/button/start_dynamic_background_correction_button.cpp b/esphome/components/ld2412/button/start_dynamic_background_correction_button.cpp new file mode 100644 index 0000000000..8ba41a03fb --- /dev/null +++ b/esphome/components/ld2412/button/start_dynamic_background_correction_button.cpp @@ -0,0 +1,9 @@ +#include "start_dynamic_background_correction_button.h" + +#include "restart_button.h" + +namespace esphome::ld2412 { + +void StartDynamicBackgroundCorrectionButton::press_action() { this->parent_->start_dynamic_background_correction(); } + +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/button/start_dynamic_background_correction_button.h b/esphome/components/ld2412/button/start_dynamic_background_correction_button.h new file mode 100644 index 0000000000..b1f2127896 --- /dev/null +++ b/esphome/components/ld2412/button/start_dynamic_background_correction_button.h @@ -0,0 +1,16 @@ +#pragma once + +#include "esphome/components/button/button.h" +#include "../ld2412.h" + +namespace esphome::ld2412 { + +class StartDynamicBackgroundCorrectionButton : public button::Button, public Parented { + public: + StartDynamicBackgroundCorrectionButton() = default; + + protected: + void press_action() override; +}; + +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/ld2412.cpp b/esphome/components/ld2412/ld2412.cpp new file mode 100644 index 0000000000..0f6fe62d30 --- /dev/null +++ b/esphome/components/ld2412/ld2412.cpp @@ -0,0 +1,857 @@ +#include "ld2412.h" + +#ifdef USE_NUMBER +#include "esphome/components/number/number.h" +#endif +#ifdef USE_SENSOR +#include "esphome/components/sensor/sensor.h" +#endif + +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" + +namespace esphome::ld2412 { + +static const char *const TAG = "ld2412"; + +enum BaudRate : uint8_t { + BAUD_RATE_9600 = 1, + BAUD_RATE_19200 = 2, + BAUD_RATE_38400 = 3, + BAUD_RATE_57600 = 4, + BAUD_RATE_115200 = 5, + BAUD_RATE_230400 = 6, + BAUD_RATE_256000 = 7, + BAUD_RATE_460800 = 8, +}; + +enum DistanceResolution : uint8_t { + DISTANCE_RESOLUTION_0_2 = 0x03, + DISTANCE_RESOLUTION_0_5 = 0x01, + DISTANCE_RESOLUTION_0_75 = 0x00, +}; + +enum LightFunction : uint8_t { + LIGHT_FUNCTION_OFF = 0x00, + LIGHT_FUNCTION_BELOW = 0x01, + LIGHT_FUNCTION_ABOVE = 0x02, +}; + +enum OutPinLevel : uint8_t { + OUT_PIN_LEVEL_LOW = 0x01, + OUT_PIN_LEVEL_HIGH = 0x00, +}; + +/* +Data Type: 6th byte +Target states: 9th byte + Moving target distance: 10~11th bytes + Moving target energy: 12th byte + Still target distance: 13~14th bytes + Still target energy: 15th byte + Detect distance: 16~17th bytes +*/ +enum PeriodicData : uint8_t { + DATA_TYPES = 6, + TARGET_STATES = 8, + MOVING_TARGET_LOW = 9, + MOVING_TARGET_HIGH = 10, + MOVING_ENERGY = 11, + STILL_TARGET_LOW = 12, + STILL_TARGET_HIGH = 13, + STILL_ENERGY = 14, + MOVING_SENSOR_START = 17, + STILL_SENSOR_START = 31, + LIGHT_SENSOR = 45, + OUT_PIN_SENSOR = 38, +}; + +enum PeriodicDataValue : uint8_t { + HEADER = 0XAA, + FOOTER = 0x55, + CHECK = 0x00, +}; + +enum AckData : uint8_t { + COMMAND = 6, + COMMAND_STATUS = 7, +}; + +// Memory-efficient lookup tables +struct StringToUint8 { + const char *str; + const uint8_t value; +}; + +struct Uint8ToString { + const uint8_t value; + const char *str; +}; + +constexpr StringToUint8 BAUD_RATES_BY_STR[] = { + {"9600", BAUD_RATE_9600}, {"19200", BAUD_RATE_19200}, {"38400", BAUD_RATE_38400}, + {"57600", BAUD_RATE_57600}, {"115200", BAUD_RATE_115200}, {"230400", BAUD_RATE_230400}, + {"256000", BAUD_RATE_256000}, {"460800", BAUD_RATE_460800}, +}; + +constexpr StringToUint8 DISTANCE_RESOLUTIONS_BY_STR[] = { + {"0.2m", DISTANCE_RESOLUTION_0_2}, + {"0.5m", DISTANCE_RESOLUTION_0_5}, + {"0.75m", DISTANCE_RESOLUTION_0_75}, +}; + +constexpr Uint8ToString DISTANCE_RESOLUTIONS_BY_UINT[] = { + {DISTANCE_RESOLUTION_0_2, "0.2m"}, + {DISTANCE_RESOLUTION_0_5, "0.5m"}, + {DISTANCE_RESOLUTION_0_75, "0.75m"}, +}; + +constexpr StringToUint8 LIGHT_FUNCTIONS_BY_STR[] = { + {"off", LIGHT_FUNCTION_OFF}, + {"below", LIGHT_FUNCTION_BELOW}, + {"above", LIGHT_FUNCTION_ABOVE}, +}; + +constexpr Uint8ToString LIGHT_FUNCTIONS_BY_UINT[] = { + {LIGHT_FUNCTION_OFF, "off"}, + {LIGHT_FUNCTION_BELOW, "below"}, + {LIGHT_FUNCTION_ABOVE, "above"}, +}; + +constexpr StringToUint8 OUT_PIN_LEVELS_BY_STR[] = { + {"low", OUT_PIN_LEVEL_LOW}, + {"high", OUT_PIN_LEVEL_HIGH}, +}; + +constexpr Uint8ToString OUT_PIN_LEVELS_BY_UINT[] = { + {OUT_PIN_LEVEL_LOW, "low"}, + {OUT_PIN_LEVEL_HIGH, "high"}, +}; + +// Helper functions for lookups +template uint8_t find_uint8(const StringToUint8 (&arr)[N], const char *str) { + for (const auto &entry : arr) { + if (strcmp(str, entry.str) == 0) { + return entry.value; + } + } + return 0xFF; // Not found +} + +template const char *find_str(const Uint8ToString (&arr)[N], uint8_t value) { + for (const auto &entry : arr) { + if (value == entry.value) { + return entry.str; + } + } + return ""; // Not found +} + +static constexpr uint8_t DEFAULT_PRESENCE_TIMEOUT = 5; // Default used when number component is not defined +// Commands +static constexpr uint8_t CMD_ENABLE_CONF = 0xFF; +static constexpr uint8_t CMD_DISABLE_CONF = 0xFE; +static constexpr uint8_t CMD_ENABLE_ENG = 0x62; +static constexpr uint8_t CMD_DISABLE_ENG = 0x63; +static constexpr uint8_t CMD_QUERY_BASIC_CONF = 0x12; +static constexpr uint8_t CMD_BASIC_CONF = 0x02; +static constexpr uint8_t CMD_QUERY_VERSION = 0xA0; +static constexpr uint8_t CMD_QUERY_DISTANCE_RESOLUTION = 0x11; +static constexpr uint8_t CMD_SET_DISTANCE_RESOLUTION = 0x01; +static constexpr uint8_t CMD_QUERY_LIGHT_CONTROL = 0x1C; +static constexpr uint8_t CMD_SET_LIGHT_CONTROL = 0x0C; +static constexpr uint8_t CMD_SET_BAUD_RATE = 0xA1; +static constexpr uint8_t CMD_QUERY_MAC_ADDRESS = 0xA5; +static constexpr uint8_t CMD_FACTORY_RESET = 0xA2; +static constexpr uint8_t CMD_RESTART = 0xA3; +static constexpr uint8_t CMD_BLUETOOTH = 0xA4; +static constexpr uint8_t CMD_DYNAMIC_BACKGROUND_CORRECTION = 0x0B; +static constexpr uint8_t CMD_QUERY_DYNAMIC_BACKGROUND_CORRECTION = 0x1B; +static constexpr uint8_t CMD_MOTION_GATE_SENS = 0x03; +static constexpr uint8_t CMD_QUERY_MOTION_GATE_SENS = 0x13; +static constexpr uint8_t CMD_STATIC_GATE_SENS = 0x04; +static constexpr uint8_t CMD_QUERY_STATIC_GATE_SENS = 0x14; +static constexpr uint8_t CMD_NONE = 0x00; +// Commands values +static constexpr uint8_t CMD_MAX_MOVE_VALUE = 0x00; +static constexpr uint8_t CMD_MAX_STILL_VALUE = 0x01; +static constexpr uint8_t CMD_DURATION_VALUE = 0x02; +// Bitmasks for target states +static constexpr uint8_t MOVE_BITMASK = 0x01; +static constexpr uint8_t STILL_BITMASK = 0x02; +// Header & Footer size +static constexpr uint8_t HEADER_FOOTER_SIZE = 4; +// Command Header & Footer +static constexpr uint8_t CMD_FRAME_HEADER[HEADER_FOOTER_SIZE] = {0xFD, 0xFC, 0xFB, 0xFA}; +static constexpr uint8_t CMD_FRAME_FOOTER[HEADER_FOOTER_SIZE] = {0x04, 0x03, 0x02, 0x01}; +// Data Header & Footer +static constexpr uint8_t DATA_FRAME_HEADER[HEADER_FOOTER_SIZE] = {0xF4, 0xF3, 0xF2, 0xF1}; +static constexpr uint8_t DATA_FRAME_FOOTER[HEADER_FOOTER_SIZE] = {0xF8, 0xF7, 0xF6, 0xF5}; +// MAC address the module uses when Bluetooth is disabled +static constexpr uint8_t NO_MAC[] = {0x08, 0x05, 0x04, 0x03, 0x02, 0x01}; + +static inline int two_byte_to_int(char firstbyte, char secondbyte) { return (int16_t) (secondbyte << 8) + firstbyte; } + +static inline bool validate_header_footer(const uint8_t *header_footer, const uint8_t *buffer) { + return std::memcmp(header_footer, buffer, HEADER_FOOTER_SIZE) == 0; +} + +void LD2412Component::dump_config() { + char mac_s[18]; + char version_s[20]; + const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s); + ld24xx::format_version_str(this->version_, version_s); + ESP_LOGCONFIG(TAG, + "LD2412:\n" + " Firmware version: %s\n" + " MAC address: %s", + version_s, mac_str); +#ifdef USE_BINARY_SENSOR + ESP_LOGCONFIG(TAG, "Binary Sensors:"); + LOG_BINARY_SENSOR(" ", "DynamicBackgroundCorrectionStatus", + this->dynamic_background_correction_status_binary_sensor_); + LOG_BINARY_SENSOR(" ", "MovingTarget", this->moving_target_binary_sensor_); + LOG_BINARY_SENSOR(" ", "StillTarget", this->still_target_binary_sensor_); + LOG_BINARY_SENSOR(" ", "Target", this->target_binary_sensor_); +#endif +#ifdef USE_SENSOR + ESP_LOGCONFIG(TAG, "Sensors:"); + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "Light", this->light_sensor_); + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "DetectionDistance", this->detection_distance_sensor_); + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "MovingTargetDistance", this->moving_target_distance_sensor_); + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "MovingTargetEnergy", this->moving_target_energy_sensor_); + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "StillTargetDistance", this->still_target_distance_sensor_); + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "StillTargetEnergy", this->still_target_energy_sensor_); + for (auto &s : this->gate_still_sensors_) { + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "GateStill", s); + } + for (auto &s : this->gate_move_sensors_) { + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "GateMove", s); + } +#endif +#ifdef USE_TEXT_SENSOR + ESP_LOGCONFIG(TAG, "Text Sensors:"); + LOG_TEXT_SENSOR(" ", "MAC address", this->mac_text_sensor_); + LOG_TEXT_SENSOR(" ", "Version", this->version_text_sensor_); +#endif +#ifdef USE_NUMBER + ESP_LOGCONFIG(TAG, "Numbers:"); + LOG_NUMBER(" ", "LightThreshold", this->light_threshold_number_); + LOG_NUMBER(" ", "MaxDistanceGate", this->max_distance_gate_number_); + LOG_NUMBER(" ", "MinDistanceGate", this->min_distance_gate_number_); + LOG_NUMBER(" ", "Timeout", this->timeout_number_); + for (number::Number *n : this->gate_move_threshold_numbers_) { + LOG_NUMBER(" ", "Move Thresholds", n); + } + for (number::Number *n : this->gate_still_threshold_numbers_) { + LOG_NUMBER(" ", "Still Thresholds", n); + } +#endif +#ifdef USE_SELECT + ESP_LOGCONFIG(TAG, "Selects:"); + LOG_SELECT(" ", "BaudRate", this->baud_rate_select_); + LOG_SELECT(" ", "DistanceResolution", this->distance_resolution_select_); + LOG_SELECT(" ", "LightFunction", this->light_function_select_); + LOG_SELECT(" ", "OutPinLevel", this->out_pin_level_select_); +#endif +#ifdef USE_SWITCH + ESP_LOGCONFIG(TAG, "Switches:"); + LOG_SWITCH(" ", "Bluetooth", this->bluetooth_switch_); + LOG_SWITCH(" ", "EngineeringMode", this->engineering_mode_switch_); +#endif +#ifdef USE_BUTTON + ESP_LOGCONFIG(TAG, "Buttons:"); + LOG_BUTTON(" ", "FactoryReset", this->factory_reset_button_); + LOG_BUTTON(" ", "Query", this->query_button_); + LOG_BUTTON(" ", "Restart", this->restart_button_); + LOG_BUTTON(" ", "StartDynamicBackgroundCorrection", this->start_dynamic_background_correction_button_); +#endif +} + +void LD2412Component::setup() { + ESP_LOGCONFIG(TAG, "Running setup"); + this->read_all_info(); +} + +void LD2412Component::read_all_info() { + this->set_config_mode_(true); + this->get_version_(); + delay(10); // NOLINT + this->get_mac_(); + delay(10); // NOLINT + this->get_distance_resolution_(); + delay(10); // NOLINT + this->query_parameters_(); + delay(10); // NOLINT + this->query_dynamic_background_correction_(); + delay(10); // NOLINT + this->query_light_control_(); + delay(10); // NOLINT +#ifdef USE_NUMBER + this->get_gate_threshold(); + delay(10); // NOLINT +#endif + this->set_config_mode_(false); +#ifdef USE_SELECT + const auto baud_rate = std::to_string(this->parent_->get_baud_rate()); + if (this->baud_rate_select_ != nullptr) { + this->baud_rate_select_->publish_state(baud_rate); + } +#endif +} + +void LD2412Component::restart_and_read_all_info() { + this->set_config_mode_(true); + this->restart_(); + this->set_timeout(1000, [this]() { this->read_all_info(); }); +} + +void LD2412Component::loop() { + while (this->available()) { + this->readline_(this->read()); + } +} + +void LD2412Component::send_command_(uint8_t command, const uint8_t *command_value, uint8_t command_value_len) { + ESP_LOGV(TAG, "Sending COMMAND %02X", command); + // frame header bytes + this->write_array(CMD_FRAME_HEADER, HEADER_FOOTER_SIZE); + // length bytes + uint8_t len = 2; + if (command_value != nullptr) { + len += command_value_len; + } + // 2 length bytes (low, high) + 2 command bytes (low, high) + uint8_t len_cmd[] = {len, 0x00, command, 0x00}; + this->write_array(len_cmd, sizeof(len_cmd)); + + // command value bytes + if (command_value != nullptr) { + this->write_array(command_value, command_value_len); + } + // frame footer bytes + this->write_array(CMD_FRAME_FOOTER, HEADER_FOOTER_SIZE); + + if (command != CMD_ENABLE_CONF && command != CMD_DISABLE_CONF) { + delay(30); // NOLINT + } + delay(20); // NOLINT +} + +void LD2412Component::handle_periodic_data_() { + // 4 frame header bytes + 2 length bytes + 1 data end byte + 1 crc byte + 4 frame footer bytes + // data header=0xAA, data footer=0x55, crc=0x00 + if (this->buffer_pos_ < 12 || !ld2412::validate_header_footer(DATA_FRAME_HEADER, this->buffer_data_) || + this->buffer_data_[7] != HEADER || this->buffer_data_[this->buffer_pos_ - 6] != FOOTER) { + return; + } + /* + Data Type: 7th + 0x01: Engineering mode + 0x02: Normal mode + */ + bool engineering_mode = this->buffer_data_[DATA_TYPES] == 0x01; +#ifdef USE_SWITCH + if (this->engineering_mode_switch_ != nullptr) { + this->engineering_mode_switch_->publish_state(engineering_mode); + } +#endif + +#ifdef USE_BINARY_SENSOR + /* + Target states: 9th + 0x00 = No target + 0x01 = Moving targets + 0x02 = Still targets + 0x03 = Moving+Still targets + */ + char target_state = this->buffer_data_[TARGET_STATES]; + if (this->target_binary_sensor_ != nullptr) { + this->target_binary_sensor_->publish_state(target_state != 0x00); + } + if (this->moving_target_binary_sensor_ != nullptr) { + this->moving_target_binary_sensor_->publish_state(target_state & MOVE_BITMASK); + } + if (this->still_target_binary_sensor_ != nullptr) { + this->still_target_binary_sensor_->publish_state(target_state & STILL_BITMASK); + } +#endif + /* + Moving target distance: 10~11th bytes + Moving target energy: 12th byte + Still target distance: 13~14th bytes + Still target energy: 15th byte + Detect distance: 16~17th bytes + */ +#ifdef USE_SENSOR + SAFE_PUBLISH_SENSOR( + this->moving_target_distance_sensor_, + ld2412::two_byte_to_int(this->buffer_data_[MOVING_TARGET_LOW], this->buffer_data_[MOVING_TARGET_HIGH])) + SAFE_PUBLISH_SENSOR(this->moving_target_energy_sensor_, this->buffer_data_[MOVING_ENERGY]) + SAFE_PUBLISH_SENSOR( + this->still_target_distance_sensor_, + ld2412::two_byte_to_int(this->buffer_data_[STILL_TARGET_LOW], this->buffer_data_[STILL_TARGET_HIGH])) + SAFE_PUBLISH_SENSOR(this->still_target_energy_sensor_, this->buffer_data_[STILL_ENERGY]) + if (this->detection_distance_sensor_ != nullptr) { + int new_detect_distance = 0; + if (target_state != 0x00 && (target_state & MOVE_BITMASK)) { + new_detect_distance = + ld2412::two_byte_to_int(this->buffer_data_[MOVING_TARGET_LOW], this->buffer_data_[MOVING_TARGET_HIGH]); + } else if (target_state != 0x00) { + new_detect_distance = + ld2412::two_byte_to_int(this->buffer_data_[STILL_TARGET_LOW], this->buffer_data_[STILL_TARGET_HIGH]); + } + this->detection_distance_sensor_->publish_state_if_not_dup(new_detect_distance); + } + if (engineering_mode) { + /* + Moving distance range: 18th byte + Still distance range: 19th byte + Moving energy: 20~28th bytes + */ + for (uint8_t i = 0; i < TOTAL_GATES; i++) { + SAFE_PUBLISH_SENSOR(this->gate_move_sensors_[i], this->buffer_data_[MOVING_SENSOR_START + i]) + } + /* + Still energy: 29~37th bytes + */ + for (uint8_t i = 0; i < TOTAL_GATES; i++) { + SAFE_PUBLISH_SENSOR(this->gate_still_sensors_[i], this->buffer_data_[STILL_SENSOR_START + i]) + } + /* + Light sensor: 38th bytes + */ + SAFE_PUBLISH_SENSOR(this->light_sensor_, this->buffer_data_[LIGHT_SENSOR]) + } else { + for (auto &gate_move_sensor : this->gate_move_sensors_) { + SAFE_PUBLISH_SENSOR_UNKNOWN(gate_move_sensor) + } + for (auto &gate_still_sensor : this->gate_still_sensors_) { + SAFE_PUBLISH_SENSOR_UNKNOWN(gate_still_sensor) + } + SAFE_PUBLISH_SENSOR_UNKNOWN(this->light_sensor_) + } +#endif + // the radar module won't tell us when it's done, so we just have to keep polling... + if (this->dynamic_background_correction_active_) { + this->set_config_mode_(true); + this->query_dynamic_background_correction_(); + this->set_config_mode_(false); + } +} + +#ifdef USE_NUMBER +std::function set_number_value(number::Number *n, float value) { + if (n != nullptr && (!n->has_state() || n->state != value)) { + n->state = value; + return [n, value]() { n->publish_state(value); }; + } + return []() {}; +} +#endif + +bool LD2412Component::handle_ack_data_() { + ESP_LOGV(TAG, "Handling ACK DATA for COMMAND %02X", this->buffer_data_[COMMAND]); + if (this->buffer_pos_ < 10) { + ESP_LOGW(TAG, "Invalid length"); + return true; + } + if (!ld2412::validate_header_footer(CMD_FRAME_HEADER, this->buffer_data_)) { + ESP_LOGW(TAG, "Invalid header: %s", format_hex_pretty(this->buffer_data_, HEADER_FOOTER_SIZE).c_str()); + return true; + } + if (this->buffer_data_[COMMAND_STATUS] != 0x01) { + ESP_LOGW(TAG, "Invalid status"); + return true; + } + if (this->buffer_data_[8] || this->buffer_data_[9]) { + ESP_LOGW(TAG, "Invalid command: %02X, %02X", this->buffer_data_[8], this->buffer_data_[9]); + return true; + } + + switch (this->buffer_data_[COMMAND]) { + case CMD_ENABLE_CONF: + ESP_LOGV(TAG, "Enable conf"); + break; + + case CMD_DISABLE_CONF: + ESP_LOGV(TAG, "Disabled conf"); + break; + + case CMD_SET_BAUD_RATE: + ESP_LOGV(TAG, "Baud rate change"); +#ifdef USE_SELECT + if (this->baud_rate_select_ != nullptr) { + ESP_LOGW(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->current_option()); + } +#endif + break; + + case CMD_QUERY_VERSION: { + std::memcpy(this->version_, &this->buffer_data_[12], sizeof(this->version_)); + char version_s[20]; + ld24xx::format_version_str(this->version_, version_s); + ESP_LOGV(TAG, "Firmware version: %s", version_s); +#ifdef USE_TEXT_SENSOR + if (this->version_text_sensor_ != nullptr) { + this->version_text_sensor_->publish_state(version_s); + } +#endif + break; + } + case CMD_QUERY_DISTANCE_RESOLUTION: { + const auto *distance_resolution = find_str(DISTANCE_RESOLUTIONS_BY_UINT, this->buffer_data_[10]); + ESP_LOGV(TAG, "Distance resolution: %s", distance_resolution); +#ifdef USE_SELECT + if (this->distance_resolution_select_ != nullptr) { + this->distance_resolution_select_->publish_state(distance_resolution); + } +#endif + break; + } + + case CMD_QUERY_LIGHT_CONTROL: { + this->light_function_ = this->buffer_data_[10]; + this->light_threshold_ = this->buffer_data_[11]; + const auto *light_function_str = find_str(LIGHT_FUNCTIONS_BY_UINT, this->light_function_); + ESP_LOGV(TAG, + "Light function: %s\n" + "Light threshold: %u", + light_function_str, this->light_threshold_); +#ifdef USE_SELECT + if (this->light_function_select_ != nullptr) { + this->light_function_select_->publish_state(light_function_str); + } +#endif +#ifdef USE_NUMBER + if (this->light_threshold_number_ != nullptr) { + this->light_threshold_number_->publish_state(static_cast(this->light_threshold_)); + } +#endif + break; + } + + case CMD_QUERY_MAC_ADDRESS: { + if (this->buffer_pos_ < 20) { + return false; + } + + this->bluetooth_on_ = std::memcmp(&this->buffer_data_[10], NO_MAC, sizeof(NO_MAC)) != 0; + if (this->bluetooth_on_) { + std::memcpy(this->mac_address_, &this->buffer_data_[10], sizeof(this->mac_address_)); + } + + char mac_s[18]; + const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s); + ESP_LOGV(TAG, "MAC address: %s", mac_str); +#ifdef USE_TEXT_SENSOR + if (this->mac_text_sensor_ != nullptr) { + this->mac_text_sensor_->publish_state(mac_str); + } +#endif +#ifdef USE_SWITCH + if (this->bluetooth_switch_ != nullptr) { + this->bluetooth_switch_->publish_state(this->bluetooth_on_); + } +#endif + break; + } + + case CMD_SET_DISTANCE_RESOLUTION: + ESP_LOGV(TAG, "Handled set distance resolution command"); + break; + + case CMD_QUERY_DYNAMIC_BACKGROUND_CORRECTION: { + ESP_LOGV(TAG, "Handled query dynamic background correction"); + bool dynamic_background_correction_active = (this->buffer_data_[10] != 0x00); +#ifdef USE_BINARY_SENSOR + if (this->dynamic_background_correction_status_binary_sensor_ != nullptr) { + this->dynamic_background_correction_status_binary_sensor_->publish_state(dynamic_background_correction_active); + } +#endif + this->dynamic_background_correction_active_ = dynamic_background_correction_active; + break; + } + + case CMD_BLUETOOTH: + ESP_LOGV(TAG, "Handled bluetooth command"); + break; + + case CMD_SET_LIGHT_CONTROL: + ESP_LOGV(TAG, "Handled set light control command"); + break; + + case CMD_QUERY_MOTION_GATE_SENS: { +#ifdef USE_NUMBER + std::vector> updates; + updates.reserve(this->gate_still_threshold_numbers_.size()); + for (size_t i = 0; i < this->gate_still_threshold_numbers_.size(); i++) { + updates.push_back(set_number_value(this->gate_move_threshold_numbers_[i], this->buffer_data_[10 + i])); + } + for (auto &update : updates) { + update(); + } +#endif + break; + } + + case CMD_QUERY_STATIC_GATE_SENS: { +#ifdef USE_NUMBER + std::vector> updates; + updates.reserve(this->gate_still_threshold_numbers_.size()); + for (size_t i = 0; i < this->gate_still_threshold_numbers_.size(); i++) { + updates.push_back(set_number_value(this->gate_still_threshold_numbers_[i], this->buffer_data_[10 + i])); + } + for (auto &update : updates) { + update(); + } +#endif + break; + } + + case CMD_QUERY_BASIC_CONF: // Query parameters response + { +#ifdef USE_NUMBER + /* + Moving distance range: 9th byte + Still distance range: 10th byte + */ + std::vector> updates; + updates.push_back(set_number_value(this->min_distance_gate_number_, this->buffer_data_[10])); + updates.push_back(set_number_value(this->max_distance_gate_number_, this->buffer_data_[11] - 1)); + ESP_LOGV(TAG, "min_distance_gate_number_: %u, max_distance_gate_number_ %u", this->buffer_data_[10], + this->buffer_data_[11]); + /* + None Duration: 11~12th bytes + */ + updates.push_back(set_number_value(this->timeout_number_, + ld2412::two_byte_to_int(this->buffer_data_[12], this->buffer_data_[13]))); + ESP_LOGV(TAG, "timeout_number_: %u", ld2412::two_byte_to_int(this->buffer_data_[12], this->buffer_data_[13])); + /* + Output pin configuration: 13th bytes + */ + this->out_pin_level_ = this->buffer_data_[14]; +#ifdef USE_SELECT + const auto *out_pin_level_str = find_str(OUT_PIN_LEVELS_BY_UINT, this->out_pin_level_); + if (this->out_pin_level_select_ != nullptr) { + this->out_pin_level_select_->publish_state(out_pin_level_str); + } +#endif + for (auto &update : updates) { + update(); + } +#endif + } break; + default: + break; + } + + return true; +} + +void LD2412Component::readline_(int readch) { + if (readch < 0) { + return; // No data available + } + if (this->buffer_pos_ < HEADER_FOOTER_SIZE && readch != DATA_FRAME_HEADER[this->buffer_pos_] && + readch != CMD_FRAME_HEADER[this->buffer_pos_]) { + this->buffer_pos_ = 0; + return; + } + if (this->buffer_pos_ < MAX_LINE_LENGTH - 1) { + this->buffer_data_[this->buffer_pos_++] = readch; + this->buffer_data_[this->buffer_pos_] = 0; + } else { + // We should never get here, but just in case... + ESP_LOGW(TAG, "Max command length exceeded; ignoring"); + this->buffer_pos_ = 0; + } + if (this->buffer_pos_ < 4) { + return; // Not enough data to process yet + } + if (ld2412::validate_header_footer(DATA_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) { + ESP_LOGV(TAG, "Handling Periodic Data: %s", format_hex_pretty(this->buffer_data_, this->buffer_pos_).c_str()); + this->handle_periodic_data_(); + this->buffer_pos_ = 0; // Reset position index for next message + } else if (ld2412::validate_header_footer(CMD_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) { + ESP_LOGV(TAG, "Handling Ack Data: %s", format_hex_pretty(this->buffer_data_, this->buffer_pos_).c_str()); + if (this->handle_ack_data_()) { + this->buffer_pos_ = 0; // Reset position index for next message + } else { + ESP_LOGV(TAG, "Ack Data incomplete"); + } + } +} + +void LD2412Component::set_config_mode_(bool enable) { + const uint8_t cmd = enable ? CMD_ENABLE_CONF : CMD_DISABLE_CONF; + const uint8_t cmd_value[2] = {0x01, 0x00}; + this->send_command_(cmd, enable ? cmd_value : nullptr, sizeof(cmd_value)); +} + +void LD2412Component::set_bluetooth(bool enable) { + this->set_config_mode_(true); + const uint8_t cmd_value[2] = {enable ? (uint8_t) 0x01 : (uint8_t) 0x00, 0x00}; + this->send_command_(CMD_BLUETOOTH, cmd_value, sizeof(cmd_value)); + this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); +} + +void LD2412Component::set_distance_resolution(const char *state) { + this->set_config_mode_(true); + const uint8_t cmd_value[6] = {find_uint8(DISTANCE_RESOLUTIONS_BY_STR, state), 0x00, 0x00, 0x00, 0x00, 0x00}; + this->send_command_(CMD_SET_DISTANCE_RESOLUTION, cmd_value, sizeof(cmd_value)); + this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); +} + +void LD2412Component::set_baud_rate(const char *state) { + this->set_config_mode_(true); + const uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00}; + this->send_command_(CMD_SET_BAUD_RATE, cmd_value, sizeof(cmd_value)); + this->set_timeout(200, [this]() { this->restart_(); }); +} + +void LD2412Component::query_dynamic_background_correction_() { + this->send_command_(CMD_QUERY_DYNAMIC_BACKGROUND_CORRECTION, nullptr, 0); +} + +void LD2412Component::start_dynamic_background_correction() { + if (this->dynamic_background_correction_active_) { + return; // Already in progress + } +#ifdef USE_BINARY_SENSOR + if (this->dynamic_background_correction_status_binary_sensor_ != nullptr) { + this->dynamic_background_correction_status_binary_sensor_->publish_state(true); + } +#endif + this->dynamic_background_correction_active_ = true; + this->set_config_mode_(true); + this->send_command_(CMD_DYNAMIC_BACKGROUND_CORRECTION, nullptr, 0); + this->set_config_mode_(false); +} + +void LD2412Component::set_engineering_mode(bool enable) { + const uint8_t cmd = enable ? CMD_ENABLE_ENG : CMD_DISABLE_ENG; + this->set_config_mode_(true); + this->send_command_(cmd, nullptr, 0); + this->set_config_mode_(false); +} + +void LD2412Component::factory_reset() { + this->set_config_mode_(true); + this->send_command_(CMD_FACTORY_RESET, nullptr, 0); + this->set_timeout(2000, [this]() { this->restart_and_read_all_info(); }); +} + +void LD2412Component::restart_() { this->send_command_(CMD_RESTART, nullptr, 0); } + +void LD2412Component::query_parameters_() { this->send_command_(CMD_QUERY_BASIC_CONF, nullptr, 0); } + +void LD2412Component::get_version_() { this->send_command_(CMD_QUERY_VERSION, nullptr, 0); } + +void LD2412Component::get_mac_() { + const uint8_t cmd_value[2] = {0x01, 0x00}; + this->send_command_(CMD_QUERY_MAC_ADDRESS, cmd_value, sizeof(cmd_value)); +} + +void LD2412Component::get_distance_resolution_() { this->send_command_(CMD_QUERY_DISTANCE_RESOLUTION, nullptr, 0); } + +void LD2412Component::query_light_control_() { this->send_command_(CMD_QUERY_LIGHT_CONTROL, nullptr, 0); } + +void LD2412Component::set_basic_config() { +#ifdef USE_NUMBER + if (!this->min_distance_gate_number_->has_state() || !this->max_distance_gate_number_->has_state() || + !this->timeout_number_->has_state()) { + return; + } +#endif +#ifdef USE_SELECT + if (!this->out_pin_level_select_->has_state()) { + return; + } +#endif + + uint8_t value[5] = { +#ifdef USE_NUMBER + lowbyte(static_cast(this->min_distance_gate_number_->state)), + lowbyte(static_cast(this->max_distance_gate_number_->state) + 1), + lowbyte(static_cast(this->timeout_number_->state)), + highbyte(static_cast(this->timeout_number_->state)), +#else + 1, TOTAL_GATES, DEFAULT_PRESENCE_TIMEOUT, 0, +#endif +#ifdef USE_SELECT + find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->current_option()), +#else + 0x01, // Default value if not using select +#endif + }; + this->set_config_mode_(true); + this->send_command_(CMD_BASIC_CONF, value, sizeof(value)); + this->set_config_mode_(false); +} + +#ifdef USE_NUMBER +void LD2412Component::set_gate_threshold() { + if (this->gate_move_threshold_numbers_.empty() && this->gate_still_threshold_numbers_.empty()) { + return; // No gate threshold numbers set; nothing to do here + } + uint8_t value[TOTAL_GATES] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + this->set_config_mode_(true); + if (!this->gate_move_threshold_numbers_.empty()) { + for (size_t i = 0; i < this->gate_move_threshold_numbers_.size(); i++) { + value[i] = lowbyte(static_cast(this->gate_move_threshold_numbers_[i]->state)); + } + this->send_command_(CMD_MOTION_GATE_SENS, value, sizeof(value)); + } + if (!this->gate_still_threshold_numbers_.empty()) { + for (size_t i = 0; i < this->gate_still_threshold_numbers_.size(); i++) { + value[i] = lowbyte(static_cast(this->gate_still_threshold_numbers_[i]->state)); + } + this->send_command_(CMD_STATIC_GATE_SENS, value, sizeof(value)); + } + this->set_config_mode_(false); +} + +void LD2412Component::get_gate_threshold() { + this->send_command_(CMD_QUERY_MOTION_GATE_SENS, nullptr, 0); + this->send_command_(CMD_QUERY_STATIC_GATE_SENS, nullptr, 0); +} + +void LD2412Component::set_gate_still_threshold_number(uint8_t gate, number::Number *n) { + this->gate_still_threshold_numbers_[gate] = n; +} + +void LD2412Component::set_gate_move_threshold_number(uint8_t gate, number::Number *n) { + this->gate_move_threshold_numbers_[gate] = n; +} +#endif + +void LD2412Component::set_light_out_control() { +#ifdef USE_NUMBER + if (this->light_threshold_number_ != nullptr && this->light_threshold_number_->has_state()) { + this->light_threshold_ = static_cast(this->light_threshold_number_->state); + } +#endif +#ifdef USE_SELECT + if (this->light_function_select_ != nullptr && this->light_function_select_->has_state()) { + this->light_function_ = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_select_->current_option()); + } +#endif + uint8_t value[2] = {this->light_function_, this->light_threshold_}; + this->set_config_mode_(true); + this->send_command_(CMD_SET_LIGHT_CONTROL, value, sizeof(value)); + this->query_light_control_(); + this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); +} + +#ifdef USE_SENSOR +// These could leak memory, but they are only set once prior to 'setup()' and should never be used again. +void LD2412Component::set_gate_move_sensor(uint8_t gate, sensor::Sensor *s) { + this->gate_move_sensors_[gate] = new SensorWithDedup(s); +} +void LD2412Component::set_gate_still_sensor(uint8_t gate, sensor::Sensor *s) { + this->gate_still_sensors_[gate] = new SensorWithDedup(s); +} +#endif + +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/ld2412.h b/esphome/components/ld2412/ld2412.h new file mode 100644 index 0000000000..5dd5e7bcde --- /dev/null +++ b/esphome/components/ld2412/ld2412.h @@ -0,0 +1,139 @@ +#pragma once +#include "esphome/core/defines.h" +#include "esphome/core/component.h" +#ifdef USE_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif +#ifdef USE_SENSOR +#include "esphome/components/sensor/sensor.h" +#endif +#ifdef USE_NUMBER +#include "esphome/components/number/number.h" +#endif +#ifdef USE_SWITCH +#include "esphome/components/switch/switch.h" +#endif +#ifdef USE_BUTTON +#include "esphome/components/button/button.h" +#endif +#ifdef USE_SELECT +#include "esphome/components/select/select.h" +#endif +#ifdef USE_TEXT_SENSOR +#include "esphome/components/text_sensor/text_sensor.h" +#endif +#include "esphome/components/ld24xx/ld24xx.h" +#include "esphome/components/uart/uart.h" +#include "esphome/core/automation.h" +#include "esphome/core/helpers.h" + +#include + +namespace esphome::ld2412 { + +using namespace ld24xx; + +static constexpr uint8_t MAX_LINE_LENGTH = 54; // Max characters for serial buffer +static constexpr uint8_t TOTAL_GATES = 14; // Total number of gates supported by the LD2412 + +class LD2412Component : public Component, public uart::UARTDevice { +#ifdef USE_BINARY_SENSOR + SUB_BINARY_SENSOR(dynamic_background_correction_status) + SUB_BINARY_SENSOR(moving_target) + SUB_BINARY_SENSOR(still_target) + SUB_BINARY_SENSOR(target) +#endif +#ifdef USE_SENSOR + SUB_SENSOR_WITH_DEDUP(light, uint8_t) + SUB_SENSOR_WITH_DEDUP(detection_distance, int) + SUB_SENSOR_WITH_DEDUP(moving_target_distance, int) + SUB_SENSOR_WITH_DEDUP(moving_target_energy, uint8_t) + SUB_SENSOR_WITH_DEDUP(still_target_distance, int) + SUB_SENSOR_WITH_DEDUP(still_target_energy, uint8_t) +#endif +#ifdef USE_TEXT_SENSOR + SUB_TEXT_SENSOR(mac) + SUB_TEXT_SENSOR(version) +#endif +#ifdef USE_NUMBER + SUB_NUMBER(light_threshold) + SUB_NUMBER(max_distance_gate) + SUB_NUMBER(min_distance_gate) + SUB_NUMBER(timeout) +#endif +#ifdef USE_SELECT + SUB_SELECT(baud_rate) + SUB_SELECT(distance_resolution) + SUB_SELECT(light_function) + SUB_SELECT(out_pin_level) +#endif +#ifdef USE_SWITCH + SUB_SWITCH(bluetooth) + SUB_SWITCH(engineering_mode) +#endif +#ifdef USE_BUTTON + SUB_BUTTON(factory_reset) + SUB_BUTTON(query) + SUB_BUTTON(restart) + SUB_BUTTON(start_dynamic_background_correction) +#endif + + public: + void setup() override; + void dump_config() override; + void loop() override; + void set_light_out_control(); + void set_basic_config(); +#ifdef USE_NUMBER + void set_gate_move_threshold_number(uint8_t gate, number::Number *n); + void set_gate_still_threshold_number(uint8_t gate, number::Number *n); + void set_gate_threshold(); + void get_gate_threshold(); +#endif +#ifdef USE_SENSOR + void set_gate_move_sensor(uint8_t gate, sensor::Sensor *s); + void set_gate_still_sensor(uint8_t gate, sensor::Sensor *s); +#endif + void set_engineering_mode(bool enable); + void read_all_info(); + void restart_and_read_all_info(); + void set_bluetooth(bool enable); + void set_distance_resolution(const char *state); + void set_baud_rate(const char *state); + void factory_reset(); + void start_dynamic_background_correction(); + + protected: + void send_command_(uint8_t command_str, const uint8_t *command_value, uint8_t command_value_len); + void set_config_mode_(bool enable); + void handle_periodic_data_(); + bool handle_ack_data_(); + void readline_(int readch); + void query_parameters_(); + void get_version_(); + void get_mac_(); + void get_distance_resolution_(); + void query_light_control_(); + void restart_(); + void query_dynamic_background_correction_(); + + uint8_t light_function_ = 0; + uint8_t light_threshold_ = 0; + uint8_t out_pin_level_ = 0; + uint8_t buffer_pos_ = 0; // where to resume processing/populating buffer + uint8_t buffer_data_[MAX_LINE_LENGTH]; + uint8_t mac_address_[6] = {0, 0, 0, 0, 0, 0}; + uint8_t version_[6] = {0, 0, 0, 0, 0, 0}; + bool bluetooth_on_{false}; + bool dynamic_background_correction_active_{false}; +#ifdef USE_NUMBER + std::array gate_move_threshold_numbers_{}; + std::array gate_still_threshold_numbers_{}; +#endif +#ifdef USE_SENSOR + std::array *, TOTAL_GATES> gate_move_sensors_{}; + std::array *, TOTAL_GATES> gate_still_sensors_{}; +#endif +}; + +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/number/__init__.py b/esphome/components/ld2412/number/__init__.py new file mode 100644 index 0000000000..5b0d6d8749 --- /dev/null +++ b/esphome/components/ld2412/number/__init__.py @@ -0,0 +1,126 @@ +import esphome.codegen as cg +from esphome.components import number +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + CONF_MOVE_THRESHOLD, + CONF_STILL_THRESHOLD, + CONF_TIMEOUT, + DEVICE_CLASS_DISTANCE, + DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_SIGNAL_STRENGTH, + ENTITY_CATEGORY_CONFIG, + ICON_LIGHTBULB, + ICON_MOTION_SENSOR, + ICON_TIMELAPSE, + UNIT_PERCENT, + UNIT_SECOND, +) + +from .. import CONF_LD2412_ID, LD2412_ns, LD2412Component + +GateThresholdNumber = LD2412_ns.class_("GateThresholdNumber", number.Number) +LightThresholdNumber = LD2412_ns.class_("LightThresholdNumber", number.Number) +MaxDistanceTimeoutNumber = LD2412_ns.class_("MaxDistanceTimeoutNumber", number.Number) + +CONF_LIGHT_THRESHOLD = "light_threshold" +CONF_MAX_DISTANCE_GATE = "max_distance_gate" +CONF_MIN_DISTANCE_GATE = "min_distance_gate" + +TIMEOUT_GROUP = "timeout" + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component), + cv.Optional(CONF_LIGHT_THRESHOLD): number.number_schema( + LightThresholdNumber, + device_class=DEVICE_CLASS_ILLUMINANCE, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_LIGHTBULB, + ), + cv.Optional(CONF_MAX_DISTANCE_GATE): number.number_schema( + MaxDistanceTimeoutNumber, + device_class=DEVICE_CLASS_DISTANCE, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_MOTION_SENSOR, + ), + cv.Optional(CONF_MIN_DISTANCE_GATE): number.number_schema( + MaxDistanceTimeoutNumber, + device_class=DEVICE_CLASS_DISTANCE, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_MOTION_SENSOR, + ), + cv.Optional(CONF_TIMEOUT): number.number_schema( + MaxDistanceTimeoutNumber, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_TIMELAPSE, + unit_of_measurement=UNIT_SECOND, + ), + } +) + +CONFIG_SCHEMA = CONFIG_SCHEMA.extend( + { + cv.Optional(f"gate_{x}"): ( + { + cv.Required(CONF_MOVE_THRESHOLD): number.number_schema( + GateThresholdNumber, + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_MOTION_SENSOR, + unit_of_measurement=UNIT_PERCENT, + ), + cv.Required(CONF_STILL_THRESHOLD): number.number_schema( + GateThresholdNumber, + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_MOTION_SENSOR, + unit_of_measurement=UNIT_PERCENT, + ), + } + ) + for x in range(14) + } +) + + +async def to_code(config): + LD2412_component = await cg.get_variable(config[CONF_LD2412_ID]) + if light_threshold_config := config.get(CONF_LIGHT_THRESHOLD): + n = await number.new_number( + light_threshold_config, min_value=0, max_value=255, step=1 + ) + await cg.register_parented(n, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_light_threshold_number(n)) + if max_distance_gate_config := config.get(CONF_MAX_DISTANCE_GATE): + n = await number.new_number( + max_distance_gate_config, min_value=2, max_value=13, step=1 + ) + await cg.register_parented(n, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_max_distance_gate_number(n)) + if min_distance_gate_config := config.get(CONF_MIN_DISTANCE_GATE): + n = await number.new_number( + min_distance_gate_config, min_value=1, max_value=12, step=1 + ) + await cg.register_parented(n, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_min_distance_gate_number(n)) + for x in range(14): + if gate_conf := config.get(f"gate_{x}"): + move_config = gate_conf[CONF_MOVE_THRESHOLD] + n = cg.new_Pvariable(move_config[CONF_ID], x) + await number.register_number( + n, move_config, min_value=0, max_value=100, step=1 + ) + await cg.register_parented(n, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_gate_move_threshold_number(x, n)) + still_config = gate_conf[CONF_STILL_THRESHOLD] + n = cg.new_Pvariable(still_config[CONF_ID], x) + await number.register_number( + n, still_config, min_value=0, max_value=100, step=1 + ) + await cg.register_parented(n, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_gate_still_threshold_number(x, n)) + if timeout_config := config.get(CONF_TIMEOUT): + n = await number.new_number(timeout_config, min_value=0, max_value=900, step=1) + await cg.register_parented(n, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_timeout_number(n)) diff --git a/esphome/components/ld2412/number/gate_threshold_number.cpp b/esphome/components/ld2412/number/gate_threshold_number.cpp new file mode 100644 index 0000000000..8d12bad115 --- /dev/null +++ b/esphome/components/ld2412/number/gate_threshold_number.cpp @@ -0,0 +1,12 @@ +#include "gate_threshold_number.h" + +namespace esphome::ld2412 { + +GateThresholdNumber::GateThresholdNumber(uint8_t gate) : gate_(gate) {} + +void GateThresholdNumber::control(float value) { + this->publish_state(value); + this->parent_->set_gate_threshold(); +} + +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/number/gate_threshold_number.h b/esphome/components/ld2412/number/gate_threshold_number.h new file mode 100644 index 0000000000..78c2e54d82 --- /dev/null +++ b/esphome/components/ld2412/number/gate_threshold_number.h @@ -0,0 +1,17 @@ +#pragma once + +#include "esphome/components/number/number.h" +#include "../ld2412.h" + +namespace esphome::ld2412 { + +class GateThresholdNumber : public number::Number, public Parented { + public: + GateThresholdNumber(uint8_t gate); + + protected: + uint8_t gate_; + void control(float value) override; +}; + +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/number/light_threshold_number.cpp b/esphome/components/ld2412/number/light_threshold_number.cpp new file mode 100644 index 0000000000..81e668e779 --- /dev/null +++ b/esphome/components/ld2412/number/light_threshold_number.cpp @@ -0,0 +1,10 @@ +#include "light_threshold_number.h" + +namespace esphome::ld2412 { + +void LightThresholdNumber::control(float value) { + this->publish_state(value); + this->parent_->set_light_out_control(); +} + +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/number/light_threshold_number.h b/esphome/components/ld2412/number/light_threshold_number.h new file mode 100644 index 0000000000..81fd73111c --- /dev/null +++ b/esphome/components/ld2412/number/light_threshold_number.h @@ -0,0 +1,16 @@ +#pragma once + +#include "esphome/components/number/number.h" +#include "../ld2412.h" + +namespace esphome::ld2412 { + +class LightThresholdNumber : public number::Number, public Parented { + public: + LightThresholdNumber() = default; + + protected: + void control(float value) override; +}; + +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/number/max_distance_timeout_number.cpp b/esphome/components/ld2412/number/max_distance_timeout_number.cpp new file mode 100644 index 0000000000..0d156fbc00 --- /dev/null +++ b/esphome/components/ld2412/number/max_distance_timeout_number.cpp @@ -0,0 +1,10 @@ +#include "max_distance_timeout_number.h" + +namespace esphome::ld2412 { + +void MaxDistanceTimeoutNumber::control(float value) { + this->publish_state(value); + this->parent_->set_basic_config(); +} + +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/number/max_distance_timeout_number.h b/esphome/components/ld2412/number/max_distance_timeout_number.h new file mode 100644 index 0000000000..c1e947fa19 --- /dev/null +++ b/esphome/components/ld2412/number/max_distance_timeout_number.h @@ -0,0 +1,16 @@ +#pragma once + +#include "esphome/components/number/number.h" +#include "../ld2412.h" + +namespace esphome::ld2412 { + +class MaxDistanceTimeoutNumber : public number::Number, public Parented { + public: + MaxDistanceTimeoutNumber() = default; + + protected: + void control(float value) override; +}; + +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/select/__init__.py b/esphome/components/ld2412/select/__init__.py new file mode 100644 index 0000000000..d71ce460d9 --- /dev/null +++ b/esphome/components/ld2412/select/__init__.py @@ -0,0 +1,82 @@ +import esphome.codegen as cg +from esphome.components import select +import esphome.config_validation as cv +from esphome.const import ( + CONF_BAUD_RATE, + ENTITY_CATEGORY_CONFIG, + ICON_LIGHTBULB, + ICON_RULER, + ICON_SCALE, + ICON_THERMOMETER, +) + +from .. import CONF_LD2412_ID, LD2412_ns, LD2412Component + +BaudRateSelect = LD2412_ns.class_("BaudRateSelect", select.Select) +DistanceResolutionSelect = LD2412_ns.class_("DistanceResolutionSelect", select.Select) +LightOutControlSelect = LD2412_ns.class_("LightOutControlSelect", select.Select) + +CONF_DISTANCE_RESOLUTION = "distance_resolution" +CONF_LIGHT_FUNCTION = "light_function" +CONF_OUT_PIN_LEVEL = "out_pin_level" + + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component), + cv.Optional(CONF_BAUD_RATE): select.select_schema( + BaudRateSelect, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_THERMOMETER, + ), + cv.Optional(CONF_DISTANCE_RESOLUTION): select.select_schema( + DistanceResolutionSelect, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_RULER, + ), + cv.Optional(CONF_LIGHT_FUNCTION): select.select_schema( + LightOutControlSelect, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_LIGHTBULB, + ), + cv.Optional(CONF_OUT_PIN_LEVEL): select.select_schema( + LightOutControlSelect, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_SCALE, + ), +} + + +async def to_code(config): + LD2412_component = await cg.get_variable(config[CONF_LD2412_ID]) + if baud_rate_config := config.get(CONF_BAUD_RATE): + s = await select.new_select( + baud_rate_config, + options=[ + "9600", + "19200", + "38400", + "57600", + "115200", + "230400", + "256000", + "460800", + ], + ) + await cg.register_parented(s, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_baud_rate_select(s)) + if distance_resolution_config := config.get(CONF_DISTANCE_RESOLUTION): + s = await select.new_select( + distance_resolution_config, options=["0.2m", "0.5m", "0.75m"] + ) + await cg.register_parented(s, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_distance_resolution_select(s)) + if light_function_config := config.get(CONF_LIGHT_FUNCTION): + s = await select.new_select( + light_function_config, options=["off", "below", "above"] + ) + await cg.register_parented(s, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_light_function_select(s)) + if out_pin_level_config := config.get(CONF_OUT_PIN_LEVEL): + s = await select.new_select(out_pin_level_config, options=["low", "high"]) + await cg.register_parented(s, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_out_pin_level_select(s)) diff --git a/esphome/components/ld2412/select/baud_rate_select.cpp b/esphome/components/ld2412/select/baud_rate_select.cpp new file mode 100644 index 0000000000..8e351a6541 --- /dev/null +++ b/esphome/components/ld2412/select/baud_rate_select.cpp @@ -0,0 +1,10 @@ +#include "baud_rate_select.h" + +namespace esphome::ld2412 { + +void BaudRateSelect::control(size_t index) { + this->publish_state(index); + this->parent_->set_baud_rate(this->option_at(index)); +} + +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/select/baud_rate_select.h b/esphome/components/ld2412/select/baud_rate_select.h new file mode 100644 index 0000000000..4666dd2fa0 --- /dev/null +++ b/esphome/components/ld2412/select/baud_rate_select.h @@ -0,0 +1,16 @@ +#pragma once + +#include "esphome/components/select/select.h" +#include "../ld2412.h" + +namespace esphome::ld2412 { + +class BaudRateSelect : public select::Select, public Parented { + public: + BaudRateSelect() = default; + + protected: + void control(size_t index) override; +}; + +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/select/distance_resolution_select.cpp b/esphome/components/ld2412/select/distance_resolution_select.cpp new file mode 100644 index 0000000000..95b80f87fb --- /dev/null +++ b/esphome/components/ld2412/select/distance_resolution_select.cpp @@ -0,0 +1,10 @@ +#include "distance_resolution_select.h" + +namespace esphome::ld2412 { + +void DistanceResolutionSelect::control(size_t index) { + this->publish_state(index); + this->parent_->set_distance_resolution(this->option_at(index)); +} + +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/select/distance_resolution_select.h b/esphome/components/ld2412/select/distance_resolution_select.h new file mode 100644 index 0000000000..d3b7fad2f9 --- /dev/null +++ b/esphome/components/ld2412/select/distance_resolution_select.h @@ -0,0 +1,16 @@ +#pragma once + +#include "esphome/components/select/select.h" +#include "../ld2412.h" + +namespace esphome::ld2412 { + +class DistanceResolutionSelect : public select::Select, public Parented { + public: + DistanceResolutionSelect() = default; + + protected: + void control(size_t index) override; +}; + +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/select/light_out_control_select.cpp b/esphome/components/ld2412/select/light_out_control_select.cpp new file mode 100644 index 0000000000..bdfb78d687 --- /dev/null +++ b/esphome/components/ld2412/select/light_out_control_select.cpp @@ -0,0 +1,10 @@ +#include "light_out_control_select.h" + +namespace esphome::ld2412 { + +void LightOutControlSelect::control(size_t index) { + this->publish_state(index); + this->parent_->set_light_out_control(); +} + +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/select/light_out_control_select.h b/esphome/components/ld2412/select/light_out_control_select.h new file mode 100644 index 0000000000..9f86189878 --- /dev/null +++ b/esphome/components/ld2412/select/light_out_control_select.h @@ -0,0 +1,16 @@ +#pragma once + +#include "esphome/components/select/select.h" +#include "../ld2412.h" + +namespace esphome::ld2412 { + +class LightOutControlSelect : public select::Select, public Parented { + public: + LightOutControlSelect() = default; + + protected: + void control(size_t index) override; +}; + +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/sensor.py b/esphome/components/ld2412/sensor.py new file mode 100644 index 0000000000..0bfbd9bf1d --- /dev/null +++ b/esphome/components/ld2412/sensor.py @@ -0,0 +1,184 @@ +import esphome.codegen as cg +from esphome.components import sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_LIGHT, + CONF_MOVING_DISTANCE, + DEVICE_CLASS_DISTANCE, + DEVICE_CLASS_ILLUMINANCE, + ENTITY_CATEGORY_DIAGNOSTIC, + ICON_FLASH, + ICON_LIGHTBULB, + ICON_MOTION_SENSOR, + ICON_SIGNAL, + UNIT_CENTIMETER, + UNIT_EMPTY, + UNIT_PERCENT, +) + +from . import CONF_LD2412_ID, LD2412Component + +DEPENDENCIES = ["ld2412"] + +CONF_DETECTION_DISTANCE = "detection_distance" +CONF_MOVE_ENERGY = "move_energy" +CONF_MOVING_ENERGY = "moving_energy" +CONF_STILL_DISTANCE = "still_distance" +CONF_STILL_ENERGY = "still_energy" + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component), + cv.Optional(CONF_DETECTION_DISTANCE): sensor.sensor_schema( + device_class=DEVICE_CLASS_DISTANCE, + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], + icon=ICON_SIGNAL, + unit_of_measurement=UNIT_CENTIMETER, + ), + cv.Optional(CONF_LIGHT): sensor.sensor_schema( + device_class=DEVICE_CLASS_ILLUMINANCE, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], + icon=ICON_LIGHTBULB, + unit_of_measurement=UNIT_EMPTY, # No standard unit for this light sensor + ), + cv.Optional(CONF_MOVING_DISTANCE): sensor.sensor_schema( + device_class=DEVICE_CLASS_DISTANCE, + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], + icon=ICON_SIGNAL, + unit_of_measurement=UNIT_CENTIMETER, + ), + cv.Optional(CONF_MOVING_ENERGY): sensor.sensor_schema( + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], + icon=ICON_MOTION_SENSOR, + unit_of_measurement=UNIT_PERCENT, + ), + cv.Optional(CONF_STILL_DISTANCE): sensor.sensor_schema( + device_class=DEVICE_CLASS_DISTANCE, + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], + icon=ICON_SIGNAL, + unit_of_measurement=UNIT_CENTIMETER, + ), + cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema( + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], + icon=ICON_FLASH, + unit_of_measurement=UNIT_PERCENT, + ), + } +) + +CONFIG_SCHEMA = CONFIG_SCHEMA.extend( + { + cv.Optional(f"gate_{x}"): ( + { + cv.Optional(CONF_MOVE_ENERGY): sensor.sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], + icon=ICON_MOTION_SENSOR, + unit_of_measurement=UNIT_PERCENT, + ), + cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], + icon=ICON_FLASH, + unit_of_measurement=UNIT_PERCENT, + ), + } + ) + for x in range(14) + } +) + + +async def to_code(config): + LD2412_component = await cg.get_variable(config[CONF_LD2412_ID]) + if detection_distance_config := config.get(CONF_DETECTION_DISTANCE): + sens = await sensor.new_sensor(detection_distance_config) + cg.add(LD2412_component.set_detection_distance_sensor(sens)) + if light_config := config.get(CONF_LIGHT): + sens = await sensor.new_sensor(light_config) + cg.add(LD2412_component.set_light_sensor(sens)) + if moving_distance_config := config.get(CONF_MOVING_DISTANCE): + sens = await sensor.new_sensor(moving_distance_config) + cg.add(LD2412_component.set_moving_target_distance_sensor(sens)) + if moving_energy_config := config.get(CONF_MOVING_ENERGY): + sens = await sensor.new_sensor(moving_energy_config) + cg.add(LD2412_component.set_moving_target_energy_sensor(sens)) + if still_distance_config := config.get(CONF_STILL_DISTANCE): + sens = await sensor.new_sensor(still_distance_config) + cg.add(LD2412_component.set_still_target_distance_sensor(sens)) + if still_energy_config := config.get(CONF_STILL_ENERGY): + sens = await sensor.new_sensor(still_energy_config) + cg.add(LD2412_component.set_still_target_energy_sensor(sens)) + for x in range(14): + if gate_conf := config.get(f"gate_{x}"): + if move_config := gate_conf.get(CONF_MOVE_ENERGY): + sens = await sensor.new_sensor(move_config) + cg.add(LD2412_component.set_gate_move_sensor(x, sens)) + if still_config := gate_conf.get(CONF_STILL_ENERGY): + sens = await sensor.new_sensor(still_config) + cg.add(LD2412_component.set_gate_still_sensor(x, sens)) diff --git a/esphome/components/ld2412/switch/__init__.py b/esphome/components/ld2412/switch/__init__.py new file mode 100644 index 0000000000..df994687ec --- /dev/null +++ b/esphome/components/ld2412/switch/__init__.py @@ -0,0 +1,45 @@ +import esphome.codegen as cg +from esphome.components import switch +import esphome.config_validation as cv +from esphome.const import ( + CONF_BLUETOOTH, + DEVICE_CLASS_SWITCH, + ENTITY_CATEGORY_CONFIG, + ICON_BLUETOOTH, + ICON_PULSE, +) + +from .. import CONF_LD2412_ID, LD2412_ns, LD2412Component + +BluetoothSwitch = LD2412_ns.class_("BluetoothSwitch", switch.Switch) +EngineeringModeSwitch = LD2412_ns.class_("EngineeringModeSwitch", switch.Switch) + +CONF_ENGINEERING_MODE = "engineering_mode" + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component), + cv.Optional(CONF_BLUETOOTH): switch.switch_schema( + BluetoothSwitch, + device_class=DEVICE_CLASS_SWITCH, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_BLUETOOTH, + ), + cv.Optional(CONF_ENGINEERING_MODE): switch.switch_schema( + EngineeringModeSwitch, + device_class=DEVICE_CLASS_SWITCH, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_PULSE, + ), +} + + +async def to_code(config): + LD2412_component = await cg.get_variable(config[CONF_LD2412_ID]) + if bluetooth_config := config.get(CONF_BLUETOOTH): + s = await switch.new_switch(bluetooth_config) + await cg.register_parented(s, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_bluetooth_switch(s)) + if engineering_mode_config := config.get(CONF_ENGINEERING_MODE): + s = await switch.new_switch(engineering_mode_config) + await cg.register_parented(s, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_engineering_mode_switch(s)) diff --git a/esphome/components/ld2412/switch/bluetooth_switch.cpp b/esphome/components/ld2412/switch/bluetooth_switch.cpp new file mode 100644 index 0000000000..e5f49a819a --- /dev/null +++ b/esphome/components/ld2412/switch/bluetooth_switch.cpp @@ -0,0 +1,10 @@ +#include "bluetooth_switch.h" + +namespace esphome::ld2412 { + +void BluetoothSwitch::write_state(bool state) { + this->publish_state(state); + this->parent_->set_bluetooth(state); +} + +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/switch/bluetooth_switch.h b/esphome/components/ld2412/switch/bluetooth_switch.h new file mode 100644 index 0000000000..0c0d1fa550 --- /dev/null +++ b/esphome/components/ld2412/switch/bluetooth_switch.h @@ -0,0 +1,16 @@ +#pragma once + +#include "esphome/components/switch/switch.h" +#include "../ld2412.h" + +namespace esphome::ld2412 { + +class BluetoothSwitch : public switch_::Switch, public Parented { + public: + BluetoothSwitch() = default; + + protected: + void write_state(bool state) override; +}; + +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/switch/engineering_mode_switch.cpp b/esphome/components/ld2412/switch/engineering_mode_switch.cpp new file mode 100644 index 0000000000..28b4e5d9e6 --- /dev/null +++ b/esphome/components/ld2412/switch/engineering_mode_switch.cpp @@ -0,0 +1,10 @@ +#include "engineering_mode_switch.h" + +namespace esphome::ld2412 { + +void EngineeringModeSwitch::write_state(bool state) { + this->publish_state(state); + this->parent_->set_engineering_mode(state); +} + +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/switch/engineering_mode_switch.h b/esphome/components/ld2412/switch/engineering_mode_switch.h new file mode 100644 index 0000000000..4e75a8a185 --- /dev/null +++ b/esphome/components/ld2412/switch/engineering_mode_switch.h @@ -0,0 +1,16 @@ +#pragma once + +#include "esphome/components/switch/switch.h" +#include "../ld2412.h" + +namespace esphome::ld2412 { + +class EngineeringModeSwitch : public switch_::Switch, public Parented { + public: + EngineeringModeSwitch() = default; + + protected: + void write_state(bool state) override; +}; + +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/text_sensor.py b/esphome/components/ld2412/text_sensor.py new file mode 100644 index 0000000000..1074494933 --- /dev/null +++ b/esphome/components/ld2412/text_sensor.py @@ -0,0 +1,34 @@ +import esphome.codegen as cg +from esphome.components import text_sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_MAC_ADDRESS, + CONF_VERSION, + ENTITY_CATEGORY_DIAGNOSTIC, + ICON_BLUETOOTH, + ICON_CHIP, +) + +from . import CONF_LD2412_ID, LD2412Component + +DEPENDENCIES = ["ld2412"] + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component), + cv.Optional(CONF_VERSION): text_sensor.text_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, icon=ICON_CHIP + ), + cv.Optional(CONF_MAC_ADDRESS): text_sensor.text_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, icon=ICON_BLUETOOTH + ), +} + + +async def to_code(config): + LD2412_component = await cg.get_variable(config[CONF_LD2412_ID]) + if version_config := config.get(CONF_VERSION): + sens = await text_sensor.new_text_sensor(version_config) + cg.add(LD2412_component.set_version_text_sensor(sens)) + if mac_address_config := config.get(CONF_MAC_ADDRESS): + sens = await text_sensor.new_text_sensor(mac_address_config) + cg.add(LD2412_component.set_mac_text_sensor(sens)) diff --git a/esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.cpp b/esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.cpp index d8632e9c19..6297cba2e7 100644 --- a/esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.cpp +++ b/esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace ld2420 { +namespace esphome::ld2420 { static const char *const TAG = "ld2420.binary_sensor"; @@ -12,5 +11,4 @@ void LD2420BinarySensor::dump_config() { LOG_BINARY_SENSOR(" ", "Presence", this->presence_bsensor_); } -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.h b/esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.h index ee06439090..ec52312f92 100644 --- a/esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.h +++ b/esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.h @@ -3,8 +3,7 @@ #include "../ld2420.h" #include "esphome/components/binary_sensor/binary_sensor.h" -namespace esphome { -namespace ld2420 { +namespace esphome::ld2420 { class LD2420BinarySensor : public LD2420Listener, public Component, binary_sensor::BinarySensor { public: @@ -21,5 +20,4 @@ class LD2420BinarySensor : public LD2420Listener, public Component, binary_senso binary_sensor::BinarySensor *presence_bsensor_{nullptr}; }; -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2420/button/reconfig_buttons.cpp b/esphome/components/ld2420/button/reconfig_buttons.cpp index fb8ec2b5a6..1e748e59b8 100644 --- a/esphome/components/ld2420/button/reconfig_buttons.cpp +++ b/esphome/components/ld2420/button/reconfig_buttons.cpp @@ -4,13 +4,11 @@ static const char *const TAG = "ld2420.button"; -namespace esphome { -namespace ld2420 { +namespace esphome::ld2420 { void LD2420ApplyConfigButton::press_action() { this->parent_->apply_config_action(); } void LD2420RevertConfigButton::press_action() { this->parent_->revert_config_action(); } void LD2420RestartModuleButton::press_action() { this->parent_->restart_module_action(); } void LD2420FactoryResetButton::press_action() { this->parent_->factory_reset_action(); } -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2420/button/reconfig_buttons.h b/esphome/components/ld2420/button/reconfig_buttons.h index 4e9e7a3692..72171ef386 100644 --- a/esphome/components/ld2420/button/reconfig_buttons.h +++ b/esphome/components/ld2420/button/reconfig_buttons.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../ld2420.h" -namespace esphome { -namespace ld2420 { +namespace esphome::ld2420 { class LD2420ApplyConfigButton : public button::Button, public Parented { public: @@ -38,5 +37,4 @@ class LD2420FactoryResetButton : public button::Button, public Parentedtotal_sample_number_counter); } -void LD2420Component::set_operating_mode(const std::string &state) { +void LD2420Component::set_operating_mode(const char *state) { // If unsupported firmware ignore mode select if (ld2420::get_firmware_int(firmware_ver_) >= CALIBRATE_VERSION_MIN) { this->current_operating_mode = find_uint8(OP_MODE_BY_STR, state); @@ -880,5 +879,4 @@ void LD2420Component::refresh_gate_config_numbers() { #endif -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2420/ld2420.h b/esphome/components/ld2420/ld2420.h index 812c408cfd..50ddf45264 100644 --- a/esphome/components/ld2420/ld2420.h +++ b/esphome/components/ld2420/ld2420.h @@ -17,8 +17,7 @@ #include "esphome/components/button/button.h" #endif -namespace esphome { -namespace ld2420 { +namespace esphome::ld2420 { static const uint8_t CALIBRATE_SAMPLES = 64; static const uint8_t MAX_LINE_LENGTH = 46; // Max characters for serial buffer @@ -107,7 +106,7 @@ class LD2420Component : public Component, public uart::UARTDevice { int send_cmd_from_array(CmdFrameT cmd_frame); void report_gate_data(); void handle_cmd_error(uint8_t error); - void set_operating_mode(const std::string &state); + void set_operating_mode(const char *state); void auto_calibrate_sensitivity(); void update_radar_data(uint16_t const *gate_energy, uint8_t sample_number); uint8_t set_config_mode(bool enable); @@ -193,5 +192,4 @@ class LD2420Component : public Component, public uart::UARTDevice { std::vector listeners_{}; }; -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2420/number/gate_config_number.cpp b/esphome/components/ld2420/number/gate_config_number.cpp index a373753770..998eed2188 100644 --- a/esphome/components/ld2420/number/gate_config_number.cpp +++ b/esphome/components/ld2420/number/gate_config_number.cpp @@ -4,8 +4,7 @@ static const char *const TAG = "ld2420.number"; -namespace esphome { -namespace ld2420 { +namespace esphome::ld2420 { void LD2420TimeoutNumber::control(float timeout) { this->publish_state(timeout); @@ -69,5 +68,4 @@ void LD2420StillThresholdNumbers::control(float still_threshold) { } } -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2420/number/gate_config_number.h b/esphome/components/ld2420/number/gate_config_number.h index 459a8026e3..8a8b9c61b1 100644 --- a/esphome/components/ld2420/number/gate_config_number.h +++ b/esphome/components/ld2420/number/gate_config_number.h @@ -3,8 +3,7 @@ #include "esphome/components/number/number.h" #include "../ld2420.h" -namespace esphome { -namespace ld2420 { +namespace esphome::ld2420 { class LD2420TimeoutNumber : public number::Number, public Parented { public: @@ -74,5 +73,4 @@ class LD2420MoveThresholdNumbers : public number::Number, public Parentedpublish_state(value); - this->parent_->set_operating_mode(value); +void LD2420Select::control(size_t index) { + this->publish_state(index); + this->parent_->set_operating_mode(this->option_at(index)); } -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2420/select/operating_mode_select.h b/esphome/components/ld2420/select/operating_mode_select.h index 317b2af8c0..c1b8e0b11b 100644 --- a/esphome/components/ld2420/select/operating_mode_select.h +++ b/esphome/components/ld2420/select/operating_mode_select.h @@ -3,16 +3,14 @@ #include "../ld2420.h" #include "esphome/components/select/select.h" -namespace esphome { -namespace ld2420 { +namespace esphome::ld2420 { class LD2420Select : public Component, public select::Select, public Parented { public: LD2420Select() = default; protected: - void control(const std::string &value) override; + void control(size_t index) override; }; -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2420/sensor/ld2420_sensor.cpp b/esphome/components/ld2420/sensor/ld2420_sensor.cpp index 723604f396..02b5826f98 100644 --- a/esphome/components/ld2420/sensor/ld2420_sensor.cpp +++ b/esphome/components/ld2420/sensor/ld2420_sensor.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace ld2420 { +namespace esphome::ld2420 { static const char *const TAG = "ld2420.sensor"; @@ -12,5 +11,4 @@ void LD2420Sensor::dump_config() { LOG_SENSOR(" ", "Distance", this->distance_sensor_); } -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2420/sensor/ld2420_sensor.h b/esphome/components/ld2420/sensor/ld2420_sensor.h index 82730d60e3..4849cfa047 100644 --- a/esphome/components/ld2420/sensor/ld2420_sensor.h +++ b/esphome/components/ld2420/sensor/ld2420_sensor.h @@ -3,8 +3,7 @@ #include "../ld2420.h" #include "esphome/components/sensor/sensor.h" -namespace esphome { -namespace ld2420 { +namespace esphome::ld2420 { class LD2420Sensor : public LD2420Listener, public Component, sensor::Sensor { public: @@ -30,5 +29,4 @@ class LD2420Sensor : public LD2420Listener, public Component, sensor::Sensor { std::vector energy_sensors_ = std::vector(TOTAL_GATES); }; -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2420/text_sensor/text_sensor.cpp b/esphome/components/ld2420/text_sensor/ld2420_text_sensor.cpp similarity index 70% rename from esphome/components/ld2420/text_sensor/text_sensor.cpp rename to esphome/components/ld2420/text_sensor/ld2420_text_sensor.cpp index 73af3b3660..f7b016c9d9 100644 --- a/esphome/components/ld2420/text_sensor/text_sensor.cpp +++ b/esphome/components/ld2420/text_sensor/ld2420_text_sensor.cpp @@ -1,9 +1,8 @@ -#include "text_sensor.h" +#include "ld2420_text_sensor.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace ld2420 { +namespace esphome::ld2420 { static const char *const TAG = "ld2420.text_sensor"; @@ -12,5 +11,4 @@ void LD2420TextSensor::dump_config() { LOG_TEXT_SENSOR(" ", "Firmware", this->fw_version_text_sensor_); } -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2420/text_sensor/text_sensor.h b/esphome/components/ld2420/text_sensor/ld2420_text_sensor.h similarity index 87% rename from esphome/components/ld2420/text_sensor/text_sensor.h rename to esphome/components/ld2420/text_sensor/ld2420_text_sensor.h index 073ddd5d0f..1932eaaf69 100644 --- a/esphome/components/ld2420/text_sensor/text_sensor.h +++ b/esphome/components/ld2420/text_sensor/ld2420_text_sensor.h @@ -3,8 +3,7 @@ #include "../ld2420.h" #include "esphome/components/text_sensor/text_sensor.h" -namespace esphome { -namespace ld2420 { +namespace esphome::ld2420 { class LD2420TextSensor : public LD2420Listener, public Component, text_sensor::TextSensor { public: @@ -20,5 +19,4 @@ class LD2420TextSensor : public LD2420Listener, public Component, text_sensor::T text_sensor::TextSensor *fw_version_text_sensor_{nullptr}; }; -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2450/__init__.py b/esphome/components/ld2450/__init__.py index cdbf8a17c4..bd6d697c90 100644 --- a/esphome/components/ld2450/__init__.py +++ b/esphome/components/ld2450/__init__.py @@ -17,9 +17,8 @@ CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(LD2450Component), - cv.Optional(CONF_THROTTLE, default="1000ms"): cv.All( - cv.positive_time_period_milliseconds, - cv.Range(min=cv.TimePeriod(milliseconds=1)), + cv.Optional(CONF_THROTTLE): cv.invalid( + f"{CONF_THROTTLE} has been removed; use per-sensor filters, instead" ), } ) @@ -46,4 +45,3 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await uart.register_uart_device(var, config) - cg.add(var.set_throttle(config[CONF_THROTTLE])) diff --git a/esphome/components/ld2450/binary_sensor.py b/esphome/components/ld2450/binary_sensor.py index d0082ac21a..37f722b0fa 100644 --- a/esphome/components/ld2450/binary_sensor.py +++ b/esphome/components/ld2450/binary_sensor.py @@ -21,14 +21,17 @@ CONFIG_SCHEMA = { cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component), cv.Optional(CONF_HAS_TARGET): binary_sensor.binary_sensor_schema( device_class=DEVICE_CLASS_OCCUPANCY, + filters=[{"settle": cv.TimePeriod(milliseconds=1000)}], icon=ICON_SHIELD_ACCOUNT, ), cv.Optional(CONF_HAS_MOVING_TARGET): binary_sensor.binary_sensor_schema( device_class=DEVICE_CLASS_MOTION, + filters=[{"settle": cv.TimePeriod(milliseconds=1000)}], icon=ICON_TARGET_ACCOUNT, ), cv.Optional(CONF_HAS_STILL_TARGET): binary_sensor.binary_sensor_schema( device_class=DEVICE_CLASS_OCCUPANCY, + filters=[{"settle": cv.TimePeriod(milliseconds=1000)}], icon=ICON_MEDITATION, ), } diff --git a/esphome/components/ld2450/button/factory_reset_button.cpp b/esphome/components/ld2450/button/factory_reset_button.cpp index bcac7ada2f..7a8eb5b0dd 100644 --- a/esphome/components/ld2450/button/factory_reset_button.cpp +++ b/esphome/components/ld2450/button/factory_reset_button.cpp @@ -1,9 +1,7 @@ #include "factory_reset_button.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { void FactoryResetButton::press_action() { this->parent_->factory_reset(); } -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/button/factory_reset_button.h b/esphome/components/ld2450/button/factory_reset_button.h index 8e80347119..392fc67ffd 100644 --- a/esphome/components/ld2450/button/factory_reset_button.h +++ b/esphome/components/ld2450/button/factory_reset_button.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../ld2450.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { class FactoryResetButton : public button::Button, public Parented { public: @@ -14,5 +13,4 @@ class FactoryResetButton : public button::Button, public Parentedparent_->restart_and_read_all_info(); } -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/button/restart_button.h b/esphome/components/ld2450/button/restart_button.h index a44ae5a4d2..9219011f8b 100644 --- a/esphome/components/ld2450/button/restart_button.h +++ b/esphome/components/ld2450/button/restart_button.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../ld2450.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { class RestartButton : public button::Button, public Parented { public: @@ -14,5 +13,4 @@ class RestartButton : public button::Button, public Parented { void press_action() override; }; -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index 642684266e..e69ef31d4f 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -13,12 +13,9 @@ #include #include -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { static const char *const TAG = "ld2450"; -static const char *const UNKNOWN_MAC = "unknown"; -static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X"; enum BaudRate : uint8_t { BAUD_RATE_9600 = 1, @@ -184,7 +181,7 @@ static inline bool validate_header_footer(const uint8_t *header_footer, const ui void LD2450Component::setup() { #ifdef USE_NUMBER if (this->presence_timeout_number_ != nullptr) { - this->pref_ = global_preferences->make_preference(this->presence_timeout_number_->get_object_id_hash()); + this->pref_ = global_preferences->make_preference(this->presence_timeout_number_->get_preference_hash()); this->set_presence_timeout(); } #endif @@ -192,16 +189,15 @@ void LD2450Component::setup() { } void LD2450Component::dump_config() { - std::string mac_str = - mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC; - std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5], - this->version_[4], this->version_[3], this->version_[2]); + char mac_s[18]; + char version_s[20]; + const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s); + ld24xx::format_version_str(this->version_, version_s); ESP_LOGCONFIG(TAG, "LD2450:\n" " Firmware version: %s\n" - " MAC address: %s\n" - " Throttle: %u ms", - version.c_str(), mac_str.c_str(), this->throttle_); + " MAC address: %s", + version_s, mac_str); #ifdef USE_BINARY_SENSOR ESP_LOGCONFIG(TAG, "Binary Sensors:"); LOG_BINARY_SENSOR(" ", "MovingTarget", this->moving_target_binary_sensor_); @@ -381,7 +377,7 @@ void LD2450Component::read_all_info() { this->set_config_mode_(false); #ifdef USE_SELECT const auto baud_rate = std::to_string(this->parent_->get_baud_rate()); - if (this->baud_rate_select_ != nullptr && this->baud_rate_select_->state != baud_rate) { + if (this->baud_rate_select_ != nullptr && strcmp(this->baud_rate_select_->current_option(), baud_rate.c_str()) != 0) { this->baud_rate_select_->publish_state(baud_rate); } this->publish_zone_type(); @@ -431,11 +427,6 @@ void LD2450Component::send_command_(uint8_t command, const uint8_t *command_valu // [AA FF 03 00] [0E 03 B1 86 10 00 40 01] [00 00 00 00 00 00 00 00] [00 00 00 00 00 00 00 00] [55 CC] // Header Target 1 Target 2 Target 3 End void LD2450Component::handle_periodic_data_() { - // Early throttle check - moved before any processing to save CPU cycles - if (App.get_loop_component_start_time() - this->last_periodic_millis_ < this->throttle_) { - return; - } - if (this->buffer_pos_ < 29) { // header (4 bytes) + 8 x 3 target data + footer (2 bytes) ESP_LOGE(TAG, "Invalid length"); return; @@ -446,8 +437,6 @@ void LD2450Component::handle_periodic_data_() { ESP_LOGE(TAG, "Invalid header/footer"); return; } - // Save the timestamp after validating the frame so, if invalid, we'll take the next frame immediately - this->last_periodic_millis_ = App.get_loop_component_start_time(); int16_t target_count = 0; int16_t still_target_count = 0; @@ -643,19 +632,19 @@ bool LD2450Component::handle_ack_data_() { ESP_LOGV(TAG, "Baud rate change"); #ifdef USE_SELECT if (this->baud_rate_select_ != nullptr) { - ESP_LOGE(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->state.c_str()); + ESP_LOGE(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->current_option()); } #endif break; case CMD_QUERY_VERSION: { std::memcpy(this->version_, &this->buffer_data_[12], sizeof(this->version_)); - std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5], - this->version_[4], this->version_[3], this->version_[2]); - ESP_LOGV(TAG, "Firmware version: %s", version.c_str()); + char version_s[20]; + ld24xx::format_version_str(this->version_, version_s); + ESP_LOGV(TAG, "Firmware version: %s", version_s); #ifdef USE_TEXT_SENSOR if (this->version_text_sensor_ != nullptr) { - this->version_text_sensor_->publish_state(version); + this->version_text_sensor_->publish_state(version_s); } #endif break; @@ -671,9 +660,9 @@ bool LD2450Component::handle_ack_data_() { std::memcpy(this->mac_address_, &this->buffer_data_[10], sizeof(this->mac_address_)); } - std::string mac_str = - mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC; - ESP_LOGV(TAG, "MAC address: %s", mac_str.c_str()); + char mac_s[18]; + const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s); + ESP_LOGV(TAG, "MAC address: %s", mac_str); #ifdef USE_TEXT_SENSOR if (this->mac_text_sensor_ != nullptr) { this->mac_text_sensor_->publish_state(mac_str); @@ -724,7 +713,7 @@ bool LD2450Component::handle_ack_data_() { this->publish_zone_type(); #ifdef USE_SELECT if (this->zone_type_select_ != nullptr) { - ESP_LOGV(TAG, "Change zone type to: %s", this->zone_type_select_->state.c_str()); + ESP_LOGV(TAG, "Change zone type to: %s", this->zone_type_select_->current_option()); } #endif if (this->buffer_data_[10] == 0x00) { @@ -798,7 +787,7 @@ void LD2450Component::set_bluetooth(bool enable) { } // Set Baud rate -void LD2450Component::set_baud_rate(const std::string &state) { +void LD2450Component::set_baud_rate(const char *state) { this->set_config_mode_(true); const uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00}; this->send_command_(CMD_SET_BAUD_RATE, cmd_value, sizeof(cmd_value)); @@ -806,8 +795,8 @@ void LD2450Component::set_baud_rate(const std::string &state) { } // Set Zone Type - one of: Disabled, Detection, Filter -void LD2450Component::set_zone_type(const std::string &state) { - ESP_LOGV(TAG, "Set zone type: %s", state.c_str()); +void LD2450Component::set_zone_type(const char *state) { + ESP_LOGV(TAG, "Set zone type: %s", state); uint8_t zone_type = find_uint8(ZONE_TYPE_BY_STR, state); this->zone_type_ = zone_type; this->send_set_zone_command_(); @@ -949,5 +938,4 @@ float LD2450Component::restore_from_flash_() { } #endif -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h index 0fba0f9be3..b94c3cac37 100644 --- a/esphome/components/ld2450/ld2450.h +++ b/esphome/components/ld2450/ld2450.h @@ -31,8 +31,7 @@ #include -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { using namespace ld24xx; @@ -110,14 +109,13 @@ class LD2450Component : public Component, public uart::UARTDevice { void dump_config() override; void loop() override; void set_presence_timeout(); - void set_throttle(uint16_t value) { this->throttle_ = value; } void read_all_info(); void query_zone_info(); void restart_and_read_all_info(); void set_bluetooth(bool enable); void set_multi_target(bool enable); - void set_baud_rate(const std::string &state); - void set_zone_type(const std::string &state); + void set_baud_rate(const char *state); + void set_zone_type(const char *state); void publish_zone_type(); void factory_reset(); #ifdef USE_TEXT_SENSOR @@ -161,11 +159,9 @@ class LD2450Component : public Component, public uart::UARTDevice { bool get_timeout_status_(uint32_t check_millis); uint8_t count_targets_in_zone_(const Zone &zone, bool is_moving); - uint32_t last_periodic_millis_ = 0; uint32_t presence_millis_ = 0; uint32_t still_presence_millis_ = 0; uint32_t moving_presence_millis_ = 0; - uint16_t throttle_ = 0; uint16_t timeout_ = 5; uint8_t buffer_data_[MAX_LINE_LENGTH]; uint8_t mac_address_[6] = {0, 0, 0, 0, 0, 0}; @@ -196,5 +192,4 @@ class LD2450Component : public Component, public uart::UARTDevice { #endif }; -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/number/presence_timeout_number.cpp b/esphome/components/ld2450/number/presence_timeout_number.cpp index ecfe71f484..19a1ada0d7 100644 --- a/esphome/components/ld2450/number/presence_timeout_number.cpp +++ b/esphome/components/ld2450/number/presence_timeout_number.cpp @@ -1,12 +1,10 @@ #include "presence_timeout_number.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { void PresenceTimeoutNumber::control(float value) { this->publish_state(value); this->parent_->set_presence_timeout(); } -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/number/presence_timeout_number.h b/esphome/components/ld2450/number/presence_timeout_number.h index b18699792f..09c8afca55 100644 --- a/esphome/components/ld2450/number/presence_timeout_number.h +++ b/esphome/components/ld2450/number/presence_timeout_number.h @@ -3,8 +3,7 @@ #include "esphome/components/number/number.h" #include "../ld2450.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { class PresenceTimeoutNumber : public number::Number, public Parented { public: @@ -14,5 +13,4 @@ class PresenceTimeoutNumber : public number::Number, public Parentedparent_->set_zone_coordinate(this->zone_); } -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/number/zone_coordinate_number.h b/esphome/components/ld2450/number/zone_coordinate_number.h index 72b83889c4..f5a389d712 100644 --- a/esphome/components/ld2450/number/zone_coordinate_number.h +++ b/esphome/components/ld2450/number/zone_coordinate_number.h @@ -3,8 +3,7 @@ #include "esphome/components/number/number.h" #include "../ld2450.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { class ZoneCoordinateNumber : public number::Number, public Parented { public: @@ -15,5 +14,4 @@ class ZoneCoordinateNumber : public number::Number, public Parentedpublish_state(value); - this->parent_->set_baud_rate(state); +void BaudRateSelect::control(size_t index) { + this->publish_state(index); + this->parent_->set_baud_rate(this->option_at(index)); } -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/select/baud_rate_select.h b/esphome/components/ld2450/select/baud_rate_select.h index 04fe65b4fd..cb53118170 100644 --- a/esphome/components/ld2450/select/baud_rate_select.h +++ b/esphome/components/ld2450/select/baud_rate_select.h @@ -3,16 +3,14 @@ #include "esphome/components/select/select.h" #include "../ld2450.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { class BaudRateSelect : public select::Select, public Parented { public: BaudRateSelect() = default; protected: - void control(const std::string &value) override; + void control(size_t index) override; }; -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/select/zone_type_select.cpp b/esphome/components/ld2450/select/zone_type_select.cpp index a9f6155142..39642b99ad 100644 --- a/esphome/components/ld2450/select/zone_type_select.cpp +++ b/esphome/components/ld2450/select/zone_type_select.cpp @@ -1,12 +1,10 @@ #include "zone_type_select.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { -void ZoneTypeSelect::control(const std::string &value) { - this->publish_state(value); - this->parent_->set_zone_type(state); +void ZoneTypeSelect::control(size_t index) { + this->publish_state(index); + this->parent_->set_zone_type(this->option_at(index)); } -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/select/zone_type_select.h b/esphome/components/ld2450/select/zone_type_select.h index 8aafeb6beb..566346eb48 100644 --- a/esphome/components/ld2450/select/zone_type_select.h +++ b/esphome/components/ld2450/select/zone_type_select.h @@ -3,16 +3,14 @@ #include "esphome/components/select/select.h" #include "../ld2450.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { class ZoneTypeSelect : public select::Select, public Parented { public: ZoneTypeSelect() = default; protected: - void control(const std::string &value) override; + void control(size_t index) override; }; -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/sensor.py b/esphome/components/ld2450/sensor.py index d16d9c834d..4a3597d583 100644 --- a/esphome/components/ld2450/sensor.py +++ b/esphome/components/ld2450/sensor.py @@ -42,16 +42,43 @@ CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component), cv.Optional(CONF_TARGET_COUNT): sensor.sensor_schema( - icon=ICON_ACCOUNT_GROUP, accuracy_decimals=0, + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], + icon=ICON_ACCOUNT_GROUP, ), cv.Optional(CONF_STILL_TARGET_COUNT): sensor.sensor_schema( - icon=ICON_HUMAN_GREETING_PROXIMITY, accuracy_decimals=0, + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], + icon=ICON_HUMAN_GREETING_PROXIMITY, ), cv.Optional(CONF_MOVING_TARGET_COUNT): sensor.sensor_schema( - icon=ICON_ACCOUNT_SWITCH, accuracy_decimals=0, + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], + icon=ICON_ACCOUNT_SWITCH, ), } ) @@ -62,32 +89,86 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( { cv.Optional(CONF_X): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - unit_of_measurement=UNIT_MILLIMETER, + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_ALPHA_X_BOX_OUTLINE, + unit_of_measurement=UNIT_MILLIMETER, ), cv.Optional(CONF_Y): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - unit_of_measurement=UNIT_MILLIMETER, + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_ALPHA_Y_BOX_OUTLINE, + unit_of_measurement=UNIT_MILLIMETER, ), cv.Optional(CONF_SPEED): sensor.sensor_schema( device_class=DEVICE_CLASS_SPEED, - unit_of_measurement=UNIT_MILLIMETER_PER_SECOND, + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_SPEEDOMETER_SLOW, + unit_of_measurement=UNIT_MILLIMETER_PER_SECOND, ), cv.Optional(CONF_ANGLE): sensor.sensor_schema( - unit_of_measurement=UNIT_DEGREES, + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_FORMAT_TEXT_ROTATION_ANGLE_UP, + unit_of_measurement=UNIT_DEGREES, ), cv.Optional(CONF_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - unit_of_measurement=UNIT_MILLIMETER, + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_MAP_MARKER_DISTANCE, + unit_of_measurement=UNIT_MILLIMETER, ), cv.Optional(CONF_RESOLUTION): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - unit_of_measurement=UNIT_MILLIMETER, + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_RELATION_ZERO_OR_ONE_TO_ZERO_OR_ONE, + unit_of_measurement=UNIT_MILLIMETER, ), } ) @@ -97,16 +178,43 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( cv.Optional(f"zone_{n + 1}"): cv.Schema( { cv.Optional(CONF_TARGET_COUNT): sensor.sensor_schema( - icon=ICON_MAP_MARKER_ACCOUNT, accuracy_decimals=0, + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], + icon=ICON_MAP_MARKER_ACCOUNT, ), cv.Optional(CONF_STILL_TARGET_COUNT): sensor.sensor_schema( - icon=ICON_MAP_MARKER_ACCOUNT, accuracy_decimals=0, + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], + icon=ICON_MAP_MARKER_ACCOUNT, ), cv.Optional(CONF_MOVING_TARGET_COUNT): sensor.sensor_schema( - icon=ICON_MAP_MARKER_ACCOUNT, accuracy_decimals=0, + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], + icon=ICON_MAP_MARKER_ACCOUNT, ), } ) diff --git a/esphome/components/ld2450/switch/bluetooth_switch.cpp b/esphome/components/ld2450/switch/bluetooth_switch.cpp index fa0d4fb06a..0e19a3e6c6 100644 --- a/esphome/components/ld2450/switch/bluetooth_switch.cpp +++ b/esphome/components/ld2450/switch/bluetooth_switch.cpp @@ -1,12 +1,10 @@ #include "bluetooth_switch.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { void BluetoothSwitch::write_state(bool state) { this->publish_state(state); this->parent_->set_bluetooth(state); } -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/switch/bluetooth_switch.h b/esphome/components/ld2450/switch/bluetooth_switch.h index 3c1c4f755c..3d48a89b57 100644 --- a/esphome/components/ld2450/switch/bluetooth_switch.h +++ b/esphome/components/ld2450/switch/bluetooth_switch.h @@ -3,8 +3,7 @@ #include "esphome/components/switch/switch.h" #include "../ld2450.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { class BluetoothSwitch : public switch_::Switch, public Parented { public: @@ -14,5 +13,4 @@ class BluetoothSwitch : public switch_::Switch, public Parented void write_state(bool state) override; }; -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/switch/multi_target_switch.cpp b/esphome/components/ld2450/switch/multi_target_switch.cpp index a163e29fc5..0b1cb04a68 100644 --- a/esphome/components/ld2450/switch/multi_target_switch.cpp +++ b/esphome/components/ld2450/switch/multi_target_switch.cpp @@ -1,12 +1,10 @@ #include "multi_target_switch.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { void MultiTargetSwitch::write_state(bool state) { this->publish_state(state); this->parent_->set_multi_target(state); } -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/switch/multi_target_switch.h b/esphome/components/ld2450/switch/multi_target_switch.h index ca6253588d..739f308cce 100644 --- a/esphome/components/ld2450/switch/multi_target_switch.h +++ b/esphome/components/ld2450/switch/multi_target_switch.h @@ -3,8 +3,7 @@ #include "esphome/components/switch/switch.h" #include "../ld2450.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { class MultiTargetSwitch : public switch_::Switch, public Parented { public: @@ -14,5 +13,4 @@ class MultiTargetSwitch : public switch_::Switch, public Parented +#include #ifdef USE_SENSOR -#include "esphome/core/helpers.h" #include "esphome/components/sensor/sensor.h" #define SUB_SENSOR_WITH_DEDUP(name, dedup_type) \ @@ -36,8 +37,28 @@ #define highbyte(val) (uint8_t)((val) >> 8) #define lowbyte(val) (uint8_t)((val) &0xff) -namespace esphome { -namespace ld24xx { +namespace esphome::ld24xx { + +static const char *const UNKNOWN_MAC = "unknown"; +static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X"; + +// Helper function to format MAC address with stack allocation +// Returns pointer to UNKNOWN_MAC constant or formatted buffer +// Buffer must be exactly 18 bytes (17 for "XX:XX:XX:XX:XX:XX" + null terminator) +inline const char *format_mac_str(const uint8_t *mac_address, std::span buffer) { + if (mac_address_is_valid(mac_address)) { + format_mac_addr_upper(mac_address, buffer.data()); + return buffer.data(); + } + return UNKNOWN_MAC; +} + +// Helper function to format firmware version with stack allocation +// Buffer must be exactly 20 bytes (format: "x.xxXXXXXX" fits in 11 + null terminator, 20 for safety) +inline void format_version_str(const uint8_t *version, std::span buffer) { + snprintf(buffer.data(), buffer.size(), VERSION_FMT, version[1], version[0], version[5], version[4], version[3], + version[2]); +} #ifdef USE_SENSOR // Helper class to store a sensor with a deduplicator & publish state only when the value changes @@ -61,5 +82,4 @@ template class SensorWithDedup { Deduplicator publish_dedup; }; #endif -} // namespace ld24xx -} // namespace esphome +} // namespace esphome::ld24xx diff --git a/esphome/components/ledc/ledc_output.h b/esphome/components/ledc/ledc_output.h index f04543bc5b..b24e3cfdb2 100644 --- a/esphome/components/ledc/ledc_output.h +++ b/esphome/components/ledc/ledc_output.h @@ -47,7 +47,7 @@ template class SetFrequencyAction : public Action { SetFrequencyAction(LEDCOutput *parent) : parent_(parent) {} TEMPLATABLE_VALUE(float, frequency); - void play(Ts... x) { + void play(const Ts &...x) { float freq = this->frequency_.value(x...); this->parent_->update_frequency(freq); } diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py index 178660cb40..93b66888da 100644 --- a/esphome/components/libretiny/__init__.py +++ b/esphome/components/libretiny/__init__.py @@ -1,6 +1,5 @@ import json import logging -from os.path import dirname, isfile, join import esphome.codegen as cg import esphome.config_validation as cv @@ -24,6 +23,7 @@ from esphome.const import ( __version__, ) from esphome.core import CORE +from esphome.storage_json import StorageJSON from . import gpio # noqa from .const import ( @@ -129,7 +129,7 @@ def only_on_family(*, supported=None, unsupported=None): return validator_ -def get_download_types(storage_json=None): +def get_download_types(storage_json: StorageJSON = None): types = [ { "title": "UF2 package (recommended)", @@ -139,11 +139,11 @@ def get_download_types(storage_json=None): }, ] - build_dir = dirname(storage_json.firmware_bin_path) - outputs = join(build_dir, "firmware.json") - if not isfile(outputs): + build_dir = storage_json.firmware_bin_path.parent + outputs = build_dir / "firmware.json" + if not outputs.is_file(): return types - with open(outputs, encoding="utf-8") as f: + with outputs.open(encoding="utf-8") as f: outputs = json.load(f) for output in outputs: if not output["public"]: @@ -261,6 +261,10 @@ async def component_to_code(config): cg.add_build_flag(f"-DUSE_LIBRETINY_VARIANT_{config[CONF_FAMILY]}") cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_define("ESPHOME_VARIANT", FAMILY_FRIENDLY[config[CONF_FAMILY]]) + # LibreTiny uses MULTI_NO_ATOMICS because platforms like BK7231N (ARM968E-S) lack + # exclusive load/store (no LDREX/STREX). std::atomic RMW operations require libatomic, + # which is not linked to save flash (4-8KB). Even if linked, libatomic would use locks + # (ATOMIC_INT_LOCK_FREE=1), so explicit FreeRTOS mutexes are simpler and equivalent. cg.add_define(ThreadModel.MULTI_NO_ATOMICS) # force using arduino framework diff --git a/esphome/components/libretiny/gpio.py b/esphome/components/libretiny/gpio.py index 07eb0ce133..9bad400eb7 100644 --- a/esphome/components/libretiny/gpio.py +++ b/esphome/components/libretiny/gpio.py @@ -199,6 +199,9 @@ async def component_pin_to_code(config): var = cg.new_Pvariable(config[CONF_ID]) num = config[CONF_NUMBER] cg.add(var.set_pin(num)) - cg.add(var.set_inverted(config[CONF_INVERTED])) + # Only set if true to avoid bloating setup() function + # (inverted bit in pin_flags_ bitfield is zero-initialized to false) + if config[CONF_INVERTED]: + cg.add(var.set_inverted(True)) cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) return var diff --git a/esphome/components/libretiny/gpio_arduino.h b/esphome/components/libretiny/gpio_arduino.h index 9adc425a41..3674748c18 100644 --- a/esphome/components/libretiny/gpio_arduino.h +++ b/esphome/components/libretiny/gpio_arduino.h @@ -27,8 +27,8 @@ class ArduinoInternalGPIOPin : public InternalGPIOPin { void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; uint8_t pin_; - bool inverted_; - gpio::Flags flags_; + bool inverted_{}; + gpio::Flags flags_{}; }; } // namespace libretiny diff --git a/esphome/components/libretiny/preferences.cpp b/esphome/components/libretiny/preferences.cpp index ce4ed915c0..871b186d8e 100644 --- a/esphome/components/libretiny/preferences.cpp +++ b/esphome/components/libretiny/preferences.cpp @@ -5,7 +5,7 @@ #include "esphome/core/preferences.h" #include #include -#include +#include #include namespace esphome { @@ -15,7 +15,14 @@ static const char *const TAG = "lt.preferences"; struct NVSData { std::string key; - std::vector data; + std::unique_ptr data; + size_t len; + + void set_data(const uint8_t *src, size_t size) { + data = std::make_unique(size); + memcpy(data.get(), src, size); + len = size; + } }; static std::vector s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) @@ -30,15 +37,15 @@ class LibreTinyPreferenceBackend : public ESPPreferenceBackend { // try find in pending saves and update that for (auto &obj : s_pending_save) { if (obj.key == key) { - obj.data.assign(data, data + len); + obj.set_data(data, len); return true; } } NVSData save{}; save.key = key; - save.data.assign(data, data + len); - s_pending_save.emplace_back(save); - ESP_LOGVV(TAG, "s_pending_save: key: %s, len: %d", key.c_str(), len); + save.set_data(data, len); + s_pending_save.emplace_back(std::move(save)); + ESP_LOGVV(TAG, "s_pending_save: key: %s, len: %zu", key.c_str(), len); return true; } @@ -46,11 +53,11 @@ class LibreTinyPreferenceBackend : public ESPPreferenceBackend { // try find in pending saves and load from that for (auto &obj : s_pending_save) { if (obj.key == key) { - if (obj.data.size() != len) { + if (obj.len != len) { // size mismatch return false; } - memcpy(data, obj.data.data(), len); + memcpy(data, obj.data.get(), len); return true; } } @@ -58,10 +65,10 @@ class LibreTinyPreferenceBackend : public ESPPreferenceBackend { fdb_blob_make(blob, data, len); size_t actual_len = fdb_kv_get_blob(db, key.c_str(), blob); if (actual_len != len) { - ESP_LOGVV(TAG, "NVS length does not match (%u!=%u)", actual_len, len); + ESP_LOGVV(TAG, "NVS length does not match (%zu!=%zu)", actual_len, len); return false; } else { - ESP_LOGVV(TAG, "fdb_kv_get_blob: key: %s, len: %d", key.c_str(), len); + ESP_LOGVV(TAG, "fdb_kv_get_blob: key: %s, len: %zu", key.c_str(), len); } return true; } @@ -101,7 +108,7 @@ class LibreTinyPreferences : public ESPPreferences { if (s_pending_save.empty()) return true; - ESP_LOGV(TAG, "Saving %d items...", s_pending_save.size()); + ESP_LOGV(TAG, "Saving %zu items...", s_pending_save.size()); // goal try write all pending saves even if one fails int cached = 0, written = 0, failed = 0; fdb_err_t last_err = FDB_NO_ERR; @@ -112,11 +119,11 @@ class LibreTinyPreferences : public ESPPreferences { const auto &save = s_pending_save[i]; ESP_LOGVV(TAG, "Checking if FDB data %s has changed", save.key.c_str()); if (is_changed(&db, save)) { - ESP_LOGV(TAG, "sync: key: %s, len: %d", save.key.c_str(), save.data.size()); - fdb_blob_make(&blob, save.data.data(), save.data.size()); + ESP_LOGV(TAG, "sync: key: %s, len: %zu", save.key.c_str(), save.len); + fdb_blob_make(&blob, save.data.get(), save.len); fdb_err_t err = fdb_kv_set_blob(&db, save.key.c_str(), &blob); if (err != FDB_NO_ERR) { - ESP_LOGV(TAG, "fdb_kv_set_blob('%s', len=%u) failed: %d", save.key.c_str(), save.data.size(), err); + ESP_LOGV(TAG, "fdb_kv_set_blob('%s', len=%zu) failed: %d", save.key.c_str(), save.len, err); failed++; last_err = err; last_key = save.key; @@ -124,7 +131,7 @@ class LibreTinyPreferences : public ESPPreferences { } written++; } else { - ESP_LOGD(TAG, "FDB data not changed; skipping %s len=%u", save.key.c_str(), save.data.size()); + ESP_LOGD(TAG, "FDB data not changed; skipping %s len=%zu", save.key.c_str(), save.len); cached++; } s_pending_save.erase(s_pending_save.begin() + i); @@ -139,21 +146,29 @@ class LibreTinyPreferences : public ESPPreferences { } bool is_changed(const fdb_kvdb_t db, const NVSData &to_save) { - NVSData stored_data{}; struct fdb_kv kv; fdb_kv_t kvp = fdb_kv_get_obj(db, to_save.key.c_str(), &kv); if (kvp == nullptr) { ESP_LOGV(TAG, "fdb_kv_get_obj('%s'): nullptr - the key might not be set yet", to_save.key.c_str()); return true; } - stored_data.data.resize(kv.value_len); - fdb_blob_make(&blob, stored_data.data.data(), kv.value_len); + + // Check size first - if different, data has changed + if (kv.value_len != to_save.len) { + return true; + } + + // Allocate buffer on heap to avoid stack allocation for large data + auto stored_data = std::make_unique(kv.value_len); + fdb_blob_make(&blob, stored_data.get(), kv.value_len); size_t actual_len = fdb_kv_get_blob(db, to_save.key.c_str(), &blob); if (actual_len != kv.value_len) { ESP_LOGV(TAG, "fdb_kv_get_blob('%s') len mismatch: %u != %u", to_save.key.c_str(), actual_len, kv.value_len); return true; } - return to_save.data != stored_data.data; + + // Compare the actual data + return memcmp(to_save.data.get(), stored_data.get(), kv.value_len) != 0; } bool reset() override { diff --git a/esphome/components/libretiny_pwm/libretiny_pwm.h b/esphome/components/libretiny_pwm/libretiny_pwm.h index 42ce40ca39..f911709054 100644 --- a/esphome/components/libretiny_pwm/libretiny_pwm.h +++ b/esphome/components/libretiny_pwm/libretiny_pwm.h @@ -40,7 +40,7 @@ template class SetFrequencyAction : public Action { SetFrequencyAction(LibreTinyPWM *parent) : parent_(parent) {} TEMPLATABLE_VALUE(float, frequency); - void play(Ts... x) { + void play(const Ts &...x) { float freq = this->frequency_.value(x...); this->parent_->update_frequency(freq); } diff --git a/esphome/components/light/__init__.py b/esphome/components/light/__init__.py index fa39721ee2..f1089ad64f 100644 --- a/esphome/components/light/__init__.py +++ b/esphome/components/light/__init__.py @@ -37,7 +37,7 @@ from esphome.const import ( CONF_WEB_SERVER, CONF_WHITE, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass @@ -283,6 +283,6 @@ async def new_light(config, *args): return output_var -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(light_ns.using) diff --git a/esphome/components/light/addressable_light.cpp b/esphome/components/light/addressable_light.cpp index a8e0c7b762..2f6ffc9a38 100644 --- a/esphome/components/light/addressable_light.cpp +++ b/esphome/components/light/addressable_light.cpp @@ -1,8 +1,7 @@ #include "addressable_light.h" #include "esphome/core/log.h" -namespace esphome { -namespace light { +namespace esphome::light { static const char *const TAG = "light.addressable"; @@ -61,8 +60,12 @@ void AddressableLightTransformer::start() { this->target_color_ *= to_uint8_scale(end_values.get_brightness() * end_values.get_state()); } +inline constexpr uint8_t subtract_scaled_difference(uint8_t a, uint8_t b, int32_t scale) { + return uint8_t(int32_t(a) - (((int32_t(a) - int32_t(b)) * scale) / 256)); +} + optional AddressableLightTransformer::apply() { - float smoothed_progress = LightTransitionTransformer::smoothed_progress(this->get_progress_()); + float smoothed_progress = LightTransformer::smoothed_progress(this->get_progress_()); // When running an output-buffer modifying effect, don't try to transition individual LEDs, but instead just fade the // LightColorValues. write_state() then picks up the change in brightness, and the color change is picked up by the @@ -74,40 +77,38 @@ optional AddressableLightTransformer::apply() { // all LEDs, we use the current state of each LED as the start. // We can't use a direct lerp smoothing here though - that would require creating a copy of the original - // state of each LED at the start of the transition. - // Instead, we "fake" the look of the LERP by using an exponential average over time and using - // dynamically-calculated alpha values to match the look. + // state of each LED at the start of the transition. Instead, we "fake" the look of lerp by calculating + // the delta between the current state and the target state, assuming that the delta represents the rest + // of the transition that was to be applied as of the previous transition step, and scaling the delta for + // what should be left after the current transition step. In this manner, the delta decays to zero as the + // transition progresses. + // + // Here's an example of how the algorithm progresses in discrete steps: + // + // At time = 0.00, 0% complete, 100% remaining, 100% will remain after this step, so the scale is 100% / 100% = 100%. + // At time = 0.10, 0% complete, 100% remaining, 90% will remain after this step, so the scale is 90% / 100% = 90%. + // At time = 0.20, 10% complete, 90% remaining, 80% will remain after this step, so the scale is 80% / 90% = 88.9%. + // At time = 0.50, 20% complete, 80% remaining, 50% will remain after this step, so the scale is 50% / 80% = 62.5%. + // At time = 0.90, 50% complete, 50% remaining, 10% will remain after this step, so the scale is 10% / 50% = 20%. + // At time = 0.91, 90% complete, 10% remaining, 9% will remain after this step, so the scale is 9% / 10% = 90%. + // At time = 1.00, 91% complete, 9% remaining, 0% will remain after this step, so the scale is 0% / 9% = 0%. + // + // Because the color values are quantized to 8 bit resolution after each step, the transition may appear + // non-linear when applying small deltas. - float denom = (1.0f - smoothed_progress); - float alpha = denom == 0.0f ? 1.0f : (smoothed_progress - this->last_transition_progress_) / denom; - - // We need to use a low-resolution alpha here which makes the transition set in only after ~half of the length - // We solve this by accumulating the fractional part of the alpha over time. - float alpha255 = alpha * 255.0f; - float alpha255int = floorf(alpha255); - float alpha255remainder = alpha255 - alpha255int; - - this->accumulated_alpha_ += alpha255remainder; - float alpha_add = floorf(this->accumulated_alpha_); - this->accumulated_alpha_ -= alpha_add; - - alpha255 += alpha_add; - alpha255 = clamp(alpha255, 0.0f, 255.0f); - auto alpha8 = static_cast(alpha255); - - if (alpha8 != 0) { - uint8_t inv_alpha8 = 255 - alpha8; - Color add = this->target_color_ * alpha8; - - for (auto led : this->light_) - led.set(add + led.get() * inv_alpha8); + if (smoothed_progress > this->last_transition_progress_ && this->last_transition_progress_ < 1.f) { + int32_t scale = int32_t(256.f * std::max((1.f - smoothed_progress) / (1.f - this->last_transition_progress_), 0.f)); + for (auto led : this->light_) { + led.set_rgbw(subtract_scaled_difference(this->target_color_.red, led.get_red(), scale), + subtract_scaled_difference(this->target_color_.green, led.get_green(), scale), + subtract_scaled_difference(this->target_color_.blue, led.get_blue(), scale), + subtract_scaled_difference(this->target_color_.white, led.get_white(), scale)); + } + this->last_transition_progress_ = smoothed_progress; + this->light_.schedule_show(); } - this->last_transition_progress_ = smoothed_progress; - this->light_.schedule_show(); - return {}; } -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/addressable_light.h b/esphome/components/light/addressable_light.h index baa4507d2f..2e4b984ce4 100644 --- a/esphome/components/light/addressable_light.h +++ b/esphome/components/light/addressable_light.h @@ -1,23 +1,20 @@ #pragma once -#include "esphome/core/component.h" -#include "esphome/core/defines.h" -#include "esphome/core/color.h" #include "esp_color_correction.h" #include "esp_color_view.h" #include "esp_range_view.h" +#include "esphome/core/color.h" +#include "esphome/core/component.h" +#include "esphome/core/defines.h" #include "light_output.h" #include "light_state.h" -#include "transformers.h" +#include "light_transformer.h" #ifdef USE_POWER_SUPPLY #include "esphome/components/power_supply/power_supply.h" #endif -namespace esphome { -namespace light { - -using ESPColor ESPDEPRECATED("esphome::light::ESPColor is deprecated, use esphome::Color instead.", "v1.21") = Color; +namespace esphome::light { /// Convert the color information from a `LightColorValues` object to a `Color` object (does not apply brightness). Color color_from_light_color_values(LightColorValues val); @@ -105,7 +102,7 @@ class AddressableLight : public LightOutput, public Component { bool effect_active_{false}; }; -class AddressableLightTransformer : public LightTransitionTransformer { +class AddressableLightTransformer : public LightTransformer { public: AddressableLightTransformer(AddressableLight &light) : light_(light) {} @@ -115,9 +112,7 @@ class AddressableLightTransformer : public LightTransitionTransformer { protected: AddressableLight &light_; float last_transition_progress_{0.0f}; - float accumulated_alpha_{0.0f}; Color target_color_{}; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/addressable_light_effect.h b/esphome/components/light/addressable_light_effect.h index d622ec0375..a85ea4661d 100644 --- a/esphome/components/light/addressable_light_effect.h +++ b/esphome/components/light/addressable_light_effect.h @@ -1,14 +1,13 @@ #pragma once #include -#include #include "esphome/core/component.h" +#include "esphome/core/helpers.h" #include "esphome/components/light/light_state.h" #include "esphome/components/light/addressable_light.h" -namespace esphome { -namespace light { +namespace esphome::light { inline static int16_t sin16_c(uint16_t theta) { static const uint16_t BASE[] = {0, 6393, 12539, 18204, 23170, 27245, 30273, 32137}; @@ -30,7 +29,7 @@ inline static uint8_t half_sin8(uint8_t v) { return sin16_c(uint16_t(v) * 128u) class AddressableLightEffect : public LightEffect { public: - explicit AddressableLightEffect(const std::string &name) : LightEffect(name) {} + explicit AddressableLightEffect(const char *name) : LightEffect(name) {} void start_internal() override { this->get_addressable_()->set_effect_active(true); this->get_addressable_()->clear_effect_data(); @@ -44,16 +43,22 @@ class AddressableLightEffect : public LightEffect { this->apply(*this->get_addressable_(), current_color); } + /// Get effect index specifically for addressable effects. + /// Can be used by effects to modify behavior based on their position in the list. + uint32_t get_effect_index() const { return this->get_index(); } + + /// Check if this is the currently running addressable effect. + bool is_current_effect() const { return this->is_active() && this->get_addressable_()->is_effect_active(); } + protected: AddressableLight *get_addressable_() const { return (AddressableLight *) this->state_->get_output(); } }; class AddressableLambdaLightEffect : public AddressableLightEffect { public: - AddressableLambdaLightEffect(const std::string &name, - std::function f, + AddressableLambdaLightEffect(const char *name, void (*f)(AddressableLight &, Color, bool initial_run), uint32_t update_interval) - : AddressableLightEffect(name), f_(std::move(f)), update_interval_(update_interval) {} + : AddressableLightEffect(name), f_(f), update_interval_(update_interval) {} void start() override { this->initial_run_ = true; } void apply(AddressableLight &it, const Color ¤t_color) override { const uint32_t now = millis(); @@ -66,7 +71,7 @@ class AddressableLambdaLightEffect : public AddressableLightEffect { } protected: - std::function f_; + void (*f_)(AddressableLight &, Color, bool initial_run); uint32_t update_interval_; uint32_t last_run_{0}; bool initial_run_; @@ -74,7 +79,7 @@ class AddressableLambdaLightEffect : public AddressableLightEffect { class AddressableRainbowLightEffect : public AddressableLightEffect { public: - explicit AddressableRainbowLightEffect(const std::string &name) : AddressableLightEffect(name) {} + explicit AddressableRainbowLightEffect(const char *name) : AddressableLightEffect(name) {} void apply(AddressableLight &it, const Color ¤t_color) override { ESPHSVColor hsv; hsv.value = 255; @@ -105,8 +110,8 @@ struct AddressableColorWipeEffectColor { class AddressableColorWipeEffect : public AddressableLightEffect { public: - explicit AddressableColorWipeEffect(const std::string &name) : AddressableLightEffect(name) {} - void set_colors(const std::vector &colors) { this->colors_ = colors; } + explicit AddressableColorWipeEffect(const char *name) : AddressableLightEffect(name) {} + void set_colors(const std::initializer_list &colors) { this->colors_ = colors; } void set_add_led_interval(uint32_t add_led_interval) { this->add_led_interval_ = add_led_interval; } void set_reverse(bool reverse) { this->reverse_ = reverse; } void apply(AddressableLight &it, const Color ¤t_color) override { @@ -148,7 +153,7 @@ class AddressableColorWipeEffect : public AddressableLightEffect { } protected: - std::vector colors_; + FixedVector colors_; size_t at_color_{0}; uint32_t last_add_{0}; uint32_t add_led_interval_{}; @@ -158,7 +163,7 @@ class AddressableColorWipeEffect : public AddressableLightEffect { class AddressableScanEffect : public AddressableLightEffect { public: - explicit AddressableScanEffect(const std::string &name) : AddressableLightEffect(name) {} + explicit AddressableScanEffect(const char *name) : AddressableLightEffect(name) {} void set_move_interval(uint32_t move_interval) { this->move_interval_ = move_interval; } void set_scan_width(uint32_t scan_width) { this->scan_width_ = scan_width; } void apply(AddressableLight &it, const Color ¤t_color) override { @@ -195,7 +200,7 @@ class AddressableScanEffect : public AddressableLightEffect { class AddressableTwinkleEffect : public AddressableLightEffect { public: - explicit AddressableTwinkleEffect(const std::string &name) : AddressableLightEffect(name) {} + explicit AddressableTwinkleEffect(const char *name) : AddressableLightEffect(name) {} void apply(AddressableLight &addressable, const Color ¤t_color) override { const uint32_t now = millis(); uint8_t pos_add = 0; @@ -237,7 +242,7 @@ class AddressableTwinkleEffect : public AddressableLightEffect { class AddressableRandomTwinkleEffect : public AddressableLightEffect { public: - explicit AddressableRandomTwinkleEffect(const std::string &name) : AddressableLightEffect(name) {} + explicit AddressableRandomTwinkleEffect(const char *name) : AddressableLightEffect(name) {} void apply(AddressableLight &it, const Color ¤t_color) override { const uint32_t now = millis(); uint8_t pos_add = 0; @@ -286,7 +291,7 @@ class AddressableRandomTwinkleEffect : public AddressableLightEffect { class AddressableFireworksEffect : public AddressableLightEffect { public: - explicit AddressableFireworksEffect(const std::string &name) : AddressableLightEffect(name) {} + explicit AddressableFireworksEffect(const char *name) : AddressableLightEffect(name) {} void start() override { auto &it = *this->get_addressable_(); it.all() = Color::BLACK; @@ -335,7 +340,7 @@ class AddressableFireworksEffect : public AddressableLightEffect { class AddressableFlickerEffect : public AddressableLightEffect { public: - explicit AddressableFlickerEffect(const std::string &name) : AddressableLightEffect(name) {} + explicit AddressableFlickerEffect(const char *name) : AddressableLightEffect(name) {} void apply(AddressableLight &it, const Color ¤t_color) override { const uint32_t now = millis(); const uint8_t intensity = this->intensity_; @@ -365,5 +370,4 @@ class AddressableFlickerEffect : public AddressableLightEffect { uint8_t intensity_{13}; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/addressable_light_wrapper.h b/esphome/components/light/addressable_light_wrapper.h index d358502430..8665e62a79 100644 --- a/esphome/components/light/addressable_light_wrapper.h +++ b/esphome/components/light/addressable_light_wrapper.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "addressable_light.h" -namespace esphome { -namespace light { +namespace esphome::light { class AddressableLightWrapper : public light::AddressableLight { public: @@ -123,5 +122,4 @@ class AddressableLightWrapper : public light::AddressableLight { ColorMode color_mode_{ColorMode::UNKNOWN}; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/automation.cpp b/esphome/components/light/automation.cpp index 8c1785f061..ddac2f9341 100644 --- a/esphome/components/light/automation.cpp +++ b/esphome/components/light/automation.cpp @@ -1,8 +1,7 @@ #include "automation.h" #include "esphome/core/log.h" -namespace esphome { -namespace light { +namespace esphome::light { static const char *const TAG = "light.automation"; @@ -11,5 +10,4 @@ void addressableset_warn_about_scale(const char *field) { field); } -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/automation.h b/esphome/components/light/automation.h index 6e055741da..9893c15e0c 100644 --- a/esphome/components/light/automation.h +++ b/esphome/components/light/automation.h @@ -4,8 +4,7 @@ #include "light_state.h" #include "addressable_light.h" -namespace esphome { -namespace light { +namespace esphome::light { enum class LimitMode { CLAMP, DO_NOTHING }; @@ -15,7 +14,7 @@ template class ToggleAction : public Action { TEMPLATABLE_VALUE(uint32_t, transition_length) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->state_->toggle(); call.set_transition_length(this->transition_length_.optional_value(x...)); call.perform(); @@ -44,7 +43,7 @@ template class LightControlAction : public Action { TEMPLATABLE_VALUE(float, warm_white) TEMPLATABLE_VALUE(std::string, effect) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->parent_->make_call(); call.set_color_mode(this->color_mode_.optional_value(x...)); call.set_state(this->state_.optional_value(x...)); @@ -74,7 +73,7 @@ template class DimRelativeAction : public Action { TEMPLATABLE_VALUE(float, relative_brightness) TEMPLATABLE_VALUE(uint32_t, transition_length) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->parent_->make_call(); float rel = this->relative_brightness_.value(x...); float cur; @@ -107,7 +106,7 @@ template class DimRelativeAction : public Action { template class LightIsOnCondition : public Condition { public: explicit LightIsOnCondition(LightState *state) : state_(state) {} - bool check(Ts... x) override { return this->state_->current_values.is_on(); } + bool check(const Ts &...x) override { return this->state_->current_values.is_on(); } protected: LightState *state_; @@ -115,7 +114,7 @@ template class LightIsOnCondition : public Condition { template class LightIsOffCondition : public Condition { public: explicit LightIsOffCondition(LightState *state) : state_(state) {} - bool check(Ts... x) override { return !this->state_->current_values.is_on(); } + bool check(const Ts &...x) override { return !this->state_->current_values.is_on(); } protected: LightState *state_; @@ -179,7 +178,7 @@ template class AddressableSet : public Action { TEMPLATABLE_VALUE(float, blue) TEMPLATABLE_VALUE(float, white) - void play(Ts... x) override { + void play(const Ts &...x) override { auto *out = (AddressableLight *) this->parent_->get_output(); int32_t range_from = interpret_index(this->range_from_.value_or(x..., 0), out->size()); if (range_from < 0 || range_from >= out->size()) @@ -216,5 +215,4 @@ template class AddressableSet : public Action { } }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/base_light_effects.h b/esphome/components/light/base_light_effects.h index 9e02e889c9..2eeae574e7 100644 --- a/esphome/components/light/base_light_effects.h +++ b/esphome/components/light/base_light_effects.h @@ -1,13 +1,12 @@ #pragma once #include -#include #include "esphome/core/automation.h" +#include "esphome/core/helpers.h" #include "light_effect.h" -namespace esphome { -namespace light { +namespace esphome::light { inline static float random_cubic_float() { const float r = random_float() * 2.0f - 1.0f; @@ -17,7 +16,7 @@ inline static float random_cubic_float() { /// Pulse effect. class PulseLightEffect : public LightEffect { public: - explicit PulseLightEffect(const std::string &name) : LightEffect(name) {} + explicit PulseLightEffect(const char *name) : LightEffect(name) {} void apply() override { const uint32_t now = millis(); @@ -60,7 +59,7 @@ class PulseLightEffect : public LightEffect { /// Random effect. Sets random colors every 10 seconds and slowly transitions between them. class RandomLightEffect : public LightEffect { public: - explicit RandomLightEffect(const std::string &name) : LightEffect(name) {} + explicit RandomLightEffect(const char *name) : LightEffect(name) {} void apply() override { const uint32_t now = millis(); @@ -112,8 +111,8 @@ class RandomLightEffect : public LightEffect { class LambdaLightEffect : public LightEffect { public: - LambdaLightEffect(const std::string &name, std::function f, uint32_t update_interval) - : LightEffect(name), f_(std::move(f)), update_interval_(update_interval) {} + LambdaLightEffect(const char *name, void (*f)(bool initial_run), uint32_t update_interval) + : LightEffect(name), f_(f), update_interval_(update_interval) {} void start() override { this->initial_run_ = true; } void apply() override { @@ -125,8 +124,12 @@ class LambdaLightEffect : public LightEffect { } } + /// Get the current effect index for use in lambda functions. + /// This can be useful for lambda effects that need to know their own index. + uint32_t get_current_index() const { return this->get_index(); } + protected: - std::function f_; + void (*f_)(bool initial_run); uint32_t update_interval_; uint32_t last_run_{0}; bool initial_run_; @@ -134,7 +137,7 @@ class LambdaLightEffect : public LightEffect { class AutomationLightEffect : public LightEffect { public: - AutomationLightEffect(const std::string &name) : LightEffect(name) {} + AutomationLightEffect(const char *name) : LightEffect(name) {} void stop() override { this->trig_->stop_action(); } void apply() override { if (!this->trig_->is_action_running()) { @@ -143,6 +146,10 @@ class AutomationLightEffect : public LightEffect { } Trigger<> *get_trig() const { return trig_; } + /// Get the current effect index for use in automations. + /// Useful for automations that need to know which effect is running. + uint32_t get_current_index() const { return this->get_index(); } + protected: Trigger<> *trig_{new Trigger<>}; }; @@ -155,7 +162,7 @@ struct StrobeLightEffectColor { class StrobeLightEffect : public LightEffect { public: - explicit StrobeLightEffect(const std::string &name) : LightEffect(name) {} + explicit StrobeLightEffect(const char *name) : LightEffect(name) {} void apply() override { const uint32_t now = millis(); if (now - this->last_switch_ < this->colors_[this->at_color_].duration) @@ -180,17 +187,17 @@ class StrobeLightEffect : public LightEffect { this->last_switch_ = now; } - void set_colors(const std::vector &colors) { this->colors_ = colors; } + void set_colors(const std::initializer_list &colors) { this->colors_ = colors; } protected: - std::vector colors_; + FixedVector colors_; uint32_t last_switch_{0}; size_t at_color_{0}; }; class FlickerLightEffect : public LightEffect { public: - explicit FlickerLightEffect(const std::string &name) : LightEffect(name) {} + explicit FlickerLightEffect(const char *name) : LightEffect(name) {} void apply() override { LightColorValues remote = this->state_->remote_values; @@ -227,5 +234,4 @@ class FlickerLightEffect : public LightEffect { float alpha_{}; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/color_mode.h b/esphome/components/light/color_mode.h index e524763c9f..0750ae250d 100644 --- a/esphome/components/light/color_mode.h +++ b/esphome/components/light/color_mode.h @@ -1,9 +1,9 @@ #pragma once #include +#include "esphome/core/finite_set_mask.h" -namespace esphome { -namespace light { +namespace esphome::light { /// Color capabilities are the various outputs that a light has and that can be independently controlled by the user. enum class ColorCapability : uint8_t { @@ -104,5 +104,109 @@ constexpr ColorModeHelper operator|(ColorModeHelper lhs, ColorMode rhs) { return static_cast(static_cast(lhs) | static_cast(rhs)); } -} // namespace light -} // namespace esphome +// Type alias for raw color mode bitmask values +using color_mode_bitmask_t = uint16_t; + +// Lookup table for ColorMode bit mapping +// This array defines the canonical order of color modes (bit 0-9) +constexpr ColorMode COLOR_MODE_LOOKUP[] = { + ColorMode::UNKNOWN, // bit 0 + ColorMode::ON_OFF, // bit 1 + ColorMode::BRIGHTNESS, // bit 2 + ColorMode::WHITE, // bit 3 + ColorMode::COLOR_TEMPERATURE, // bit 4 + ColorMode::COLD_WARM_WHITE, // bit 5 + ColorMode::RGB, // bit 6 + ColorMode::RGB_WHITE, // bit 7 + ColorMode::RGB_COLOR_TEMPERATURE, // bit 8 + ColorMode::RGB_COLD_WARM_WHITE, // bit 9 +}; + +/// Bit mapping policy for ColorMode +/// Uses lookup table for non-contiguous enum values +struct ColorModeBitPolicy { + using mask_t = uint16_t; // 10 bits requires uint16_t + static constexpr int MAX_BITS = sizeof(COLOR_MODE_LOOKUP) / sizeof(COLOR_MODE_LOOKUP[0]); + + static constexpr unsigned to_bit(ColorMode mode) { + // Linear search through lookup table + // Compiler optimizes this to efficient code since array is constexpr + for (int i = 0; i < MAX_BITS; ++i) { + if (COLOR_MODE_LOOKUP[i] == mode) + return i; + } + return 0; + } + + static constexpr ColorMode from_bit(unsigned bit) { + return (bit < MAX_BITS) ? COLOR_MODE_LOOKUP[bit] : ColorMode::UNKNOWN; + } +}; + +// Type alias for ColorMode bitmask using policy-based design +using ColorModeMask = FiniteSetMask; + +// Number of ColorCapability enum values +constexpr int COLOR_CAPABILITY_COUNT = 6; + +/// Helper to compute capability bitmask at compile time +constexpr uint16_t compute_capability_bitmask(ColorCapability capability) { + uint16_t mask = 0; + uint8_t cap_bit = static_cast(capability); + + // Check each ColorMode to see if it has this capability + constexpr int color_mode_count = sizeof(COLOR_MODE_LOOKUP) / sizeof(COLOR_MODE_LOOKUP[0]); + for (int bit = 0; bit < color_mode_count; ++bit) { + uint8_t mode_val = static_cast(COLOR_MODE_LOOKUP[bit]); + if ((mode_val & cap_bit) != 0) { + mask |= (1 << bit); + } + } + return mask; +} + +/// Compile-time lookup table mapping ColorCapability to bitmask +/// This array is computed at compile time using constexpr +constexpr uint16_t CAPABILITY_BITMASKS[] = { + compute_capability_bitmask(ColorCapability::ON_OFF), // 1 << 0 + compute_capability_bitmask(ColorCapability::BRIGHTNESS), // 1 << 1 + compute_capability_bitmask(ColorCapability::WHITE), // 1 << 2 + compute_capability_bitmask(ColorCapability::COLOR_TEMPERATURE), // 1 << 3 + compute_capability_bitmask(ColorCapability::COLD_WARM_WHITE), // 1 << 4 + compute_capability_bitmask(ColorCapability::RGB), // 1 << 5 +}; + +/** + * @brief Helper function to convert a power-of-2 ColorCapability value to an array index for CAPABILITY_BITMASKS + * lookup. + * + * This function maps ColorCapability values (1, 2, 4, 8, 16, 32) to array indices (0, 1, 2, 3, 4, 5). + * Used to index into the CAPABILITY_BITMASKS lookup table. + * + * @param capability A ColorCapability enum value (must be a power of 2). + * @return The corresponding array index (0-based). + */ +inline int capability_to_index(ColorCapability capability) { + uint8_t cap_val = static_cast(capability); +#if defined(__GNUC__) || defined(__clang__) + // Use compiler intrinsic for efficient bit position lookup (O(1) vs O(log n)) + return __builtin_ctz(cap_val); +#else + // Fallback for compilers without __builtin_ctz + int index = 0; + while (cap_val > 1) { + cap_val >>= 1; + ++index; + } + return index; +#endif +} + +/// Check if any mode in the bitmask has a specific capability +/// Used for checking if a light supports a capability (e.g., BRIGHTNESS, RGB) +inline bool has_capability(const ColorModeMask &mask, ColorCapability capability) { + // Lookup the pre-computed bitmask for this capability and check intersection with our mask + return (mask.get_mask() & CAPABILITY_BITMASKS[capability_to_index(capability)]) != 0; +} + +} // namespace esphome::light diff --git a/esphome/components/light/effects.py b/esphome/components/light/effects.py index 6c8fd86225..15d9272d1a 100644 --- a/esphome/components/light/effects.py +++ b/esphome/components/light/effects.py @@ -29,6 +29,7 @@ from esphome.const import ( CONF_WHITE, CONF_WIDTH, ) +from esphome.cpp_generator import MockObjClass from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor from esphome.util import Registry @@ -88,8 +89,15 @@ ADDRESSABLE_EFFECTS = [] EFFECTS_REGISTRY = Registry() -def register_effect(name, effect_type, default_name, schema, *extra_validators): - schema = cv.Schema(schema).extend( +def register_effect( + name: str, + effect_type: MockObjClass, + default_name: str, + schema: cv.Schema | dict, + *extra_validators, +): + schema = schema if isinstance(schema, cv.Schema) else cv.Schema(schema) + schema = schema.extend( { cv.Optional(CONF_NAME, default=default_name): cv.string_strict, } @@ -98,7 +106,13 @@ def register_effect(name, effect_type, default_name, schema, *extra_validators): return EFFECTS_REGISTRY.register(name, effect_type, validator) -def register_binary_effect(name, effect_type, default_name, schema, *extra_validators): +def register_binary_effect( + name: str, + effect_type: MockObjClass, + default_name: str, + schema: cv.Schema | dict, + *extra_validators, +): # binary effect can be used for all lights BINARY_EFFECTS.append(name) MONOCHROMATIC_EFFECTS.append(name) @@ -109,7 +123,11 @@ def register_binary_effect(name, effect_type, default_name, schema, *extra_valid def register_monochromatic_effect( - name, effect_type, default_name, schema, *extra_validators + name: str, + effect_type: MockObjClass, + default_name: str, + schema: cv.Schema | dict, + *extra_validators, ): # monochromatic effect can be used for all lights expect binary MONOCHROMATIC_EFFECTS.append(name) @@ -119,7 +137,13 @@ def register_monochromatic_effect( return register_effect(name, effect_type, default_name, schema, *extra_validators) -def register_rgb_effect(name, effect_type, default_name, schema, *extra_validators): +def register_rgb_effect( + name: str, + effect_type: MockObjClass, + default_name: str, + schema: cv.Schema | dict, + *extra_validators, +): # RGB effect can be used for RGB and addressable lights RGB_EFFECTS.append(name) ADDRESSABLE_EFFECTS.append(name) @@ -128,7 +152,11 @@ def register_rgb_effect(name, effect_type, default_name, schema, *extra_validato def register_addressable_effect( - name, effect_type, default_name, schema, *extra_validators + name: str, + effect_type: MockObjClass, + default_name: str, + schema: cv.Schema | dict, + *extra_validators, ): # addressable effect can be used only in addressable ADDRESSABLE_EFFECTS.append(name) diff --git a/esphome/components/light/esp_color_correction.cpp b/esphome/components/light/esp_color_correction.cpp index e5e68264cc..1b511a94b2 100644 --- a/esphome/components/light/esp_color_correction.cpp +++ b/esphome/components/light/esp_color_correction.cpp @@ -2,8 +2,7 @@ #include "light_color_values.h" #include "esphome/core/log.h" -namespace esphome { -namespace light { +namespace esphome::light { void ESPColorCorrection::calculate_gamma_table(float gamma) { for (uint16_t i = 0; i < 256; i++) { @@ -23,5 +22,4 @@ void ESPColorCorrection::calculate_gamma_table(float gamma) { } } -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/esp_color_correction.h b/esphome/components/light/esp_color_correction.h index 979a1acb07..d275e045b7 100644 --- a/esphome/components/light/esp_color_correction.h +++ b/esphome/components/light/esp_color_correction.h @@ -2,8 +2,7 @@ #include "esphome/core/color.h" -namespace esphome { -namespace light { +namespace esphome::light { class ESPColorCorrection { public: @@ -17,19 +16,19 @@ class ESPColorCorrection { this->color_correct_blue(color.blue), this->color_correct_white(color.white)); } inline uint8_t color_correct_red(uint8_t red) const ESPHOME_ALWAYS_INLINE { - uint8_t res = esp_scale8(esp_scale8(red, this->max_brightness_.red), this->local_brightness_); + uint8_t res = esp_scale8_twice(red, this->max_brightness_.red, this->local_brightness_); return this->gamma_table_[res]; } inline uint8_t color_correct_green(uint8_t green) const ESPHOME_ALWAYS_INLINE { - uint8_t res = esp_scale8(esp_scale8(green, this->max_brightness_.green), this->local_brightness_); + uint8_t res = esp_scale8_twice(green, this->max_brightness_.green, this->local_brightness_); return this->gamma_table_[res]; } inline uint8_t color_correct_blue(uint8_t blue) const ESPHOME_ALWAYS_INLINE { - uint8_t res = esp_scale8(esp_scale8(blue, this->max_brightness_.blue), this->local_brightness_); + uint8_t res = esp_scale8_twice(blue, this->max_brightness_.blue, this->local_brightness_); return this->gamma_table_[res]; } inline uint8_t color_correct_white(uint8_t white) const ESPHOME_ALWAYS_INLINE { - uint8_t res = esp_scale8(esp_scale8(white, this->max_brightness_.white), this->local_brightness_); + uint8_t res = esp_scale8_twice(white, this->max_brightness_.white, this->local_brightness_); return this->gamma_table_[res]; } inline Color color_uncorrect(Color color) const ESPHOME_ALWAYS_INLINE { @@ -73,5 +72,4 @@ class ESPColorCorrection { uint8_t local_brightness_{255}; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/esp_color_view.h b/esphome/components/light/esp_color_view.h index 35117e7dd8..440a23e9c9 100644 --- a/esphome/components/light/esp_color_view.h +++ b/esphome/components/light/esp_color_view.h @@ -4,8 +4,7 @@ #include "esp_hsv_color.h" #include "esp_color_correction.h" -namespace esphome { -namespace light { +namespace esphome::light { class ESPColorSettable { public: @@ -106,5 +105,4 @@ class ESPColorView : public ESPColorSettable { const ESPColorCorrection *color_correction_; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/esp_hsv_color.cpp b/esphome/components/light/esp_hsv_color.cpp index 450c2e11ce..07205ea6d0 100644 --- a/esphome/components/light/esp_hsv_color.cpp +++ b/esphome/components/light/esp_hsv_color.cpp @@ -1,7 +1,6 @@ #include "esp_hsv_color.h" -namespace esphome { -namespace light { +namespace esphome::light { Color ESPHSVColor::to_rgb() const { // based on FastLED's hsv rainbow to rgb @@ -70,5 +69,4 @@ Color ESPHSVColor::to_rgb() const { return rgb; } -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/esp_hsv_color.h b/esphome/components/light/esp_hsv_color.h index cdde91c71c..4b54039258 100644 --- a/esphome/components/light/esp_hsv_color.h +++ b/esphome/components/light/esp_hsv_color.h @@ -3,8 +3,7 @@ #include "esphome/core/color.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace light { +namespace esphome::light { struct ESPHSVColor { union { @@ -32,5 +31,4 @@ struct ESPHSVColor { Color to_rgb() const; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/esp_range_view.cpp b/esphome/components/light/esp_range_view.cpp index e1f0a507bd..58d552031a 100644 --- a/esphome/components/light/esp_range_view.cpp +++ b/esphome/components/light/esp_range_view.cpp @@ -1,8 +1,7 @@ #include "esp_range_view.h" #include "addressable_light.h" -namespace esphome { -namespace light { +namespace esphome::light { int32_t HOT interpret_index(int32_t index, int32_t size) { if (index < 0) @@ -92,5 +91,4 @@ ESPRangeView &ESPRangeView::operator=(const ESPRangeView &rhs) { // NOLINT ESPColorView ESPRangeIterator::operator*() const { return this->range_.parent_->get(this->i_); } -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/esp_range_view.h b/esphome/components/light/esp_range_view.h index 07d18af79f..f5e4ebb83f 100644 --- a/esphome/components/light/esp_range_view.h +++ b/esphome/components/light/esp_range_view.h @@ -3,8 +3,7 @@ #include "esp_color_view.h" #include "esp_hsv_color.h" -namespace esphome { -namespace light { +namespace esphome::light { int32_t interpret_index(int32_t index, int32_t size); @@ -76,5 +75,4 @@ class ESPRangeIterator { int32_t i_; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index 60945531cf..b3bdb16c73 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -4,28 +4,32 @@ #include "esphome/core/log.h" #include "esphome/core/optional.h" -namespace esphome { -namespace light { +namespace esphome::light { static const char *const TAG = "light"; // Helper functions to reduce code size for logging +static void clamp_and_log_if_invalid(const char *name, float &value, const LogString *param_name, float min = 0.0f, + float max = 1.0f) { + if (value < min || value > max) { + ESP_LOGW(TAG, "'%s': %s value %.2f is out of range [%.1f - %.1f]", name, LOG_STR_ARG(param_name), value, min, max); + value = clamp(value, min, max); + } +} + #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_WARN -static void log_validation_warning(const char *name, const char *param_name, float val, float min, float max) { - ESP_LOGW(TAG, "'%s': %s value %.2f is out of range [%.1f - %.1f]", name, param_name, val, min, max); +static void log_feature_not_supported(const char *name, const LogString *feature) { + ESP_LOGW(TAG, "'%s': %s not supported", name, LOG_STR_ARG(feature)); } -static void log_feature_not_supported(const char *name, const char *feature) { - ESP_LOGW(TAG, "'%s': %s not supported", name, feature); +static void log_color_mode_not_supported(const char *name, const LogString *feature) { + ESP_LOGW(TAG, "'%s': color mode does not support setting %s", name, LOG_STR_ARG(feature)); } -static void log_color_mode_not_supported(const char *name, const char *feature) { - ESP_LOGW(TAG, "'%s': color mode does not support setting %s", name, feature); +static void log_invalid_parameter(const char *name, const LogString *message) { + ESP_LOGW(TAG, "'%s': %s", name, LOG_STR_ARG(message)); } - -static void log_invalid_parameter(const char *name, const char *message) { ESP_LOGW(TAG, "'%s': %s", name, message); } #else -#define log_validation_warning(name, param_name, val, min, max) #define log_feature_not_supported(name, feature) #define log_color_mode_not_supported(name, feature) #define log_invalid_parameter(name, message) @@ -42,13 +46,15 @@ static void log_invalid_parameter(const char *name, const char *message) { ESP_L } \ LightCall &LightCall::set_##name(type name) { \ this->name##_ = name; \ - this->set_flag_(flag, true); \ + this->set_flag_(flag); \ return *this; \ } static const LogString *color_mode_to_human(ColorMode color_mode) { - if (color_mode == ColorMode::UNKNOWN) - return LOG_STR("Unknown"); + if (color_mode == ColorMode::ON_OFF) + return LOG_STR("On/Off"); + if (color_mode == ColorMode::BRIGHTNESS) + return LOG_STR("Brightness"); if (color_mode == ColorMode::WHITE) return LOG_STR("White"); if (color_mode == ColorMode::COLOR_TEMPERATURE) @@ -63,7 +69,7 @@ static const LogString *color_mode_to_human(ColorMode color_mode) { return LOG_STR("RGB + cold/warm white"); if (color_mode == ColorMode::RGB_COLOR_TEMPERATURE) return LOG_STR("RGB + color temperature"); - return LOG_STR(""); + return LOG_STR("Unknown"); } // Helper to log percentage values @@ -151,7 +157,7 @@ void LightCall::perform() { if (this->effect_ == 0u) { effect_s = "None"; } else { - effect_s = this->parent_->effects_[this->effect_ - 1]->get_name().c_str(); + effect_s = this->parent_->effects_[this->effect_ - 1]->get_name(); } if (publish) { @@ -179,6 +185,16 @@ void LightCall::perform() { } } +void LightCall::log_and_clear_unsupported_(FieldFlags flag, const LogString *feature, bool use_color_mode_log) { + auto *name = this->parent_->get_name().c_str(); + if (use_color_mode_log) { + log_color_mode_not_supported(name, feature); + } else { + log_feature_not_supported(name, feature); + } + this->clear_flag_(flag); +} + LightColorValues LightCall::validate_() { auto *name = this->parent_->get_name().c_str(); auto traits = this->parent_->get_traits(); @@ -186,141 +202,108 @@ LightColorValues LightCall::validate_() { // Color mode check if (this->has_color_mode() && !traits.supports_color_mode(this->color_mode_)) { ESP_LOGW(TAG, "'%s' does not support color mode %s", name, LOG_STR_ARG(color_mode_to_human(this->color_mode_))); - this->set_flag_(FLAG_HAS_COLOR_MODE, false); + this->clear_flag_(FLAG_HAS_COLOR_MODE); } // Ensure there is always a color mode set if (!this->has_color_mode()) { this->color_mode_ = this->compute_color_mode_(); - this->set_flag_(FLAG_HAS_COLOR_MODE, true); + this->set_flag_(FLAG_HAS_COLOR_MODE); } auto color_mode = this->color_mode_; // Transform calls that use non-native parameters for the current mode. this->transform_parameters_(); - // Brightness exists check - if (this->has_brightness() && this->brightness_ > 0.0f && !(color_mode & ColorCapability::BRIGHTNESS)) { - log_feature_not_supported(name, "brightness"); - this->set_flag_(FLAG_HAS_BRIGHTNESS, false); - } - - // Transition length possible check - if (this->has_transition_() && this->transition_length_ != 0 && !(color_mode & ColorCapability::BRIGHTNESS)) { - log_feature_not_supported(name, "transitions"); - this->set_flag_(FLAG_HAS_TRANSITION, false); - } - - // Color brightness exists check - if (this->has_color_brightness() && this->color_brightness_ > 0.0f && !(color_mode & ColorCapability::RGB)) { - log_color_mode_not_supported(name, "RGB brightness"); - this->set_flag_(FLAG_HAS_COLOR_BRIGHTNESS, false); - } - - // RGB exists check - if ((this->has_red() && this->red_ > 0.0f) || (this->has_green() && this->green_ > 0.0f) || - (this->has_blue() && this->blue_ > 0.0f)) { - if (!(color_mode & ColorCapability::RGB)) { - log_color_mode_not_supported(name, "RGB color"); - this->set_flag_(FLAG_HAS_RED, false); - this->set_flag_(FLAG_HAS_GREEN, false); - this->set_flag_(FLAG_HAS_BLUE, false); - } - } - - // White value exists check - if (this->has_white() && this->white_ > 0.0f && - !(color_mode & ColorCapability::WHITE || color_mode & ColorCapability::COLD_WARM_WHITE)) { - log_color_mode_not_supported(name, "white value"); - this->set_flag_(FLAG_HAS_WHITE, false); - } - - // Color temperature exists check - if (this->has_color_temperature() && - !(color_mode & ColorCapability::COLOR_TEMPERATURE || color_mode & ColorCapability::COLD_WARM_WHITE)) { - log_color_mode_not_supported(name, "color temperature"); - this->set_flag_(FLAG_HAS_COLOR_TEMPERATURE, false); - } - - // Cold/warm white value exists check - if ((this->has_cold_white() && this->cold_white_ > 0.0f) || (this->has_warm_white() && this->warm_white_ > 0.0f)) { - if (!(color_mode & ColorCapability::COLD_WARM_WHITE)) { - log_color_mode_not_supported(name, "cold/warm white value"); - this->set_flag_(FLAG_HAS_COLD_WHITE, false); - this->set_flag_(FLAG_HAS_WARM_WHITE, false); - } - } - -#define VALIDATE_RANGE_(name_, upper_name, min, max) \ - if (this->has_##name_()) { \ - auto val = this->name_##_; \ - if (val < (min) || val > (max)) { \ - log_validation_warning(name, LOG_STR_LITERAL(upper_name), val, (min), (max)); \ - this->name_##_ = clamp(val, (min), (max)); \ - } \ - } -#define VALIDATE_RANGE(name, upper_name) VALIDATE_RANGE_(name, upper_name, 0.0f, 1.0f) - - // Range checks - VALIDATE_RANGE(brightness, "Brightness") - VALIDATE_RANGE(color_brightness, "Color brightness") - VALIDATE_RANGE(red, "Red") - VALIDATE_RANGE(green, "Green") - VALIDATE_RANGE(blue, "Blue") - VALIDATE_RANGE(white, "White") - VALIDATE_RANGE(cold_white, "Cold white") - VALIDATE_RANGE(warm_white, "Warm white") - VALIDATE_RANGE_(color_temperature, "Color temperature", traits.get_min_mireds(), traits.get_max_mireds()) - + // Business logic adjustments before validation // Flag whether an explicit turn off was requested, in which case we'll also stop the effect. bool explicit_turn_off_request = this->has_state() && !this->state_; // Turn off when brightness is set to zero, and reset brightness (so that it has nonzero brightness when turned on). if (this->has_brightness() && this->brightness_ == 0.0f) { this->state_ = false; - this->set_flag_(FLAG_HAS_STATE, true); + this->set_flag_(FLAG_HAS_STATE); this->brightness_ = 1.0f; } // Set color brightness to 100% if currently zero and a color is set. - if (this->has_red() || this->has_green() || this->has_blue()) { - if (!this->has_color_brightness() && this->parent_->remote_values.get_color_brightness() == 0.0f) { - this->color_brightness_ = 1.0f; - this->set_flag_(FLAG_HAS_COLOR_BRIGHTNESS, true); - } + if ((this->has_red() || this->has_green() || this->has_blue()) && !this->has_color_brightness() && + this->parent_->remote_values.get_color_brightness() == 0.0f) { + this->color_brightness_ = 1.0f; + this->set_flag_(FLAG_HAS_COLOR_BRIGHTNESS); } - // Create color values for the light with this call applied. + // Capability validation + if (this->has_brightness() && this->brightness_ > 0.0f && !(color_mode & ColorCapability::BRIGHTNESS)) + this->log_and_clear_unsupported_(FLAG_HAS_BRIGHTNESS, LOG_STR("brightness"), false); + + // Transition length possible check + if (this->has_transition_() && this->transition_length_ != 0 && !(color_mode & ColorCapability::BRIGHTNESS)) + this->log_and_clear_unsupported_(FLAG_HAS_TRANSITION, LOG_STR("transitions"), false); + + if (this->has_color_brightness() && this->color_brightness_ > 0.0f && !(color_mode & ColorCapability::RGB)) + this->log_and_clear_unsupported_(FLAG_HAS_COLOR_BRIGHTNESS, LOG_STR("RGB brightness"), true); + + // RGB exists check + if (((this->has_red() && this->red_ > 0.0f) || (this->has_green() && this->green_ > 0.0f) || + (this->has_blue() && this->blue_ > 0.0f)) && + !(color_mode & ColorCapability::RGB)) { + log_color_mode_not_supported(name, LOG_STR("RGB color")); + this->clear_flag_(FLAG_HAS_RED); + this->clear_flag_(FLAG_HAS_GREEN); + this->clear_flag_(FLAG_HAS_BLUE); + } + + // White value exists check + if (this->has_white() && this->white_ > 0.0f && + !(color_mode & ColorCapability::WHITE || color_mode & ColorCapability::COLD_WARM_WHITE)) + this->log_and_clear_unsupported_(FLAG_HAS_WHITE, LOG_STR("white value"), true); + + // Color temperature exists check + if (this->has_color_temperature() && + !(color_mode & ColorCapability::COLOR_TEMPERATURE || color_mode & ColorCapability::COLD_WARM_WHITE)) + this->log_and_clear_unsupported_(FLAG_HAS_COLOR_TEMPERATURE, LOG_STR("color temperature"), true); + + // Cold/warm white value exists check + if (((this->has_cold_white() && this->cold_white_ > 0.0f) || (this->has_warm_white() && this->warm_white_ > 0.0f)) && + !(color_mode & ColorCapability::COLD_WARM_WHITE)) { + log_color_mode_not_supported(name, LOG_STR("cold/warm white value")); + this->clear_flag_(FLAG_HAS_COLD_WHITE); + this->clear_flag_(FLAG_HAS_WARM_WHITE); + } + + // Create color values and validate+apply ranges in one step to eliminate duplicate checks auto v = this->parent_->remote_values; if (this->has_color_mode()) v.set_color_mode(this->color_mode_); if (this->has_state()) v.set_state(this->state_); - if (this->has_brightness()) - v.set_brightness(this->brightness_); - if (this->has_color_brightness()) - v.set_color_brightness(this->color_brightness_); - if (this->has_red()) - v.set_red(this->red_); - if (this->has_green()) - v.set_green(this->green_); - if (this->has_blue()) - v.set_blue(this->blue_); - if (this->has_white()) - v.set_white(this->white_); - if (this->has_color_temperature()) - v.set_color_temperature(this->color_temperature_); - if (this->has_cold_white()) - v.set_cold_white(this->cold_white_); - if (this->has_warm_white()) - v.set_warm_white(this->warm_white_); + +#define VALIDATE_AND_APPLY(field, setter, name_str, ...) \ + if (this->has_##field()) { \ + clamp_and_log_if_invalid(name, this->field##_, LOG_STR(name_str), ##__VA_ARGS__); \ + v.setter(this->field##_); \ + } + + VALIDATE_AND_APPLY(brightness, set_brightness, "Brightness") + VALIDATE_AND_APPLY(color_brightness, set_color_brightness, "Color brightness") + VALIDATE_AND_APPLY(red, set_red, "Red") + VALIDATE_AND_APPLY(green, set_green, "Green") + VALIDATE_AND_APPLY(blue, set_blue, "Blue") + VALIDATE_AND_APPLY(white, set_white, "White") + VALIDATE_AND_APPLY(cold_white, set_cold_white, "Cold white") + VALIDATE_AND_APPLY(warm_white, set_warm_white, "Warm white") + VALIDATE_AND_APPLY(color_temperature, set_color_temperature, "Color temperature", traits.get_min_mireds(), + traits.get_max_mireds()) + +#undef VALIDATE_AND_APPLY v.normalize_color(); // Flash length check if (this->has_flash_() && this->flash_length_ == 0) { - log_invalid_parameter(name, "flash length must be greater than zero"); - this->set_flag_(FLAG_HAS_FLASH, false); + log_invalid_parameter(name, LOG_STR("flash length must be >0")); + this->clear_flag_(FLAG_HAS_FLASH); } // validate transition length/flash length/effect not used at the same time @@ -328,42 +311,40 @@ LightColorValues LightCall::validate_() { // If effect is already active, remove effect start if (this->has_effect_() && this->effect_ == this->parent_->active_effect_index_) { - this->set_flag_(FLAG_HAS_EFFECT, false); + this->clear_flag_(FLAG_HAS_EFFECT); } // validate effect index if (this->has_effect_() && this->effect_ > this->parent_->effects_.size()) { ESP_LOGW(TAG, "'%s': invalid effect index %" PRIu32, name, this->effect_); - this->set_flag_(FLAG_HAS_EFFECT, false); + this->clear_flag_(FLAG_HAS_EFFECT); } if (this->has_effect_() && (this->has_transition_() || this->has_flash_())) { - log_invalid_parameter(name, "effect cannot be used with transition/flash"); - this->set_flag_(FLAG_HAS_TRANSITION, false); - this->set_flag_(FLAG_HAS_FLASH, false); + log_invalid_parameter(name, LOG_STR("effect cannot be used with transition/flash")); + this->clear_flag_(FLAG_HAS_TRANSITION); + this->clear_flag_(FLAG_HAS_FLASH); } if (this->has_flash_() && this->has_transition_()) { - log_invalid_parameter(name, "flash cannot be used with transition"); - this->set_flag_(FLAG_HAS_TRANSITION, false); + log_invalid_parameter(name, LOG_STR("flash cannot be used with transition")); + this->clear_flag_(FLAG_HAS_TRANSITION); } if (!this->has_transition_() && !this->has_flash_() && (!this->has_effect_() || this->effect_ == 0) && supports_transition) { // nothing specified and light supports transitions, set default transition length this->transition_length_ = this->parent_->default_transition_length_; - this->set_flag_(FLAG_HAS_TRANSITION, true); + this->set_flag_(FLAG_HAS_TRANSITION); } if (this->has_transition_() && this->transition_length_ == 0) { // 0 transition is interpreted as no transition (instant change) - this->set_flag_(FLAG_HAS_TRANSITION, false); + this->clear_flag_(FLAG_HAS_TRANSITION); } - if (this->has_transition_() && !supports_transition) { - log_feature_not_supported(name, "transitions"); - this->set_flag_(FLAG_HAS_TRANSITION, false); - } + if (this->has_transition_() && !supports_transition) + this->log_and_clear_unsupported_(FLAG_HAS_TRANSITION, LOG_STR("transitions"), false); // If not a flash and turning the light off, then disable the light // Do not use light color values directly, so that effects can set 0% brightness @@ -371,18 +352,18 @@ LightColorValues LightCall::validate_() { bool target_state = this->has_state() ? this->state_ : v.is_on(); if (!this->has_flash_() && !target_state) { if (this->has_effect_()) { - log_invalid_parameter(name, "cannot start effect when turning off"); - this->set_flag_(FLAG_HAS_EFFECT, false); + log_invalid_parameter(name, LOG_STR("cannot start effect when turning off")); + this->clear_flag_(FLAG_HAS_EFFECT); } else if (this->parent_->active_effect_index_ != 0 && explicit_turn_off_request) { // Auto turn off effect this->effect_ = 0; - this->set_flag_(FLAG_HAS_EFFECT, true); + this->set_flag_(FLAG_HAS_EFFECT); } } // Disable saving for flashes if (this->has_flash_()) - this->set_flag_(FLAG_SAVE, false); + this->clear_flag_(FLAG_SAVE); return v; } @@ -416,12 +397,12 @@ void LightCall::transform_parameters_() { const float gamma = this->parent_->get_gamma_correct(); this->cold_white_ = gamma_uncorrect(cw_fraction / max_cw_ww, gamma); this->warm_white_ = gamma_uncorrect(ww_fraction / max_cw_ww, gamma); - this->set_flag_(FLAG_HAS_COLD_WHITE, true); - this->set_flag_(FLAG_HAS_WARM_WHITE, true); + this->set_flag_(FLAG_HAS_COLD_WHITE); + this->set_flag_(FLAG_HAS_WARM_WHITE); } if (this->has_white()) { this->brightness_ = this->white_; - this->set_flag_(FLAG_HAS_BRIGHTNESS, true); + this->set_flag_(FLAG_HAS_BRIGHTNESS); } } } @@ -445,20 +426,19 @@ ColorMode LightCall::compute_color_mode_() { // If no color mode is specified, we try to guess the color mode. This is needed for backward compatibility to // pre-colormode clients and automations, but also for the MQTT API, where HA doesn't let us know which color mode // was used for some reason. - std::set suitable_modes = this->get_suitable_color_modes_(); + // Compute intersection of suitable and supported modes using bitwise AND + color_mode_bitmask_t intersection = this->get_suitable_color_modes_mask_() & supported_modes.get_mask(); - // Don't change if the current mode is suitable. - if (suitable_modes.count(current_mode) > 0) { + // Don't change if the current mode is in the intersection (suitable AND supported) + if (ColorModeMask::mask_contains(intersection, current_mode)) { ESP_LOGI(TAG, "'%s': color mode not specified; retaining %s", this->parent_->get_name().c_str(), LOG_STR_ARG(color_mode_to_human(current_mode))); return current_mode; } // Use the preferred suitable mode. - for (auto mode : suitable_modes) { - if (supported_modes.count(mode) == 0) - continue; - + if (intersection != 0) { + ColorMode mode = ColorModeMask::first_value_from_mask(intersection); ESP_LOGI(TAG, "'%s': color mode not specified; using %s", this->parent_->get_name().c_str(), LOG_STR_ARG(color_mode_to_human(mode))); return mode; @@ -471,7 +451,7 @@ ColorMode LightCall::compute_color_mode_() { LOG_STR_ARG(color_mode_to_human(color_mode))); return color_mode; } -std::set LightCall::get_suitable_color_modes_() { +color_mode_bitmask_t LightCall::get_suitable_color_modes_mask_() { bool has_white = this->has_white() && this->white_ > 0.0f; bool has_ct = this->has_color_temperature(); bool has_cwww = @@ -479,36 +459,44 @@ std::set LightCall::get_suitable_color_modes_() { bool has_rgb = (this->has_color_brightness() && this->color_brightness_ > 0.0f) || (this->has_red() || this->has_green() || this->has_blue()); -// Build key from flags: [rgb][cwww][ct][white] + // Build key from flags: [rgb][cwww][ct][white] #define KEY(white, ct, cwww, rgb) ((white) << 0 | (ct) << 1 | (cwww) << 2 | (rgb) << 3) uint8_t key = KEY(has_white, has_ct, has_cwww, has_rgb); switch (key) { case KEY(true, false, false, false): // white only - return {ColorMode::WHITE, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE, - ColorMode::RGB_COLD_WARM_WHITE}; + return ColorModeMask({ColorMode::WHITE, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, + ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE}) + .get_mask(); case KEY(false, true, false, false): // ct only - return {ColorMode::COLOR_TEMPERATURE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE, - ColorMode::RGB_COLD_WARM_WHITE}; + return ColorModeMask({ColorMode::COLOR_TEMPERATURE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE, + ColorMode::RGB_COLD_WARM_WHITE}) + .get_mask(); case KEY(true, true, false, false): // white + ct - return {ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}; + return ColorModeMask( + {ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}) + .get_mask(); case KEY(false, false, true, false): // cwww only - return {ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE}; + return ColorModeMask({ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE}).get_mask(); case KEY(false, false, false, false): // none - return {ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE, ColorMode::RGB, - ColorMode::WHITE, ColorMode::COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE}; + return ColorModeMask({ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE, + ColorMode::RGB, ColorMode::WHITE, ColorMode::COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE}) + .get_mask(); case KEY(true, false, false, true): // rgb + white - return {ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}; + return ColorModeMask({ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}) + .get_mask(); case KEY(false, true, false, true): // rgb + ct case KEY(true, true, false, true): // rgb + white + ct - return {ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}; + return ColorModeMask({ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}).get_mask(); case KEY(false, false, true, true): // rgb + cwww - return {ColorMode::RGB_COLD_WARM_WHITE}; + return ColorModeMask({ColorMode::RGB_COLD_WARM_WHITE}).get_mask(); case KEY(false, false, false, true): // rgb only - return {ColorMode::RGB, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}; + return ColorModeMask({ColorMode::RGB, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, + ColorMode::RGB_COLD_WARM_WHITE}) + .get_mask(); default: - return {}; // conflicting flags + return 0; // conflicting flags } #undef KEY @@ -524,7 +512,7 @@ LightCall &LightCall::set_effect(const std::string &effect) { for (uint32_t i = 0; i < this->parent_->effects_.size(); i++) { LightEffect *e = this->parent_->effects_[i]; - if (strcasecmp(effect.c_str(), e->get_name().c_str()) == 0) { + if (strcasecmp(effect.c_str(), e->get_name()) == 0) { this->set_effect(i + 1); found = true; break; @@ -628,7 +616,7 @@ LightCall &LightCall::set_effect(optional effect) { } LightCall &LightCall::set_effect(uint32_t effect_number) { this->effect_ = effect_number; - this->set_flag_(FLAG_HAS_EFFECT, true); + this->set_flag_(FLAG_HAS_EFFECT); return *this; } LightCall &LightCall::set_effect(optional effect_number) { @@ -658,5 +646,4 @@ LightCall &LightCall::set_rgbw(float red, float green, float blue, float white) return *this; } -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/light_call.h b/esphome/components/light/light_call.h index 7e04e1a767..6931b58b9d 100644 --- a/esphome/components/light/light_call.h +++ b/esphome/components/light/light_call.h @@ -1,9 +1,12 @@ #pragma once #include "light_color_values.h" -#include namespace esphome { + +// Forward declaration +struct LogString; + namespace light { class LightState; @@ -182,8 +185,8 @@ class LightCall { //// Compute the color mode that should be used for this call. ColorMode compute_color_mode_(); - /// Get potential color modes for this light call. - std::set get_suitable_color_modes_(); + /// Get potential color modes bitmask for this light call. + color_mode_bitmask_t get_suitable_color_modes_mask_(); /// Some color modes also can be set using non-native parameters, transform those calls. void transform_parameters_(); @@ -207,14 +210,14 @@ class LightCall { FLAG_SAVE = 1 << 15, }; - bool has_transition_() { return (this->flags_ & FLAG_HAS_TRANSITION) != 0; } - bool has_flash_() { return (this->flags_ & FLAG_HAS_FLASH) != 0; } - bool has_effect_() { return (this->flags_ & FLAG_HAS_EFFECT) != 0; } - bool get_publish_() { return (this->flags_ & FLAG_PUBLISH) != 0; } - bool get_save_() { return (this->flags_ & FLAG_SAVE) != 0; } + inline bool has_transition_() { return (this->flags_ & FLAG_HAS_TRANSITION) != 0; } + inline bool has_flash_() { return (this->flags_ & FLAG_HAS_FLASH) != 0; } + inline bool has_effect_() { return (this->flags_ & FLAG_HAS_EFFECT) != 0; } + inline bool get_publish_() { return (this->flags_ & FLAG_PUBLISH) != 0; } + inline bool get_save_() { return (this->flags_ & FLAG_SAVE) != 0; } - // Helper to set flag - void set_flag_(FieldFlags flag, bool value) { + // Helper to set flag - defaults to true for common case + void set_flag_(FieldFlags flag, bool value = true) { if (value) { this->flags_ |= flag; } else { @@ -222,6 +225,12 @@ class LightCall { } } + // Helper to clear flag - reduces code size for common case + void clear_flag_(FieldFlags flag) { this->flags_ &= ~flag; } + + // Helper to log unsupported feature and clear flag - reduces code duplication + void log_and_clear_unsupported_(FieldFlags flag, const LogString *feature, bool use_color_mode_log); + LightState *parent_; // Light state values - use flags_ to check if a value has been set. diff --git a/esphome/components/light/light_color_values.h b/esphome/components/light/light_color_values.h index 04d7d1e7d8..bedfad2c35 100644 --- a/esphome/components/light/light_color_values.h +++ b/esphome/components/light/light_color_values.h @@ -4,8 +4,7 @@ #include "color_mode.h" #include -namespace esphome { -namespace light { +namespace esphome::light { inline static uint8_t to_uint8_scale(float x) { return static_cast(roundf(x * 255.0f)); } @@ -310,5 +309,4 @@ class LightColorValues { ColorMode color_mode_; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/light_effect.cpp b/esphome/components/light/light_effect.cpp new file mode 100644 index 0000000000..81b923f7f9 --- /dev/null +++ b/esphome/components/light/light_effect.cpp @@ -0,0 +1,34 @@ +#include "light_effect.h" +#include "light_state.h" + +namespace esphome::light { + +uint32_t LightEffect::get_index() const { + if (this->state_ == nullptr) { + return 0; + } + return this->get_index_in_parent_(); +} + +bool LightEffect::is_active() const { + if (this->state_ == nullptr) { + return false; + } + return this->get_index() != 0 && this->state_->get_current_effect_index() == this->get_index(); +} + +uint32_t LightEffect::get_index_in_parent_() const { + if (this->state_ == nullptr) { + return 0; + } + + const auto &effects = this->state_->get_effects(); + for (size_t i = 0; i < effects.size(); i++) { + if (effects[i] == this) { + return i + 1; // Effects are 1-indexed in the API + } + } + return 0; // Not found +} + +} // namespace esphome::light diff --git a/esphome/components/light/light_effect.h b/esphome/components/light/light_effect.h index 8da51fe8b3..aa1f6f7899 100644 --- a/esphome/components/light/light_effect.h +++ b/esphome/components/light/light_effect.h @@ -1,17 +1,14 @@ #pragma once -#include - #include "esphome/core/component.h" -namespace esphome { -namespace light { +namespace esphome::light { class LightState; class LightEffect { public: - explicit LightEffect(std::string name) : name_(std::move(name)) {} + explicit LightEffect(const char *name) : name_(name) {} /// Initialize this LightEffect. Will be called once after creation. virtual void start() {} @@ -24,7 +21,11 @@ class LightEffect { /// Apply this effect. Use the provided state for starting transitions, ... virtual void apply() = 0; - const std::string &get_name() { return this->name_; } + /** + * Returns the name of this effect. + * The returned pointer is valid for the lifetime of the program and must not be freed. + */ + const char *get_name() const { return this->name_; } /// Internal method called by the LightState when this light effect is registered in it. virtual void init() {} @@ -34,10 +35,23 @@ class LightEffect { this->init(); } + /// Get the index of this effect in the parent light's effect list. + /// Returns 0 if not found or not initialized. + uint32_t get_index() const; + + /// Check if this effect is currently active. + bool is_active() const; + + /// Get a reference to the parent light state. + /// Returns nullptr if not initialized. + LightState *get_light_state() const { return this->state_; } + protected: LightState *state_{nullptr}; - std::string name_; + const char *name_; + + /// Internal method to find this effect's index in the parent light's effect list. + uint32_t get_index_in_parent_() const; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/light_json_schema.cpp b/esphome/components/light/light_json_schema.cpp index 896b821705..1c9b92f504 100644 --- a/esphome/components/light/light_json_schema.cpp +++ b/esphome/components/light/light_json_schema.cpp @@ -3,8 +3,7 @@ #ifdef USE_JSON -namespace esphome { -namespace light { +namespace esphome::light { // See https://www.home-assistant.io/integrations/light.mqtt/#json-schema for documentation on the schema @@ -36,11 +35,13 @@ static constexpr const char *get_color_mode_json_str(ColorMode mode) { void LightJSONSchema::dump_json(LightState &state, JsonObject root) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - if (state.supports_effects()) + if (state.supports_effects()) { root["effect"] = state.get_effect_name(); + root["effect_index"] = state.get_current_effect_index(); + root["effect_count"] = state.get_effect_count(); + } auto values = state.remote_values; - auto traits = state.get_output()->get_traits(); const auto color_mode = values.get_color_mode(); const char *mode_str = get_color_mode_json_str(color_mode); @@ -160,9 +161,13 @@ void LightJSONSchema::parse_json(LightState &state, LightCall &call, JsonObject const char *effect = root["effect"]; call.set_effect(effect); } + + if (root["effect_index"].is()) { + uint32_t effect_index = root["effect_index"]; + call.set_effect(effect_index); + } } -} // namespace light -} // namespace esphome +} // namespace esphome::light #endif diff --git a/esphome/components/light/light_json_schema.h b/esphome/components/light/light_json_schema.h index c92dd7b655..dac81e32e3 100644 --- a/esphome/components/light/light_json_schema.h +++ b/esphome/components/light/light_json_schema.h @@ -8,8 +8,7 @@ #include "light_call.h" #include "light_state.h" -namespace esphome { -namespace light { +namespace esphome::light { class LightJSONSchema { public: @@ -22,7 +21,6 @@ class LightJSONSchema { static void parse_color_json(LightState &state, LightCall &call, JsonObject root); }; -} // namespace light -} // namespace esphome +} // namespace esphome::light #endif diff --git a/esphome/components/light/light_output.cpp b/esphome/components/light/light_output.cpp index e805a0b694..a86e8e5bf1 100644 --- a/esphome/components/light/light_output.cpp +++ b/esphome/components/light/light_output.cpp @@ -1,12 +1,10 @@ #include "light_output.h" #include "transformers.h" -namespace esphome { -namespace light { +namespace esphome::light { std::unique_ptr LightOutput::create_default_transition() { return make_unique(); } -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/light_output.h b/esphome/components/light/light_output.h index 73ba0371cd..c82d270be8 100644 --- a/esphome/components/light/light_output.h +++ b/esphome/components/light/light_output.h @@ -5,8 +5,7 @@ #include "light_state.h" #include "light_transformer.h" -namespace esphome { -namespace light { +namespace esphome::light { /// Interface to write LightStates to hardware. class LightOutput { @@ -29,5 +28,4 @@ class LightOutput { virtual void write_state(LightState *state) = 0; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index 5b57707d6b..9cde9077da 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -1,11 +1,11 @@ -#include "esphome/core/log.h" - -#include "light_output.h" #include "light_state.h" +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" +#include "esphome/core/log.h" +#include "light_output.h" #include "transformers.h" -namespace esphome { -namespace light { +namespace esphome::light { static const char *const TAG = "light"; @@ -23,6 +23,9 @@ void LightState::setup() { effect->init_internal(this); } + // Start with loop disabled if idle - respects any effects/transitions set up during initialization + this->disable_loop_if_idle_(); + // When supported color temperature range is known, initialize color temperature setting within bounds. auto traits = this->get_traits(); float min_mireds = traits.get_min_mireds(); @@ -41,7 +44,7 @@ void LightState::setup() { case LIGHT_RESTORE_DEFAULT_ON: case LIGHT_RESTORE_INVERTED_DEFAULT_OFF: case LIGHT_RESTORE_INVERTED_DEFAULT_ON: - this->rtc_ = global_preferences->make_preference(this->get_object_id_hash()); + this->rtc_ = global_preferences->make_preference(this->get_preference_hash()); // Attempt to load from preferences, else fall back to default values if (!this->rtc_.load(&recovered)) { recovered.state = (this->restore_mode_ == LIGHT_RESTORE_DEFAULT_ON || @@ -54,7 +57,7 @@ void LightState::setup() { break; case LIGHT_RESTORE_AND_OFF: case LIGHT_RESTORE_AND_ON: - this->rtc_ = global_preferences->make_preference(this->get_object_id_hash()); + this->rtc_ = global_preferences->make_preference(this->get_preference_hash()); this->rtc_.load(&recovered); recovered.state = (this->restore_mode_ == LIGHT_RESTORE_AND_ON); break; @@ -125,6 +128,9 @@ void LightState::loop() { this->is_transformer_active_ = false; this->transformer_ = nullptr; this->target_state_reached_callback_.call(); + + // Disable loop if idle (no transformer and no effect) + this->disable_loop_if_idle_(); } } @@ -132,20 +138,37 @@ void LightState::loop() { if (this->next_write_) { this->next_write_ = false; this->output_->write_state(this); + // Disable loop if idle (no transformer and no effect) + this->disable_loop_if_idle_(); } } float LightState::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; } -void LightState::publish_state() { this->remote_values_callback_.call(); } +void LightState::publish_state() { + this->remote_values_callback_.call(); +#if defined(USE_LIGHT) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_light_update(this); +#endif +} LightOutput *LightState::get_output() const { return this->output_; } + +static constexpr const char *EFFECT_NONE = "None"; +static constexpr auto EFFECT_NONE_REF = StringRef::from_lit("None"); + std::string LightState::get_effect_name() { if (this->active_effect_index_ > 0) { return this->effects_[this->active_effect_index_ - 1]->get_name(); - } else { - return "None"; } + return EFFECT_NONE; +} + +StringRef LightState::get_effect_name_ref() { + if (this->active_effect_index_ > 0) { + return StringRef(this->effects_[this->active_effect_index_ - 1]->get_name()); + } + return EFFECT_NONE_REF; } void LightState::add_new_remote_values_callback(std::function &&send_callback) { @@ -167,12 +190,10 @@ void LightState::set_gamma_correct(float gamma_correct) { this->gamma_correct_ = void LightState::set_restore_mode(LightRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } void LightState::set_initial_state(const LightStateRTCState &initial_state) { this->initial_state_ = initial_state; } bool LightState::supports_effects() { return !this->effects_.empty(); } -const std::vector &LightState::get_effects() const { return this->effects_; } -void LightState::add_effects(const std::vector &effects) { - this->effects_.reserve(this->effects_.size() + effects.size()); - for (auto *effect : effects) { - this->effects_.push_back(effect); - } +const FixedVector &LightState::get_effects() const { return this->effects_; } +void LightState::add_effects(const std::initializer_list &effects) { + // Called once from Python codegen during setup with all effects from YAML config + this->effects_ = effects; } void LightState::current_values_as_binary(bool *binary) { this->current_values.as_binary(binary); } @@ -180,11 +201,9 @@ void LightState::current_values_as_brightness(float *brightness) { this->current_values.as_brightness(brightness, this->gamma_correct_); } void LightState::current_values_as_rgb(float *red, float *green, float *blue, bool color_interlock) { - auto traits = this->get_traits(); this->current_values.as_rgb(red, green, blue, this->gamma_correct_, false); } void LightState::current_values_as_rgbw(float *red, float *green, float *blue, float *white, bool color_interlock) { - auto traits = this->get_traits(); this->current_values.as_rgbw(red, green, blue, white, this->gamma_correct_, false); } void LightState::current_values_as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white, @@ -198,7 +217,6 @@ void LightState::current_values_as_rgbct(float *red, float *green, float *blue, white_brightness, this->gamma_correct_); } void LightState::current_values_as_cwww(float *cold_white, float *warm_white, bool constant_brightness) { - auto traits = this->get_traits(); this->current_values.as_cwww(cold_white, warm_white, this->gamma_correct_, constant_brightness); } void LightState::current_values_as_ct(float *color_temperature, float *white_brightness) { @@ -217,6 +235,8 @@ void LightState::start_effect_(uint32_t effect_index) { this->active_effect_index_ = effect_index; auto *effect = this->get_active_effect_(); effect->start_internal(); + // Enable loop while effect is active + this->enable_loop(); } LightEffect *LightState::get_active_effect_() { if (this->active_effect_index_ == 0) { @@ -231,6 +251,8 @@ void LightState::stop_effect_() { effect->stop(); } this->active_effect_index_ = 0; + // Disable loop if idle (no effect and no transformer) + this->disable_loop_if_idle_(); } void LightState::start_transition_(const LightColorValues &target, uint32_t length, bool set_remote_values) { @@ -240,6 +262,8 @@ void LightState::start_transition_(const LightColorValues &target, uint32_t leng if (set_remote_values) { this->remote_values = target; } + // Enable loop while transition is active + this->enable_loop(); } void LightState::start_flash_(const LightColorValues &target, uint32_t length, bool set_remote_values) { @@ -255,6 +279,8 @@ void LightState::start_flash_(const LightColorValues &target, uint32_t length, b if (set_remote_values) { this->remote_values = target; }; + // Enable loop while flash is active + this->enable_loop(); } void LightState::set_immediately_(const LightColorValues &target, bool set_remote_values) { @@ -266,6 +292,14 @@ void LightState::set_immediately_(const LightColorValues &target, bool set_remot } this->output_->update_state(this); this->next_write_ = true; + this->enable_loop(); +} + +void LightState::disable_loop_if_idle_() { + // Only disable loop if both transformer and effect are inactive, and no pending writes + if (this->transformer_ == nullptr && this->get_active_effect_() == nullptr && !this->next_write_) { + this->disable_loop(); + } } void LightState::save_remote_values_() { @@ -293,5 +327,4 @@ void LightState::save_remote_values_() { this->rtc_.save(&saved); } -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index 72cb99223e..ad8922b46f 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -4,16 +4,18 @@ #include "esphome/core/entity_base.h" #include "esphome/core/optional.h" #include "esphome/core/preferences.h" +#include "esphome/core/string_ref.h" #include "light_call.h" #include "light_color_values.h" #include "light_effect.h" #include "light_traits.h" #include "light_transformer.h" +#include "esphome/core/helpers.h" +#include #include -namespace esphome { -namespace light { +namespace esphome::light { class LightOutput; @@ -116,6 +118,8 @@ class LightState : public EntityBase, public Component { /// Return the name of the current effect, or if no effect is active "None". std::string get_effect_name(); + /// Return the name of the current effect as StringRef (for API usage) + StringRef get_effect_name_ref(); /** * This lets front-end components subscribe to light change events. This callback is called once @@ -155,10 +159,48 @@ class LightState : public EntityBase, public Component { bool supports_effects(); /// Get all effects for this light state. - const std::vector &get_effects() const; + const FixedVector &get_effects() const; /// Add effects for this light state. - void add_effects(const std::vector &effects); + void add_effects(const std::initializer_list &effects); + + /// Get the total number of effects available for this light. + size_t get_effect_count() const { return this->effects_.size(); } + + /// Get the currently active effect index (0 = no effect, 1+ = effect index). + uint32_t get_current_effect_index() const { return this->active_effect_index_; } + + /// Get effect index by name. Returns 0 if effect not found. + uint32_t get_effect_index(const std::string &effect_name) const { + if (strcasecmp(effect_name.c_str(), "none") == 0) { + return 0; + } + for (size_t i = 0; i < this->effects_.size(); i++) { + if (strcasecmp(effect_name.c_str(), this->effects_[i]->get_name()) == 0) { + return i + 1; // Effects are 1-indexed in active_effect_index_ + } + } + return 0; // Effect not found + } + + /// Get effect by index. Returns nullptr if index is invalid. + LightEffect *get_effect_by_index(uint32_t index) const { + if (index == 0 || index > this->effects_.size()) { + return nullptr; + } + return this->effects_[index - 1]; // Effects are 1-indexed in active_effect_index_ + } + + /// Get effect name by index. Returns "None" for index 0, empty string for invalid index. + std::string get_effect_name_by_index(uint32_t index) const { + if (index == 0) { + return "None"; + } + if (index > this->effects_.size()) { + return ""; // Invalid index + } + return this->effects_[index - 1]->get_name(); + } /// The result of all the current_values_as_* methods have gamma correction applied. void current_values_as_binary(bool *binary); @@ -213,12 +255,15 @@ class LightState : public EntityBase, public Component { /// Internal method to save the current remote_values to the preferences void save_remote_values_(); + /// Disable loop if neither transformer nor effect is active + void disable_loop_if_idle_(); + /// Store the output to allow effects to have more access. LightOutput *output_; /// The currently active transformer for this light (transition/flash). std::unique_ptr transformer_{nullptr}; /// List of effects for this light. - std::vector effects_; + FixedVector effects_; /// Object used to store the persisted values of the light. ESPPreferenceObject rtc_; /// Value for storing the index of the currently active effect. 0 if no effect is active @@ -255,5 +300,4 @@ class LightState : public EntityBase, public Component { LightRestoreMode restore_mode_; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/light_traits.h b/esphome/components/light/light_traits.h index a45301d148..c3bb27a964 100644 --- a/esphome/components/light/light_traits.h +++ b/esphome/components/light/light_traits.h @@ -1,8 +1,7 @@ #pragma once -#include "esphome/core/helpers.h" #include "color_mode.h" -#include +#include "esphome/core/helpers.h" namespace esphome { @@ -19,38 +18,18 @@ class LightTraits { public: LightTraits() = default; - const std::set &get_supported_color_modes() const { return this->supported_color_modes_; } - void set_supported_color_modes(std::set supported_color_modes) { - this->supported_color_modes_ = std::move(supported_color_modes); + // Return by value to avoid dangling reference when get_traits() returns a temporary + ColorModeMask get_supported_color_modes() const { return this->supported_color_modes_; } + void set_supported_color_modes(ColorModeMask supported_color_modes) { + this->supported_color_modes_ = supported_color_modes; + } + void set_supported_color_modes(std::initializer_list modes) { + this->supported_color_modes_ = ColorModeMask(modes); } - bool supports_color_mode(ColorMode color_mode) const { return this->supported_color_modes_.count(color_mode); } + bool supports_color_mode(ColorMode color_mode) const { return this->supported_color_modes_.count(color_mode) > 0; } bool supports_color_capability(ColorCapability color_capability) const { - for (auto mode : this->supported_color_modes_) { - if (mode & color_capability) - return true; - } - return false; - } - - ESPDEPRECATED("get_supports_brightness() is deprecated, use color modes instead.", "v1.21") - bool get_supports_brightness() const { return this->supports_color_capability(ColorCapability::BRIGHTNESS); } - ESPDEPRECATED("get_supports_rgb() is deprecated, use color modes instead.", "v1.21") - bool get_supports_rgb() const { return this->supports_color_capability(ColorCapability::RGB); } - ESPDEPRECATED("get_supports_rgb_white_value() is deprecated, use color modes instead.", "v1.21") - bool get_supports_rgb_white_value() const { - return this->supports_color_mode(ColorMode::RGB_WHITE) || - this->supports_color_mode(ColorMode::RGB_COLOR_TEMPERATURE); - } - ESPDEPRECATED("get_supports_color_temperature() is deprecated, use color modes instead.", "v1.21") - bool get_supports_color_temperature() const { - return this->supports_color_capability(ColorCapability::COLOR_TEMPERATURE); - } - ESPDEPRECATED("get_supports_color_interlock() is deprecated, use color modes instead.", "v1.21") - bool get_supports_color_interlock() const { - return this->supports_color_mode(ColorMode::RGB) && - (this->supports_color_mode(ColorMode::WHITE) || this->supports_color_mode(ColorMode::COLD_WARM_WHITE) || - this->supports_color_mode(ColorMode::COLOR_TEMPERATURE)); + return has_capability(this->supported_color_modes_, color_capability); } float get_min_mireds() const { return this->min_mireds_; } @@ -59,19 +38,9 @@ class LightTraits { void set_max_mireds(float max_mireds) { this->max_mireds_ = max_mireds; } protected: -#ifdef USE_API - // The API connection is a friend class to access internal methods - friend class api::APIConnection; - // This method returns a reference to the internal color modes set. - // It is used by the API to avoid copying data when encoding messages. - // Warning: Do not use this method outside of the API connection code. - // It returns a reference to internal data that can be invalidated. - const std::set &get_supported_color_modes_for_api_() const { return this->supported_color_modes_; } -#endif - - std::set supported_color_modes_{}; float min_mireds_{0}; float max_mireds_{0}; + ColorModeMask supported_color_modes_{}; }; } // namespace light diff --git a/esphome/components/light/light_transformer.h b/esphome/components/light/light_transformer.h index fb9b709187..079c2d2ae0 100644 --- a/esphome/components/light/light_transformer.h +++ b/esphome/components/light/light_transformer.h @@ -4,8 +4,7 @@ #include "esphome/core/helpers.h" #include "light_color_values.h" -namespace esphome { -namespace light { +namespace esphome::light { /// Base class for all light color transformers, such as transitions or flashes. class LightTransformer { @@ -38,6 +37,10 @@ class LightTransformer { const LightColorValues &get_target_values() const { return this->target_values_; } protected: + // This looks crazy, but it reduces to 6x^5 - 15x^4 + 10x^3 which is just a smooth sigmoid-like + // transition from 0 to 1 on x = [0, 1] + static float smoothed_progress(float x) { return x * x * x * (x * (x * 6.0f - 15.0f) + 10.0f); } + /// The progress of this transition, on a scale of 0 to 1. float get_progress_() { uint32_t now = esphome::millis(); @@ -55,5 +58,4 @@ class LightTransformer { LightColorValues target_values_; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/transformers.h b/esphome/components/light/transformers.h index 8d49acff97..a26713b723 100644 --- a/esphome/components/light/transformers.h +++ b/esphome/components/light/transformers.h @@ -6,8 +6,7 @@ #include "light_state.h" #include "light_transformer.h" -namespace esphome { -namespace light { +namespace esphome::light { class LightTransitionTransformer : public LightTransformer { public: @@ -50,15 +49,11 @@ class LightTransitionTransformer : public LightTransformer { if (this->changing_color_mode_) p = p < 0.5f ? p * 2 : (p - 0.5) * 2; - float v = LightTransitionTransformer::smoothed_progress(p); + float v = LightTransformer::smoothed_progress(p); return LightColorValues::lerp(start, end, v); } protected: - // This looks crazy, but it reduces to 6x^5 - 15x^4 + 10x^3 which is just a smooth sigmoid-like - // transition from 0 to 1 on x = [0, 1] - static float smoothed_progress(float x) { return x * x * x * (x * (x * 6.0f - 15.0f) + 10.0f); } - LightColorValues end_values_{}; LightColorValues intermediate_values_{}; bool changing_color_mode_{false}; @@ -122,5 +117,4 @@ class LightFlashTransformer : public LightTransformer { bool begun_lightstate_restore_; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/lightwaverf/lightwaverf.h b/esphome/components/lightwaverf/lightwaverf.h index b9f2abfcb3..ee4e91e9d1 100644 --- a/esphome/components/lightwaverf/lightwaverf.h +++ b/esphome/components/lightwaverf/lightwaverf.h @@ -51,7 +51,7 @@ template class SendRawAction : public Action { void set_pulse_length(const int &data) { pulse_length_ = data; } void set_data(const std::vector &data) { code_ = data; } - void play(Ts... x) { + void play(const Ts &...x) { int repeats = this->repeat_.value(x...); int inverted = this->inverted_.value(x...); int pulse_length = this->pulse_length_.value(x...); diff --git a/esphome/components/lm75b/__init__.py b/esphome/components/lm75b/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/lm75b/lm75b.cpp b/esphome/components/lm75b/lm75b.cpp new file mode 100644 index 0000000000..19398eda85 --- /dev/null +++ b/esphome/components/lm75b/lm75b.cpp @@ -0,0 +1,39 @@ +#include "lm75b.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace lm75b { + +static const char *const TAG = "lm75b"; + +void LM75BComponent::dump_config() { + ESP_LOGCONFIG(TAG, "LM75B:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Setting up LM75B failed!"); + } + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Temperature", this); +} + +void LM75BComponent::update() { + // Create a temporary buffer + uint8_t buff[2]; + if (this->read_register(LM75B_REG_TEMPERATURE, buff, 2) != i2c::ERROR_OK) { + this->status_set_warning(); + return; + } + // Obtain combined 16-bit value + int16_t raw_temperature = (buff[0] << 8) | buff[1]; + // Read the 11-bit raw temperature value + raw_temperature >>= 5; + // Publish the temperature in °C + this->publish_state(raw_temperature * 0.125); + if (this->status_has_warning()) { + this->status_clear_warning(); + } +} + +} // namespace lm75b +} // namespace esphome diff --git a/esphome/components/lm75b/lm75b.h b/esphome/components/lm75b/lm75b.h new file mode 100644 index 0000000000..79d9fa3f32 --- /dev/null +++ b/esphome/components/lm75b/lm75b.h @@ -0,0 +1,19 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace lm75b { + +static const uint8_t LM75B_REG_TEMPERATURE = 0x00; + +class LM75BComponent : public PollingComponent, public i2c::I2CDevice, public sensor::Sensor { + public: + void dump_config() override; + void update() override; +}; + +} // namespace lm75b +} // namespace esphome diff --git a/esphome/components/lm75b/sensor.py b/esphome/components/lm75b/sensor.py new file mode 100644 index 0000000000..335446b62f --- /dev/null +++ b/esphome/components/lm75b/sensor.py @@ -0,0 +1,34 @@ +import esphome.codegen as cg +from esphome.components import i2c, sensor +import esphome.config_validation as cv +from esphome.const import ( + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, +) + +CODEOWNERS = ["@beormund"] +DEPENDENCIES = ["i2c"] + +lm75b_ns = cg.esphome_ns.namespace("lm75b") +LM75BComponent = lm75b_ns.class_( + "LM75BComponent", cg.PollingComponent, i2c.I2CDevice, sensor.Sensor +) + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + LM75BComponent, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x48)) +) + + +async def to_code(config): + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/lock/__init__.py b/esphome/components/lock/__init__.py index 7977efd264..9d893d3ad9 100644 --- a/esphome/components/lock/__init__.py +++ b/esphome/components/lock/__init__.py @@ -13,7 +13,7 @@ from esphome.const import ( CONF_TRIGGER_ID, CONF_WEB_SERVER, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass @@ -91,11 +91,6 @@ def lock_schema( return _LOCK_SCHEMA.extend(schema) -# Remove before 2025.11.0 -LOCK_SCHEMA = lock_schema() -LOCK_SCHEMA.add_extra(cv.deprecated_schema_constant("lock")) - - async def _setup_lock_core(var, config): await setup_entity(var, config, "lock") @@ -155,6 +150,6 @@ async def lock_is_off_to_code(config, condition_id, template_arg, args): return cg.new_Pvariable(condition_id, template_arg, paren, False) -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(lock_ns.using) diff --git a/esphome/components/lock/automation.h b/esphome/components/lock/automation.h index 8cb3b64ffe..cba2c3fdda 100644 --- a/esphome/components/lock/automation.h +++ b/esphome/components/lock/automation.h @@ -4,14 +4,13 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" -namespace esphome { -namespace lock { +namespace esphome::lock { template class LockAction : public Action { public: explicit LockAction(Lock *a_lock) : lock_(a_lock) {} - void play(Ts... x) override { this->lock_->lock(); } + void play(const Ts &...x) override { this->lock_->lock(); } protected: Lock *lock_; @@ -21,7 +20,7 @@ template class UnlockAction : public Action { public: explicit UnlockAction(Lock *a_lock) : lock_(a_lock) {} - void play(Ts... x) override { this->lock_->unlock(); } + void play(const Ts &...x) override { this->lock_->unlock(); } protected: Lock *lock_; @@ -31,7 +30,7 @@ template class OpenAction : public Action { public: explicit OpenAction(Lock *a_lock) : lock_(a_lock) {} - void play(Ts... x) override { this->lock_->open(); } + void play(const Ts &...x) override { this->lock_->open(); } protected: Lock *lock_; @@ -40,7 +39,7 @@ template class OpenAction : public Action { template class LockCondition : public Condition { public: LockCondition(Lock *parent, bool state) : parent_(parent), state_(state) {} - bool check(Ts... x) override { + bool check(const Ts &...x) override { auto check_state = this->state_ ? LockState::LOCK_STATE_LOCKED : LockState::LOCK_STATE_UNLOCKED; return this->parent_->state == check_state; } @@ -72,5 +71,4 @@ class LockUnlockTrigger : public Trigger<> { } }; -} // namespace lock -} // namespace esphome +} // namespace esphome::lock diff --git a/esphome/components/lock/lock.cpp b/esphome/components/lock/lock.cpp index ddc5445349..b8f0fbe011 100644 --- a/esphome/components/lock/lock.cpp +++ b/esphome/components/lock/lock.cpp @@ -1,8 +1,9 @@ #include "lock.h" +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" -namespace esphome { -namespace lock { +namespace esphome::lock { static const char *const TAG = "lock"; @@ -53,6 +54,9 @@ void Lock::publish_state(LockState state) { this->rtc_.save(&this->state); ESP_LOGD(TAG, "'%s': Sending state %s", this->name_.c_str(), lock_state_to_string(state)); this->state_callback_.call(); +#if defined(USE_LOCK) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_lock_update(this); +#endif } void Lock::add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } @@ -103,5 +107,4 @@ LockCall &LockCall::set_state(const std::string &state) { } const optional &LockCall::get_state() const { return this->state_; } -} // namespace lock -} // namespace esphome +} // namespace esphome::lock diff --git a/esphome/components/lock/lock.h b/esphome/components/lock/lock.h index 2173c84903..8a906ef9fc 100644 --- a/esphome/components/lock/lock.h +++ b/esphome/components/lock/lock.h @@ -5,18 +5,17 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "esphome/core/preferences.h" -#include +#include -namespace esphome { -namespace lock { +namespace esphome::lock { class Lock; #define LOG_LOCK(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - if (!(obj)->get_icon().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \ + if (!(obj)->get_icon_ref().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon_ref().c_str()); \ } \ if ((obj)->traits.get_assumed_state()) { \ ESP_LOGCONFIG(TAG, "%s Assumed State: YES", prefix); \ @@ -44,16 +43,22 @@ class LockTraits { bool get_assumed_state() const { return this->assumed_state_; } void set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } - bool supports_state(LockState state) const { return supported_states_.count(state); } - std::set get_supported_states() const { return supported_states_; } - void set_supported_states(std::set states) { supported_states_ = std::move(states); } - void add_supported_state(LockState state) { supported_states_.insert(state); } + bool supports_state(LockState state) const { return supported_states_mask_ & (1 << state); } + void set_supported_states(std::initializer_list states) { + supported_states_mask_ = 0; + for (auto state : states) { + supported_states_mask_ |= (1 << state); + } + } + uint8_t get_supported_states_mask() const { return supported_states_mask_; } + void set_supported_states_mask(uint8_t mask) { supported_states_mask_ = mask; } + void add_supported_state(LockState state) { supported_states_mask_ |= (1 << state); } protected: bool supports_open_{false}; bool requires_code_{false}; bool assumed_state_{false}; - std::set supported_states_ = {LOCK_STATE_NONE, LOCK_STATE_LOCKED, LOCK_STATE_UNLOCKED}; + uint8_t supported_states_mask_{(1 << LOCK_STATE_NONE) | (1 << LOCK_STATE_LOCKED) | (1 << LOCK_STATE_UNLOCKED)}; }; /** This class is used to encode all control actions on a lock device. @@ -171,5 +176,4 @@ class Lock : public EntityBase { ESPPreferenceObject rtc_; }; -} // namespace lock -} // namespace esphome +} // namespace esphome::lock diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index d8c95d75f2..39877030e9 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -1,7 +1,7 @@ import re from esphome import automation -from esphome.automation import LambdaAction +from esphome.automation import LambdaAction, StatelessLambdaAction import esphome.codegen as cg from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant from esphome.components.esp32.const import ( @@ -51,7 +51,7 @@ from esphome.const import ( PLATFORM_RTL87XX, PlatformFramework, ) -from esphome.core import CORE, Lambda, coroutine_with_priority +from esphome.core import CORE, CoroPriority, Lambda, coroutine_with_priority CODEOWNERS = ["@esphome/core"] logger_ns = cg.esphome_ns.namespace("logger") @@ -95,6 +95,7 @@ DEFAULT = "DEFAULT" CONF_INITIAL_LEVEL = "initial_level" CONF_LOGGER_ID = "logger_id" +CONF_RUNTIME_TAG_LEVELS = "runtime_tag_levels" CONF_TASK_LOG_BUFFER_SIZE = "task_log_buffer_size" UART_SELECTION_ESP32 = { @@ -117,8 +118,6 @@ UART_SELECTION_LIBRETINY = { COMPONENT_RTL87XX: [DEFAULT, UART0, UART1, UART2], } -ESP_ARDUINO_UNSUPPORTED_USB_UARTS = [USB_SERIAL_JTAG] - UART_SELECTION_RP2040 = [USB_CDC, UART0, UART1] UART_SELECTION_NRF52 = [USB_CDC, UART0] @@ -153,13 +152,7 @@ is_log_level = cv.one_of(*LOG_LEVELS, upper=True) def uart_selection(value): if CORE.is_esp32: - if CORE.using_arduino and value.upper() in ESP_ARDUINO_UNSUPPORTED_USB_UARTS: - raise cv.Invalid(f"Arduino framework does not support {value}.") variant = get_esp32_variant() - if CORE.using_esp_idf and variant == VARIANT_ESP32C3 and value == USB_CDC: - raise cv.Invalid( - f"{value} is not supported for variant {variant} when using ESP-IDF." - ) if variant in UART_SELECTION_ESP32: return cv.one_of(*UART_SELECTION_ESP32[variant], upper=True)(value) if CORE.is_esp8266: @@ -180,14 +173,34 @@ def uart_selection(value): raise NotImplementedError -def validate_local_no_higher_than_global(value): - global_level = LOG_LEVEL_SEVERITY.index(value[CONF_LEVEL]) - for tag, level in value.get(CONF_LOGS, {}).items(): - if LOG_LEVEL_SEVERITY.index(level) > global_level: - raise cv.Invalid( - f"The configured log level for {tag} ({level}) must be no more severe than the global log level {value[CONF_LEVEL]}." +def validate_local_no_higher_than_global(config): + global_level = config[CONF_LEVEL] + global_level_index = LOG_LEVEL_SEVERITY.index(global_level) + errs = [] + for tag, level in config.get(CONF_LOGS, {}).items(): + if LOG_LEVEL_SEVERITY.index(level) > global_level_index: + errs.append( + cv.Invalid( + f"The configured log level for {tag} ({level}) must not be less severe than the global log level ({global_level})", + [CONF_LOGS, tag], + ) ) - return value + if errs: + raise cv.MultipleInvalid(errs) + return config + + +def validate_initial_no_higher_than_global(config): + if initial_level := config.get(CONF_INITIAL_LEVEL): + global_level = config[CONF_LEVEL] + if LOG_LEVEL_SEVERITY.index(initial_level) > LOG_LEVEL_SEVERITY.index( + global_level + ): + raise cv.Invalid( + f"The initial log level ({initial_level}) must not be less severe than the global log level ({global_level})", + [CONF_INITIAL_LEVEL], + ) + return config Logger = logger_ns.class_("Logger", cg.Component) @@ -226,14 +239,11 @@ CONFIG_SCHEMA = cv.All( esp8266=UART0, esp32=UART0, esp32_s2=USB_CDC, - esp32_s3_arduino=USB_CDC, - esp32_s3_idf=USB_SERIAL_JTAG, - esp32_c3_arduino=USB_CDC, - esp32_c3_idf=USB_SERIAL_JTAG, - esp32_c5_idf=USB_SERIAL_JTAG, - esp32_c6_arduino=USB_CDC, - esp32_c6_idf=USB_SERIAL_JTAG, - esp32_p4_idf=USB_SERIAL_JTAG, + esp32_s3=USB_SERIAL_JTAG, + esp32_c3=USB_SERIAL_JTAG, + esp32_c5=USB_SERIAL_JTAG, + esp32_c6=USB_SERIAL_JTAG, + esp32_p4=USB_SERIAL_JTAG, rp2040=USB_CDC, bk72xx=DEFAULT, ln882x=DEFAULT, @@ -260,6 +270,7 @@ CONFIG_SCHEMA = cv.All( } ), cv.Optional(CONF_INITIAL_LEVEL): is_log_level, + cv.Optional(CONF_RUNTIME_TAG_LEVELS, default=False): cv.boolean, cv.Optional(CONF_ON_MESSAGE): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LoggerMessageTrigger), @@ -272,10 +283,11 @@ CONFIG_SCHEMA = cv.All( } ).extend(cv.COMPONENT_SCHEMA), validate_local_no_higher_than_global, + validate_initial_no_higher_than_global, ) -@coroutine_with_priority(90.0) +@coroutine_with_priority(CoroPriority.DIAGNOSTICS) async def to_code(config): baud_rate = config[CONF_BAUD_RATE] level = config[CONF_LEVEL] @@ -302,8 +314,12 @@ async def to_code(config): ) cg.add(log.pre_setup()) - for tag, log_level in config[CONF_LOGS].items(): - cg.add(log.set_log_level(tag, LOG_LEVELS[log_level])) + # Enable runtime tag levels if logs are configured or explicitly enabled + logs_config = config[CONF_LOGS] + if logs_config or config[CONF_RUNTIME_TAG_LEVELS]: + cg.add_define("USE_LOGGER_RUNTIME_TAG_LEVELS") + for tag, log_level in logs_config.items(): + cg.add(log.set_log_level(tag, LOG_LEVELS[log_level])) cg.add_define("USE_LOGGER") this_severity = LOG_LEVEL_SEVERITY.index(level) @@ -346,19 +362,13 @@ async def to_code(config): if config.get(CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH): cg.add_build_flag("-DUSE_STORE_LOG_STR_IN_FLASH") - if CORE.using_arduino and config[CONF_HARDWARE_UART] == USB_CDC: - cg.add_build_flag("-DARDUINO_USB_CDC_ON_BOOT=1") - if CORE.is_esp32 and get_esp32_variant() in ( - VARIANT_ESP32C3, - VARIANT_ESP32C6, - ): - cg.add_build_flag("-DARDUINO_USB_MODE=1") - - if CORE.using_esp_idf: + if CORE.is_esp32: if config[CONF_HARDWARE_UART] == USB_CDC: add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_CDC", True) + cg.add_define("USE_LOGGER_UART_SELECTION_USB_CDC") elif config[CONF_HARDWARE_UART] == USB_SERIAL_JTAG: add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG", True) + cg.add_define("USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG") try: uart_selection(USB_SERIAL_JTAG) cg.add_define("USE_LOGGER_USB_SERIAL_JTAG") @@ -443,7 +453,9 @@ async def logger_log_action_to_code(config, action_id, template_arg, args): text = str(cg.statement(esp_log(config[CONF_TAG], config[CONF_FORMAT], *args_))) lambda_ = await cg.process_lambda(Lambda(text), args, return_type=cg.void) - return cg.new_Pvariable(action_id, template_arg, lambda_) + return automation.new_lambda_pvariable( + action_id, lambda_, StatelessLambdaAction, template_arg + ) @automation.register_action( @@ -462,12 +474,15 @@ async def logger_set_level_to_code(config, action_id, template_arg, args): level = LOG_LEVELS[config[CONF_LEVEL]] logger = await cg.get_variable(config[CONF_LOGGER_ID]) if tag := config.get(CONF_TAG): + cg.add_define("USE_LOGGER_RUNTIME_TAG_LEVELS") text = str(cg.statement(logger.set_log_level(tag, level))) else: text = str(cg.statement(logger.set_log_level(level))) lambda_ = await cg.process_lambda(Lambda(text), args, return_type=cg.void) - return cg.new_Pvariable(action_id, template_arg, lambda_) + return automation.new_lambda_pvariable( + action_id, lambda_, StatelessLambdaAction, template_arg + ) FILTER_SOURCE_FILES = filter_source_files_from_platform( diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 195e04948d..9803bf528c 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -65,7 +65,9 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch uint16_t buffer_at = 0; // Initialize buffer position this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, console_buffer, &buffer_at, MAX_CONSOLE_LOG_MSG_SIZE); - this->write_msg_(console_buffer); + // Add newline if platform needs it (ESP32 doesn't add via write_msg_) + this->add_newline_to_buffer_if_needed_(console_buffer, &buffer_at, MAX_CONSOLE_LOG_MSG_SIZE); + this->write_msg_(console_buffer, buffer_at); } // Reset the recursion guard for this task @@ -131,26 +133,29 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas // Save the offset before calling format_log_to_buffer_with_terminator_ // since it will increment tx_buffer_at_ to the end of the formatted string - uint32_t msg_start = this->tx_buffer_at_; + uint16_t msg_start = this->tx_buffer_at_; this->format_log_to_buffer_with_terminator_(level, tag, line, this->tx_buffer_, args, this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_); - // Write to console and send callback starting at the msg_start - if (this->baud_rate_ > 0) { - this->write_msg_(this->tx_buffer_ + msg_start); - } - size_t msg_length = + uint16_t msg_length = this->tx_buffer_at_ - msg_start; // Don't subtract 1 - tx_buffer_at_ is already at the null terminator position + + // Callbacks get message first (before console write) this->log_callback_.call(level, tag, this->tx_buffer_ + msg_start, msg_length); + // Write to console starting at the msg_start + this->write_tx_buffer_to_console_(msg_start, &msg_length); + global_recursion_guard_ = false; } #endif // USE_STORE_LOG_STR_IN_FLASH inline uint8_t Logger::level_for(const char *tag) { +#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS auto it = this->log_levels_.find(tag); if (it != this->log_levels_.end()) return it->second; +#endif return this->current_level_; } @@ -173,24 +178,8 @@ void Logger::init_log_buffer(size_t total_buffer_size) { } #endif -#ifndef USE_ZEPHYR -#if defined(USE_LOGGER_USB_CDC) || defined(USE_ESP32) -void Logger::loop() { -#if defined(USE_LOGGER_USB_CDC) && defined(USE_ARDUINO) - if (this->uart_ == UART_SELECTION_USB_CDC) { - static bool opened = false; - if (opened == Serial) { - return; - } - if (false == opened) { - App.schedule_dump_config(); - } - opened = !opened; - } -#endif - this->process_messages_(); -} -#endif +#ifdef USE_ESPHOME_TASK_LOG_BUFFER +void Logger::loop() { this->process_messages_(); } #endif void Logger::process_messages_() { @@ -223,9 +212,7 @@ void Logger::process_messages_() { // This ensures all log messages appear on the console in a clean, serialized manner // Note: Messages may appear slightly out of order due to async processing, but // this is preferred over corrupted/interleaved console output - if (this->baud_rate_ > 0) { - this->write_msg_(this->tx_buffer_); - } + this->write_tx_buffer_to_console_(); } } else { // No messages to process, disable loop if appropriate @@ -236,7 +223,9 @@ void Logger::process_messages_() { } void Logger::set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; } -void Logger::set_log_level(const std::string &tag, uint8_t log_level) { this->log_levels_[tag] = log_level; } +#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS +void Logger::set_log_level(const char *tag, uint8_t log_level) { this->log_levels_[tag] = log_level; } +#endif #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) UARTSelection Logger::get_uart() const { return this->uart_; } @@ -246,19 +235,40 @@ void Logger::add_on_log_callback(std::functionlog_callback_.add(std::move(callback)); } float Logger::get_setup_priority() const { return setup_priority::BUS + 500.0f; } + +#ifdef USE_STORE_LOG_STR_IN_FLASH +// ESP8266: PSTR() cannot be used in array initializers, so we need to declare +// each string separately as a global constant first +static const char LOG_LEVEL_NONE[] PROGMEM = "NONE"; +static const char LOG_LEVEL_ERROR[] PROGMEM = "ERROR"; +static const char LOG_LEVEL_WARN[] PROGMEM = "WARN"; +static const char LOG_LEVEL_INFO[] PROGMEM = "INFO"; +static const char LOG_LEVEL_CONFIG[] PROGMEM = "CONFIG"; +static const char LOG_LEVEL_DEBUG[] PROGMEM = "DEBUG"; +static const char LOG_LEVEL_VERBOSE[] PROGMEM = "VERBOSE"; +static const char LOG_LEVEL_VERY_VERBOSE[] PROGMEM = "VERY_VERBOSE"; + +static const LogString *const LOG_LEVELS[] = { + reinterpret_cast(LOG_LEVEL_NONE), reinterpret_cast(LOG_LEVEL_ERROR), + reinterpret_cast(LOG_LEVEL_WARN), reinterpret_cast(LOG_LEVEL_INFO), + reinterpret_cast(LOG_LEVEL_CONFIG), reinterpret_cast(LOG_LEVEL_DEBUG), + reinterpret_cast(LOG_LEVEL_VERBOSE), reinterpret_cast(LOG_LEVEL_VERY_VERBOSE), +}; +#else static const char *const LOG_LEVELS[] = {"NONE", "ERROR", "WARN", "INFO", "CONFIG", "DEBUG", "VERBOSE", "VERY_VERBOSE"}; +#endif void Logger::dump_config() { ESP_LOGCONFIG(TAG, "Logger:\n" " Max Level: %s\n" " Initial Level: %s", - LOG_LEVELS[ESPHOME_LOG_LEVEL], LOG_LEVELS[this->current_level_]); + LOG_STR_ARG(LOG_LEVELS[ESPHOME_LOG_LEVEL]), LOG_STR_ARG(LOG_LEVELS[this->current_level_])); #ifndef USE_HOST ESP_LOGCONFIG(TAG, " Log Baud Rate: %" PRIu32 "\n" " Hardware UART: %s", - this->baud_rate_, get_uart_selection_()); + this->baud_rate_, LOG_STR_ARG(get_uart_selection_())); #endif #ifdef USE_ESPHOME_TASK_LOG_BUFFER if (this->log_buffer_) { @@ -266,15 +276,17 @@ void Logger::dump_config() { } #endif +#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS for (auto &it : this->log_levels_) { - ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first.c_str(), LOG_LEVELS[it.second]); + ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first, LOG_STR_ARG(LOG_LEVELS[it.second])); } +#endif } void Logger::set_log_level(uint8_t level) { if (level > ESPHOME_LOG_LEVEL) { level = ESPHOME_LOG_LEVEL; - ESP_LOGW(TAG, "Cannot set log level higher than pre-compiled %s", LOG_LEVELS[ESPHOME_LOG_LEVEL]); + ESP_LOGW(TAG, "Cannot set log level higher than pre-compiled %s", LOG_STR_ARG(LOG_LEVELS[ESPHOME_LOG_LEVEL])); } this->current_level_ = level; this->level_callback_.call(level); diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index aa76a188c9..6a8b640331 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -16,18 +16,18 @@ #endif #ifdef USE_ARDUINO -#if defined(USE_ESP8266) || defined(USE_ESP32) +#if defined(USE_ESP8266) #include -#endif // USE_ESP8266 || USE_ESP32 +#endif // USE_ESP8266 #ifdef USE_RP2040 #include #include #endif // USE_RP2040 #endif // USE_ARDUINO -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include -#endif // USE_ESP_IDF +#endif // USE_ESP32 #ifdef USE_ZEPHYR #include @@ -36,29 +36,52 @@ struct device; namespace esphome::logger { -// Color and letter constants for log levels -static const char *const LOG_LEVEL_COLORS[] = { - "", // NONE - ESPHOME_LOG_BOLD(ESPHOME_LOG_COLOR_RED), // ERROR - ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_YELLOW), // WARNING - ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_GREEN), // INFO - ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_MAGENTA), // CONFIG - ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_CYAN), // DEBUG - ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_GRAY), // VERBOSE - ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_WHITE), // VERY_VERBOSE +#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS +// Comparison function for const char* keys in log_levels_ map +struct CStrCompare { + bool operator()(const char *a, const char *b) const { return strcmp(a, b) < 0; } +}; +#endif + +// ANSI color code last digit (30-38 range, store only last digit to save RAM) +static constexpr char LOG_LEVEL_COLOR_DIGIT[] = { + '\0', // NONE + '1', // ERROR (31 = red) + '3', // WARNING (33 = yellow) + '2', // INFO (32 = green) + '5', // CONFIG (35 = magenta) + '6', // DEBUG (36 = cyan) + '7', // VERBOSE (37 = gray) + '8', // VERY_VERBOSE (38 = white) }; -static const char *const LOG_LEVEL_LETTERS[] = { - "", // NONE - "E", // ERROR - "W", // WARNING - "I", // INFO - "C", // CONFIG - "D", // DEBUG - "V", // VERBOSE - "VV", // VERY_VERBOSE +static constexpr char LOG_LEVEL_LETTER_CHARS[] = { + '\0', // NONE + 'E', // ERROR + 'W', // WARNING + 'I', // INFO + 'C', // CONFIG + 'D', // DEBUG + 'V', // VERBOSE (VERY_VERBOSE uses two 'V's) }; +// Maximum header size: 35 bytes fixed + 32 bytes tag + 16 bytes thread name = 83 bytes (45 byte safety margin) +static constexpr uint16_t MAX_HEADER_SIZE = 128; + +// "0x" + 2 hex digits per byte + '\0' +static constexpr size_t MAX_POINTER_REPRESENTATION = 2 + sizeof(void *) * 2 + 1; + +// Platform-specific: does write_msg_ add its own newline? +// false: Caller must add newline to buffer before calling write_msg_ (ESP32, ESP8266, LibreTiny) +// Allows single write call with newline included for efficiency +// true: write_msg_ adds newline itself via puts()/println() (other platforms) +// Newline should NOT be added to buffer +#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_LIBRETINY) +static constexpr bool WRITE_MSG_ADDS_NEWLINE = false; +#else +static constexpr bool WRITE_MSG_ADDS_NEWLINE = true; +#endif + #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) /** Enum for logging UART selection * @@ -110,19 +133,17 @@ class Logger : public Component { #ifdef USE_ESPHOME_TASK_LOG_BUFFER void init_log_buffer(size_t total_buffer_size); #endif -#if defined(USE_LOGGER_USB_CDC) || defined(USE_ESP32) || defined(USE_ZEPHYR) +#if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_USB_CDC)) void loop() override; #endif /// Manually set the baud rate for serial, set to 0 to disable. void set_baud_rate(uint32_t baud_rate); uint32_t get_baud_rate() const { return baud_rate_; } -#ifdef USE_ARDUINO +#if defined(USE_ARDUINO) && !defined(USE_ESP32) Stream *get_hw_serial() const { return hw_serial_; } #endif -#ifdef USE_ESP_IDF - uart_port_t get_uart_num() const { return uart_num_; } -#endif #ifdef USE_ESP32 + uart_port_t get_uart_num() const { return uart_num_; } void create_pthread_key() { pthread_key_create(&log_recursion_key_, nullptr); } #endif #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) @@ -133,8 +154,10 @@ class Logger : public Component { /// Set the default log level for this logger. void set_log_level(uint8_t level); +#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS /// Set the log level of the specified tag. - void set_log_level(const std::string &tag, uint8_t log_level); + void set_log_level(const char *tag, uint8_t log_level); +#endif uint8_t get_log_level() { return this->current_level_; } // ========== INTERNAL METHODS ========== @@ -161,15 +184,18 @@ class Logger : public Component { protected: void process_messages_(); - void write_msg_(const char *msg); + void write_msg_(const char *msg, size_t len); // Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator // It's the caller's responsibility to initialize buffer_at (typically to 0) inline void HOT format_log_to_buffer_with_terminator_(uint8_t level, const char *tag, int line, const char *format, va_list args, char *buffer, uint16_t *buffer_at, uint16_t buffer_size) { -#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) +#if defined(USE_ESP32) || defined(USE_LIBRETINY) this->write_header_to_buffer_(level, tag, line, this->get_thread_name_(), buffer, buffer_at, buffer_size); +#elif defined(USE_ZEPHYR) + char buff[MAX_POINTER_REPRESENTATION]; + this->write_header_to_buffer_(level, tag, line, this->get_thread_name_(buff), buffer, buffer_at, buffer_size); #else this->write_header_to_buffer_(level, tag, line, nullptr, buffer, buffer_at, buffer_size); #endif @@ -185,6 +211,35 @@ class Logger : public Component { } } + // Helper to add newline to buffer for platforms that need it + // Modifies buffer_at to include the newline + inline void HOT add_newline_to_buffer_if_needed_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size) { + if constexpr (!WRITE_MSG_ADDS_NEWLINE) { + // Add newline - don't need to maintain null termination + // write_msg_ now always receives explicit length, so we can safely overwrite the null terminator + // This is safe because: + // 1. Callbacks already received the message (before we add newline) + // 2. write_msg_ receives the length explicitly (doesn't need null terminator) + if (*buffer_at < buffer_size) { + buffer[(*buffer_at)++] = '\n'; + } else if (buffer_size > 0) { + // Buffer was full - replace last char with newline to ensure it's visible + buffer[buffer_size - 1] = '\n'; + *buffer_at = buffer_size; + } + } + } + + // Helper to write tx_buffer_ to console if logging is enabled + // INTERNAL USE ONLY - offset > 0 requires length parameter to be non-null + inline void HOT write_tx_buffer_to_console_(uint16_t offset = 0, uint16_t *length = nullptr) { + if (this->baud_rate_ > 0) { + uint16_t *len_ptr = length ? length : &this->tx_buffer_at_; + this->add_newline_to_buffer_if_needed_(this->tx_buffer_ + offset, len_ptr, this->tx_buffer_size_ - offset); + this->write_msg_(this->tx_buffer_ + offset, *len_ptr); + } + } + // Helper to format and send a log message to both console and callbacks inline void HOT log_message_to_buffer_and_send_(uint8_t level, const char *tag, int line, const char *format, va_list args) { @@ -193,10 +248,11 @@ class Logger : public Component { this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_); - if (this->baud_rate_ > 0) { - this->write_msg_(this->tx_buffer_); // If logging is enabled, write to console - } + // Callbacks get message WITHOUT newline (for API/MQTT/syslog) this->log_callback_.call(level, tag, this->tx_buffer_, this->tx_buffer_at_); + + // Console gets message WITH newline (if platform needs it) + this->write_tx_buffer_to_console_(); } // Write the body of the log message to the buffer @@ -217,22 +273,14 @@ class Logger : public Component { } } - // Format string to explicit buffer with varargs - inline void printf_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size, const char *format, ...) { - va_list arg; - va_start(arg, format); - this->format_body_to_buffer_(buffer, buffer_at, buffer_size, format, arg); - va_end(arg); - } - #ifndef USE_HOST - const char *get_uart_selection_(); + const LogString *get_uart_selection_(); #endif // Group 4-byte aligned members first uint32_t baud_rate_; char *tx_buffer_{nullptr}; -#ifdef USE_ARDUINO +#if defined(USE_ARDUINO) && !defined(USE_ESP32) Stream *hw_serial_{nullptr}; #endif #if defined(USE_ZEPHYR) @@ -246,13 +294,13 @@ class Logger : public Component { // - Main task uses a dedicated member variable for efficiency // - Other tasks use pthread TLS with a dynamically created key via pthread_key_create pthread_key_t log_recursion_key_; // 4 bytes -#endif -#ifdef USE_ESP_IDF - uart_port_t uart_num_; // 4 bytes (enum defaults to int size) + uart_port_t uart_num_; // 4 bytes (enum defaults to int size) #endif // Large objects (internally aligned) - std::map log_levels_{}; +#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS + std::map log_levels_{}; +#endif CallbackManager log_callback_{}; CallbackManager level_callback_{}; #ifdef USE_ESPHOME_TASK_LOG_BUFFER @@ -276,7 +324,11 @@ class Logger : public Component { #endif #if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) - const char *HOT get_thread_name_() { + const char *HOT get_thread_name_( +#ifdef USE_ZEPHYR + char *buff +#endif + ) { #ifdef USE_ZEPHYR k_tid_t current_task = k_current_get(); #else @@ -290,7 +342,13 @@ class Logger : public Component { #elif defined(USE_LIBRETINY) return pcTaskGetTaskName(current_task); #elif defined(USE_ZEPHYR) - return k_thread_name_get(current_task); + const char *name = k_thread_name_get(current_task); + if (name) { + // zephyr print task names only if debug component is present + return name; + } + std::snprintf(buff, MAX_POINTER_REPRESENTATION, "%p", current_task); + return buff; #endif } } @@ -322,26 +380,76 @@ class Logger : public Component { } #endif + static inline void copy_string(char *buffer, uint16_t &pos, const char *str) { + const size_t len = strlen(str); + // Intentionally no null terminator, building larger string + memcpy(buffer + pos, str, len); // NOLINT(bugprone-not-null-terminated-result) + pos += len; + } + + static inline void write_ansi_color_for_level(char *buffer, uint16_t &pos, uint8_t level) { + if (level == 0) + return; + // Construct ANSI escape sequence: "\033[{bold};3{color}m" + // Example: "\033[1;31m" for ERROR (bold red) + buffer[pos++] = '\033'; + buffer[pos++] = '['; + buffer[pos++] = (level == 1) ? '1' : '0'; // Only ERROR is bold + buffer[pos++] = ';'; + buffer[pos++] = '3'; + buffer[pos++] = LOG_LEVEL_COLOR_DIGIT[level]; + buffer[pos++] = 'm'; + } + inline void HOT write_header_to_buffer_(uint8_t level, const char *tag, int line, const char *thread_name, char *buffer, uint16_t *buffer_at, uint16_t buffer_size) { - // Format header - // uint8_t level is already bounded 0-255, just ensure it's <= 7 - if (level > 7) - level = 7; + uint16_t pos = *buffer_at; + // Early return if insufficient space - intentionally don't update buffer_at to prevent partial writes + if (pos + MAX_HEADER_SIZE > buffer_size) + return; - const char *color = esphome::logger::LOG_LEVEL_COLORS[level]; - const char *letter = esphome::logger::LOG_LEVEL_LETTERS[level]; + // Construct: [LEVEL][tag:line]: + write_ansi_color_for_level(buffer, pos, level); + buffer[pos++] = '['; + if (level != 0) { + if (level >= 7) { + buffer[pos++] = 'V'; // VERY_VERBOSE = "VV" + buffer[pos++] = 'V'; + } else { + buffer[pos++] = LOG_LEVEL_LETTER_CHARS[level]; + } + } + buffer[pos++] = ']'; + buffer[pos++] = '['; + copy_string(buffer, pos, tag); + buffer[pos++] = ':'; + // Format line number without modulo operations (passed by value, safe to mutate) + if (line > 999) [[unlikely]] { + int thousands = line / 1000; + buffer[pos++] = '0' + thousands; + line -= thousands * 1000; + } + int hundreds = line / 100; + int remainder = line - hundreds * 100; + int tens = remainder / 10; + buffer[pos++] = '0' + hundreds; + buffer[pos++] = '0' + tens; + buffer[pos++] = '0' + (remainder - tens * 10); + buffer[pos++] = ']'; #if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) if (thread_name != nullptr) { - // Non-main task with thread name - this->printf_to_buffer_(buffer, buffer_at, buffer_size, "%s[%s][%s:%03u]%s[%s]%s: ", color, letter, tag, line, - ESPHOME_LOG_BOLD(ESPHOME_LOG_COLOR_RED), thread_name, color); - return; + write_ansi_color_for_level(buffer, pos, 1); // Always use bold red for thread name + buffer[pos++] = '['; + copy_string(buffer, pos, thread_name); + buffer[pos++] = ']'; + write_ansi_color_for_level(buffer, pos, level); // Restore original color } #endif - // Main task or non ESP32/LibreTiny platform - this->printf_to_buffer_(buffer, buffer_at, buffer_size, "%s[%s][%s:%03u]: ", color, letter, tag, line); + + buffer[pos++] = ':'; + buffer[pos++] = ' '; + *buffer_at = pos; } inline void HOT format_body_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size, const char *format, @@ -358,7 +466,9 @@ class Logger : public Component { } // Update buffer_at with the formatted length (handle truncation) - uint16_t formatted_len = (ret >= remaining) ? remaining : ret; + // When vsnprintf truncates (ret >= remaining), it writes (remaining - 1) chars + null terminator + // When it doesn't truncate (ret < remaining), it writes ret chars + null terminator + uint16_t formatted_len = (ret >= remaining) ? (remaining - 1) : ret; *buffer_at += formatted_len; // Remove all trailing newlines right after formatting @@ -380,15 +490,7 @@ class Logger : public Component { // will be processed on the next main loop iteration since: // - disable_loop() takes effect immediately // - enable_loop_soon_any_context() sets a pending flag that's checked at loop start -#if defined(USE_LOGGER_USB_CDC) && defined(USE_ARDUINO) - // Only disable if not using USB CDC (which needs loop for connection detection) - if (this->uart_ != UART_SELECTION_USB_CDC) { - this->disable_loop(); - } -#else - // No USB CDC support, always safe to disable this->disable_loop(); -#endif } #endif }; diff --git a/esphome/components/logger/logger_esp32.cpp b/esphome/components/logger/logger_esp32.cpp index 44243d4aa8..32ef752462 100644 --- a/esphome/components/logger/logger_esp32.cpp +++ b/esphome/components/logger/logger_esp32.cpp @@ -1,11 +1,8 @@ #ifdef USE_ESP32 #include "logger.h" -#if defined(USE_ESP32_FRAMEWORK_ARDUINO) || defined(USE_ESP_IDF) #include -#endif // USE_ESP32_FRAMEWORK_ARDUINO || USE_ESP_IDF -#ifdef USE_ESP_IDF #include #ifdef USE_LOGGER_USB_SERIAL_JTAG @@ -25,16 +22,12 @@ #include #include -#endif // USE_ESP_IDF - #include "esphome/core/log.h" namespace esphome::logger { static const char *const TAG = "logger"; -#ifdef USE_ESP_IDF - #ifdef USE_LOGGER_USB_SERIAL_JTAG static void init_usb_serial_jtag_() { setvbuf(stdin, NULL, _IONBF, 0); // Disable buffering on stdin @@ -89,42 +82,8 @@ void init_uart(uart_port_t uart_num, uint32_t baud_rate, int tx_buffer_size) { uart_driver_install(uart_num, uart_buffer_size, uart_buffer_size, 10, nullptr, 0); } -#endif // USE_ESP_IDF - void Logger::pre_setup() { if (this->baud_rate_ > 0) { -#ifdef USE_ARDUINO - switch (this->uart_) { - case UART_SELECTION_UART0: -#if ARDUINO_USB_CDC_ON_BOOT - this->hw_serial_ = &Serial0; - Serial0.begin(this->baud_rate_); -#else - this->hw_serial_ = &Serial; - Serial.begin(this->baud_rate_); -#endif - break; - case UART_SELECTION_UART1: - this->hw_serial_ = &Serial1; - Serial1.begin(this->baud_rate_); - break; -#ifdef USE_ESP32_VARIANT_ESP32 - case UART_SELECTION_UART2: - this->hw_serial_ = &Serial2; - Serial2.begin(this->baud_rate_); - break; -#endif - -#ifdef USE_LOGGER_USB_CDC - case UART_SELECTION_USB_CDC: - this->hw_serial_ = &Serial; - Serial.begin(this->baud_rate_); - break; -#endif - } -#endif // USE_ARDUINO - -#ifdef USE_ESP_IDF this->uart_num_ = UART_NUM_0; switch (this->uart_) { case UART_SELECTION_UART0: @@ -151,59 +110,58 @@ void Logger::pre_setup() { break; #endif } -#endif // USE_ESP_IDF } global_logger = this; -#if defined(USE_ESP_IDF) || defined(USE_ESP32_FRAMEWORK_ARDUINO) esp_log_set_vprintf(esp_idf_log_vprintf_); if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE) { esp_log_level_set("*", ESP_LOG_VERBOSE); } -#endif // USE_ESP_IDF || USE_ESP32_FRAMEWORK_ARDUINO ESP_LOGI(TAG, "Log initialized"); } -#ifdef USE_ESP_IDF -void HOT Logger::write_msg_(const char *msg) { - if ( -#if defined(USE_LOGGER_USB_CDC) && !defined(USE_LOGGER_USB_SERIAL_JTAG) - this->uart_ == UART_SELECTION_USB_CDC -#elif defined(USE_LOGGER_USB_SERIAL_JTAG) && !defined(USE_LOGGER_USB_CDC) - this->uart_ == UART_SELECTION_USB_SERIAL_JTAG -#elif defined(USE_LOGGER_USB_CDC) && defined(USE_LOGGER_USB_SERIAL_JTAG) - this->uart_ == UART_SELECTION_USB_CDC || this->uart_ == UART_SELECTION_USB_SERIAL_JTAG -#else - /* DISABLES CODE */ (false) // NOLINT -#endif - ) { - puts(msg); - } else { - // Use tx_buffer_at_ if msg points to tx_buffer_, otherwise fall back to strlen - size_t len = (msg == this->tx_buffer_) ? this->tx_buffer_at_ : strlen(msg); - uart_write_bytes(this->uart_num_, msg, len); - uart_write_bytes(this->uart_num_, "\n", 1); - } -} -#else -void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); } -#endif +void HOT Logger::write_msg_(const char *msg, size_t len) { + // Length is now always passed explicitly - no strlen() fallback needed -const char *const UART_SELECTIONS[] = { - "UART0", "UART1", +#if defined(USE_LOGGER_UART_SELECTION_USB_CDC) || defined(USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG) + // USB CDC/JTAG - single write including newline (already in buffer) + // Use fwrite to stdout which goes through VFS to USB console + // + // Note: These defines indicate the user's YAML configuration choice (hardware_uart: USB_CDC/USB_SERIAL_JTAG). + // They are ONLY defined when the user explicitly selects USB as the logger output in their config. + // This is compile-time selection, not runtime detection - if USB is configured, it's always used. + // There is no fallback to regular UART if "USB isn't connected" - that's the user's responsibility + // to configure correctly for their hardware. This approach eliminates runtime overhead. + fwrite(msg, 1, len, stdout); +#else + // Regular UART - single write including newline (already in buffer) + uart_write_bytes(this->uart_num_, msg, len); +#endif +} + +const LogString *Logger::get_uart_selection_() { + switch (this->uart_) { + case UART_SELECTION_UART0: + return LOG_STR("UART0"); + case UART_SELECTION_UART1: + return LOG_STR("UART1"); #ifdef USE_ESP32_VARIANT_ESP32 - "UART2", + case UART_SELECTION_UART2: + return LOG_STR("UART2"); #endif #ifdef USE_LOGGER_USB_CDC - "USB_CDC", + case UART_SELECTION_USB_CDC: + return LOG_STR("USB_CDC"); #endif #ifdef USE_LOGGER_USB_SERIAL_JTAG - "USB_SERIAL_JTAG", + case UART_SELECTION_USB_SERIAL_JTAG: + return LOG_STR("USB_SERIAL_JTAG"); #endif -}; - -const char *Logger::get_uart_selection_() { return UART_SELECTIONS[this->uart_]; } + default: + return LOG_STR("UNKNOWN"); + } +} } // namespace esphome::logger #endif diff --git a/esphome/components/logger/logger_esp8266.cpp b/esphome/components/logger/logger_esp8266.cpp index fb5f6cee5d..0fc73b747a 100644 --- a/esphome/components/logger/logger_esp8266.cpp +++ b/esphome/components/logger/logger_esp8266.cpp @@ -33,11 +33,22 @@ void Logger::pre_setup() { ESP_LOGI(TAG, "Log initialized"); } -void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); } +void HOT Logger::write_msg_(const char *msg, size_t len) { + // Single write with newline already in buffer (added by caller) + this->hw_serial_->write(msg, len); +} -const char *const UART_SELECTIONS[] = {"UART0", "UART1", "UART0_SWAP"}; - -const char *Logger::get_uart_selection_() { return UART_SELECTIONS[this->uart_]; } +const LogString *Logger::get_uart_selection_() { + switch (this->uart_) { + case UART_SELECTION_UART0: + return LOG_STR("UART0"); + case UART_SELECTION_UART1: + return LOG_STR("UART1"); + case UART_SELECTION_UART0_SWAP: + default: + return LOG_STR("UART0_SWAP"); + } +} } // namespace esphome::logger #endif diff --git a/esphome/components/logger/logger_host.cpp b/esphome/components/logger/logger_host.cpp index 4abe92286a..c5e1e6f865 100644 --- a/esphome/components/logger/logger_host.cpp +++ b/esphome/components/logger/logger_host.cpp @@ -3,7 +3,7 @@ namespace esphome::logger { -void HOT Logger::write_msg_(const char *msg) { +void HOT Logger::write_msg_(const char *msg, size_t) { time_t rawtime; struct tm *timeinfo; char buffer[80]; diff --git a/esphome/components/logger/logger_libretiny.cpp b/esphome/components/logger/logger_libretiny.cpp index 09d0622bc3..cdf55e710c 100644 --- a/esphome/components/logger/logger_libretiny.cpp +++ b/esphome/components/logger/logger_libretiny.cpp @@ -49,11 +49,21 @@ void Logger::pre_setup() { ESP_LOGI(TAG, "Log initialized"); } -void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); } +void HOT Logger::write_msg_(const char *msg, size_t len) { this->hw_serial_->write(msg, len); } -const char *const UART_SELECTIONS[] = {"DEFAULT", "UART0", "UART1", "UART2"}; - -const char *Logger::get_uart_selection_() { return UART_SELECTIONS[this->uart_]; } +const LogString *Logger::get_uart_selection_() { + switch (this->uart_) { + case UART_SELECTION_DEFAULT: + return LOG_STR("DEFAULT"); + case UART_SELECTION_UART0: + return LOG_STR("UART0"); + case UART_SELECTION_UART1: + return LOG_STR("UART1"); + case UART_SELECTION_UART2: + default: + return LOG_STR("UART2"); + } +} } // namespace esphome::logger diff --git a/esphome/components/logger/logger_rp2040.cpp b/esphome/components/logger/logger_rp2040.cpp index f1cad9b283..4a8535c8e4 100644 --- a/esphome/components/logger/logger_rp2040.cpp +++ b/esphome/components/logger/logger_rp2040.cpp @@ -27,11 +27,22 @@ void Logger::pre_setup() { ESP_LOGI(TAG, "Log initialized"); } -void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); } +void HOT Logger::write_msg_(const char *msg, size_t) { this->hw_serial_->println(msg); } -const char *const UART_SELECTIONS[] = {"UART0", "UART1", "USB_CDC"}; - -const char *Logger::get_uart_selection_() { return UART_SELECTIONS[this->uart_]; } +const LogString *Logger::get_uart_selection_() { + switch (this->uart_) { + case UART_SELECTION_UART0: + return LOG_STR("UART0"); + case UART_SELECTION_UART1: + return LOG_STR("UART1"); +#ifdef USE_LOGGER_USB_CDC + case UART_SELECTION_USB_CDC: + return LOG_STR("USB_CDC"); +#endif + default: + return LOG_STR("UNKNOWN"); + } +} } // namespace esphome::logger #endif // USE_RP2040 diff --git a/esphome/components/logger/logger_zephyr.cpp b/esphome/components/logger/logger_zephyr.cpp index 58a09facd5..ec2ff3013c 100644 --- a/esphome/components/logger/logger_zephyr.cpp +++ b/esphome/components/logger/logger_zephyr.cpp @@ -12,8 +12,8 @@ namespace esphome::logger { static const char *const TAG = "logger"; -void Logger::loop() { #ifdef USE_LOGGER_USB_CDC +void Logger::loop() { if (this->uart_ != UART_SELECTION_USB_CDC || nullptr == this->uart_dev_) { return; } @@ -30,9 +30,8 @@ void Logger::loop() { App.schedule_dump_config(); } opened = !opened; -#endif - this->process_messages_(); } +#endif void Logger::pre_setup() { if (this->baud_rate_ > 0) { @@ -54,7 +53,7 @@ void Logger::pre_setup() { #endif } if (!device_is_ready(uart_dev)) { - ESP_LOGE(TAG, "%s is not ready.", get_uart_selection_()); + ESP_LOGE(TAG, "%s is not ready.", LOG_STR_ARG(get_uart_selection_())); } else { this->uart_dev_ = uart_dev; } @@ -63,7 +62,7 @@ void Logger::pre_setup() { ESP_LOGI(TAG, "Log initialized"); } -void HOT Logger::write_msg_(const char *msg) { +void HOT Logger::write_msg_(const char *msg, size_t) { #ifdef CONFIG_PRINTK printk("%s\n", msg); #endif @@ -77,9 +76,20 @@ void HOT Logger::write_msg_(const char *msg) { uart_poll_out(this->uart_dev_, '\n'); } -const char *const UART_SELECTIONS[] = {"UART0", "UART1", "USB_CDC"}; - -const char *Logger::get_uart_selection_() { return UART_SELECTIONS[this->uart_]; } +const LogString *Logger::get_uart_selection_() { + switch (this->uart_) { + case UART_SELECTION_UART0: + return LOG_STR("UART0"); + case UART_SELECTION_UART1: + return LOG_STR("UART1"); +#ifdef USE_LOGGER_USB_CDC + case UART_SELECTION_USB_CDC: + return LOG_STR("USB_CDC"); +#endif + default: + return LOG_STR("UNKNOWN"); + } +} } // namespace esphome::logger diff --git a/esphome/components/logger/select/logger_level_select.cpp b/esphome/components/logger/select/logger_level_select.cpp index d9c950ce3c..e2ec28a390 100644 --- a/esphome/components/logger/select/logger_level_select.cpp +++ b/esphome/components/logger/select/logger_level_select.cpp @@ -3,11 +3,10 @@ namespace esphome::logger { void LoggerLevelSelect::publish_state(int level) { - auto value = this->at(level); - if (!value) { + auto index = level_to_index(level); + if (!this->has_index(index)) return; - } - Select::publish_state(value.value()); + Select::publish_state(index); } void LoggerLevelSelect::setup() { @@ -15,11 +14,6 @@ void LoggerLevelSelect::setup() { this->publish_state(this->parent_->get_log_level()); } -void LoggerLevelSelect::control(const std::string &value) { - auto level = this->index_of(value); - if (!level) - return; - this->parent_->set_log_level(level.value()); -} +void LoggerLevelSelect::control(size_t index) { this->parent_->set_log_level(index_to_level(index)); } } // namespace esphome::logger diff --git a/esphome/components/logger/select/logger_level_select.h b/esphome/components/logger/select/logger_level_select.h index f31a6f6cdb..950edd29ac 100644 --- a/esphome/components/logger/select/logger_level_select.h +++ b/esphome/components/logger/select/logger_level_select.h @@ -3,11 +3,18 @@ #include "esphome/components/select/select.h" #include "esphome/core/component.h" #include "esphome/components/logger/logger.h" + namespace esphome::logger { class LoggerLevelSelect : public Component, public select::Select, public Parented { public: void publish_state(int level); void setup() override; - void control(const std::string &value) override; + void control(size_t index) override; + + protected: + // Convert log level to option index (skip CONFIG at level 4) + static uint8_t level_to_index(uint8_t level) { return (level > ESPHOME_LOG_LEVEL_CONFIG) ? level - 1 : level; } + // Convert option index to log level (skip CONFIG at level 4) + static uint8_t index_to_level(uint8_t index) { return (index >= ESPHOME_LOG_LEVEL_CONFIG) ? index + 1 : index; } }; } // namespace esphome::logger diff --git a/esphome/components/ltr390/ltr390.cpp b/esphome/components/ltr390/ltr390.cpp index c1885dcb6f..ba4a7ea5cb 100644 --- a/esphome/components/ltr390/ltr390.cpp +++ b/esphome/components/ltr390/ltr390.cpp @@ -104,12 +104,17 @@ void LTR390Component::read_uvs_() { } } -void LTR390Component::read_mode_(int mode_index) { - // Set mode - LTR390MODE mode = std::get<0>(this->mode_funcs_[mode_index]); - +void LTR390Component::standby_() { std::bitset<8> ctrl = this->reg(LTR390_MAIN_CTRL).get(); - ctrl[LTR390_CTRL_MODE] = mode; + ctrl[LTR390_CTRL_EN] = false; + this->reg(LTR390_MAIN_CTRL) = ctrl.to_ulong(); + this->reading_ = false; +} + +void LTR390Component::read_mode_(LTR390MODE mode) { + // Set mode + std::bitset<8> ctrl = this->reg(LTR390_MAIN_CTRL).get(); + ctrl[LTR390_CTRL_MODE] = (mode == LTR390_MODE_UVS); ctrl[LTR390_CTRL_EN] = true; this->reg(LTR390_MAIN_CTRL) = ctrl.to_ulong(); @@ -129,21 +134,18 @@ void LTR390Component::read_mode_(int mode_index) { } // After the sensor integration time do the following - this->set_timeout(int_time + LTR390_WAKEUP_TIME + LTR390_SETTLE_TIME, [this, mode_index]() { - // Read from the sensor - std::get<1>(this->mode_funcs_[mode_index])(); - - // If there are more modes to read then begin the next - // otherwise stop - if (mode_index + 1 < (int) this->mode_funcs_.size()) { - this->read_mode_(mode_index + 1); + this->set_timeout(int_time + LTR390_WAKEUP_TIME + LTR390_SETTLE_TIME, [this, mode]() { + // Read from the sensor and continue to next mode or standby + if (mode == LTR390_MODE_ALS) { + this->read_als_(); + if (this->enabled_modes_ & ENABLED_MODE_UVS) { + this->read_mode_(LTR390_MODE_UVS); + return; + } } else { - // put sensor in standby - std::bitset<8> ctrl = this->reg(LTR390_MAIN_CTRL).get(); - ctrl[LTR390_CTRL_EN] = false; - this->reg(LTR390_MAIN_CTRL) = ctrl.to_ulong(); - this->reading_ = false; + this->read_uvs_(); } + this->standby_(); }); } @@ -172,14 +174,12 @@ void LTR390Component::setup() { // Set sensor read state this->reading_ = false; - // If we need the light sensor then add to the list + // Determine which modes are enabled based on configured sensors if (this->light_sensor_ != nullptr || this->als_sensor_ != nullptr) { - this->mode_funcs_.emplace_back(LTR390_MODE_ALS, std::bind(<R390Component::read_als_, this)); + this->enabled_modes_ |= ENABLED_MODE_ALS; } - - // If we need the UV sensor then add to the list if (this->uvi_sensor_ != nullptr || this->uv_sensor_ != nullptr) { - this->mode_funcs_.emplace_back(LTR390_MODE_UVS, std::bind(<R390Component::read_uvs_, this)); + this->enabled_modes_ |= ENABLED_MODE_UVS; } } @@ -195,10 +195,11 @@ void LTR390Component::dump_config() { } void LTR390Component::update() { - if (!this->reading_ && !mode_funcs_.empty()) { - this->reading_ = true; - this->read_mode_(0); - } + if (this->reading_ || this->enabled_modes_ == 0) + return; + + this->reading_ = true; + this->read_mode_((this->enabled_modes_ & ENABLED_MODE_ALS) ? LTR390_MODE_ALS : LTR390_MODE_UVS); } } // namespace ltr390 diff --git a/esphome/components/ltr390/ltr390.h b/esphome/components/ltr390/ltr390.h index 7db73d68ff..47884b9166 100644 --- a/esphome/components/ltr390/ltr390.h +++ b/esphome/components/ltr390/ltr390.h @@ -1,7 +1,5 @@ #pragma once -#include -#include #include "esphome/components/i2c/i2c.h" #include "esphome/components/sensor/sensor.h" #include "esphome/core/component.h" @@ -60,17 +58,19 @@ class LTR390Component : public PollingComponent, public i2c::I2CDevice { void set_uv_sensor(sensor::Sensor *uv_sensor) { this->uv_sensor_ = uv_sensor; } protected: + static constexpr uint8_t ENABLED_MODE_ALS = 1 << 0; + static constexpr uint8_t ENABLED_MODE_UVS = 1 << 1; + optional read_sensor_data_(LTR390MODE mode); void read_als_(); void read_uvs_(); - void read_mode_(int mode_index); + void read_mode_(LTR390MODE mode); + void standby_(); - bool reading_; - - // a list of modes and corresponding read functions - std::vector>> mode_funcs_; + bool reading_{false}; + uint8_t enabled_modes_{0}; LTR390GAIN gain_als_; LTR390GAIN gain_uv_; diff --git a/esphome/components/ltr501/ltr501.cpp b/esphome/components/ltr501/ltr501.cpp index b249d23666..04de91e362 100644 --- a/esphome/components/ltr501/ltr501.cpp +++ b/esphome/components/ltr501/ltr501.cpp @@ -2,6 +2,7 @@ #include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include using esphome::i2c::ErrorCode; @@ -28,30 +29,30 @@ bool operator!=(const GainTimePair &lhs, const GainTimePair &rhs) { template T get_next(const T (&array)[size], const T val) { size_t i = 0; - size_t idx = -1; - while (idx == -1 && i < size) { + size_t idx = std::numeric_limits::max(); + while (idx == std::numeric_limits::max() && i < size) { if (array[i] == val) { idx = i; break; } i++; } - if (idx == -1 || i + 1 >= size) + if (idx == std::numeric_limits::max() || i + 1 >= size) return val; return array[i + 1]; } template T get_prev(const T (&array)[size], const T val) { size_t i = size - 1; - size_t idx = -1; - while (idx == -1 && i > 0) { + size_t idx = std::numeric_limits::max(); + while (idx == std::numeric_limits::max() && i > 0) { if (array[i] == val) { idx = i; break; } i--; } - if (idx == -1 || i == 0) + if (idx == std::numeric_limits::max() || i == 0) return val; return array[i - 1]; } @@ -173,7 +174,7 @@ void LTRAlsPs501Component::loop() { break; case State::WAITING_FOR_DATA: - if (this->is_als_data_ready_(this->als_readings_) == DataAvail::DATA_OK) { + if (this->is_als_data_ready_(this->als_readings_) == LtrDataAvail::LTR_DATA_OK) { tries = 0; ESP_LOGV(TAG, "Reading sensor data assuming gain = %.0fx, time = %d ms", get_gain_coeff(this->als_readings_.gain), get_itime_ms(this->als_readings_.integration_time)); @@ -378,18 +379,18 @@ void LTRAlsPs501Component::configure_integration_time_(IntegrationTime501 time) } } -DataAvail LTRAlsPs501Component::is_als_data_ready_(AlsReadings &data) { +LtrDataAvail LTRAlsPs501Component::is_als_data_ready_(AlsReadings &data) { AlsPsStatusRegister als_status{0}; als_status.raw = this->reg((uint8_t) CommandRegisters::ALS_PS_STATUS).get(); if (!als_status.als_new_data) - return DataAvail::NO_DATA; + return LtrDataAvail::LTR_NO_DATA; ESP_LOGV(TAG, "Data ready, reported gain is %.0fx", get_gain_coeff(als_status.gain)); if (data.gain != als_status.gain) { ESP_LOGW(TAG, "Actual gain differs from requested (%.0f)", get_gain_coeff(data.gain)); - return DataAvail::BAD_DATA; + return LtrDataAvail::LTR_BAD_DATA; } data.gain = als_status.gain; - return DataAvail::DATA_OK; + return LtrDataAvail::LTR_DATA_OK; } void LTRAlsPs501Component::read_sensor_data_(AlsReadings &data) { diff --git a/esphome/components/ltr501/ltr501.h b/esphome/components/ltr501/ltr501.h index 849ff6bc23..02c025da30 100644 --- a/esphome/components/ltr501/ltr501.h +++ b/esphome/components/ltr501/ltr501.h @@ -11,7 +11,7 @@ namespace esphome { namespace ltr501 { -enum DataAvail : uint8_t { NO_DATA, BAD_DATA, DATA_OK }; +enum LtrDataAvail : uint8_t { LTR_NO_DATA, LTR_BAD_DATA, LTR_DATA_OK }; enum LtrType : uint8_t { LTR_TYPE_UNKNOWN = 0, @@ -106,7 +106,7 @@ class LTRAlsPs501Component : public PollingComponent, public i2c::I2CDevice { void configure_als_(); void configure_integration_time_(IntegrationTime501 time); void configure_gain_(AlsGain501 gain); - DataAvail is_als_data_ready_(AlsReadings &data); + LtrDataAvail is_als_data_ready_(AlsReadings &data); void read_sensor_data_(AlsReadings &data); bool are_adjustments_required_(AlsReadings &data); void apply_lux_calculation_(AlsReadings &data); diff --git a/esphome/components/ltr_als_ps/ltr_als_ps.cpp b/esphome/components/ltr_als_ps/ltr_als_ps.cpp index bf27c01e26..f9c1474c85 100644 --- a/esphome/components/ltr_als_ps/ltr_als_ps.cpp +++ b/esphome/components/ltr_als_ps/ltr_als_ps.cpp @@ -2,6 +2,7 @@ #include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include using esphome::i2c::ErrorCode; @@ -14,30 +15,30 @@ static const uint8_t MAX_TRIES = 5; template T get_next(const T (&array)[size], const T val) { size_t i = 0; - size_t idx = -1; - while (idx == -1 && i < size) { + size_t idx = std::numeric_limits::max(); + while (idx == std::numeric_limits::max() && i < size) { if (array[i] == val) { idx = i; break; } i++; } - if (idx == -1 || i + 1 >= size) + if (idx == std::numeric_limits::max() || i + 1 >= size) return val; return array[i + 1]; } template T get_prev(const T (&array)[size], const T val) { size_t i = size - 1; - size_t idx = -1; - while (idx == -1 && i > 0) { + size_t idx = std::numeric_limits::max(); + while (idx == std::numeric_limits::max() && i > 0) { if (array[i] == val) { idx = i; break; } i--; } - if (idx == -1 || i == 0) + if (idx == std::numeric_limits::max() || i == 0) return val; return array[i - 1]; } @@ -164,7 +165,7 @@ void LTRAlsPsComponent::loop() { break; case State::WAITING_FOR_DATA: - if (this->is_als_data_ready_(this->als_readings_) == DataAvail::DATA_OK) { + if (this->is_als_data_ready_(this->als_readings_) == LtrDataAvail::LTR_DATA_OK) { tries = 0; ESP_LOGV(TAG, "Reading sensor data having gain = %.0fx, time = %d ms", get_gain_coeff(this->als_readings_.gain), get_itime_ms(this->als_readings_.integration_time)); @@ -375,23 +376,23 @@ void LTRAlsPsComponent::configure_integration_time_(IntegrationTime time) { } } -DataAvail LTRAlsPsComponent::is_als_data_ready_(AlsReadings &data) { +LtrDataAvail LTRAlsPsComponent::is_als_data_ready_(AlsReadings &data) { AlsPsStatusRegister als_status{0}; als_status.raw = this->reg((uint8_t) CommandRegisters::ALS_PS_STATUS).get(); if (!als_status.als_new_data) - return DataAvail::NO_DATA; + return LtrDataAvail::LTR_NO_DATA; if (als_status.data_invalid) { ESP_LOGW(TAG, "Data available but not valid"); - return DataAvail::BAD_DATA; + return LtrDataAvail::LTR_BAD_DATA; } ESP_LOGV(TAG, "Data ready, reported gain is %.0f", get_gain_coeff(als_status.gain)); if (data.gain != als_status.gain) { ESP_LOGW(TAG, "Actual gain differs from requested (%.0f)", get_gain_coeff(data.gain)); - return DataAvail::BAD_DATA; + return LtrDataAvail::LTR_BAD_DATA; } - return DataAvail::DATA_OK; + return LtrDataAvail::LTR_DATA_OK; } void LTRAlsPsComponent::read_sensor_data_(AlsReadings &data) { diff --git a/esphome/components/ltr_als_ps/ltr_als_ps.h b/esphome/components/ltr_als_ps/ltr_als_ps.h index 2c768009ab..c6052300de 100644 --- a/esphome/components/ltr_als_ps/ltr_als_ps.h +++ b/esphome/components/ltr_als_ps/ltr_als_ps.h @@ -11,7 +11,7 @@ namespace esphome { namespace ltr_als_ps { -enum DataAvail : uint8_t { NO_DATA, BAD_DATA, DATA_OK }; +enum LtrDataAvail : uint8_t { LTR_NO_DATA, LTR_BAD_DATA, LTR_DATA_OK }; enum LtrType : uint8_t { LTR_TYPE_UNKNOWN = 0, @@ -106,7 +106,7 @@ class LTRAlsPsComponent : public PollingComponent, public i2c::I2CDevice { void configure_als_(); void configure_integration_time_(IntegrationTime time); void configure_gain_(AlsGain gain); - DataAvail is_als_data_ready_(AlsReadings &data); + LtrDataAvail is_als_data_ready_(AlsReadings &data); void read_sensor_data_(AlsReadings &data); bool are_adjustments_required_(AlsReadings &data); void apply_lux_calculation_(AlsReadings &data); diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index a37f4570f3..040661495c 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -1,6 +1,8 @@ +import importlib import logging +import pkgutil -from esphome.automation import build_automation, register_action, validate_automation +from esphome.automation import build_automation, validate_automation import esphome.codegen as cg from esphome.components.const import CONF_COLOR_DEPTH, CONF_DRAW_ROUNDING from esphome.components.display import Display @@ -12,6 +14,7 @@ from esphome.const import ( CONF_GROUP, CONF_ID, CONF_LAMBDA, + CONF_LOG_LEVEL, CONF_ON_BOOT, CONF_ON_IDLE, CONF_PAGES, @@ -24,8 +27,8 @@ from esphome.cpp_generator import MockObj from esphome.final_validate import full_config from esphome.helpers import write_file_if_changed -from . import defines as df, helpers, lv_validation as lvalid -from .automation import disp_update, focused_widgets, refreshed_widgets, update_to_code +from . import defines as df, helpers, lv_validation as lvalid, widgets +from .automation import disp_update, focused_widgets, refreshed_widgets from .defines import add_define from .encoders import ( ENCODERS_CONFIG, @@ -40,29 +43,16 @@ from .lv_validation import lv_bool, lv_images_used from .lvcode import LvContext, LvglComponent, lvgl_static from .schemas import ( DISP_BG_SCHEMA, - FLEX_OBJ_SCHEMA, FULL_STYLE_SCHEMA, - GRID_CELL_SCHEMA, - LAYOUT_SCHEMAS, WIDGET_TYPES, any_widget_schema, container_schema, - create_modify_schema, obj_schema, ) from .styles import add_top_layer, styles_to_code, theme_to_code from .touchscreens import touchscreen_schema, touchscreens_to_code from .trigger import add_on_boot_triggers, generate_triggers -from .types import ( - FontEngine, - IdleTrigger, - ObjUpdateAction, - PauseTrigger, - lv_font_t, - lv_group_t, - lv_style_t, - lvgl_ns, -) +from .types import IdleTrigger, PlainTrigger, lv_font_t, lv_group_t, lv_style_t, lvgl_ns from .widgets import ( LvScrActType, Widget, @@ -71,32 +61,23 @@ from .widgets import ( set_obj_properties, styles_used, ) -from .widgets.animimg import animimg_spec -from .widgets.arc import arc_spec -from .widgets.button import button_spec -from .widgets.buttonmatrix import buttonmatrix_spec -from .widgets.canvas import canvas_spec -from .widgets.checkbox import checkbox_spec -from .widgets.dropdown import dropdown_spec -from .widgets.img import img_spec -from .widgets.keyboard import keyboard_spec -from .widgets.label import label_spec -from .widgets.led import led_spec -from .widgets.line import line_spec -from .widgets.lv_bar import bar_spec -from .widgets.meter import meter_spec + +# Import only what we actually use directly in this file from .widgets.msgbox import MSGBOX_SCHEMA, msgboxes_to_code -from .widgets.obj import obj_spec -from .widgets.page import add_pages, generate_page_triggers, page_spec -from .widgets.qrcode import qr_code_spec -from .widgets.roller import roller_spec -from .widgets.slider import slider_spec -from .widgets.spinbox import spinbox_spec -from .widgets.spinner import spinner_spec -from .widgets.switch import switch_spec -from .widgets.tabview import tabview_spec -from .widgets.textarea import textarea_spec -from .widgets.tileview import tileview_spec +from .widgets.obj import obj_spec # Used in LVGL_SCHEMA +from .widgets.page import ( # page_spec used in LVGL_SCHEMA + add_pages, + generate_page_triggers, + page_spec, +) + +# Widget registration happens via WidgetType.__init__ in individual widget files +# The imports below trigger creation of the widget types +# Action registration (lvgl.{widget}.update) happens automatically +# in the WidgetType.__init__ method + +for module_info in pkgutil.iter_modules(widgets.__path__): + importlib.import_module(f".widgets.{module_info.name}", package=__package__) DOMAIN = "lvgl" DEPENDENCIES = ["display"] @@ -104,51 +85,13 @@ AUTO_LOAD = ["key_provider"] CODEOWNERS = ["@clydebarrow"] LOGGER = logging.getLogger(__name__) -for w_type in ( - label_spec, - obj_spec, - button_spec, - bar_spec, - slider_spec, - arc_spec, - line_spec, - spinner_spec, - led_spec, - animimg_spec, - checkbox_spec, - img_spec, - switch_spec, - tabview_spec, - buttonmatrix_spec, - meter_spec, - dropdown_spec, - roller_spec, - textarea_spec, - spinbox_spec, - keyboard_spec, - tileview_spec, - qr_code_spec, - canvas_spec, -): - WIDGET_TYPES[w_type.name] = w_type -WIDGET_SCHEMA = any_widget_schema() - -LAYOUT_SCHEMAS[df.TYPE_GRID] = { - cv.Optional(df.CONF_WIDGETS): cv.ensure_list(any_widget_schema(GRID_CELL_SCHEMA)) -} -LAYOUT_SCHEMAS[df.TYPE_FLEX] = { - cv.Optional(df.CONF_WIDGETS): cv.ensure_list(any_widget_schema(FLEX_OBJ_SCHEMA)) -} -LAYOUT_SCHEMAS[df.TYPE_NONE] = { - cv.Optional(df.CONF_WIDGETS): cv.ensure_list(any_widget_schema()) -} -for w_type in WIDGET_TYPES.values(): - register_action( - f"lvgl.{w_type.name}.update", - ObjUpdateAction, - create_modify_schema(w_type), - )(update_to_code) +SIMPLE_TRIGGERS = ( + df.CONF_ON_PAUSE, + df.CONF_ON_RESUME, + df.CONF_ON_DRAW_START, + df.CONF_ON_DRAW_END, +) def as_macro(macro, value): @@ -165,7 +108,7 @@ LV_CONF_H_FORMAT = """\ def generate_lv_conf_h(): - definitions = [as_macro(m, v) for m, v in df.lv_defines.items()] + definitions = [as_macro(m, v) for m, v in df.get_data(df.KEY_LV_DEFINES).items()] definitions.sort() return LV_CONF_H_FORMAT.format("\n".join(definitions)) @@ -186,7 +129,7 @@ def multi_conf_validate(configs: list[dict]): base_config = configs[0] for config in configs[1:]: for item in ( - df.CONF_LOG_LEVEL, + CONF_LOG_LEVEL, CONF_COLOR_DEPTH, df.CONF_BYTE_ORDER, df.CONF_TRANSPARENCY_KEY, @@ -197,11 +140,11 @@ def multi_conf_validate(configs: list[dict]): ) -def final_validation(configs): - if len(configs) != 1: - multi_conf_validate(configs) +def final_validation(config_list): + if len(config_list) != 1: + multi_conf_validate(config_list) global_config = full_config.get() - for config in configs: + for config in config_list: if (pages := config.get(CONF_PAGES)) and all(p[df.CONF_SKIP] for p in pages): raise cv.Invalid("At least one page must not be skipped") for display_id in config[df.CONF_DISPLAYS]: @@ -243,10 +186,18 @@ def final_validation(configs): for w in refreshed_widgets: path = global_config.get_path_for_id(w) widget_conf = global_config.get_config_for_path(path[:-1]) - if not any(isinstance(v, Lambda) for v in widget_conf.values()): + if not any(isinstance(v, (Lambda, dict)) for v in widget_conf.values()): raise cv.Invalid( - f"Widget '{w}' does not have any templated properties to refresh", + f"Widget '{w}' does not have any dynamic properties to refresh", ) + # Do per-widget type final validation for update actions + for widget_type, update_configs in df.get_data(df.KEY_UPDATED_WIDGETS).items(): + for conf in update_configs: + for id_conf in conf.get(CONF_ID, ()): + name = id_conf[CONF_ID] + path = global_config.get_path_for_id(name) + widget_conf = global_config.get_config_for_path(path[:-1]) + widget_type.final_validate(name, conf, widget_conf, path[1:]) async def to_code(configs): @@ -269,11 +220,11 @@ async def to_code(configs): add_define( "LV_LOG_LEVEL", - f"LV_LOG_LEVEL_{df.LV_LOG_LEVELS[config_0[df.CONF_LOG_LEVEL]]}", + f"LV_LOG_LEVEL_{df.LV_LOG_LEVELS[config_0[CONF_LOG_LEVEL]]}", ) cg.add_define( "LVGL_LOG_LEVEL", - cg.RawExpression(f"ESPHOME_LOG_LEVEL_{config_0[df.CONF_LOG_LEVEL]}"), + cg.RawExpression(f"ESPHOME_LOG_LEVEL_{config_0[CONF_LOG_LEVEL]}"), ) add_define("LV_COLOR_DEPTH", config_0[CONF_COLOR_DEPTH]) for font in helpers.lv_fonts_used: @@ -293,7 +244,6 @@ async def to_code(configs): cg.add_global(lvgl_ns.using) for font in helpers.esphome_fonts_used: await cg.get_variable(font) - cg.new_Pvariable(ID(f"{font}_engine", True, type=FontEngine), MockObj(font)) default_font = config_0[df.CONF_DEFAULT_FONT] if not lvalid.is_lv_font(default_font): add_define( @@ -305,7 +255,8 @@ async def to_code(configs): type=lv_font_t.operator("ptr").operator("const"), ) cg.new_variable( - globfont_id, MockObj(await lvalid.lv_font.process(default_font)) + globfont_id, + MockObj(await lvalid.lv_font.process(default_font), "->").get_lv_font(), ) add_define("LV_FONT_DEFAULT", df.DEFAULT_ESPHOME_FONT) else: @@ -333,6 +284,7 @@ async def to_code(configs): config[df.CONF_FULL_REFRESH], config[CONF_DRAW_ROUNDING], config[df.CONF_RESUME_ON_INPUT], + config[df.CONF_UPDATE_WHEN_DISPLAY_IDLE], ) await cg.register_component(lv_component, config) Widget.create(config[CONF_ID], lv_component, LvScrActType(), config) @@ -365,22 +317,22 @@ async def to_code(configs): conf[CONF_TRIGGER_ID], lv_component, templ ) await build_automation(idle_trigger, [], conf) - for conf in config.get(df.CONF_ON_PAUSE, ()): - pause_trigger = cg.new_Pvariable( - conf[CONF_TRIGGER_ID], lv_component, True - ) - await build_automation(pause_trigger, [], conf) - for conf in config.get(df.CONF_ON_RESUME, ()): - resume_trigger = cg.new_Pvariable( - conf[CONF_TRIGGER_ID], lv_component, False - ) - await build_automation(resume_trigger, [], conf) + for trigger_name in SIMPLE_TRIGGERS: + if conf := config.get(trigger_name): + trigger_var = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + await build_automation(trigger_var, [], conf) + cg.add( + getattr( + lv_component, + f"set_{trigger_name.removeprefix('on_')}_trigger", + )(trigger_var) + ) await add_on_boot_triggers(config.get(CONF_ON_BOOT, ())) # This must be done after all widgets are created for comp in helpers.lvgl_components_required: cg.add_define(f"USE_LVGL_{comp.upper()}") - if "transform_angle" in styles_used: + if {"transform_angle", "transform_zoom"} & styles_used: add_define("LV_COLOR_SCREEN_TRANSP", "1") for use in helpers.lv_uses: add_define(f"LV_USE_{use.upper()}") @@ -402,10 +354,19 @@ def display_schema(config): def add_hello_world(config): if df.CONF_WIDGETS not in config and CONF_PAGES not in config: LOGGER.info("No pages or widgets configured, creating default hello_world page") - config[df.CONF_WIDGETS] = cv.ensure_list(WIDGET_SCHEMA)(get_hello_world()) + config[df.CONF_WIDGETS] = any_widget_schema()(get_hello_world()) return config +def _theme_schema(value): + return cv.Schema( + { + cv.Optional(name): obj_schema(w).extend(FULL_STYLE_SCHEMA) + for name, w in WIDGET_TYPES.items() + } + )(value) + + FINAL_VALIDATE_SCHEMA = final_validation LVGL_SCHEMA = cv.All( @@ -421,9 +382,12 @@ LVGL_SCHEMA = cv.All( df.CONF_DEFAULT_FONT, default="montserrat_14" ): lvalid.lv_font, cv.Optional(df.CONF_FULL_REFRESH, default=False): cv.boolean, + cv.Optional( + df.CONF_UPDATE_WHEN_DISPLAY_IDLE, default=False + ): cv.boolean, cv.Optional(CONF_DRAW_ROUNDING, default=2): cv.positive_int, cv.Optional(CONF_BUFFER_SIZE, default=0): cv.percentage, - cv.Optional(df.CONF_LOG_LEVEL, default="WARN"): cv.one_of( + cv.Optional(CONF_LOG_LEVEL, default="WARN"): cv.one_of( *df.LV_LOG_LEVELS, upper=True ), cv.Optional(df.CONF_BYTE_ORDER, default="big_endian"): cv.one_of( @@ -442,34 +406,23 @@ LVGL_SCHEMA = cv.All( ), } ), - cv.Optional(df.CONF_ON_PAUSE): validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PauseTrigger), - } - ), - cv.Optional(df.CONF_ON_RESUME): validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PauseTrigger), - } - ), - cv.Exclusive(df.CONF_WIDGETS, CONF_PAGES): cv.ensure_list( - WIDGET_SCHEMA - ), - cv.Exclusive(CONF_PAGES, CONF_PAGES): cv.ensure_list( - container_schema(page_spec) - ), + cv.Optional(CONF_PAGES): cv.ensure_list(container_schema(page_spec)), + **{ + cv.Optional(x): validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PlainTrigger), + }, + single=True, + ) + for x in SIMPLE_TRIGGERS + }, cv.Optional(df.CONF_MSGBOXES): cv.ensure_list(MSGBOX_SCHEMA), cv.Optional(df.CONF_PAGE_WRAP, default=True): lv_bool, cv.Optional(df.CONF_TOP_LAYER): container_schema(obj_spec), cv.Optional( df.CONF_TRANSPARENCY_KEY, default=0x000400 ): lvalid.lv_color, - cv.Optional(df.CONF_THEME): cv.Schema( - { - cv.Optional(name): obj_schema(w).extend(FULL_STYLE_SCHEMA) - for name, w in WIDGET_TYPES.items() - } - ), + cv.Optional(df.CONF_THEME): _theme_schema, cv.Optional(df.CONF_GRADIENTS): GRADIENT_SCHEMA, cv.Optional(df.CONF_TOUCHSCREENS, default=None): touchscreen_schema, cv.Optional(df.CONF_ENCODERS, default=None): ENCODERS_CONFIG, diff --git a/esphome/components/lvgl/automation.py b/esphome/components/lvgl/automation.py index fc70b0f682..9b58727f2a 100644 --- a/esphome/components/lvgl/automation.py +++ b/esphome/components/lvgl/automation.py @@ -137,7 +137,11 @@ async def lvgl_is_idle(config, condition_id, template_arg, args): lvgl = config[CONF_LVGL_ID] timeout = await lv_milliseconds.process(config[CONF_TIMEOUT]) async with LambdaContext(LVGL_COMP_ARG, return_type=cg.bool_) as context: - lv_add(ReturnStatement(lvgl_comp.is_idle(timeout))) + lv_add( + ReturnStatement( + lv_expr.disp_get_inactive_time(lvgl_comp.get_disp()) > timeout + ) + ) var = cg.new_Pvariable( condition_id, TemplateArguments(LvglComponent, *template_arg), @@ -400,7 +404,8 @@ async def obj_refresh_to_code(config, action_id, template_arg, args): # must pass all widget-specific options here, even if not templated, but only do so if at least one is # templated. First filter out common style properties. config = {k: v for k, v in widget.config.items() if k not in ALL_STYLES} - if any(isinstance(v, Lambda) for v in config.values()): + # Check if v is a Lambda or a dict, implying it is dynamic + if any(isinstance(v, (Lambda, dict)) for v in config.values()): await widget.type.to_code(widget, config) if ( widget.type.w_type.value_property is not None diff --git a/esphome/components/lvgl/binary_sensor/__init__.py b/esphome/components/lvgl/binary_sensor/__init__.py index ffbdc977b2..f9df7d23fa 100644 --- a/esphome/components/lvgl/binary_sensor/__init__.py +++ b/esphome/components/lvgl/binary_sensor/__init__.py @@ -31,7 +31,7 @@ async def to_code(config): lvgl_static.add_event_cb( widget.obj, await pressed_ctx.get_lambda(), - LV_EVENT.PRESSING, + LV_EVENT.PRESSED, LV_EVENT.RELEASED, ) ) diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index 206a3d1622..1d528b2f73 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -5,24 +5,42 @@ Constants already defined in esphome.const are not duplicated here and must be i """ import logging +from typing import TYPE_CHECKING, Any from esphome import codegen as cg, config_validation as cv from esphome.const import CONF_ITEMS -from esphome.core import ID, Lambda +from esphome.core import CORE, ID, Lambda from esphome.cpp_generator import LambdaExpression, MockObj from esphome.cpp_types import uint32 from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor +from esphome.types import Expression, SafeExpType from .helpers import requires_component LOGGER = logging.getLogger(__name__) lvgl_ns = cg.esphome_ns.namespace("lvgl") -lv_defines = {} # Dict of #defines to provide as build flags +DOMAIN = "lvgl" +KEY_LV_DEFINES = "lv_defines" +KEY_UPDATED_WIDGETS = "updated_widgets" + + +def get_data(key, default=None): + """ + Get a data structure from the global data store by key + :param key: A key for the data + :param default: The default data - the default is an empty dict + :return: + """ + return CORE.data.setdefault(DOMAIN, {}).setdefault( + key, default if default is not None else {} + ) def add_define(macro, value="1"): - if macro in lv_defines and lv_defines[macro] != value: + lv_defines = get_data(KEY_LV_DEFINES) + value = str(value) + if lv_defines.setdefault(macro, value) != value: LOGGER.error( "Redefinition of %s - was %s now %s", macro, lv_defines[macro], value ) @@ -42,7 +60,13 @@ def static_cast(type, value): def call_lambda(lamb: LambdaExpression): expr = lamb.content.strip() if expr.startswith("return") and expr.endswith(";"): - return expr[6:][:-1].strip() + return expr[6:-1].strip() + # If lambda has parameters, call it with those parameter names + # Parameter names come from hardcoded component code (like "x", "it", "event") + # not from user input, so they're safe to use directly + if lamb.parameters and lamb.parameters.parameters: + param_names = ", ".join(str(param.id) for param in lamb.parameters.parameters) + return f"{lamb}({param_names})" return f"{lamb}()" @@ -65,10 +89,20 @@ class LValidator: return cv.returning_lambda(value) return self.validator(value) - async def process(self, value, args=()): + async def process( + self, value: Any, args: list[tuple[SafeExpType, str]] | None = None + ) -> Expression: if value is None: return None if isinstance(value, Lambda): + # Local import to avoid circular import + from .lvcode import CodeContext, LambdaContext + + if TYPE_CHECKING: + # CodeContext does not have get_automation_parameters + # so we need to assert the type here + assert isinstance(CodeContext.code_context, LambdaContext) + args = args or CodeContext.code_context.get_automation_parameters() return cg.RawExpression( call_lambda( await cg.process_lambda(value, args, return_type=self.rtype) @@ -261,6 +295,8 @@ KEYBOARD_MODES = LvConstant( ) ROLLER_MODES = LvConstant("LV_ROLLER_MODE_", "NORMAL", "INFINITE") TILE_DIRECTIONS = DIRECTIONS.extend("HOR", "VER", "ALL") +SCROLL_DIRECTIONS = TILE_DIRECTIONS.extend("NONE") +SNAP_DIRECTIONS = LvConstant("LV_SCROLL_SNAP_", "NONE", "START", "END", "CENTER") CHILD_ALIGNMENTS = LvConstant( "LV_ALIGN_", "TOP_LEFT", @@ -376,6 +412,8 @@ LV_FLEX_ALIGNMENTS = LvConstant( "SPACE_BETWEEN", ) +LV_FLEX_CROSS_ALIGNMENTS = LV_FLEX_ALIGNMENTS.extend("STRETCH") + LV_MENU_MODES = LvConstant( "LV_MENU_HEADER_", "TOP_FIXED", @@ -418,6 +456,7 @@ CONF_BUTTONS = "buttons" CONF_BYTE_ORDER = "byte_order" CONF_CHANGE_RATE = "change_rate" CONF_CLOSE_BUTTON = "close_button" +CONF_CONTAINER = "container" CONF_CONTROL = "control" CONF_DEFAULT_FONT = "default_font" CONF_DEFAULT_GROUP = "default_group" @@ -451,12 +490,12 @@ CONF_GRID_ROWS = "grid_rows" CONF_HEADER_MODE = "header_mode" CONF_HOME = "home" CONF_INITIAL_FOCUS = "initial_focus" +CONF_SELECTED_DIGIT = "selected_digit" CONF_KEY_CODE = "key_code" CONF_KEYPADS = "keypads" CONF_LAYOUT = "layout" CONF_LEFT_BUTTON = "left_button" CONF_LINE_WIDTH = "line_width" -CONF_LOG_LEVEL = "log_level" CONF_LONG_PRESS_TIME = "long_press_time" CONF_LONG_PRESS_REPEAT_TIME = "long_press_repeat_time" CONF_LVGL_ID = "lvgl_id" @@ -465,6 +504,8 @@ CONF_MSGBOXES = "msgboxes" CONF_OBJ = "obj" CONF_ONE_CHECKED = "one_checked" CONF_ONE_LINE = "one_line" +CONF_ON_DRAW_START = "on_draw_start" +CONF_ON_DRAW_END = "on_draw_end" CONF_ON_PAUSE = "on_pause" CONF_ON_RESUME = "on_resume" CONF_ON_SELECT = "on_select" @@ -486,9 +527,11 @@ CONF_RESUME_ON_INPUT = "resume_on_input" CONF_RIGHT_BUTTON = "right_button" CONF_ROLLOVER = "rollover" CONF_ROOT_BACK_BTN = "root_back_btn" -CONF_ROWS = "rows" CONF_SCALE_LINES = "scale_lines" CONF_SCROLLBAR_MODE = "scrollbar_mode" +CONF_SCROLL_DIR = "scroll_dir" +CONF_SCROLL_SNAP_X = "scroll_snap_x" +CONF_SCROLL_SNAP_Y = "scroll_snap_y" CONF_SELECTED_INDEX = "selected_index" CONF_SELECTED_TEXT = "selected_text" CONF_SHOW_SNOW = "show_snow" @@ -515,6 +558,7 @@ CONF_TOUCHSCREENS = "touchscreens" CONF_TRANSPARENCY_KEY = "transparency_key" CONF_THEME = "theme" CONF_UPDATE_ON_RELEASE = "update_on_release" +CONF_UPDATE_WHEN_DISPLAY_IDLE = "update_when_display_idle" CONF_VISIBLE_ROW_COUNT = "visible_row_count" CONF_WIDGET = "widget" CONF_WIDGETS = "widgets" diff --git a/esphome/components/lvgl/font.cpp b/esphome/components/lvgl/font.cpp deleted file mode 100644 index a0d5127570..0000000000 --- a/esphome/components/lvgl/font.cpp +++ /dev/null @@ -1,76 +0,0 @@ -#include "lvgl_esphome.h" - -#ifdef USE_LVGL_FONT -namespace esphome { -namespace lvgl { - -static const uint8_t *get_glyph_bitmap(const lv_font_t *font, uint32_t unicode_letter) { - auto *fe = (FontEngine *) font->dsc; - const auto *gd = fe->get_glyph_data(unicode_letter); - if (gd == nullptr) - return nullptr; - // esph_log_d(TAG, "Returning bitmap @ %X", (uint32_t)gd->data); - - return gd->data; -} - -static bool get_glyph_dsc_cb(const lv_font_t *font, lv_font_glyph_dsc_t *dsc, uint32_t unicode_letter, uint32_t next) { - auto *fe = (FontEngine *) font->dsc; - const auto *gd = fe->get_glyph_data(unicode_letter); - if (gd == nullptr) - return false; - dsc->adv_w = gd->advance; - dsc->ofs_x = gd->offset_x; - dsc->ofs_y = fe->height - gd->height - gd->offset_y - fe->baseline; - dsc->box_w = gd->width; - dsc->box_h = gd->height; - dsc->is_placeholder = 0; - dsc->bpp = fe->bpp; - return true; -} - -FontEngine::FontEngine(font::Font *esp_font) : font_(esp_font) { - this->bpp = esp_font->get_bpp(); - this->lv_font_.dsc = this; - this->lv_font_.line_height = this->height = esp_font->get_height(); - this->lv_font_.base_line = this->baseline = this->lv_font_.line_height - esp_font->get_baseline(); - this->lv_font_.get_glyph_dsc = get_glyph_dsc_cb; - this->lv_font_.get_glyph_bitmap = get_glyph_bitmap; - this->lv_font_.subpx = LV_FONT_SUBPX_NONE; - this->lv_font_.underline_position = -1; - this->lv_font_.underline_thickness = 1; -} - -const lv_font_t *FontEngine::get_lv_font() { return &this->lv_font_; } - -const font::GlyphData *FontEngine::get_glyph_data(uint32_t unicode_letter) { - if (unicode_letter == last_letter_) - return this->last_data_; - uint8_t unicode[5]; - memset(unicode, 0, sizeof unicode); - if (unicode_letter > 0xFFFF) { - unicode[0] = 0xF0 + ((unicode_letter >> 18) & 0x7); - unicode[1] = 0x80 + ((unicode_letter >> 12) & 0x3F); - unicode[2] = 0x80 + ((unicode_letter >> 6) & 0x3F); - unicode[3] = 0x80 + (unicode_letter & 0x3F); - } else if (unicode_letter > 0x7FF) { - unicode[0] = 0xE0 + ((unicode_letter >> 12) & 0xF); - unicode[1] = 0x80 + ((unicode_letter >> 6) & 0x3F); - unicode[2] = 0x80 + (unicode_letter & 0x3F); - } else if (unicode_letter > 0x7F) { - unicode[0] = 0xC0 + ((unicode_letter >> 6) & 0x1F); - unicode[1] = 0x80 + (unicode_letter & 0x3F); - } else { - unicode[0] = unicode_letter; - } - int match_length; - int glyph_n = this->font_->match_next_glyph(unicode, &match_length); - if (glyph_n < 0) - return nullptr; - this->last_data_ = this->font_->get_glyphs()[glyph_n].get_glyph_data(); - this->last_letter_ = unicode_letter; - return this->last_data_; -} -} // namespace lvgl -} // namespace esphome -#endif // USES_LVGL_FONT diff --git a/esphome/components/lvgl/hello_world.py b/esphome/components/lvgl/hello_world.py index 2c2ec6732c..f85da9d8e4 100644 --- a/esphome/components/lvgl/hello_world.py +++ b/esphome/components/lvgl/hello_world.py @@ -4,49 +4,112 @@ from esphome.yaml_util import parse_yaml CONFIG = """ - obj: - radius: 0 + id: hello_world_card_ pad_all: 12 - bg_color: 0xFFFFFF + bg_color: white height: 100% width: 100% + scrollable: false widgets: - - spinner: - id: hello_world_spinner_ - align: center - indicator: - arc_color: tomato - height: 100 - width: 100 - spin_time: 2s - arc_length: 60deg - - label: - id: hello_world_label_ - text: "Hello World!" + - obj: + align: top_mid + outline_width: 0 + border_width: 0 + pad_all: 4 + scrollable: false + height: size_content + width: 100% + layout: + type: flex + flex_flow: row + flex_align_cross: center + flex_align_track: start + flex_align_main: space_between + widgets: + - button: + checkable: true + radius: 4 + text_font: montserrat_20 + on_click: + lvgl.label.update: + id: hello_world_label_ + text: "Clicked!" + widgets: + - label: + text: "Button" + - label: + id: hello_world_title_ + text: ESPHome + text_font: montserrat_20 + width: 100% + text_align: center + on_boot: + lvgl.widget.refresh: hello_world_title_ + hidden: !lambda |- + return lv_obj_get_width(lv_scr_act()) < 400; + - checkbox: + text: Checkbox + id: hello_world_checkbox_ + on_boot: + lvgl.widget.refresh: hello_world_checkbox_ + hidden: !lambda |- + return lv_obj_get_width(lv_scr_act()) < 240; + on_click: + lvgl.label.update: + id: hello_world_label_ + text: "Checked!" + - obj: + id: hello_world_container_ align: center + y: 14 + pad_all: 0 + outline_width: 0 + border_width: 0 + width: 100% + height: size_content + scrollable: false on_click: lvgl.spinner.update: id: hello_world_spinner_ arc_color: springgreen - - checkbox: - pad_all: 8 - text: Checkbox - align: top_right - on_click: - lvgl.label.update: - id: hello_world_label_ - text: "Checked!" - - button: - pad_all: 8 - checkable: true - align: top_left - text_font: montserrat_20 - on_click: - lvgl.label.update: - id: hello_world_label_ - text: "Clicked!" + layout: + type: flex + flex_flow: row_wrap + flex_align_cross: center + flex_align_track: center + flex_align_main: space_evenly widgets: - - label: - text: "Button" + - spinner: + id: hello_world_spinner_ + indicator: + arc_color: tomato + height: 100 + width: 100 + spin_time: 2s + arc_length: 60deg + widgets: + - label: + id: hello_world_label_ + text: "Hello World!" + align: center + - obj: + id: hello_world_qrcode_ + outline_width: 0 + border_width: 0 + hidden: !lambda |- + return lv_obj_get_width(lv_scr_act()) < 300 && lv_obj_get_height(lv_scr_act()) < 400; + widgets: + - label: + text_font: montserrat_14 + text: esphome.io + align: top_mid + - qrcode: + text: "https://esphome.io" + size: 80 + align: bottom_mid + on_boot: + lvgl.widget.refresh: hello_world_qrcode_ + - slider: width: 80% align: bottom_mid diff --git a/esphome/components/lvgl/helpers.py b/esphome/components/lvgl/helpers.py index 8d5b6354bb..c2bd58f71c 100644 --- a/esphome/components/lvgl/helpers.py +++ b/esphome/components/lvgl/helpers.py @@ -3,6 +3,8 @@ import re from esphome import config_validation as cv from esphome.const import CONF_ARGS, CONF_FORMAT +CONF_IF_NAN = "if_nan" + lv_uses = { "USER_DATA", "LOG", @@ -21,23 +23,48 @@ lv_fonts_used = set() esphome_fonts_used = set() lvgl_components_required = set() - -def validate_printf(value): - cfmt = r""" +# noqa +f_regex = re.compile( + r""" ( # start of capture group 1 % # literal "%" - (?:[-+0 #]{0,5}) # optional flags + [-+0 #]{0,5} # optional flags + (?:\d+|\*)? # width + (?:\.(?:\d+|\*))? # precision + (?:h|l|ll|w|I|I32|I64)? # size + f # type + ) + """, + flags=re.VERBOSE, +) +# noqa +c_regex = re.compile( + r""" + ( # start of capture group 1 + % # literal "%" + [-+0 #]{0,5} # optional flags (?:\d+|\*)? # width (?:\.(?:\d+|\*))? # precision (?:h|l|ll|w|I|I32|I64)? # size [cCdiouxXeEfgGaAnpsSZ] # type ) - """ # noqa - matches = re.findall(cfmt, value[CONF_FORMAT], flags=re.VERBOSE) + """, + flags=re.VERBOSE, +) + + +def validate_printf(value): + format_string = value[CONF_FORMAT] + matches = c_regex.findall(format_string) if len(matches) != len(value[CONF_ARGS]): raise cv.Invalid( f"Found {len(matches)} printf-patterns ({', '.join(matches)}), but {len(value[CONF_ARGS])} args were given!" ) + + if value.get(CONF_IF_NAN) and len(f_regex.findall(format_string)) != 1: + raise cv.Invalid( + "Use of 'if_nan' requires a single valid printf-pattern of type %f" + ) return value diff --git a/esphome/components/lvgl/layout.py b/esphome/components/lvgl/layout.py new file mode 100644 index 0000000000..b27a0b54a2 --- /dev/null +++ b/esphome/components/lvgl/layout.py @@ -0,0 +1,389 @@ +import re +import textwrap + +import esphome.config_validation as cv +from esphome.const import CONF_HEIGHT, CONF_TYPE, CONF_WIDTH + +from .defines import ( + CONF_FLEX_ALIGN_CROSS, + CONF_FLEX_ALIGN_MAIN, + CONF_FLEX_ALIGN_TRACK, + CONF_FLEX_FLOW, + CONF_FLEX_GROW, + CONF_GRID_CELL_COLUMN_POS, + CONF_GRID_CELL_COLUMN_SPAN, + CONF_GRID_CELL_ROW_POS, + CONF_GRID_CELL_ROW_SPAN, + CONF_GRID_CELL_X_ALIGN, + CONF_GRID_CELL_Y_ALIGN, + CONF_GRID_COLUMN_ALIGN, + CONF_GRID_COLUMNS, + CONF_GRID_ROW_ALIGN, + CONF_GRID_ROWS, + CONF_LAYOUT, + CONF_PAD_COLUMN, + CONF_PAD_ROW, + CONF_WIDGETS, + FLEX_FLOWS, + LV_CELL_ALIGNMENTS, + LV_FLEX_ALIGNMENTS, + LV_FLEX_CROSS_ALIGNMENTS, + LV_GRID_ALIGNMENTS, + TYPE_FLEX, + TYPE_GRID, + TYPE_NONE, + LvConstant, +) +from .lv_validation import padding, size + +CONF_MULTIPLE_WIDGETS_PER_CELL = "multiple_widgets_per_cell" + +cell_alignments = LV_CELL_ALIGNMENTS.one_of +grid_alignments = LV_GRID_ALIGNMENTS.one_of +flex_alignments = LV_FLEX_ALIGNMENTS.one_of + +FLEX_LAYOUT_SCHEMA = { + cv.Required(CONF_TYPE): cv.one_of(TYPE_FLEX, lower=True), + cv.Optional(CONF_FLEX_FLOW, default="row_wrap"): FLEX_FLOWS.one_of, + cv.Optional(CONF_FLEX_ALIGN_MAIN, default="start"): flex_alignments, + cv.Optional( + CONF_FLEX_ALIGN_CROSS, default="start" + ): LV_FLEX_CROSS_ALIGNMENTS.one_of, + cv.Optional(CONF_FLEX_ALIGN_TRACK, default="start"): flex_alignments, + cv.Optional(CONF_PAD_ROW): padding, + cv.Optional(CONF_PAD_COLUMN): padding, + cv.Optional(CONF_FLEX_GROW): cv.int_, +} + +FLEX_HV_STYLE = { + CONF_FLEX_ALIGN_MAIN: "LV_FLEX_ALIGN_SPACE_EVENLY", + CONF_FLEX_ALIGN_TRACK: "LV_FLEX_ALIGN_CENTER", + CONF_FLEX_ALIGN_CROSS: "LV_FLEX_ALIGN_CENTER", + CONF_TYPE: TYPE_FLEX, +} + +FLEX_OBJ_SCHEMA = { + cv.Optional(CONF_FLEX_GROW): cv.int_, +} + + +def flex_hv_schema(dir): + dir = CONF_HEIGHT if dir == "horizontal" else CONF_WIDTH + return { + cv.Optional(CONF_FLEX_GROW, default=1): cv.int_, + cv.Optional(dir, default="100%"): size, + } + + +def grid_free_space(value): + value = cv.Upper(value) + if value.startswith("FR(") and value.endswith(")"): + value = value.removesuffix(")").removeprefix("FR(") + return f"LV_GRID_FR({cv.positive_int(value)})" + raise cv.Invalid("must be a size in pixels, CONTENT or FR(nn)") + + +grid_spec = cv.Any(size, LvConstant("LV_GRID_", "CONTENT").one_of, grid_free_space) + +GRID_CELL_SCHEMA = { + cv.Optional(CONF_GRID_CELL_ROW_POS): cv.positive_int, + cv.Optional(CONF_GRID_CELL_COLUMN_POS): cv.positive_int, + cv.Optional(CONF_GRID_CELL_ROW_SPAN, default=1): cv.positive_int, + cv.Optional(CONF_GRID_CELL_COLUMN_SPAN, default=1): cv.positive_int, + cv.Optional(CONF_GRID_CELL_X_ALIGN): grid_alignments, + cv.Optional(CONF_GRID_CELL_Y_ALIGN): grid_alignments, +} + + +class Layout: + """ + Define properties for a layout + The base class is layout "none" + """ + + def get_type(self): + return TYPE_NONE + + def get_layout_schemas(self, config: dict) -> tuple: + """ + Get the layout and child schema for a given widget based on its layout type. + """ + return None, {} + + def validate(self, config): + """ + Validate the layout configuration. This is called late in the schema validation + :param config: The input configuration + :return: The validated configuration + """ + return config + + +class FlexLayout(Layout): + def get_type(self): + return TYPE_FLEX + + def get_layout_schemas(self, config: dict) -> tuple: + layout = config.get(CONF_LAYOUT) + if not isinstance(layout, dict) or layout.get(CONF_TYPE).lower() != TYPE_FLEX: + return None, {} + child_schema = FLEX_OBJ_SCHEMA + if grow := layout.get(CONF_FLEX_GROW): + child_schema = {cv.Optional(CONF_FLEX_GROW, default=grow): cv.int_} + # Polyfill to implement stretch alignment for flex containers + # LVGL does not support this natively, so we add a 100% size property to the children in the cross-axis + if layout.get(CONF_FLEX_ALIGN_CROSS) == "LV_FLEX_ALIGN_STRETCH": + dimension = ( + CONF_WIDTH + if "COLUMN" in layout[CONF_FLEX_FLOW].upper() + else CONF_HEIGHT + ) + child_schema[cv.Optional(dimension, default="100%")] = size + return FLEX_LAYOUT_SCHEMA, child_schema + + def validate(self, config): + """ + Perform validation on the container and its children for this layout + :param config: + :return: + """ + return config + + +class DirectionalLayout(FlexLayout): + def __init__(self, direction: str, flow): + """ + :param direction: "horizontal" or "vertical" + :param flow: "row" or "column" + """ + super().__init__() + self.direction = direction + self.flow = flow + + def get_type(self): + return self.direction + + def get_layout_schemas(self, config: dict) -> tuple: + if not isinstance(config.get(CONF_LAYOUT), str): + return None, {} + if config.get(CONF_LAYOUT, "").lower() != self.direction: + return None, {} + return cv.one_of(self.direction, lower=True), flex_hv_schema(self.direction) + + def validate(self, config): + assert config[CONF_LAYOUT].lower() == self.direction + layout = { + **FLEX_HV_STYLE, + CONF_FLEX_FLOW: "LV_FLEX_FLOW_" + self.flow.upper(), + } + if pad_all := config.get("pad_all"): + layout[CONF_PAD_ROW] = pad_all + layout[CONF_PAD_COLUMN] = pad_all + config[CONF_LAYOUT] = layout + return config + + +class GridLayout(Layout): + _GRID_LAYOUT_REGEX = re.compile(r"^\s*(\d+)\s*x\s*(\d+)\s*$") + + def get_type(self): + return TYPE_GRID + + def get_layout_schemas(self, config: dict) -> tuple: + layout = config.get(CONF_LAYOUT) + if isinstance(layout, str): + if GridLayout._GRID_LAYOUT_REGEX.match(layout): + return ( + cv.string, + { + cv.Optional(CONF_GRID_CELL_ROW_POS): cv.positive_int, + cv.Optional(CONF_GRID_CELL_COLUMN_POS): cv.positive_int, + cv.Optional( + CONF_GRID_CELL_ROW_SPAN, default=1 + ): cv.positive_int, + cv.Optional( + CONF_GRID_CELL_COLUMN_SPAN, default=1 + ): cv.positive_int, + cv.Optional( + CONF_GRID_CELL_X_ALIGN, default="center" + ): grid_alignments, + cv.Optional( + CONF_GRID_CELL_Y_ALIGN, default="center" + ): grid_alignments, + }, + ) + # Not a valid grid layout string + return None, {} + + if not isinstance(layout, dict) or layout.get(CONF_TYPE).lower() != TYPE_GRID: + return None, {} + return ( + { + cv.Required(CONF_TYPE): cv.one_of(TYPE_GRID, lower=True), + cv.Required(CONF_GRID_ROWS): [grid_spec], + cv.Required(CONF_GRID_COLUMNS): [grid_spec], + cv.Optional(CONF_GRID_COLUMN_ALIGN): grid_alignments, + cv.Optional(CONF_GRID_ROW_ALIGN): grid_alignments, + cv.Optional(CONF_PAD_ROW): padding, + cv.Optional(CONF_PAD_COLUMN): padding, + cv.Optional(CONF_MULTIPLE_WIDGETS_PER_CELL, default=False): cv.boolean, + }, + { + cv.Optional(CONF_GRID_CELL_ROW_POS): cv.positive_int, + cv.Optional(CONF_GRID_CELL_COLUMN_POS): cv.positive_int, + cv.Optional(CONF_GRID_CELL_ROW_SPAN, default=1): cv.positive_int, + cv.Optional(CONF_GRID_CELL_COLUMN_SPAN, default=1): cv.positive_int, + cv.Optional(CONF_GRID_CELL_X_ALIGN): grid_alignments, + cv.Optional(CONF_GRID_CELL_Y_ALIGN): grid_alignments, + }, + ) + + def validate(self, config: dict): + """ + Validate the grid layout. + The `layout:` key may be a dictionary with `rows` and `columns` keys, or a string in the format "rows x columns". + Either all cells must have a row and column, + or none, in which case the grid layout is auto-generated. + :param config: + :return: The config updated with auto-generated values + """ + layout = config.get(CONF_LAYOUT) + if isinstance(layout, str): + # If the layout is a string, assume it is in the format "rows x columns", implying + # a grid layout with the specified number of rows and columns each with CONTENT sizing. + layout = layout.strip() + match = GridLayout._GRID_LAYOUT_REGEX.match(layout) + if match: + rows = int(match.group(1)) + cols = int(match.group(2)) + layout = { + CONF_TYPE: TYPE_GRID, + CONF_GRID_ROWS: ["LV_GRID_FR(1)"] * rows, + CONF_GRID_COLUMNS: ["LV_GRID_FR(1)"] * cols, + } + config[CONF_LAYOUT] = layout + else: + raise cv.Invalid( + f"Invalid grid layout format: {config}, expected 'rows x columns'", + [CONF_LAYOUT], + ) + # should be guaranteed to be a dict at this point + assert isinstance(layout, dict) + assert layout.get(CONF_TYPE).lower() == TYPE_GRID + allow_multiple = layout.get(CONF_MULTIPLE_WIDGETS_PER_CELL, False) + rows = len(layout[CONF_GRID_ROWS]) + columns = len(layout[CONF_GRID_COLUMNS]) + used_cells = [[None] * columns for _ in range(rows)] + for index, widget in enumerate(config.get(CONF_WIDGETS, [])): + _, w = next(iter(widget.items())) + if (CONF_GRID_CELL_COLUMN_POS in w) != (CONF_GRID_CELL_ROW_POS in w): + raise cv.Invalid( + "Both row and column positions must be specified, or both omitted", + [CONF_WIDGETS, index], + ) + if CONF_GRID_CELL_ROW_POS in w: + row = w[CONF_GRID_CELL_ROW_POS] + column = w[CONF_GRID_CELL_COLUMN_POS] + else: + try: + row, column = next( + (r_idx, c_idx) + for r_idx, row in enumerate(used_cells) + for c_idx, value in enumerate(row) + if value is None + ) + except StopIteration: + raise cv.Invalid( + "No free cells available in grid layout", [CONF_WIDGETS, index] + ) from None + w[CONF_GRID_CELL_ROW_POS] = row + w[CONF_GRID_CELL_COLUMN_POS] = column + + for i in range(w[CONF_GRID_CELL_ROW_SPAN]): + for j in range(w[CONF_GRID_CELL_COLUMN_SPAN]): + if row + i >= rows or column + j >= columns: + raise cv.Invalid( + f"Cell at {row}/{column} span {w[CONF_GRID_CELL_ROW_SPAN]}x{w[CONF_GRID_CELL_COLUMN_SPAN]} " + f"exceeds grid size {rows}x{columns}", + [CONF_WIDGETS, index], + ) + if ( + not allow_multiple + and used_cells[row + i][column + j] is not None + ): + raise cv.Invalid( + f"Cell span {row + i}/{column + j} already occupied by widget at index {used_cells[row + i][column + j]}", + [CONF_WIDGETS, index], + ) + used_cells[row + i][column + j] = index + + return config + + +LAYOUT_CLASSES = ( + FlexLayout(), + GridLayout(), + DirectionalLayout("horizontal", "row"), + DirectionalLayout("vertical", "column"), +) +LAYOUT_CHOICES = [x.get_type() for x in LAYOUT_CLASSES] + + +def append_layout_schema(schema, config: dict): + """ + Get the child layout schema for a given widget based on its layout type. + :param config: The config to check + :return: A schema for the layout including a widgets key + """ + # Local import to avoid circular dependencies + if CONF_WIDGETS not in config: + if CONF_LAYOUT in config: + raise cv.Invalid( + f"Layout {config[CONF_LAYOUT]} requires a {CONF_WIDGETS} key", + [CONF_LAYOUT], + ) + return schema + + from .schemas import any_widget_schema + + if CONF_LAYOUT not in config: + # If no layout is specified, return the schema as is + return schema.extend({cv.Optional(CONF_WIDGETS): any_widget_schema()}) + layout = config[CONF_LAYOUT] + # Sanity check the layout to avoid redundant checks in each type + if not isinstance(layout, str) and not isinstance(layout, dict): + raise cv.Invalid( + "The 'layout' option must be a string or a dictionary", [CONF_LAYOUT] + ) + if isinstance(layout, dict) and not isinstance(layout.get(CONF_TYPE), str): + raise cv.Invalid( + "Invalid layout type; must be a string ('flex' or 'grid')", + [CONF_LAYOUT, CONF_TYPE], + ) + + for layout_class in LAYOUT_CLASSES: + layout_schema, child_schema = layout_class.get_layout_schemas(config) + if layout_schema: + layout_schema = cv.Schema( + { + cv.Required(CONF_LAYOUT): layout_schema, + cv.Required(CONF_WIDGETS): any_widget_schema(child_schema), + } + ) + layout_schema.add_extra(layout_class.validate) + return layout_schema.extend(schema) + + if isinstance(layout, dict): + raise cv.Invalid( + "Invalid layout type; must be 'flex' or 'grid'", [CONF_LAYOUT, CONF_TYPE] + ) + raise cv.Invalid( + textwrap.dedent( + """ + Invalid 'layout' value + layout choices are 'horizontal', 'vertical', 'x', + or a dictionary with a 'type' key + """ + ), + [CONF_LAYOUT], + ) diff --git a/esphome/components/lvgl/lv_validation.py b/esphome/components/lvgl/lv_validation.py index 5a1b99cf7c..9c1dd22085 100644 --- a/esphome/components/lvgl/lv_validation.py +++ b/esphome/components/lvgl/lv_validation.py @@ -1,3 +1,6 @@ +import re +from typing import TYPE_CHECKING, Any + import esphome.codegen as cg from esphome.components import image from esphome.components.color import CONF_HEX, ColorStruct, from_rgbw @@ -17,6 +20,7 @@ from esphome.cpp_generator import MockObj from esphome.cpp_types import ESPTime, int32, uint32 from esphome.helpers import cpp_string_escape from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor +from esphome.types import Expression, SafeExpType from . import types as ty from .defines import ( @@ -29,8 +33,14 @@ from .defines import ( call_lambda, literal, ) -from .helpers import add_lv_use, esphome_fonts_used, lv_fonts_used, requires_component -from .types import lv_font_t, lv_gradient_t +from .helpers import ( + CONF_IF_NAN, + add_lv_use, + esphome_fonts_used, + lv_fonts_used, + requires_component, +) +from .types import lv_gradient_t opacity_consts = LvConstant("LV_OPA_", "TRANSP", "COVER") @@ -243,6 +253,8 @@ def pixels_or_percent_validator(value): return ["pixels", "..%"] if isinstance(value, str) and value.lower().endswith("px"): value = cv.int_(value[:-2]) + if isinstance(value, str) and re.match(r"^lv_pct\((\d+)\)$", value): + return value value = cv.Any(cv.int_, cv.percentage)(value) if isinstance(value, int): return value @@ -287,10 +299,14 @@ def angle(value): :param value: The input in the range 0..360 :return: An angle in 1/10 degree units. """ - return int(cv.float_range(0.0, 360.0)(cv.angle(value)) * 10) + return cv.float_range(0.0, 360.0)(cv.angle(value)) -lv_angle = LValidator(angle, uint32) +# Validator for angles in LVGL expressed in 1/10 degree units. +lv_angle = LValidator(angle, uint32, retmapper=lambda x: int(x * 10)) + +# Validator for angles in LVGL expressed in whole degrees +lv_angle_degrees = LValidator(angle, uint32, retmapper=int) @schema_extractor("one_of") @@ -384,13 +400,31 @@ class TextValidator(LValidator): return value return super().__call__(value) - async def process(self, value, args=()): + async def process( + self, value: Any, args: list[tuple[SafeExpType, str]] | None = None + ) -> Expression: + # Local import to avoid circular import at module level + + from .lvcode import CodeContext, LambdaContext + + if TYPE_CHECKING: + # CodeContext does not have get_automation_parameters + # so we need to assert the type here + assert isinstance(CodeContext.code_context, LambdaContext) + args = args or CodeContext.code_context.get_automation_parameters() + if isinstance(value, dict): if format_str := value.get(CONF_FORMAT): - args = [str(x) for x in value[CONF_ARGS]] - arg_expr = cg.RawExpression(",".join(args)) + str_args = [str(x) for x in value[CONF_ARGS]] + arg_expr = cg.RawExpression(",".join(str_args)) format_str = cpp_string_escape(format_str) - return literal(f"str_sprintf({format_str}, {arg_expr}).c_str()") + sprintf_str = f"str_sprintf({format_str}, {arg_expr}).c_str()" + if nanval := value.get(CONF_IF_NAN): + nanval = cpp_string_escape(nanval) + return literal( + f"(std::isfinite({arg_expr}) ? {sprintf_str} : {nanval})" + ) + return literal(sprintf_str) if time_format := value.get(CONF_TIME_FORMAT): source = value[CONF_TIME] if isinstance(source, Lambda): @@ -459,16 +493,21 @@ class LvFont(LValidator): return LV_FONTS if is_lv_font(value): return lv_builtin_font(value) + add_lv_use("font") fontval = cv.use_id(Font)(value) esphome_fonts_used.add(fontval) return requires_component("font")(fontval) - super().__init__(validator, lv_font_t) + # Use font::Font* as return type for lambdas returning ESPHome fonts + # The inline overloads in lvgl_esphome.h handle conversion to lv_font_t* + super().__init__(validator, Font.operator("ptr")) async def process(self, value, args=()): if is_lv_font(value): return literal(f"&lv_font_{value}") - return literal(f"{value}_engine->get_lv_font()") + if isinstance(value, str): + return literal(f"{value}") + return await super().process(value, args) lv_font = LvFont() diff --git a/esphome/components/lvgl/lvcode.py b/esphome/components/lvgl/lvcode.py index 7a5c35f896..c11597131f 100644 --- a/esphome/components/lvgl/lvcode.py +++ b/esphome/components/lvgl/lvcode.py @@ -164,6 +164,9 @@ class LambdaContext(CodeContext): code_text.append(text) return code_text + def get_automation_parameters(self) -> list[tuple[SafeExpType, str]]: + return self.parameters + async def __aenter__(self): await super().__aenter__() add_line_marks(self.where) @@ -178,9 +181,8 @@ class LvContext(LambdaContext): added_lambda_count = 0 - def __init__(self, args=None): - self.args = args or LVGL_COMP_ARG - super().__init__(parameters=self.args) + def __init__(self): + super().__init__(parameters=LVGL_COMP_ARG) async def __aexit__(self, exc_type, exc_val, exc_tb): await super().__aexit__(exc_type, exc_val, exc_tb) @@ -189,6 +191,11 @@ class LvContext(LambdaContext): cg.add(expression) return expression + def get_automation_parameters(self) -> list[tuple[SafeExpType, str]]: + # When generating automations, we don't want the `lv_component` parameter to be passed + # to the lambda. + return [] + def __call__(self, *args): return self.add(*args) @@ -292,6 +299,7 @@ class LvExpr(MockLv): # Top level mock for generic lv_ calls to be recorded lv = MockLv("lv_") +LV = MockLv("LV_") # Just generate an expression lv_expr = LvExpr("lv_") # Mock for lv_obj_ calls @@ -320,7 +328,7 @@ def lv_assign(target, expression): lv_add(AssignmentExpression("", "", target, expression)) -def lv_Pvariable(type, name): +def lv_Pvariable(type, name) -> MockObj: """ Create but do not initialise a pointer variable :param type: Type of the variable target @@ -336,7 +344,7 @@ def lv_Pvariable(type, name): return var -def lv_variable(type, name): +def lv_variable(type, name) -> MockObj: """ Create but do not initialise a variable :param type: Type of the variable target diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index 32930ddec4..18226a9f57 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -82,6 +82,18 @@ static void rounder_cb(lv_disp_drv_t *disp_drv, lv_area_t *area) { area->y2 = (area->y2 + draw_rounding) / draw_rounding * draw_rounding - 1; } +void LvglComponent::monitor_cb(lv_disp_drv_t *disp_drv, uint32_t time, uint32_t px) { + ESP_LOGVV(TAG, "Draw end: %" PRIu32 " pixels in %" PRIu32 " ms", px, time); + auto *comp = static_cast(disp_drv->user_data); + comp->draw_end_(); +} + +void LvglComponent::render_start_cb(lv_disp_drv_t *disp_drv) { + ESP_LOGVV(TAG, "Draw start"); + auto *comp = static_cast(disp_drv->user_data); + comp->draw_start_(); +} + lv_event_code_t lv_api_event; // NOLINT lv_event_code_t lv_update_event; // NOLINT void LvglComponent::dump_config() { @@ -94,6 +106,7 @@ void LvglComponent::dump_config() { this->disp_drv_.hor_res, this->disp_drv_.ver_res, 100 / this->buffer_frac_, this->rotation, (int) this->draw_rounding); } + void LvglComponent::set_paused(bool paused, bool show_snow) { this->paused_ = paused; this->show_snow_ = show_snow; @@ -101,7 +114,10 @@ void LvglComponent::set_paused(bool paused, bool show_snow) { lv_disp_trig_activity(this->disp_); // resets the inactivity time lv_obj_invalidate(lv_scr_act()); } - this->pause_callbacks_.call(paused); + if (paused && this->pause_callback_ != nullptr) + this->pause_callback_->trigger(); + if (!paused && this->resume_callback_ != nullptr) + this->resume_callback_->trigger(); } void LvglComponent::esphome_lvgl_init() { @@ -109,32 +125,38 @@ void LvglComponent::esphome_lvgl_init() { lv_update_event = static_cast(lv_event_register_id()); lv_api_event = static_cast(lv_event_register_id()); } + void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event) { lv_obj_add_event_cb(obj, callback, event, nullptr); } + void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2) { add_event_cb(obj, callback, event1); add_event_cb(obj, callback, event2); } + void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2, lv_event_code_t event3) { add_event_cb(obj, callback, event1); add_event_cb(obj, callback, event2); add_event_cb(obj, callback, event3); } + void LvglComponent::add_page(LvPageType *page) { this->pages_.push_back(page); page->set_parent(this); lv_disp_set_default(this->disp_); page->setup(this->pages_.size() - 1); } + void LvglComponent::show_page(size_t index, lv_scr_load_anim_t anim, uint32_t time) { if (index >= this->pages_.size()) return; this->current_page_ = index; lv_scr_load_anim(this->pages_[this->current_page_]->obj, anim, time, 0, false); } + void LvglComponent::show_next_page(lv_scr_load_anim_t anim, uint32_t time) { if (this->pages_.empty() || (this->current_page_ == this->pages_.size() - 1 && !this->page_wrap_)) return; @@ -143,6 +165,7 @@ void LvglComponent::show_next_page(lv_scr_load_anim_t anim, uint32_t time) { } while (this->pages_[this->current_page_]->skip); // skip empty pages() this->show_page(this->current_page_, anim, time); } + void LvglComponent::show_prev_page(lv_scr_load_anim_t anim, uint32_t time) { if (this->pages_.empty() || (this->current_page_ == 0 && !this->page_wrap_)) return; @@ -151,11 +174,14 @@ void LvglComponent::show_prev_page(lv_scr_load_anim_t anim, uint32_t time) { } while (this->pages_[this->current_page_]->skip); // skip empty pages() this->show_page(this->current_page_, anim, time); } + size_t LvglComponent::get_current_page() const { return this->current_page_; } bool LvPageType::is_showing() const { return this->parent_->get_current_page() == this->index; } + void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) { auto width = lv_area_get_width(area); auto height = lv_area_get_height(area); + auto height_rounded = (height + this->draw_rounding - 1) / this->draw_rounding * this->draw_rounding; auto x1 = area->x1; auto y1 = area->y1; lv_color_t *dst = this->rotate_buf_; @@ -163,13 +189,13 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) { case display::DISPLAY_ROTATION_90_DEGREES: for (lv_coord_t x = height; x-- != 0;) { for (lv_coord_t y = 0; y != width; y++) { - dst[y * height + x] = *ptr++; + dst[y * height_rounded + x] = *ptr++; } } y1 = x1; x1 = this->disp_drv_.ver_res - area->y1 - height; - width = height; - height = lv_area_get_width(area); + height = width; + width = height_rounded; break; case display::DISPLAY_ROTATION_180_DEGREES: @@ -185,13 +211,13 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) { case display::DISPLAY_ROTATION_270_DEGREES: for (lv_coord_t x = 0; x != height; x++) { for (lv_coord_t y = width; y-- != 0;) { - dst[y * height + x] = *ptr++; + dst[y * height_rounded + x] = *ptr++; } } x1 = y1; y1 = this->disp_drv_.hor_res - area->x1 - width; - width = height; - height = lv_area_get_width(area); + height = width; + width = height_rounded; break; default: @@ -206,7 +232,7 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) { } void LvglComponent::flush_cb_(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) { - if (!this->paused_) { + if (!this->is_paused()) { auto now = millis(); this->draw_buffer_(area, color_p); ESP_LOGVV(TAG, "flush_cb, area=%d/%d, %d/%d took %dms", area->x1, area->y1, lv_area_get_width(area), @@ -214,6 +240,7 @@ void LvglComponent::flush_cb_(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv } lv_disp_flush_ready(disp_drv); } + IdleTrigger::IdleTrigger(LvglComponent *parent, TemplatableValue timeout) : timeout_(std::move(timeout)) { parent->add_on_idle_callback([this](uint32_t idle_time) { if (!this->is_idle_ && idle_time > this->timeout_.value()) { @@ -225,13 +252,6 @@ IdleTrigger::IdleTrigger(LvglComponent *parent, TemplatableValue timeo }); } -PauseTrigger::PauseTrigger(LvglComponent *parent, TemplatableValue paused) : paused_(std::move(paused)) { - parent->add_on_pause_callback([this](bool pausing) { - if (this->paused_.value() == pausing) - this->trigger(); - }); -} - #ifdef USE_LVGL_TOUCHSCREEN LVTouchListener::LVTouchListener(uint16_t long_press_time, uint16_t long_press_repeat_time, LvglComponent *parent) { this->set_parent(parent); @@ -368,6 +388,27 @@ void LvKeyboardType::set_obj(lv_obj_t *lv_obj) { } #endif // USE_LVGL_KEYBOARD +void LvglComponent::draw_end_() { + if (this->draw_end_callback_ != nullptr) + this->draw_end_callback_->trigger(); + if (this->update_when_display_idle_) { + for (auto *disp : this->displays_) + disp->update(); + } +} + +bool LvglComponent::is_paused() const { + if (this->paused_) + return true; + if (this->update_when_display_idle_) { + for (auto *disp : this->displays_) { + if (!disp->is_idle()) + return true; + } + } + return false; +} + void LvglComponent::write_random_() { int iterations = 6 - lv_disp_get_inactive_time(this->disp_) / 60000; if (iterations <= 0) @@ -417,12 +458,13 @@ void LvglComponent::write_random_() { * presses a key or clicks on the screen. */ LvglComponent::LvglComponent(std::vector displays, float buffer_frac, bool full_refresh, - int draw_rounding, bool resume_on_input) + int draw_rounding, bool resume_on_input, bool update_when_display_idle) : draw_rounding(draw_rounding), displays_(std::move(displays)), buffer_frac_(buffer_frac), full_refresh_(full_refresh), - resume_on_input_(resume_on_input) { + resume_on_input_(resume_on_input), + update_when_display_idle_(update_when_display_idle) { lv_disp_draw_buf_init(&this->draw_buf_, nullptr, nullptr, 0); lv_disp_drv_init(&this->disp_drv_); this->disp_drv_.draw_buf = &this->draw_buf_; @@ -435,8 +477,10 @@ LvglComponent::LvglComponent(std::vector displays, float buf void LvglComponent::setup() { auto *display = this->displays_[0]; - auto width = display->get_width(); - auto height = display->get_height(); + auto rounding = this->draw_rounding; + // cater for displays with dimensions that don't divide by the required rounding + auto width = (display->get_width() + rounding - 1) / rounding * rounding; + auto height = (display->get_height() + rounding - 1) / rounding * rounding; auto frac = this->buffer_frac_; if (frac == 0) frac = 1; @@ -451,28 +495,34 @@ void LvglComponent::setup() { if (buffer == nullptr && this->buffer_frac_ == 0) { frac = MIN_BUFFER_FRAC; buffer_pixels /= MIN_BUFFER_FRAC; - buffer = lv_custom_mem_alloc(buf_bytes / MIN_BUFFER_FRAC); // NOLINT + buf_bytes /= MIN_BUFFER_FRAC; + buffer = lv_custom_mem_alloc(buf_bytes); // NOLINT } if (buffer == nullptr) { - this->status_set_error("Memory allocation failure"); + this->status_set_error(LOG_STR("Memory allocation failure")); this->mark_failed(); return; } this->buffer_frac_ = frac; lv_disp_draw_buf_init(&this->draw_buf_, buffer, nullptr, buffer_pixels); - this->disp_drv_.hor_res = width; - this->disp_drv_.ver_res = height; - // this->setup_driver_(display->get_width(), display->get_height()); + this->disp_drv_.hor_res = display->get_width(); + this->disp_drv_.ver_res = display->get_height(); lv_disp_drv_update(this->disp_, &this->disp_drv_); this->rotation = display->get_rotation(); if (this->rotation != display::DISPLAY_ROTATION_0_DEGREES) { this->rotate_buf_ = static_cast(lv_custom_mem_alloc(buf_bytes)); // NOLINT if (this->rotate_buf_ == nullptr) { - this->status_set_error("Memory allocation failure"); + this->status_set_error(LOG_STR("Memory allocation failure")); this->mark_failed(); return; } } + if (this->draw_start_callback_ != nullptr) { + this->disp_drv_.render_start_cb = render_start_cb; + } + if (this->draw_end_callback_ != nullptr || this->update_when_display_idle_) { + this->disp_drv_.monitor_cb = monitor_cb; + } #if LV_USE_LOG lv_log_register_print_cb([](const char *buf) { auto next = strchr(buf, ')'); @@ -492,17 +542,19 @@ void LvglComponent::setup() { void LvglComponent::update() { // update indicators - if (this->paused_) { + if (this->is_paused()) { return; } this->idle_callbacks_.call(lv_disp_get_inactive_time(this->disp_)); } + void LvglComponent::loop() { - if (this->paused_) { - if (this->show_snow_) + if (this->is_paused()) { + if (this->paused_ && this->show_snow_) this->write_random_(); + } else { + lv_timer_handler_run_in_period(5); } - lv_timer_handler_run_in_period(5); } #ifdef USE_LVGL_ANIMIMG diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 3ae67e8a0b..9c82f3646b 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -50,6 +50,14 @@ static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BIT static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BITNESS_332; #endif // LV_COLOR_DEPTH +#ifdef USE_LVGL_FONT +inline void lv_obj_set_style_text_font(lv_obj_t *obj, const font::Font *font, lv_style_selector_t part) { + lv_obj_set_style_text_font(obj, font->get_lv_font(), part); +} +inline void lv_style_set_text_font(lv_style_t *style, const font::Font *font) { + lv_style_set_text_font(style, font->get_lv_font()); +} +#endif #ifdef USE_LVGL_IMAGE // Shortcut / overload, so that the source of an image can easily be updated // from within a lambda. @@ -129,29 +137,11 @@ template class ObjUpdateAction : public Action { public: explicit ObjUpdateAction(std::function &&lamb) : lamb_(std::move(lamb)) {} - void play(Ts... x) override { this->lamb_(x...); } + void play(const Ts &...x) override { this->lamb_(x...); } protected: std::function lamb_; }; -#ifdef USE_LVGL_FONT -class FontEngine { - public: - FontEngine(font::Font *esp_font); - const lv_font_t *get_lv_font(); - - const font::GlyphData *get_glyph_data(uint32_t unicode_letter); - uint16_t baseline{}; - uint16_t height{}; - uint8_t bpp{}; - - protected: - font::Font *font_{}; - uint32_t last_letter_{}; - const font::GlyphData *last_data_{}; - lv_font_t lv_font_{}; -}; -#endif // USE_LVGL_FONT #ifdef USE_LVGL_ANIMIMG void lv_animimg_stop(lv_obj_t *obj); #endif // USE_LVGL_ANIMIMG @@ -161,7 +151,7 @@ class LvglComponent : public PollingComponent { public: LvglComponent(std::vector displays, float buffer_frac, bool full_refresh, int draw_rounding, - bool resume_on_input); + bool resume_on_input, bool update_when_display_idle); static void static_flush_cb(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p); float get_setup_priority() const override { return setup_priority::PROCESSOR; } @@ -171,16 +161,19 @@ class LvglComponent : public PollingComponent { void add_on_idle_callback(std::function &&callback) { this->idle_callbacks_.add(std::move(callback)); } - void add_on_pause_callback(std::function &&callback) { this->pause_callbacks_.add(std::move(callback)); } + + static void monitor_cb(lv_disp_drv_t *disp_drv, uint32_t time, uint32_t px); + static void render_start_cb(lv_disp_drv_t *disp_drv); void dump_config() override; - bool is_idle(uint32_t idle_ms) { return lv_disp_get_inactive_time(this->disp_) > idle_ms; } lv_disp_t *get_disp() { return this->disp_; } lv_obj_t *get_scr_act() { return lv_disp_get_scr_act(this->disp_); } // Pause or resume the display. // @param paused If true, pause the display. If false, resume the display. // @param show_snow If true, show the snow effect when paused. void set_paused(bool paused, bool show_snow); - bool is_paused() const { return this->paused_; } + + // Returns true if the display is explicitly paused, or a blocking display update is in progress. + bool is_paused() const; // If the display is paused and we have resume_on_input_ set to true, resume the display. void maybe_wakeup() { if (this->paused_ && this->resume_on_input_) { @@ -213,16 +206,25 @@ class LvglComponent : public PollingComponent { size_t draw_rounding{2}; display::DisplayRotation rotation{display::DISPLAY_ROTATION_0_DEGREES}; + void set_pause_trigger(Trigger<> *trigger) { this->pause_callback_ = trigger; } + void set_resume_trigger(Trigger<> *trigger) { this->resume_callback_ = trigger; } + void set_draw_start_trigger(Trigger<> *trigger) { this->draw_start_callback_ = trigger; } + void set_draw_end_trigger(Trigger<> *trigger) { this->draw_end_callback_ = trigger; } protected: + void draw_end_(); + // Not checking for non-null callback since the + // LVGL callback that calls it is not set in that case + void draw_start_() const { this->draw_start_callback_->trigger(); } + void write_random_(); void draw_buffer_(const lv_area_t *area, lv_color_t *ptr); void flush_cb_(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p); - std::vector displays_{}; size_t buffer_frac_{1}; bool full_refresh_{}; bool resume_on_input_{}; + bool update_when_display_idle_{}; lv_disp_draw_buf_t draw_buf_{}; lv_disp_drv_t disp_drv_{}; @@ -235,7 +237,10 @@ class LvglComponent : public PollingComponent { std::map focus_marks_{}; CallbackManager idle_callbacks_{}; - CallbackManager pause_callbacks_{}; + Trigger<> *pause_callback_{}; + Trigger<> *resume_callback_{}; + Trigger<> *draw_start_callback_{}; + Trigger<> *draw_end_callback_{}; lv_color_t *rotate_buf_{}; }; @@ -248,18 +253,10 @@ class IdleTrigger : public Trigger<> { bool is_idle_{}; }; -class PauseTrigger : public Trigger<> { - public: - explicit PauseTrigger(LvglComponent *parent, TemplatableValue paused); - - protected: - TemplatableValue paused_; -}; - template class LvglAction : public Action, public Parented { public: explicit LvglAction(std::function &&lamb) : action_(std::move(lamb)) {} - void play(Ts... x) override { this->action_(this->parent_); } + void play(const Ts &...x) override { this->action_(this->parent_); } protected: std::function action_{}; @@ -268,7 +265,7 @@ template class LvglAction : public Action, public Parente template class LvglCondition : public Condition, public Parented { public: LvglCondition(std::function &&condition_lambda) : condition_lambda_(std::move(condition_lambda)) {} - bool check(Ts... x) override { return this->condition_lambda_(this->parent_); } + bool check(const Ts &...x) override { return this->condition_lambda_(this->parent_); } protected: std::function condition_lambda_{}; @@ -358,7 +355,7 @@ class LvSelectable : public LvCompound { virtual void set_selected_index(size_t index, lv_anim_enable_t anim) = 0; void set_selected_text(const std::string &text, lv_anim_enable_t anim); std::string get_selected_text(); - std::vector get_options() { return this->options_; } + const std::vector &get_options() { return this->options_; } void set_options(std::vector options); protected: diff --git a/esphome/components/lvgl/number/lvgl_number.h b/esphome/components/lvgl/number/lvgl_number.h index 277494673b..7bc44c9e20 100644 --- a/esphome/components/lvgl/number/lvgl_number.h +++ b/esphome/components/lvgl/number/lvgl_number.h @@ -21,7 +21,7 @@ class LVGLNumber : public number::Number, public Component { void setup() override { float value = this->value_lambda_(); if (this->restore_) { - this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); + this->pref_ = global_preferences->make_preference(this->get_preference_hash()); if (this->pref_.load(&value)) { this->control_lambda_(value); } diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index 959d203c41..45d933c00e 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -1,6 +1,9 @@ +from collections.abc import Callable + from esphome import config_validation as cv from esphome.automation import Trigger, validate_automation from esphome.components.time import RealTimeClock +from esphome.config_validation import prepend_path from esphome.const import ( CONF_ARGS, CONF_FORMAT, @@ -12,17 +15,28 @@ from esphome.const import ( CONF_TEXT, CONF_TIME, CONF_TRIGGER_ID, - CONF_TYPE, CONF_X, CONF_Y, ) from esphome.core import TimePeriod from esphome.core.config import StartupTrigger -from esphome.schema_extractors import SCHEMA_EXTRACT from . import defines as df, lv_validation as lvalid -from .defines import CONF_TIME_FORMAT, LV_GRAD_DIR, TYPE_GRID -from .helpers import add_lv_use, requires_component, validate_printf +from .defines import ( + CONF_SCROLL_DIR, + CONF_SCROLL_SNAP_X, + CONF_SCROLL_SNAP_Y, + CONF_SCROLLBAR_MODE, + CONF_TIME_FORMAT, + LV_GRAD_DIR, +) +from .helpers import CONF_IF_NAN, requires_component, validate_printf +from .layout import ( + FLEX_OBJ_SCHEMA, + GRID_CELL_SCHEMA, + append_layout_schema, + grid_alignments, +) from .lv_validation import lv_color, lv_font, lv_gradient, lv_image, opacity from .lvcode import LvglComponent, lv_event_t_ptr from .types import ( @@ -50,6 +64,7 @@ PRINTF_TEXT_SCHEMA = cv.All( { cv.Required(CONF_FORMAT): cv.string, cv.Optional(CONF_ARGS, default=list): cv.ensure_list(cv.lambda_), + cv.Optional(CONF_IF_NAN): cv.string, }, ), validate_printf, @@ -72,11 +87,9 @@ def _validate_text(value): # A schema for text properties -TEXT_SCHEMA = cv.Schema( - { - cv.Optional(CONF_TEXT): _validate_text, - } -) +TEXT_SCHEMA = { + cv.Optional(CONF_TEXT): _validate_text, +} LIST_ACTION_SCHEMA = cv.ensure_list( cv.maybe_simple_value( @@ -136,7 +149,7 @@ STYLE_PROPS = { "arc_opa": lvalid.opacity, "arc_color": lvalid.lv_color, "arc_rounded": lvalid.lv_bool, - "arc_width": lvalid.lv_positive_int, + "arc_width": lvalid.pixels, "anim_time": lvalid.lv_milliseconds, "bg_color": lvalid.lv_color, "bg_grad": lv_gradient, @@ -223,10 +236,6 @@ STYLE_REMAP = { "image_recolor_opa": "img_recolor_opa", } -cell_alignments = df.LV_CELL_ALIGNMENTS.one_of -grid_alignments = df.LV_GRID_ALIGNMENTS.one_of -flex_alignments = df.LV_FLEX_ALIGNMENTS.one_of - # Complete object style schema STYLE_SCHEMA = cv.Schema({cv.Optional(k): v for k, v in STYLE_PROPS.items()}).extend( { @@ -234,9 +243,19 @@ STYLE_SCHEMA = cv.Schema({cv.Optional(k): v for k, v in STYLE_PROPS.items()}).ex cv.Optional(df.CONF_SCROLLBAR_MODE): df.LvConstant( "LV_SCROLLBAR_MODE_", "OFF", "ON", "ACTIVE", "AUTO" ).one_of, + cv.Optional(CONF_SCROLL_DIR): df.SCROLL_DIRECTIONS.one_of, + cv.Optional(CONF_SCROLL_SNAP_X): df.SNAP_DIRECTIONS.one_of, + cv.Optional(CONF_SCROLL_SNAP_Y): df.SNAP_DIRECTIONS.one_of, } ) +OBJ_PROPERTIES = { + CONF_SCROLL_SNAP_X, + CONF_SCROLL_SNAP_Y, + CONF_SCROLL_DIR, + CONF_SCROLLBAR_MODE, +} + # Also allow widget specific properties for use in style definitions FULL_STYLE_SCHEMA = STYLE_SCHEMA.extend( { @@ -266,10 +285,8 @@ def part_schema(parts): :param parts: The parts to include :return: The schema """ - return ( - cv.Schema({cv.Optional(part): STATE_SCHEMA for part in parts}) - .extend(STATE_SCHEMA) - .extend(FLAG_SCHEMA) + return STATE_SCHEMA.extend(FLAG_SCHEMA).extend( + {cv.Optional(part): STATE_SCHEMA for part in parts} ) @@ -277,10 +294,10 @@ def automation_schema(typ: LvType): events = df.LV_EVENT_TRIGGERS + df.SWIPE_TRIGGERS if typ.has_on_value: events = events + (CONF_ON_VALUE,) - args = typ.get_arg_type() if isinstance(typ, LvType) else [] + args = typ.get_arg_type() args.append(lv_event_t_ptr) - return cv.Schema( - { + return { + **{ cv.Optional(event): validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( @@ -289,29 +306,43 @@ def automation_schema(typ: LvType): } ) for event in events - } - ).extend( - { - cv.Optional(CONF_ON_BOOT): validate_automation( - {cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StartupTrigger)} - ) - } - ) + }, + cv.Optional(CONF_ON_BOOT): validate_automation( + {cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StartupTrigger)} + ), + } -def base_update_schema(widget_type, parts): +def _update_widget(widget_type: WidgetType) -> Callable[[dict], dict]: """ - Create a schema for updating a widgets style properties, states and flags + During validation of update actions, create a map of action types to affected widgets + for use in final validation. + :param widget_type: + :return: + """ + + def validator(value: dict) -> dict: + df.get_data(df.KEY_UPDATED_WIDGETS).setdefault(widget_type, []).append(value) + return value + + return validator + + +def base_update_schema(widget_type: WidgetType | LvType, parts): + """ + Create a schema for updating a widget's style properties, states and flags. :param widget_type: The type of the ID :param parts: The allowable parts to specify :return: """ - return part_schema(parts).extend( + + w_type = widget_type.w_type if isinstance(widget_type, WidgetType) else widget_type + schema = part_schema(parts).extend( { cv.Required(CONF_ID): cv.ensure_list( cv.maybe_simple_value( { - cv.Required(CONF_ID): cv.use_id(widget_type), + cv.Required(CONF_ID): cv.use_id(w_type), }, key=CONF_ID, ) @@ -320,11 +351,9 @@ def base_update_schema(widget_type, parts): } ) - -def create_modify_schema(widget_type): - return base_update_schema(widget_type.w_type, widget_type.parts).extend( - widget_type.modify_schema - ) + if isinstance(widget_type, WidgetType): + schema.add_extra(_update_widget(widget_type)) + return schema def obj_schema(widget_type: WidgetType): @@ -335,75 +364,17 @@ def obj_schema(widget_type: WidgetType): """ return ( part_schema(widget_type.parts) - .extend(LAYOUT_SCHEMA) .extend(ALIGN_TO_SCHEMA) .extend(automation_schema(widget_type.w_type)) .extend( - cv.Schema( - { - cv.Optional(CONF_STATE): SET_STATE_SCHEMA, - cv.Optional(CONF_GROUP): cv.use_id(lv_group_t), - } - ) + { + cv.Optional(CONF_STATE): SET_STATE_SCHEMA, + cv.Optional(CONF_GROUP): cv.use_id(lv_group_t), + } ) ) -def _validate_grid_layout(config): - layout = config[df.CONF_LAYOUT] - rows = len(layout[df.CONF_GRID_ROWS]) - columns = len(layout[df.CONF_GRID_COLUMNS]) - used_cells = [[None] * columns for _ in range(rows)] - for index, widget in enumerate(config[df.CONF_WIDGETS]): - _, w = next(iter(widget.items())) - if (df.CONF_GRID_CELL_COLUMN_POS in w) != (df.CONF_GRID_CELL_ROW_POS in w): - # pylint: disable=raise-missing-from - raise cv.Invalid( - "Both row and column positions must be specified, or both omitted", - [df.CONF_WIDGETS, index], - ) - if df.CONF_GRID_CELL_ROW_POS in w: - row = w[df.CONF_GRID_CELL_ROW_POS] - column = w[df.CONF_GRID_CELL_COLUMN_POS] - else: - try: - row, column = next( - (r_idx, c_idx) - for r_idx, row in enumerate(used_cells) - for c_idx, value in enumerate(row) - if value is None - ) - except StopIteration: - # pylint: disable=raise-missing-from - raise cv.Invalid( - "No free cells available in grid layout", [df.CONF_WIDGETS, index] - ) - w[df.CONF_GRID_CELL_ROW_POS] = row - w[df.CONF_GRID_CELL_COLUMN_POS] = column - - for i in range(w[df.CONF_GRID_CELL_ROW_SPAN]): - for j in range(w[df.CONF_GRID_CELL_COLUMN_SPAN]): - if row + i >= rows or column + j >= columns: - # pylint: disable=raise-missing-from - raise cv.Invalid( - f"Cell at {row}/{column} span {w[df.CONF_GRID_CELL_ROW_SPAN]}x{w[df.CONF_GRID_CELL_COLUMN_SPAN]} " - f"exceeds grid size {rows}x{columns}", - [df.CONF_WIDGETS, index], - ) - if used_cells[row + i][column + j] is not None: - # pylint: disable=raise-missing-from - raise cv.Invalid( - f"Cell span {row + i}/{column + j} already occupied by widget at index {used_cells[row + i][column + j]}", - [df.CONF_WIDGETS, index], - ) - used_cells[row + i][column + j] = index - - return config - - -LAYOUT_SCHEMAS = {} -LAYOUT_VALIDATORS = {TYPE_GRID: _validate_grid_layout} - ALIGN_TO_SCHEMA = { cv.Optional(df.CONF_ALIGN_TO): cv.Schema( { @@ -416,57 +387,6 @@ ALIGN_TO_SCHEMA = { } -def grid_free_space(value): - value = cv.Upper(value) - if value.startswith("FR(") and value.endswith(")"): - value = value.removesuffix(")").removeprefix("FR(") - return f"LV_GRID_FR({cv.positive_int(value)})" - raise cv.Invalid("must be a size in pixels, CONTENT or FR(nn)") - - -grid_spec = cv.Any( - lvalid.size, df.LvConstant("LV_GRID_", "CONTENT").one_of, grid_free_space -) - -LAYOUT_SCHEMA = { - cv.Optional(df.CONF_LAYOUT): cv.typed_schema( - { - df.TYPE_GRID: { - cv.Required(df.CONF_GRID_ROWS): [grid_spec], - cv.Required(df.CONF_GRID_COLUMNS): [grid_spec], - cv.Optional(df.CONF_GRID_COLUMN_ALIGN): grid_alignments, - cv.Optional(df.CONF_GRID_ROW_ALIGN): grid_alignments, - cv.Optional(df.CONF_PAD_ROW): lvalid.padding, - cv.Optional(df.CONF_PAD_COLUMN): lvalid.padding, - }, - df.TYPE_FLEX: { - cv.Optional( - df.CONF_FLEX_FLOW, default="row_wrap" - ): df.FLEX_FLOWS.one_of, - cv.Optional(df.CONF_FLEX_ALIGN_MAIN, default="start"): flex_alignments, - cv.Optional(df.CONF_FLEX_ALIGN_CROSS, default="start"): flex_alignments, - cv.Optional(df.CONF_FLEX_ALIGN_TRACK, default="start"): flex_alignments, - cv.Optional(df.CONF_PAD_ROW): lvalid.padding, - cv.Optional(df.CONF_PAD_COLUMN): lvalid.padding, - }, - }, - lower=True, - ) -} - -GRID_CELL_SCHEMA = { - cv.Optional(df.CONF_GRID_CELL_ROW_POS): cv.positive_int, - cv.Optional(df.CONF_GRID_CELL_COLUMN_POS): cv.positive_int, - cv.Optional(df.CONF_GRID_CELL_ROW_SPAN, default=1): cv.positive_int, - cv.Optional(df.CONF_GRID_CELL_COLUMN_SPAN, default=1): cv.positive_int, - cv.Optional(df.CONF_GRID_CELL_X_ALIGN): grid_alignments, - cv.Optional(df.CONF_GRID_CELL_Y_ALIGN): grid_alignments, -} - -FLEX_OBJ_SCHEMA = { - cv.Optional(df.CONF_FLEX_GROW): cv.int_, -} - DISP_BG_SCHEMA = cv.Schema( { cv.Optional(df.CONF_DISP_BG_IMAGE): cv.Any( @@ -498,48 +418,11 @@ ALL_STYLES = { } -def container_validator(schema, widget_type: WidgetType): - """ - Create a validator for a container given the widget type - :param schema: Base schema to extend - :param widget_type: - :return: - """ - - def validator(value): - if w_sch := widget_type.schema: - if isinstance(w_sch, dict): - w_sch = cv.Schema(w_sch) - # order is important here to preserve extras - result = w_sch.extend(schema) - else: - result = schema - ltype = df.TYPE_NONE - if value and (layout := value.get(df.CONF_LAYOUT)): - if not isinstance(layout, dict): - raise cv.Invalid("Layout value must be a dict") - ltype = layout.get(CONF_TYPE) - if not ltype: - raise (cv.Invalid("Layout schema requires type:")) - add_lv_use(ltype) - if value == SCHEMA_EXTRACT: - return result - result = result.extend( - LAYOUT_SCHEMAS.get(ltype.lower(), LAYOUT_SCHEMAS[df.TYPE_NONE]) - ) - value = result(value) - if layout_validator := LAYOUT_VALIDATORS.get(ltype): - value = layout_validator(value) - return value - - return validator - - def container_schema(widget_type: WidgetType, extras=None): """ Create a schema for a container widget of a given type. All obj properties are available, plus the extras passed in, plus any defined for the specific widget being specified. - :param widget_type: The widget type, e.g. "img" + :param widget_type: The widget type, e.g. "image" :param extras: Additional options to be made available, e.g. layout properties for children :return: The schema for this type of widget. """ @@ -549,31 +432,58 @@ def container_schema(widget_type: WidgetType, extras=None): if extras: schema = schema.extend(extras) # Delayed evaluation for recursion - return container_validator(schema, widget_type) + schema = schema.extend(widget_type.schema) -def widget_schema(widget_type: WidgetType, extras=None): - """ - Create a schema for a given widget type - :param widget_type: The name of the widget - :param extras: - :return: - """ - validator = container_schema(widget_type, extras=extras) - if required := widget_type.required_component: - validator = cv.All(validator, requires_component(required)) - return cv.Exclusive(widget_type.name, df.CONF_WIDGETS), validator + def validator(value): + return append_layout_schema(schema, value)(value) - -# All widget schemas must be defined before this is called. + return validator def any_widget_schema(extras=None): """ - Generate schemas for all possible LVGL widgets. This is what implements the ability to have a list of any kind of + Dynamically generate schemas for all possible LVGL widgets. This is what implements the ability to have a list of any kind of widget under the widgets: key. + This uses lazy evaluation - the schema is built when called during validation, + not at import time. This allows external components to register widgets + before schema validation begins. + :param extras: Additional schema to be applied to each generated one - :return: + :return: A validator for the Widgets key """ - return cv.Any(dict(widget_schema(wt, extras) for wt in WIDGET_TYPES.values())) + + def validator(value): + if isinstance(value, dict): + # Convert to list + is_dict = True + value = [{k: v} for k, v in value.items()] + else: + is_dict = False + if not isinstance(value, list): + raise cv.Invalid("Expected a list of widgets") + result = [] + for index, entry in enumerate(value): + if not isinstance(entry, dict) or len(entry) != 1: + raise cv.Invalid( + "Each widget must be a dictionary with a single key", path=[index] + ) + [(key, value)] = entry.items() + # Validate the widget against its schema + widget_type = WIDGET_TYPES.get(key) + if not widget_type: + raise cv.Invalid(f"Unknown widget type: {key}", path=[index]) + container_validator = container_schema(widget_type, extras=extras) + if required := widget_type.required_component: + container_validator = cv.All( + container_validator, requires_component(required) + ) + # Apply custom validation + value = widget_type.validate(value or {}) + path = [key] if is_dict else [index, key] + with prepend_path(path): + result.append({key: container_validator(value)}) + return result + + return validator diff --git a/esphome/components/lvgl/select/lvgl_select.h b/esphome/components/lvgl/select/lvgl_select.h index 5b43209a5f..70bb3e7bcb 100644 --- a/esphome/components/lvgl/select/lvgl_select.h +++ b/esphome/components/lvgl/select/lvgl_select.h @@ -20,7 +20,7 @@ class LVGLSelect : public select::Select, public Component { this->set_options_(); if (this->restore_) { size_t index; - this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); + this->pref_ = global_preferences->make_preference(this->get_preference_hash()); if (this->pref_.load(&index)) this->widget_->set_selected_index(index, LV_ANIM_OFF); } @@ -41,19 +41,29 @@ class LVGLSelect : public select::Select, public Component { } void publish() { - this->publish_state(this->widget_->get_selected_text()); + auto index = this->widget_->get_selected_index(); + this->publish_state(index); if (this->restore_) { - auto index = this->widget_->get_selected_index(); this->pref_.save(&index); } } protected: - void control(const std::string &value) override { - this->widget_->set_selected_text(value, this->anim_); + void control(size_t index) override { + this->widget_->set_selected_index(index, this->anim_); this->publish(); } - void set_options_() { this->traits.set_options(this->widget_->get_options()); } + void set_options_() { + // Widget uses std::vector, SelectTraits uses FixedVector + // Convert by extracting c_str() pointers + const auto &opts = this->widget_->get_options(); + FixedVector opt_ptrs; + opt_ptrs.init(opts.size()); + for (const auto &opt : opts) { + opt_ptrs.push_back(opt.c_str()); + } + this->traits.set_options(opt_ptrs); + } LvSelectable *widget_; lv_anim_enable_t anim_; diff --git a/esphome/components/lvgl/sensor/__init__.py b/esphome/components/lvgl/sensor/__init__.py index 03b2638ed0..167af9c6e1 100644 --- a/esphome/components/lvgl/sensor/__init__.py +++ b/esphome/components/lvgl/sensor/__init__.py @@ -5,7 +5,6 @@ from ..defines import CONF_WIDGET from ..lvcode import ( API_EVENT, EVENT_ARG, - LVGL_COMP_ARG, UPDATE_EVENT, LambdaContext, LvContext, @@ -30,7 +29,7 @@ async def to_code(config): await wait_for_widgets() async with LambdaContext(EVENT_ARG) as lamb: lv_add(sensor.publish_state(widget.get_value())) - async with LvContext(LVGL_COMP_ARG): + async with LvContext(): lv_add( lvgl_static.add_event_cb( widget.obj, diff --git a/esphome/components/lvgl/types.py b/esphome/components/lvgl/types.py index 10b6f63528..9c92ca7e98 100644 --- a/esphome/components/lvgl/types.py +++ b/esphome/components/lvgl/types.py @@ -1,8 +1,12 @@ import sys from esphome import automation, codegen as cg +from esphome.automation import register_action +from esphome.config_validation import Schema from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_TEXT, CONF_VALUE +from esphome.core import EsphomeError from esphome.cpp_generator import MockObj, MockObjClass +from esphome.cpp_types import esphome_ns from .defines import lvgl_ns from .lvcode import lv_expr @@ -41,9 +45,11 @@ lv_coord_t = cg.global_ns.namespace("lv_coord_t") lv_event_code_t = cg.global_ns.enum("lv_event_code_t") lv_indev_type_t = cg.global_ns.enum("lv_indev_type_t") lv_key_t = cg.global_ns.enum("lv_key_t") -FontEngine = lvgl_ns.class_("FontEngine") +PlainTrigger = esphome_ns.class_("Trigger<>", automation.Trigger.template()) +DrawEndTrigger = esphome_ns.class_( + "Trigger", automation.Trigger.template(cg.uint32, cg.uint32) +) IdleTrigger = lvgl_ns.class_("IdleTrigger", automation.Trigger.template()) -PauseTrigger = lvgl_ns.class_("PauseTrigger", automation.Trigger.template()) ObjUpdateAction = lvgl_ns.class_("ObjUpdateAction", automation.Action) LvglCondition = lvgl_ns.class_("LvglCondition", automation.Condition) LvglAction = lvgl_ns.class_("LvglAction", automation.Action) @@ -119,28 +125,47 @@ class WidgetType: schema=None, modify_schema=None, lv_name=None, + is_mock: bool = False, ): """ :param name: The widget name, e.g. "bar" :param w_type: The C type of the widget :param parts: What parts this widget supports :param schema: The config schema for defining a widget - :param modify_schema: A schema to update the widget + :param modify_schema: A schema to update the widget, defaults to the same as the schema + :param lv_name: The name of the LVGL widget in the LVGL library, if different from the name + :param is_mock: Whether this widget is a mock widget, i.e. not a real LVGL widget """ self.name = name self.lv_name = lv_name or name self.w_type = w_type self.parts = parts - if schema is None: - self.schema = {} - else: - self.schema = schema + if not isinstance(schema, Schema): + schema = Schema(schema or {}) + self.schema = schema if modify_schema is None: - self.modify_schema = self.schema - else: - self.modify_schema = modify_schema + modify_schema = schema + if not isinstance(modify_schema, Schema): + modify_schema = Schema(modify_schema) + self.modify_schema = modify_schema self.mock_obj = MockObj(f"lv_{self.lv_name}", "_") + # Local import to avoid circular import + from .automation import update_to_code + from .schemas import WIDGET_TYPES, base_update_schema + + if not is_mock: + if self.name in WIDGET_TYPES: + raise EsphomeError(f"Duplicate definition of widget type '{self.name}'") + WIDGET_TYPES[self.name] = self + + # Register the update action automatically, adding widget-specific properties + register_action( + f"lvgl.{self.name}.update", + ObjUpdateAction, + base_update_schema(self, self.parts).extend(self.modify_schema), + )(update_to_code) + @property def animated(self): return False @@ -157,11 +182,9 @@ class WidgetType: Generate code for a given widget :param w: The widget :param config: Its configuration - :return: Generated code as a list of text lines """ - return [] - def obj_creator(self, parent: MockObjClass, config: dict): + async def obj_creator(self, parent: MockObjClass, config: dict): """ Create an instance of the widget type :param parent: The parent to which it should be attached @@ -170,6 +193,13 @@ class WidgetType: """ return lv_expr.call(f"{self.lv_name}_create", parent) + def on_create(self, var: MockObj, config: dict): + """ + Called from to_code when the widget is created, to set up any initial properties + :param var: The variable representing the widget + :param config: Its configuration + """ + def get_uses(self): """ Get a list of other widgets used by this one @@ -189,6 +219,23 @@ class WidgetType: def get_scale(self, config: dict): return 1.0 + def validate(self, value): + """ + Provides an opportunity for custom validation for a given widget type + :param value: + :return: + """ + return value + + def final_validate(self, widget, update_config, widget_config, path): + """ + Allow final validation for a given widget type update action + :param widget: A widget + :param update_config: The configuration for the update action + :param widget_config: The configuration for the widget itself + :param path: The path to the widget, for error reporting + """ + class NumberType(WidgetType): def get_max(self, config: dict): diff --git a/esphome/components/lvgl/widgets/__init__.py b/esphome/components/lvgl/widgets/__init__.py index d12464fe71..b1d157325b 100644 --- a/esphome/components/lvgl/widgets/__init__.py +++ b/esphome/components/lvgl/widgets/__init__.py @@ -21,7 +21,6 @@ from ..defines import ( CONF_MAIN, CONF_PAD_COLUMN, CONF_PAD_ROW, - CONF_SCROLLBAR_MODE, CONF_STYLES, CONF_WIDGETS, OBJ_FLAGS, @@ -45,7 +44,7 @@ from ..lvcode import ( lv_obj, lv_Pvariable, ) -from ..schemas import ALL_STYLES, STYLE_REMAP, WIDGET_TYPES +from ..schemas import ALL_STYLES, OBJ_PROPERTIES, STYLE_REMAP, WIDGET_TYPES from ..types import LV_STATE, LvType, WidgetType, lv_coord_t, lv_obj_t, lv_obj_t_ptr EVENT_LAMB = "event_lamb__" @@ -67,7 +66,6 @@ class Widget: self.type = wtype self.config = config self.scale = 1.0 - self.step = 1.0 self.range_from = -sys.maxsize self.range_to = sys.maxsize if wtype.is_compound(): @@ -214,17 +212,14 @@ class LvScrActType(WidgetType): """ def __init__(self): - super().__init__("lv_scr_act()", lv_obj_t, ()) + super().__init__("lv_scr_act()", lv_obj_t, (), is_mock=True) async def to_code(self, w, config: dict): return [] -lv_scr_act_spec = LvScrActType() - - def get_scr_act(lv_comp: MockObj) -> Widget: - return Widget.create(None, lv_comp.get_scr_act(), lv_scr_act_spec, {}) + return Widget.create(None, lv_comp.get_scr_act(), LvScrActType(), {}) def get_widget_generator(wid): @@ -340,7 +335,10 @@ async def set_obj_properties(w: Widget, config): if layout_type == TYPE_FLEX: lv_obj.set_flex_flow(w.obj, literal(layout[CONF_FLEX_FLOW])) main = literal(layout[CONF_FLEX_ALIGN_MAIN]) - cross = literal(layout[CONF_FLEX_ALIGN_CROSS]) + cross = layout[CONF_FLEX_ALIGN_CROSS] + if cross == "LV_FLEX_ALIGN_STRETCH": + cross = "LV_FLEX_ALIGN_CENTER" + cross = literal(cross) track = literal(layout[CONF_FLEX_ALIGN_TRACK]) lv_obj.set_flex_align(w.obj, main, cross, track) parts = collect_parts(config) @@ -384,7 +382,7 @@ async def set_obj_properties(w: Widget, config): clrs = join_enums(flag_clr, "LV_OBJ_FLAG_") w.clear_flag(clrs) for key, value in lambs.items(): - lamb = await cg.process_lambda(value, [], return_type=cg.bool_) + lamb = await cg.process_lambda(value, [], capture="=", return_type=cg.bool_) flag = f"LV_OBJ_FLAG_{key.upper()}" with LvConditional(call_lambda(lamb)) as cond: w.add_flag(flag) @@ -409,13 +407,14 @@ async def set_obj_properties(w: Widget, config): clears = join_enums(clears, "LV_STATE_") w.clear_state(clears) for key, value in lambs.items(): - lamb = await cg.process_lambda(value, [], return_type=cg.bool_) + lamb = await cg.process_lambda(value, [], capture="=", return_type=cg.bool_) state = f"LV_STATE_{key.upper()}" with LvConditional(call_lambda(lamb)) as cond: w.add_state(state) cond.else_() w.clear_state(state) - await w.set_property(CONF_SCROLLBAR_MODE, config, lv_name="obj") + for property in OBJ_PROPERTIES: + await w.set_property(property, config, lv_name="obj") async def add_widgets(parent: Widget, config: dict): @@ -439,7 +438,7 @@ async def widget_to_code(w_cnfig, w_type: WidgetType, parent): :return: """ spec: WidgetType = WIDGET_TYPES[w_type] - creator = spec.obj_creator(parent, w_cnfig) + creator = await spec.obj_creator(parent, w_cnfig) add_lv_use(spec.name) add_lv_use(*spec.get_uses()) wid = w_cnfig[CONF_ID] @@ -447,9 +446,11 @@ async def widget_to_code(w_cnfig, w_type: WidgetType, parent): if spec.is_compound(): var = cg.new_Pvariable(wid) lv_add(var.set_obj(creator)) + spec.on_create(var.obj, w_cnfig) else: var = lv_Pvariable(lv_obj_t, wid) lv_assign(var, creator) + spec.on_create(var, w_cnfig) w = Widget.create(wid, var, spec, w_cnfig) if theme := theme_widget_map.get(w_type): diff --git a/esphome/components/lvgl/widgets/arc.py b/esphome/components/lvgl/widgets/arc.py index 65f0e785b6..21530441f8 100644 --- a/esphome/components/lvgl/widgets/arc.py +++ b/esphome/components/lvgl/widgets/arc.py @@ -20,7 +20,13 @@ from ..defines import ( CONF_START_ANGLE, literal, ) -from ..lv_validation import angle, get_start_value, lv_float +from ..lv_validation import ( + get_start_value, + lv_angle_degrees, + lv_float, + lv_int, + lv_positive_int, +) from ..lvcode import lv, lv_expr, lv_obj from ..types import LvNumber, NumberType from . import Widget @@ -29,20 +35,27 @@ CONF_ARC = "arc" ARC_SCHEMA = cv.Schema( { cv.Optional(CONF_VALUE): lv_float, - cv.Optional(CONF_MIN_VALUE, default=0): cv.int_, - cv.Optional(CONF_MAX_VALUE, default=100): cv.int_, - cv.Optional(CONF_START_ANGLE, default=135): angle, - cv.Optional(CONF_END_ANGLE, default=45): angle, - cv.Optional(CONF_ROTATION, default=0.0): angle, + cv.Optional(CONF_MIN_VALUE, default=0): lv_int, + cv.Optional(CONF_MAX_VALUE, default=100): lv_int, + cv.Optional(CONF_START_ANGLE, default=135): lv_angle_degrees, + cv.Optional(CONF_END_ANGLE, default=45): lv_angle_degrees, + cv.Optional(CONF_ROTATION, default=0.0): lv_angle_degrees, cv.Optional(CONF_ADJUSTABLE, default=False): bool, cv.Optional(CONF_MODE, default="NORMAL"): ARC_MODES.one_of, - cv.Optional(CONF_CHANGE_RATE, default=720): cv.uint16_t, + cv.Optional(CONF_CHANGE_RATE, default=720): lv_positive_int, } ) ARC_MODIFY_SCHEMA = cv.Schema( { cv.Optional(CONF_VALUE): lv_float, + cv.Optional(CONF_MIN_VALUE): lv_int, + cv.Optional(CONF_MAX_VALUE): lv_int, + cv.Optional(CONF_START_ANGLE): lv_angle_degrees, + cv.Optional(CONF_END_ANGLE): lv_angle_degrees, + cv.Optional(CONF_ROTATION): lv_angle_degrees, + cv.Optional(CONF_MODE): ARC_MODES.one_of, + cv.Optional(CONF_CHANGE_RATE): lv_positive_int, } ) @@ -58,14 +71,34 @@ class ArcType(NumberType): ) async def to_code(self, w: Widget, config): - if CONF_MIN_VALUE in config: - lv.arc_set_range(w.obj, config[CONF_MIN_VALUE], config[CONF_MAX_VALUE]) - lv.arc_set_bg_angles( - w.obj, config[CONF_START_ANGLE] // 10, config[CONF_END_ANGLE] // 10 - ) - lv.arc_set_rotation(w.obj, config[CONF_ROTATION] // 10) - lv.arc_set_mode(w.obj, literal(config[CONF_MODE])) - lv.arc_set_change_rate(w.obj, config[CONF_CHANGE_RATE]) + if CONF_MIN_VALUE in config and CONF_MAX_VALUE in config: + max_value = await lv_int.process(config[CONF_MAX_VALUE]) + min_value = await lv_int.process(config[CONF_MIN_VALUE]) + lv.arc_set_range(w.obj, min_value, max_value) + elif CONF_MIN_VALUE in config: + max_value = w.get_property(CONF_MAX_VALUE) + min_value = await lv_int.process(config[CONF_MIN_VALUE]) + lv.arc_set_range(w.obj, min_value, max_value) + elif CONF_MAX_VALUE in config: + max_value = await lv_int.process(config[CONF_MAX_VALUE]) + min_value = w.get_property(CONF_MIN_VALUE) + lv.arc_set_range(w.obj, min_value, max_value) + + await w.set_property( + CONF_START_ANGLE, + await lv_angle_degrees.process(config.get(CONF_START_ANGLE)), + ) + await w.set_property( + CONF_END_ANGLE, await lv_angle_degrees.process(config.get(CONF_END_ANGLE)) + ) + await w.set_property( + CONF_ROTATION, await lv_angle_degrees.process(config.get(CONF_ROTATION)) + ) + await w.set_property(CONF_MODE, config) + await w.set_property( + CONF_CHANGE_RATE, + await lv_positive_int.process(config.get(CONF_CHANGE_RATE)), + ) if CONF_ADJUSTABLE in config: if not config[CONF_ADJUSTABLE]: @@ -75,9 +108,7 @@ class ArcType(NumberType): # For some reason arc does not get automatically added to the default group lv.group_add_obj(lv_expr.group_get_default(), w.obj) - value = await get_start_value(config) - if value is not None: - lv.arc_set_value(w.obj, value) + await w.set_property(CONF_VALUE, await get_start_value(config)) arc_spec = ArcType() diff --git a/esphome/components/lvgl/widgets/button.py b/esphome/components/lvgl/widgets/button.py index b59884ee67..5f2910174f 100644 --- a/esphome/components/lvgl/widgets/button.py +++ b/esphome/components/lvgl/widgets/button.py @@ -1,20 +1,52 @@ -from esphome.const import CONF_BUTTON +from esphome import config_validation as cv +from esphome.const import CONF_BUTTON, CONF_TEXT +from esphome.cpp_generator import MockObj -from ..defines import CONF_MAIN +from ..defines import CONF_MAIN, CONF_WIDGETS +from ..helpers import add_lv_use +from ..lv_validation import lv_text +from ..lvcode import lv, lv_expr +from ..schemas import TEXT_SCHEMA from ..types import LvBoolean, WidgetType +from . import Widget +from .label import label_spec lv_button_t = LvBoolean("lv_btn_t") class ButtonType(WidgetType): def __init__(self): - super().__init__(CONF_BUTTON, lv_button_t, (CONF_MAIN,), lv_name="btn") + super().__init__( + CONF_BUTTON, lv_button_t, (CONF_MAIN,), schema=TEXT_SCHEMA, lv_name="btn" + ) + + def validate(self, value): + if CONF_TEXT in value: + if CONF_WIDGETS in value: + raise cv.Invalid("Cannot use both text and widgets in a button") + add_lv_use("label") + return value def get_uses(self): return ("btn",) - async def to_code(self, w, config): - return [] + def on_create(self, var: MockObj, config: dict): + if CONF_TEXT in config: + lv.label_create(var) + return var + + async def to_code(self, w: Widget, config): + if text := config.get(CONF_TEXT): + label_widget = Widget.create( + None, lv_expr.obj_get_child(w.obj, 0), label_spec + ) + await label_widget.set_property(CONF_TEXT, await lv_text.process(text)) + + def final_validate(self, widget, update_config, widget_config, path): + if CONF_TEXT in update_config and CONF_TEXT not in widget_config: + raise cv.Invalid( + "Button must have 'text:' configured to allow updating text", path + ) button_spec = ButtonType() diff --git a/esphome/components/lvgl/widgets/buttonmatrix.py b/esphome/components/lvgl/widgets/buttonmatrix.py index c6b6d2440f..fe421aa477 100644 --- a/esphome/components/lvgl/widgets/buttonmatrix.py +++ b/esphome/components/lvgl/widgets/buttonmatrix.py @@ -1,5 +1,6 @@ from esphome import automation import esphome.codegen as cg +from esphome.components.const import CONF_ROWS from esphome.components.key_provider import KeyProvider import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_ITEMS, CONF_TEXT, CONF_WIDTH @@ -15,7 +16,6 @@ from ..defines import ( CONF_ONE_CHECKED, CONF_PAD_COLUMN, CONF_PAD_ROW, - CONF_ROWS, CONF_SELECTED, ) from ..helpers import lvgl_components_required diff --git a/esphome/components/lvgl/widgets/canvas.py b/esphome/components/lvgl/widgets/canvas.py index 4fd81b6e4a..ead352aa77 100644 --- a/esphome/components/lvgl/widgets/canvas.py +++ b/esphome/components/lvgl/widgets/canvas.py @@ -24,7 +24,7 @@ from ..defines import ( literal, ) from ..lv_validation import ( - lv_angle, + lv_angle_degrees, lv_bool, lv_color, lv_image, @@ -33,7 +33,7 @@ from ..lv_validation import ( pixels, size, ) -from ..lvcode import LocalVariable, lv, lv_assign +from ..lvcode import LocalVariable, lv, lv_assign, lv_expr from ..schemas import STYLE_PROPS, STYLE_REMAP, TEXT_SCHEMA, point_schema from ..types import LvType, ObjUpdateAction, WidgetType from . import Widget, get_widgets @@ -70,15 +70,18 @@ class CanvasType(WidgetType): width = config[CONF_WIDTH] height = config[CONF_HEIGHT] use_alpha = "_ALPHA" if config[CONF_TRANSPARENT] else "" - lv.canvas_set_buffer( - w.obj, - lv.custom_mem_alloc( - literal(f"LV_CANVAS_BUF_SIZE_TRUE_COLOR{use_alpha}({width}, {height})") - ), - width, - height, - literal(f"LV_IMG_CF_TRUE_COLOR{use_alpha}"), + buf_size = literal( + f"LV_CANVAS_BUF_SIZE_TRUE_COLOR{use_alpha}({width}, {height})" ) + with LocalVariable("buf", cg.void, lv_expr.custom_mem_alloc(buf_size)) as buf: + cg.add(cg.RawExpression(f"memset({buf}, 0, {buf_size});")) + lv.canvas_set_buffer( + w.obj, + buf, + width, + height, + literal(f"LV_IMG_CF_TRUE_COLOR{use_alpha}"), + ) canvas_spec = CanvasType() @@ -156,18 +159,15 @@ async def canvas_set_pixel(config, action_id, template_arg, args): ) -DRAW_SCHEMA = cv.Schema( - { - cv.GenerateID(CONF_ID): cv.use_id(lv_canvas_t), - cv.Required(CONF_X): pixels, - cv.Required(CONF_Y): pixels, - } -) -DRAW_OPA_SCHEMA = DRAW_SCHEMA.extend( - { - cv.Optional(CONF_OPA): opacity, - } -) +DRAW_SCHEMA = { + cv.GenerateID(CONF_ID): cv.use_id(lv_canvas_t), + cv.Required(CONF_X): pixels, + cv.Required(CONF_Y): pixels, +} +DRAW_OPA_SCHEMA = { + **DRAW_SCHEMA, + cv.Optional(CONF_OPA): opacity, +} async def draw_to_code(config, dsc_type, props, do_draw, action_id, template_arg, args): @@ -221,12 +221,14 @@ RECT_PROPS = { @automation.register_action( "lvgl.canvas.draw_rectangle", ObjUpdateAction, - DRAW_SCHEMA.extend( + cv.Schema( { + **DRAW_OPA_SCHEMA, cv.Required(CONF_WIDTH): cv.templatable(cv.int_), cv.Required(CONF_HEIGHT): cv.templatable(cv.int_), - }, - ).extend({cv.Optional(prop): STYLE_PROPS[prop] for prop in RECT_PROPS}), + **{cv.Optional(prop): STYLE_PROPS[prop] for prop in RECT_PROPS}, + } + ), ) async def canvas_draw_rect(config, action_id, template_arg, args): width = await pixels.process(config[CONF_WIDTH]) @@ -258,13 +260,14 @@ TEXT_PROPS = { @automation.register_action( "lvgl.canvas.draw_text", ObjUpdateAction, - TEXT_SCHEMA.extend(DRAW_OPA_SCHEMA) - .extend( + cv.Schema( { + **TEXT_SCHEMA, + **DRAW_OPA_SCHEMA, cv.Required(CONF_MAX_WIDTH): cv.templatable(cv.int_), + **{cv.Optional(prop): STYLE_PROPS[f"text_{prop}"] for prop in TEXT_PROPS}, }, - ) - .extend({cv.Optional(prop): STYLE_PROPS[f"text_{prop}"] for prop in TEXT_PROPS}), + ), ) async def canvas_draw_text(config, action_id, template_arg, args): text = await lv_text.process(config[CONF_TEXT]) @@ -290,13 +293,15 @@ IMG_PROPS = { @automation.register_action( "lvgl.canvas.draw_image", ObjUpdateAction, - DRAW_OPA_SCHEMA.extend( + cv.Schema( { + **DRAW_OPA_SCHEMA, cv.Required(CONF_SRC): lv_image, cv.Optional(CONF_PIVOT_X, default=0): pixels, cv.Optional(CONF_PIVOT_Y, default=0): pixels, - }, - ).extend({cv.Optional(prop): validator for prop, validator in IMG_PROPS.items()}), + **{cv.Optional(prop): validator for prop, validator in IMG_PROPS.items()}, + } + ), ) async def canvas_draw_image(config, action_id, template_arg, args): src = await lv_image.process(config[CONF_SRC]) @@ -333,8 +338,9 @@ LINE_PROPS = { cv.GenerateID(CONF_ID): cv.use_id(lv_canvas_t), cv.Optional(CONF_OPA): opacity, cv.Required(CONF_POINTS): cv.ensure_list(point_schema), - }, - ).extend({cv.Optional(prop): validator for prop, validator in LINE_PROPS.items()}), + **{cv.Optional(prop): validator for prop, validator in LINE_PROPS.items()}, + } + ), ) async def canvas_draw_line(config, action_id, template_arg, args): points = [ @@ -360,8 +366,9 @@ async def canvas_draw_line(config, action_id, template_arg, args): { cv.GenerateID(CONF_ID): cv.use_id(lv_canvas_t), cv.Required(CONF_POINTS): cv.ensure_list(point_schema), + **{cv.Optional(prop): STYLE_PROPS[prop] for prop in RECT_PROPS}, }, - ).extend({cv.Optional(prop): STYLE_PROPS[prop] for prop in RECT_PROPS}), + ), ) async def canvas_draw_polygon(config, action_id, template_arg, args): points = [ @@ -392,18 +399,20 @@ ARC_PROPS = { @automation.register_action( "lvgl.canvas.draw_arc", ObjUpdateAction, - DRAW_OPA_SCHEMA.extend( + cv.Schema( { + **DRAW_OPA_SCHEMA, cv.Required(CONF_RADIUS): pixels, - cv.Required(CONF_START_ANGLE): lv_angle, - cv.Required(CONF_END_ANGLE): lv_angle, + cv.Required(CONF_START_ANGLE): lv_angle_degrees, + cv.Required(CONF_END_ANGLE): lv_angle_degrees, + **{cv.Optional(prop): validator for prop, validator in ARC_PROPS.items()}, } - ).extend({cv.Optional(prop): validator for prop, validator in ARC_PROPS.items()}), + ), ) async def canvas_draw_arc(config, action_id, template_arg, args): radius = await size.process(config[CONF_RADIUS]) - start_angle = await lv_angle.process(config[CONF_START_ANGLE]) - end_angle = await lv_angle.process(config[CONF_END_ANGLE]) + start_angle = await lv_angle_degrees.process(config[CONF_START_ANGLE]) + end_angle = await lv_angle_degrees.process(config[CONF_END_ANGLE]) async def do_draw_arc(w: Widget, x, y, dsc_addr): lv.canvas_draw_arc(w.obj, x, y, radius, start_angle, end_angle, dsc_addr) diff --git a/esphome/components/lvgl/widgets/checkbox.py b/esphome/components/lvgl/widgets/checkbox.py index c344fbfe75..ca97e2d843 100644 --- a/esphome/components/lvgl/widgets/checkbox.py +++ b/esphome/components/lvgl/widgets/checkbox.py @@ -17,11 +17,10 @@ class CheckboxType(WidgetType): CONF_CHECKBOX, LvBoolean("lv_checkbox_t"), (CONF_MAIN, CONF_INDICATOR), - TEXT_SCHEMA.extend( - { - Optional(CONF_PAD_COLUMN): padding, - } - ), + { + **TEXT_SCHEMA, + Optional(CONF_PAD_COLUMN): padding, + }, ) async def to_code(self, w: Widget, config): diff --git a/esphome/components/lvgl/widgets/container.py b/esphome/components/lvgl/widgets/container.py new file mode 100644 index 0000000000..2ac1a3b244 --- /dev/null +++ b/esphome/components/lvgl/widgets/container.py @@ -0,0 +1,39 @@ +import esphome.config_validation as cv +from esphome.const import CONF_HEIGHT, CONF_WIDTH +from esphome.cpp_generator import MockObj + +from ..defines import CONF_CONTAINER, CONF_MAIN, CONF_OBJ, CONF_SCROLLBAR +from ..lv_validation import size +from ..lvcode import lv +from ..types import WidgetType, lv_obj_t + +CONTAINER_SCHEMA = cv.Schema( + { + cv.Optional(CONF_HEIGHT, default="100%"): size, + cv.Optional(CONF_WIDTH, default="100%"): size, + } +) + + +class ContainerType(WidgetType): + """ + A simple container widget that can hold other widgets and which defaults to a 100% size. + Made from an obj with all styles removed + """ + + def __init__(self): + super().__init__( + CONF_CONTAINER, + lv_obj_t, + (CONF_MAIN, CONF_SCROLLBAR), + schema=CONTAINER_SCHEMA, + modify_schema={}, + lv_name=CONF_OBJ, + ) + self.styles = {} + + def on_create(self, var: MockObj, config: dict): + lv.obj_remove_style_all(var) + + +container_spec = ContainerType() diff --git a/esphome/components/lvgl/widgets/label.py b/esphome/components/lvgl/widgets/label.py index 6b04235674..3a3a997737 100644 --- a/esphome/components/lvgl/widgets/label.py +++ b/esphome/components/lvgl/widgets/label.py @@ -23,12 +23,11 @@ class LabelType(WidgetType): CONF_LABEL, LvText("lv_label_t"), (CONF_MAIN, CONF_SCROLLBAR, CONF_SELECTED), - TEXT_SCHEMA.extend( - { - cv.Optional(CONF_RECOLOR): lv_bool, - cv.Optional(CONF_LONG_MODE): LV_LONG_MODES.one_of, - } - ), + { + **TEXT_SCHEMA, + cv.Optional(CONF_RECOLOR): lv_bool, + cv.Optional(CONF_LONG_MODE): LV_LONG_MODES.one_of, + }, ) async def to_code(self, w: Widget, config): diff --git a/esphome/components/lvgl/widgets/line.py b/esphome/components/lvgl/widgets/line.py index bd90edbefc..57cb965737 100644 --- a/esphome/components/lvgl/widgets/line.py +++ b/esphome/components/lvgl/widgets/line.py @@ -6,7 +6,7 @@ from esphome.core import Lambda from ..defines import CONF_MAIN, call_lambda from ..lvcode import lv_add from ..schemas import point_schema -from ..types import LvCompound, LvType +from ..types import LvCompound, LvType, lv_coord_t from . import Widget, WidgetType CONF_LINE = "line" @@ -23,9 +23,7 @@ LINE_SCHEMA = { async def process_coord(coord): if isinstance(coord, Lambda): - coord = call_lambda( - await cg.process_lambda(coord, [], return_type="lv_coord_t") - ) + coord = call_lambda(await cg.process_lambda(coord, [], return_type=lv_coord_t)) if not coord.endswith("()"): coord = f"static_cast({coord})" return cg.RawExpression(coord) diff --git a/esphome/components/lvgl/widgets/meter.py b/esphome/components/lvgl/widgets/meter.py index acec986f99..aefda0e71a 100644 --- a/esphome/components/lvgl/widgets/meter.py +++ b/esphome/components/lvgl/widgets/meter.py @@ -14,7 +14,6 @@ from esphome.const import ( CONF_VALUE, CONF_WIDTH, ) -from esphome.cpp_generator import IntLiteral from ..automation import action_to_code from ..defines import ( @@ -32,7 +31,7 @@ from ..helpers import add_lv_use, lvgl_components_required from ..lv_validation import ( get_end_value, get_start_value, - lv_angle, + lv_angle_degrees, lv_bool, lv_color, lv_float, @@ -163,7 +162,7 @@ SCALE_SCHEMA = cv.Schema( cv.Optional(CONF_RANGE_FROM, default=0.0): cv.float_, cv.Optional(CONF_RANGE_TO, default=100.0): cv.float_, cv.Optional(CONF_ANGLE_RANGE, default=270): cv.int_range(0, 360), - cv.Optional(CONF_ROTATION): lv_angle, + cv.Optional(CONF_ROTATION): lv_angle_degrees, cv.Optional(CONF_INDICATORS): cv.ensure_list(INDICATOR_SCHEMA), } ) @@ -188,9 +187,7 @@ class MeterType(WidgetType): for scale_conf in config.get(CONF_SCALES, ()): rotation = 90 + (360 - scale_conf[CONF_ANGLE_RANGE]) / 2 if CONF_ROTATION in scale_conf: - rotation = await lv_angle.process(scale_conf[CONF_ROTATION]) - if isinstance(rotation, IntLiteral): - rotation = int(str(rotation)) // 10 + rotation = await lv_angle_degrees.process(scale_conf[CONF_ROTATION]) with LocalVariable( "meter_var", "lv_meter_scale_t", lv_expr.meter_add_scale(var) ) as meter_var: diff --git a/esphome/components/lvgl/widgets/qrcode.py b/esphome/components/lvgl/widgets/qrcode.py index 7d8d13d8c4..ad46f67c6b 100644 --- a/esphome/components/lvgl/widgets/qrcode.py +++ b/esphome/components/lvgl/widgets/qrcode.py @@ -4,7 +4,7 @@ from esphome.const import CONF_SIZE, CONF_TEXT from esphome.cpp_generator import MockObjClass from ..defines import CONF_MAIN -from ..lv_validation import color, color_retmapper, lv_text +from ..lv_validation import lv_color, lv_text from ..lvcode import LocalVariable, lv, lv_expr from ..schemas import TEXT_SCHEMA from ..types import WidgetType, lv_obj_t @@ -14,13 +14,12 @@ CONF_QRCODE = "qrcode" CONF_DARK_COLOR = "dark_color" CONF_LIGHT_COLOR = "light_color" -QRCODE_SCHEMA = TEXT_SCHEMA.extend( - { - cv.Optional(CONF_DARK_COLOR, default="black"): color, - cv.Optional(CONF_LIGHT_COLOR, default="white"): color, - cv.Required(CONF_SIZE): cv.int_, - } -) +QRCODE_SCHEMA = { + **TEXT_SCHEMA, + cv.Optional(CONF_DARK_COLOR, default="black"): lv_color, + cv.Optional(CONF_LIGHT_COLOR, default="white"): lv_color, + cv.Required(CONF_SIZE): cv.int_, +} class QrCodeType(WidgetType): @@ -34,11 +33,11 @@ class QrCodeType(WidgetType): ) def get_uses(self): - return ("canvas", "img", "label") + return "canvas", "img", "label" - def obj_creator(self, parent: MockObjClass, config: dict): - dark_color = color_retmapper(config[CONF_DARK_COLOR]) - light_color = color_retmapper(config[CONF_LIGHT_COLOR]) + async def obj_creator(self, parent: MockObjClass, config: dict): + dark_color = await lv_color.process(config[CONF_DARK_COLOR]) + light_color = await lv_color.process(config[CONF_LIGHT_COLOR]) size = config[CONF_SIZE] return lv_expr.call("qrcode_create", parent, size, dark_color, light_color) diff --git a/esphome/components/lvgl/widgets/spinbox.py b/esphome/components/lvgl/widgets/spinbox.py index b84dc7cd23..c6f25e9587 100644 --- a/esphome/components/lvgl/widgets/spinbox.py +++ b/esphome/components/lvgl/widgets/spinbox.py @@ -1,8 +1,9 @@ from esphome import automation import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_RANGE_FROM, CONF_RANGE_TO, CONF_STEP, CONF_VALUE +from esphome.cpp_generator import MockObj -from ..automation import action_to_code, update_to_code +from ..automation import action_to_code from ..defines import ( CONF_CURSOR, CONF_DECIMAL_PLACES, @@ -11,6 +12,7 @@ from ..defines import ( CONF_ROLLOVER, CONF_SCROLLBAR, CONF_SELECTED, + CONF_SELECTED_DIGIT, CONF_TEXTAREA_PLACEHOLDER, ) from ..lv_validation import lv_bool, lv_float @@ -38,18 +40,24 @@ def validate_spinbox(config): min_val = -1 - max_val range_from = int(config[CONF_RANGE_FROM]) range_to = int(config[CONF_RANGE_TO]) - step = int(config[CONF_STEP]) + step = config[CONF_SELECTED_DIGIT] + digits = config[CONF_DIGITS] if ( range_from > max_val or range_from < min_val or range_to > max_val or range_to < min_val ): - raise cv.Invalid("Range outside allowed limits") - if step <= 0 or step >= (range_to - range_from) / 2: - raise cv.Invalid("Invalid step value") - if config[CONF_DIGITS] <= config[CONF_DECIMAL_PLACES]: - raise cv.Invalid("Number of digits must exceed number of decimal places") + raise cv.Invalid("Range outside allowed limits", path=[CONF_RANGE_FROM]) + if digits <= config[CONF_DECIMAL_PLACES]: + raise cv.Invalid( + "Number of digits must exceed number of decimal places", path=[CONF_DIGITS] + ) + if step >= digits: + raise cv.Invalid( + "Initial selected digit must be less than number of digits", + path=[CONF_SELECTED_DIGIT], + ) return config @@ -59,7 +67,10 @@ SPINBOX_SCHEMA = cv.Schema( cv.Optional(CONF_RANGE_FROM, default=0): cv.float_, cv.Optional(CONF_RANGE_TO, default=100): cv.float_, cv.Optional(CONF_DIGITS, default=4): cv.int_range(1, 10), - cv.Optional(CONF_STEP, default=1.0): cv.positive_float, + cv.Optional(CONF_STEP): cv.invalid( + f"{CONF_STEP} has been replaced by {CONF_SELECTED_DIGIT}" + ), + cv.Optional(CONF_SELECTED_DIGIT, default=0): cv.positive_int, cv.Optional(CONF_DECIMAL_PLACES, default=0): cv.int_range(0, 6), cv.Optional(CONF_ROLLOVER, default=False): lv_bool, } @@ -93,19 +104,20 @@ class SpinboxType(WidgetType): scale = 10 ** config[CONF_DECIMAL_PLACES] range_from = int(config[CONF_RANGE_FROM]) * scale range_to = int(config[CONF_RANGE_TO]) * scale - step = int(config[CONF_STEP]) * scale + step = config[CONF_SELECTED_DIGIT] w.scale = scale - w.step = step w.range_to = range_to w.range_from = range_from lv.spinbox_set_range(w.obj, range_from, range_to) - await w.set_property(CONF_STEP, step) + await w.set_property("step", 10**step) await w.set_property(CONF_ROLLOVER, config) lv.spinbox_set_digit_format( w.obj, digits, digits - config[CONF_DECIMAL_PLACES] ) if (value := config.get(CONF_VALUE)) is not None: - lv.spinbox_set_value(w.obj, await lv_float.process(value)) + lv.spinbox_set_value( + w.obj, MockObj(await lv_float.process(value)) * w.get_scale() + ) def get_scale(self, config): return 10 ** config[CONF_DECIMAL_PLACES] @@ -120,7 +132,7 @@ class SpinboxType(WidgetType): return config[CONF_RANGE_FROM] def get_step(self, config: dict): - return config[CONF_STEP] + return 10 ** config[CONF_SELECTED_DIGIT] spinbox_spec = SpinboxType() @@ -162,17 +174,3 @@ async def spinbox_decrement(config, action_id, template_arg, args): lv.spinbox_decrement(w.obj) return await action_to_code(widgets, do_increment, action_id, template_arg, args) - - -@automation.register_action( - "lvgl.spinbox.update", - ObjUpdateAction, - cv.Schema( - { - cv.Required(CONF_ID): cv.use_id(lv_spinbox_t), - cv.Required(CONF_VALUE): lv_float, - } - ), -) -async def spinbox_update_to_code(config, action_id, template_arg, args): - return await update_to_code(config, action_id, template_arg, args) diff --git a/esphome/components/lvgl/widgets/spinner.py b/esphome/components/lvgl/widgets/spinner.py index 2940feb594..83aac25a59 100644 --- a/esphome/components/lvgl/widgets/spinner.py +++ b/esphome/components/lvgl/widgets/spinner.py @@ -2,7 +2,7 @@ import esphome.config_validation as cv from esphome.cpp_generator import MockObjClass from ..defines import CONF_ARC_LENGTH, CONF_INDICATOR, CONF_MAIN, CONF_SPIN_TIME -from ..lv_validation import angle +from ..lv_validation import lv_angle_degrees, lv_milliseconds from ..lvcode import lv_expr from ..types import LvType from . import Widget, WidgetType @@ -12,8 +12,8 @@ CONF_SPINNER = "spinner" SPINNER_SCHEMA = cv.Schema( { - cv.Required(CONF_ARC_LENGTH): angle, - cv.Required(CONF_SPIN_TIME): cv.positive_time_period_milliseconds, + cv.Required(CONF_ARC_LENGTH): lv_angle_degrees, + cv.Required(CONF_SPIN_TIME): lv_milliseconds, } ) @@ -34,9 +34,9 @@ class SpinnerType(WidgetType): def get_uses(self): return (CONF_ARC,) - def obj_creator(self, parent: MockObjClass, config: dict): - spin_time = config[CONF_SPIN_TIME].total_milliseconds - arc_length = config[CONF_ARC_LENGTH] // 10 + async def obj_creator(self, parent: MockObjClass, config: dict): + spin_time = await lv_milliseconds.process(config[CONF_SPIN_TIME]) + arc_length = await lv_angle_degrees.process(config[CONF_ARC_LENGTH]) return lv_expr.call("spinner_create", parent, spin_time, arc_length) diff --git a/esphome/components/lvgl/widgets/tabview.py b/esphome/components/lvgl/widgets/tabview.py index 42cf486e1c..e8931bab7c 100644 --- a/esphome/components/lvgl/widgets/tabview.py +++ b/esphome/components/lvgl/widgets/tabview.py @@ -87,12 +87,12 @@ class TabviewType(WidgetType): ) as content_obj: await set_obj_properties(Widget(content_obj, obj_spec), content_style) - def obj_creator(self, parent: MockObjClass, config: dict): + async def obj_creator(self, parent: MockObjClass, config: dict): return lv_expr.call( "tabview_create", parent, - literal(config[CONF_POSITION]), - literal(config[CONF_SIZE]), + await DIRECTIONS.process(config[CONF_POSITION]), + await size.process(config[CONF_SIZE]), ) diff --git a/esphome/components/lvgl/widgets/textarea.py b/esphome/components/lvgl/widgets/textarea.py index 23d50b3894..e5ab884685 100644 --- a/esphome/components/lvgl/widgets/textarea.py +++ b/esphome/components/lvgl/widgets/textarea.py @@ -21,15 +21,14 @@ CONF_TEXTAREA = "textarea" lv_textarea_t = LvText("lv_textarea_t") -TEXTAREA_SCHEMA = TEXT_SCHEMA.extend( - { - cv.Optional(CONF_PLACEHOLDER_TEXT): lv_text, - cv.Optional(CONF_ACCEPTED_CHARS): lv_text, - cv.Optional(CONF_ONE_LINE): lv_bool, - cv.Optional(CONF_PASSWORD_MODE): lv_bool, - cv.Optional(CONF_MAX_LENGTH): lv_int, - } -) +TEXTAREA_SCHEMA = { + **TEXT_SCHEMA, + cv.Optional(CONF_PLACEHOLDER_TEXT): lv_text, + cv.Optional(CONF_ACCEPTED_CHARS): lv_text, + cv.Optional(CONF_ONE_LINE): lv_bool, + cv.Optional(CONF_PASSWORD_MODE): lv_bool, + cv.Optional(CONF_MAX_LENGTH): lv_int, +} class TextareaType(WidgetType): diff --git a/esphome/components/m5stack_8angle/binary_sensor/m5stack_8angle_binary_sensor.cpp b/esphome/components/m5stack_8angle/binary_sensor/m5stack_8angle_binary_sensor.cpp index 2f68d9f254..3eeba4a644 100644 --- a/esphome/components/m5stack_8angle/binary_sensor/m5stack_8angle_binary_sensor.cpp +++ b/esphome/components/m5stack_8angle/binary_sensor/m5stack_8angle_binary_sensor.cpp @@ -6,7 +6,7 @@ namespace m5stack_8angle { void M5Stack8AngleSwitchBinarySensor::update() { int8_t out = this->parent_->read_switch(); if (out == -1) { - this->status_set_warning("Could not read binary sensor state from M5Stack 8Angle."); + this->status_set_warning(LOG_STR("Could not read binary sensor state from M5Stack 8Angle.")); return; } this->publish_state(out != 0); diff --git a/esphome/components/m5stack_8angle/sensor/m5stack_8angle_sensor.cpp b/esphome/components/m5stack_8angle/sensor/m5stack_8angle_sensor.cpp index 5e034f1dd3..d22b345141 100644 --- a/esphome/components/m5stack_8angle/sensor/m5stack_8angle_sensor.cpp +++ b/esphome/components/m5stack_8angle/sensor/m5stack_8angle_sensor.cpp @@ -7,7 +7,7 @@ void M5Stack8AngleKnobSensor::update() { if (this->parent_ != nullptr) { int32_t raw_pos = this->parent_->read_knob_pos_raw(this->channel_, this->bits_); if (raw_pos == -1) { - this->status_set_warning("Could not read knob position from M5Stack 8Angle."); + this->status_set_warning(LOG_STR("Could not read knob position from M5Stack 8Angle.")); return; } if (this->raw_) { diff --git a/esphome/components/mapping/__init__.py b/esphome/components/mapping/__init__.py index 79657084fa..94c7c10a82 100644 --- a/esphome/components/mapping/__init__.py +++ b/esphome/components/mapping/__init__.py @@ -10,7 +10,8 @@ from esphome.loader import get_component CODEOWNERS = ["@clydebarrow"] MULTI_CONF = True -map_ = cg.std_ns.class_("map") +mapping_ns = cg.esphome_ns.namespace("mapping") +mapping_class = mapping_ns.class_("Mapping") CONF_ENTRIES = "entries" CONF_CLASS = "class" @@ -29,7 +30,11 @@ class IndexType: INDEX_TYPES = { "int": IndexType(cv.int_, cg.int_, int), - "string": IndexType(cv.string, cg.std_string, str), + "string": IndexType( + cv.string, + cg.std_string, + str, + ), } @@ -47,7 +52,7 @@ def to_schema(value): BASE_SCHEMA = cv.Schema( { - cv.Required(CONF_ID): cv.declare_id(map_), + cv.Required(CONF_ID): cv.declare_id(mapping_class), cv.Required(CONF_FROM): cv.one_of(*INDEX_TYPES, lower=True), cv.Required(CONF_TO): cv.string, }, @@ -123,12 +128,15 @@ async def to_code(config): if list(entries.values())[0].op != ".": value_type = value_type.operator("ptr") varid = config[CONF_ID] - varid.type = map_.template(index_type, value_type) + varid.type = mapping_class.template( + index_type, + value_type, + ) var = MockObj(varid, ".") decl = VariableDeclarationExpression(varid.type, "", varid) add_global(decl) CORE.register_variable(varid, var) for key, value in entries.items(): - cg.add(var.insert((key, value))) + cg.add(var.set(key, value)) return var diff --git a/esphome/components/mapping/mapping.h b/esphome/components/mapping/mapping.h new file mode 100644 index 0000000000..99c1f38829 --- /dev/null +++ b/esphome/components/mapping/mapping.h @@ -0,0 +1,69 @@ +#pragma once + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include +#include + +namespace esphome::mapping { + +using alloc_string_t = std::basic_string, RAMAllocator>; + +/** + * + * Mapping class with custom allocator. + * Additionally, when std::string is used as key or value, it will be replaced with a custom string type + * that uses RAMAllocator. + * @tparam K The type of the key in the mapping. + * @tparam V The type of the value in the mapping. Should be a basic type or pointer. + */ + +static const char *const TAG = "mapping"; + +template class Mapping { + public: + // Constructor + Mapping() = default; + + using key_t = const std::conditional_t, + alloc_string_t, // if K is std::string, custom string type + K>; + using value_t = std::conditional_t, + alloc_string_t, // if V is std::string, custom string type + V>; + + void set(const K &key, const V &value) { this->map_[key_t{key}] = value; } + + V get(const K &key) const { + auto it = this->map_.find(key_t{key}); + if (it != this->map_.end()) { + return V{it->second}; + } + if constexpr (std::is_pointer_v) { + esph_log_e(TAG, "Key '%p' not found in mapping", key); + } else if constexpr (std::is_same_v) { + esph_log_e(TAG, "Key '%s' not found in mapping", key.c_str()); + } else { + esph_log_e(TAG, "Key '%s' not found in mapping", to_string(key).c_str()); + } + return {}; + } + + // index map overload + V operator[](K key) { return this->get(key); } + + // convenience function for strings to get a C-style string + template, int> = 0> + const char *operator[](K key) const { + auto it = this->map_.find(key_t{key}); + if (it != this->map_.end()) { + return it->second.c_str(); // safe since value remains in map + } + return ""; + } + + protected: + std::map, RAMAllocator>> map_; +}; + +} // namespace esphome::mapping diff --git a/esphome/components/matrix_keypad/__init__.py b/esphome/components/matrix_keypad/__init__.py index f7a1d622a1..868b149211 100644 --- a/esphome/components/matrix_keypad/__init__.py +++ b/esphome/components/matrix_keypad/__init__.py @@ -1,6 +1,7 @@ from esphome import automation, pins import esphome.codegen as cg from esphome.components import key_provider +from esphome.components.const import CONF_ROWS import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_ON_KEY, CONF_PIN, CONF_TRIGGER_ID @@ -19,7 +20,6 @@ MatrixKeyTrigger = matrix_keypad_ns.class_( ) CONF_KEYPAD_ID = "keypad_id" -CONF_ROWS = "rows" CONF_COLUMNS = "columns" CONF_KEYS = "keys" CONF_DEBOUNCE_TIME = "debounce_time" diff --git a/esphome/components/matrix_keypad/matrix_keypad.h b/esphome/components/matrix_keypad/matrix_keypad.h index 8b309b42c2..258ab4fadc 100644 --- a/esphome/components/matrix_keypad/matrix_keypad.h +++ b/esphome/components/matrix_keypad/matrix_keypad.h @@ -29,9 +29,9 @@ class MatrixKeypad : public key_provider::KeyProvider, public Component { void set_columns(std::vector pins) { columns_ = std::move(pins); }; void set_rows(std::vector pins) { rows_ = std::move(pins); }; void set_keys(std::string keys) { keys_ = std::move(keys); }; - void set_debounce_time(int debounce_time) { debounce_time_ = debounce_time; }; - void set_has_diodes(int has_diodes) { has_diodes_ = has_diodes; }; - void set_has_pulldowns(int has_pulldowns) { has_pulldowns_ = has_pulldowns; }; + void set_debounce_time(uint32_t debounce_time) { debounce_time_ = debounce_time; }; + void set_has_diodes(bool has_diodes) { has_diodes_ = has_diodes; }; + void set_has_pulldowns(bool has_pulldowns) { has_pulldowns_ = has_pulldowns; }; void register_listener(MatrixKeypadListener *listener); void register_key_trigger(MatrixKeyTrigger *trig); @@ -40,7 +40,7 @@ class MatrixKeypad : public key_provider::KeyProvider, public Component { std::vector rows_; std::vector columns_; std::string keys_; - int debounce_time_ = 0; + uint32_t debounce_time_ = 0; bool has_diodes_{false}; bool has_pulldowns_{false}; int pressed_key_ = -1; diff --git a/esphome/components/max17043/automation.h b/esphome/components/max17043/automation.h index 44729d119b..ac201a7309 100644 --- a/esphome/components/max17043/automation.h +++ b/esphome/components/max17043/automation.h @@ -10,7 +10,7 @@ template class SleepAction : public Action { public: explicit SleepAction(MAX17043Component *max17043) : max17043_(max17043) {} - void play(Ts... x) override { this->max17043_->sleep_mode(); } + void play(const Ts &...x) override { this->max17043_->sleep_mode(); } protected: MAX17043Component *max17043_; diff --git a/esphome/components/max17043/max17043.cpp b/esphome/components/max17043/max17043.cpp index 8f486de6b7..e8cf4d5ab1 100644 --- a/esphome/components/max17043/max17043.cpp +++ b/esphome/components/max17043/max17043.cpp @@ -22,7 +22,7 @@ void MAX17043Component::update() { if (this->voltage_sensor_ != nullptr) { if (!this->read_byte_16(MAX17043_VCELL, &raw_voltage)) { - this->status_set_warning("Unable to read MAX17043_VCELL"); + this->status_set_warning(LOG_STR("Unable to read MAX17043_VCELL")); } else { float voltage = (1.25 * (float) (raw_voltage >> 4)) / 1000.0; this->voltage_sensor_->publish_state(voltage); @@ -31,7 +31,7 @@ void MAX17043Component::update() { } if (this->battery_remaining_sensor_ != nullptr) { if (!this->read_byte_16(MAX17043_SOC, &raw_percent)) { - this->status_set_warning("Unable to read MAX17043_SOC"); + this->status_set_warning(LOG_STR("Unable to read MAX17043_SOC")); } else { float percent = (float) ((raw_percent >> 8) + 0.003906f * (raw_percent & 0x00ff)); this->battery_remaining_sensor_->publish_state(percent); @@ -57,14 +57,14 @@ void MAX17043Component::setup() { if (config_reg != MAX17043_CONFIG_POWER_UP_DEFAULT) { ESP_LOGE(TAG, "Device does not appear to be a MAX17043"); - this->status_set_error("unrecognised"); + this->status_set_error(LOG_STR("unrecognised")); this->mark_failed(); return; } // need to write back to config register to reset the sleep bit if (!this->write_byte_16(MAX17043_CONFIG, MAX17043_CONFIG_POWER_UP_DEFAULT)) { - this->status_set_error("sleep reset failed"); + this->status_set_error(LOG_STR("sleep reset failed")); this->mark_failed(); return; } diff --git a/esphome/components/max6956/automation.h b/esphome/components/max6956/automation.h index c0b491dc7f..ca2c3e3ce4 100644 --- a/esphome/components/max6956/automation.h +++ b/esphome/components/max6956/automation.h @@ -13,7 +13,7 @@ template class SetCurrentGlobalAction : public Action { TEMPLATABLE_VALUE(uint8_t, brightness_global) - void play(Ts... x) override { + void play(const Ts &...x) override { this->max6956_->set_brightness_global(this->brightness_global_.value(x...)); this->max6956_->write_brightness_global(); } @@ -28,7 +28,7 @@ template class SetCurrentModeAction : public Action { TEMPLATABLE_VALUE(max6956::MAX6956CURRENTMODE, brightness_mode) - void play(Ts... x) override { + void play(const Ts &...x) override { this->max6956_->set_brightness_mode(this->brightness_mode_.value(x...)); this->max6956_->write_brightness_mode(); } diff --git a/esphome/components/max7219/max7219.h b/esphome/components/max7219/max7219.h index 270edf3282..58d871d54c 100644 --- a/esphome/components/max7219/max7219.h +++ b/esphome/components/max7219/max7219.h @@ -4,13 +4,14 @@ #include "esphome/core/time.h" #include "esphome/components/spi/spi.h" +#include "esphome/components/display/display.h" namespace esphome { namespace max7219 { class MAX7219Component; -using max7219_writer_t = std::function; +using max7219_writer_t = display::DisplayWriter; class MAX7219Component : public PollingComponent, public spi::SPIDevice writer_{}; + max7219_writer_t writer_{}; }; } // namespace max7219 diff --git a/esphome/components/max7219digit/automation.h b/esphome/components/max7219digit/automation.h index 02acebb109..be8245d14d 100644 --- a/esphome/components/max7219digit/automation.h +++ b/esphome/components/max7219digit/automation.h @@ -12,7 +12,7 @@ template class DisplayInvertAction : public Action, publi public: TEMPLATABLE_VALUE(bool, state) - void play(Ts... x) override { + void play(const Ts &...x) override { bool state = this->state_.value(x...); this->parent_->invert_on_off(state); } @@ -22,7 +22,7 @@ template class DisplayVisibilityAction : public Action, p public: TEMPLATABLE_VALUE(bool, state) - void play(Ts... x) override { + void play(const Ts &...x) override { bool state = this->state_.value(x...); this->parent_->turn_on_off(state); } @@ -32,7 +32,7 @@ template class DisplayReverseAction : public Action, publ public: TEMPLATABLE_VALUE(bool, state) - void play(Ts... x) override { + void play(const Ts &...x) override { bool state = this->state_.value(x...); this->parent_->set_reverse(state); } @@ -42,7 +42,7 @@ template class DisplayIntensityAction : public Action, pu public: TEMPLATABLE_VALUE(uint8_t, state) - void play(Ts... x) override { + void play(const Ts &...x) override { uint8_t state = this->state_.value(x...); this->parent_->set_intensity(state); } diff --git a/esphome/components/max7219digit/max7219digit.cpp b/esphome/components/max7219digit/max7219digit.cpp index 9b9921d2f0..cdceafad50 100644 --- a/esphome/components/max7219digit/max7219digit.cpp +++ b/esphome/components/max7219digit/max7219digit.cpp @@ -90,7 +90,7 @@ void MAX7219Component::loop() { } if (this->scroll_mode_ == ScrollMode::STOP) { - if (this->stepsleft_ + get_width_internal() == first_line_size + 1) { + if (static_cast(this->stepsleft_ + get_width_internal()) == first_line_size + 1) { if (millis_since_last_scroll < this->scroll_dwell_) { ESP_LOGVV(TAG, "Dwell time at end of string in case of stop at end. Step %d, since last scroll %d, dwell %d.", this->stepsleft_, millis_since_last_scroll, this->scroll_dwell_); @@ -271,7 +271,11 @@ void MAX7219Component::send64pixels(uint8_t chip, const uint8_t pixels[8]) { } } } else if (this->orientation_ == 1) { - b = pixels[col]; + if (this->flip_x_) { + b = pixels[7 - col]; + } else { + b = pixels[col]; + } } else if (this->orientation_ == 2) { for (uint8_t i = 0; i < 8; i++) { if (this->flip_x_) { @@ -282,7 +286,11 @@ void MAX7219Component::send64pixels(uint8_t chip, const uint8_t pixels[8]) { } } else { for (uint8_t i = 0; i < 8; i++) { - b |= ((pixels[7 - col] >> i) & 1) << (7 - i); + if (this->flip_x_) { + b |= ((pixels[col] >> i) & 1) << (7 - i); + } else { + b |= ((pixels[7 - col] >> i) & 1) << (7 - i); + } } } // send this byte to display at selected chip diff --git a/esphome/components/max7219digit/max7219digit.h b/esphome/components/max7219digit/max7219digit.h index ead8033803..af419b9b38 100644 --- a/esphome/components/max7219digit/max7219digit.h +++ b/esphome/components/max7219digit/max7219digit.h @@ -23,7 +23,7 @@ enum ScrollMode { class MAX7219Component; -using max7219_writer_t = std::function; +using max7219_writer_t = display::DisplayWriter; class MAX7219Component : public display::DisplayBuffer, public spi::SPIDevice writer_local_{}; + max7219_writer_t writer_local_{}; }; } // namespace max7219digit diff --git a/esphome/components/mcp23008/__init__.py b/esphome/components/mcp23008/__init__.py index ed48eb06a6..8ff938114a 100644 --- a/esphome/components/mcp23008/__init__.py +++ b/esphome/components/mcp23008/__init__.py @@ -24,5 +24,5 @@ CONFIG_SCHEMA = ( async def to_code(config): - var = await mcp23xxx_base.register_mcp23xxx(config) + var = await mcp23xxx_base.register_mcp23xxx(config, mcp23x08_base.NUM_PINS) await i2c.register_i2c_device(var, config) diff --git a/esphome/components/mcp23016/__init__.py b/esphome/components/mcp23016/__init__.py index 3333e46c97..5a1f011617 100644 --- a/esphome/components/mcp23016/__init__.py +++ b/esphome/components/mcp23016/__init__.py @@ -11,6 +11,7 @@ from esphome.const import ( CONF_OUTPUT, ) +AUTO_LOAD = ["gpio_expander"] DEPENDENCIES = ["i2c"] MULTI_CONF = True diff --git a/esphome/components/mcp23016/mcp23016.cpp b/esphome/components/mcp23016/mcp23016.cpp index 9d8d6e4dae..be86cb2256 100644 --- a/esphome/components/mcp23016/mcp23016.cpp +++ b/esphome/components/mcp23016/mcp23016.cpp @@ -22,14 +22,29 @@ void MCP23016::setup() { this->write_reg_(MCP23016_IODIR0, 0xFF); this->write_reg_(MCP23016_IODIR1, 0xFF); } -bool MCP23016::digital_read(uint8_t pin) { - uint8_t bit = pin % 8; + +void MCP23016::loop() { + // Invalidate cache at the start of each loop + this->reset_pin_cache_(); +} +bool MCP23016::digital_read_hw(uint8_t pin) { uint8_t reg_addr = pin < 8 ? MCP23016_GP0 : MCP23016_GP1; uint8_t value = 0; - this->read_reg_(reg_addr, &value); - return value & (1 << bit); + if (!this->read_reg_(reg_addr, &value)) { + return false; + } + + // Update the appropriate part of input_mask_ + if (pin < 8) { + this->input_mask_ = (this->input_mask_ & 0xFF00) | value; + } else { + this->input_mask_ = (this->input_mask_ & 0x00FF) | (uint16_t(value) << 8); + } + return true; } -void MCP23016::digital_write(uint8_t pin, bool value) { + +bool MCP23016::digital_read_cache(uint8_t pin) { return this->input_mask_ & (1 << pin); } +void MCP23016::digital_write_hw(uint8_t pin, bool value) { uint8_t reg_addr = pin < 8 ? MCP23016_OLAT0 : MCP23016_OLAT1; this->update_reg_(pin, value, reg_addr); } diff --git a/esphome/components/mcp23016/mcp23016.h b/esphome/components/mcp23016/mcp23016.h index e4ed47a3b2..781c207de0 100644 --- a/esphome/components/mcp23016/mcp23016.h +++ b/esphome/components/mcp23016/mcp23016.h @@ -3,6 +3,7 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" #include "esphome/components/i2c/i2c.h" +#include "esphome/components/gpio_expander/cached_gpio.h" namespace esphome { namespace mcp23016 { @@ -24,19 +25,22 @@ enum MCP23016GPIORegisters { MCP23016_IOCON1 = 0x0B, }; -class MCP23016 : public Component, public i2c::I2CDevice { +class MCP23016 : public Component, public i2c::I2CDevice, public gpio_expander::CachedGpioExpander { public: MCP23016() = default; void setup() override; - - bool digital_read(uint8_t pin); - void digital_write(uint8_t pin, bool value); + void loop() override; void pin_mode(uint8_t pin, gpio::Flags flags); float get_setup_priority() const override; protected: + // Virtual methods from CachedGpioExpander + bool digital_read_hw(uint8_t pin) override; + bool digital_read_cache(uint8_t pin) override; + void digital_write_hw(uint8_t pin, bool value) override; + // read a given register bool read_reg_(uint8_t reg, uint8_t *value); // write a value to a given register @@ -46,6 +50,8 @@ class MCP23016 : public Component, public i2c::I2CDevice { uint8_t olat_0_{0x00}; uint8_t olat_1_{0x00}; + // Cache for input values (16-bit combined for both banks) + uint16_t input_mask_{0x00}; }; class MCP23016GPIOPin : public GPIOPin { diff --git a/esphome/components/mcp23017/__init__.py b/esphome/components/mcp23017/__init__.py index 33b8a680cf..e5cc1856eb 100644 --- a/esphome/components/mcp23017/__init__.py +++ b/esphome/components/mcp23017/__init__.py @@ -24,5 +24,5 @@ CONFIG_SCHEMA = ( async def to_code(config): - var = await mcp23xxx_base.register_mcp23xxx(config) + var = await mcp23xxx_base.register_mcp23xxx(config, mcp23x17_base.NUM_PINS) await i2c.register_i2c_device(var, config) diff --git a/esphome/components/mcp23s08/__init__.py b/esphome/components/mcp23s08/__init__.py index c6152d58c0..3d4e304f9b 100644 --- a/esphome/components/mcp23s08/__init__.py +++ b/esphome/components/mcp23s08/__init__.py @@ -27,6 +27,6 @@ CONFIG_SCHEMA = ( async def to_code(config): - var = await mcp23xxx_base.register_mcp23xxx(config) + var = await mcp23xxx_base.register_mcp23xxx(config, mcp23x08_base.NUM_PINS) cg.add(var.set_device_address(config[CONF_DEVICEADDRESS])) await spi.register_spi_device(var, config) diff --git a/esphome/components/mcp23s17/__init__.py b/esphome/components/mcp23s17/__init__.py index 9a763d09b0..ea8433af2e 100644 --- a/esphome/components/mcp23s17/__init__.py +++ b/esphome/components/mcp23s17/__init__.py @@ -27,6 +27,6 @@ CONFIG_SCHEMA = ( async def to_code(config): - var = await mcp23xxx_base.register_mcp23xxx(config) + var = await mcp23xxx_base.register_mcp23xxx(config, mcp23x17_base.NUM_PINS) cg.add(var.set_device_address(config[CONF_DEVICEADDRESS])) await spi.register_spi_device(var, config) diff --git a/esphome/components/mcp23x08_base/__init__.py b/esphome/components/mcp23x08_base/__init__.py index ba44917202..a3c12165f0 100644 --- a/esphome/components/mcp23x08_base/__init__.py +++ b/esphome/components/mcp23x08_base/__init__.py @@ -4,5 +4,7 @@ from esphome.components import mcp23xxx_base AUTO_LOAD = ["mcp23xxx_base"] CODEOWNERS = ["@jesserockz"] +NUM_PINS = 8 + mcp23x08_base_ns = cg.esphome_ns.namespace("mcp23x08_base") MCP23X08Base = mcp23x08_base_ns.class_("MCP23X08Base", mcp23xxx_base.MCP23XXXBase) diff --git a/esphome/components/mcp23x08_base/mcp23x08_base.cpp b/esphome/components/mcp23x08_base/mcp23x08_base.cpp index 0c20e902c4..1593c376cd 100644 --- a/esphome/components/mcp23x08_base/mcp23x08_base.cpp +++ b/esphome/components/mcp23x08_base/mcp23x08_base.cpp @@ -6,19 +6,21 @@ namespace mcp23x08_base { static const char *const TAG = "mcp23x08_base"; -bool MCP23X08Base::digital_read(uint8_t pin) { - uint8_t bit = pin % 8; - uint8_t reg_addr = mcp23x08_base::MCP23X08_GPIO; - uint8_t value = 0; - this->read_reg(reg_addr, &value); - return value & (1 << bit); +bool MCP23X08Base::digital_read_hw(uint8_t pin) { + if (!this->read_reg(mcp23x08_base::MCP23X08_GPIO, &this->input_mask_)) { + this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); + return false; + } + return true; } -void MCP23X08Base::digital_write(uint8_t pin, bool value) { +void MCP23X08Base::digital_write_hw(uint8_t pin, bool value) { uint8_t reg_addr = mcp23x08_base::MCP23X08_OLAT; this->update_reg(pin, value, reg_addr); } +bool MCP23X08Base::digital_read_cache(uint8_t pin) { return this->input_mask_ & (1 << pin); } + void MCP23X08Base::pin_mode(uint8_t pin, gpio::Flags flags) { uint8_t iodir = mcp23x08_base::MCP23X08_IODIR; uint8_t gppu = mcp23x08_base::MCP23X08_GPPU; diff --git a/esphome/components/mcp23x08_base/mcp23x08_base.h b/esphome/components/mcp23x08_base/mcp23x08_base.h index 910519119b..6eee8274b1 100644 --- a/esphome/components/mcp23x08_base/mcp23x08_base.h +++ b/esphome/components/mcp23x08_base/mcp23x08_base.h @@ -1,7 +1,7 @@ #pragma once -#include "esphome/core/component.h" #include "esphome/components/mcp23xxx_base/mcp23xxx_base.h" +#include "esphome/core/component.h" #include "esphome/core/hal.h" namespace esphome { @@ -22,10 +22,12 @@ enum MCP23S08GPIORegisters { MCP23X08_OLAT = 0x0A, }; -class MCP23X08Base : public mcp23xxx_base::MCP23XXXBase { +class MCP23X08Base : public mcp23xxx_base::MCP23XXXBase<8> { public: - bool digital_read(uint8_t pin) override; - void digital_write(uint8_t pin, bool value) override; + bool digital_read_hw(uint8_t pin) override; + void digital_write_hw(uint8_t pin, bool value) override; + bool digital_read_cache(uint8_t pin) override; + void pin_mode(uint8_t pin, gpio::Flags flags) override; void pin_interrupt_mode(uint8_t pin, mcp23xxx_base::MCP23XXXInterruptMode interrupt_mode) override; @@ -33,6 +35,9 @@ class MCP23X08Base : public mcp23xxx_base::MCP23XXXBase { void update_reg(uint8_t pin, bool pin_value, uint8_t reg_a) override; uint8_t olat_{0x00}; + + /// State read in digital_read_hw + uint8_t input_mask_{0x00}; }; } // namespace mcp23x08_base diff --git a/esphome/components/mcp23x17_base/__init__.py b/esphome/components/mcp23x17_base/__init__.py index 97e0b3823d..1b93d16ff3 100644 --- a/esphome/components/mcp23x17_base/__init__.py +++ b/esphome/components/mcp23x17_base/__init__.py @@ -4,5 +4,7 @@ from esphome.components import mcp23xxx_base AUTO_LOAD = ["mcp23xxx_base"] CODEOWNERS = ["@jesserockz"] +NUM_PINS = 16 + mcp23x17_base_ns = cg.esphome_ns.namespace("mcp23x17_base") MCP23X17Base = mcp23x17_base_ns.class_("MCP23X17Base", mcp23xxx_base.MCP23XXXBase) diff --git a/esphome/components/mcp23x17_base/mcp23x17_base.cpp b/esphome/components/mcp23x17_base/mcp23x17_base.cpp index 99064f8880..b1f1f260b4 100644 --- a/esphome/components/mcp23x17_base/mcp23x17_base.cpp +++ b/esphome/components/mcp23x17_base/mcp23x17_base.cpp @@ -1,4 +1,5 @@ #include "mcp23x17_base.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" namespace esphome { @@ -6,19 +7,31 @@ namespace mcp23x17_base { static const char *const TAG = "mcp23x17_base"; -bool MCP23X17Base::digital_read(uint8_t pin) { - uint8_t bit = pin % 8; - uint8_t reg_addr = pin < 8 ? mcp23x17_base::MCP23X17_GPIOA : mcp23x17_base::MCP23X17_GPIOB; - uint8_t value = 0; - this->read_reg(reg_addr, &value); - return value & (1 << bit); +bool MCP23X17Base::digital_read_hw(uint8_t pin) { + uint8_t data; + if (pin < 8) { + if (!this->read_reg(mcp23x17_base::MCP23X17_GPIOA, &data)) { + this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); + return false; + } + this->input_mask_ = encode_uint16(this->input_mask_ >> 8, data); + } else { + if (!this->read_reg(mcp23x17_base::MCP23X17_GPIOB, &data)) { + this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); + return false; + } + this->input_mask_ = encode_uint16(data, this->input_mask_ & 0xFF); + } + return true; } -void MCP23X17Base::digital_write(uint8_t pin, bool value) { +void MCP23X17Base::digital_write_hw(uint8_t pin, bool value) { uint8_t reg_addr = pin < 8 ? mcp23x17_base::MCP23X17_OLATA : mcp23x17_base::MCP23X17_OLATB; this->update_reg(pin, value, reg_addr); } +bool MCP23X17Base::digital_read_cache(uint8_t pin) { return this->input_mask_ & (1 << pin); } + void MCP23X17Base::pin_mode(uint8_t pin, gpio::Flags flags) { uint8_t iodir = pin < 8 ? mcp23x17_base::MCP23X17_IODIRA : mcp23x17_base::MCP23X17_IODIRB; uint8_t gppu = pin < 8 ? mcp23x17_base::MCP23X17_GPPUA : mcp23x17_base::MCP23X17_GPPUB; diff --git a/esphome/components/mcp23x17_base/mcp23x17_base.h b/esphome/components/mcp23x17_base/mcp23x17_base.h index 3d50ee8c03..bdd66503e2 100644 --- a/esphome/components/mcp23x17_base/mcp23x17_base.h +++ b/esphome/components/mcp23x17_base/mcp23x17_base.h @@ -1,7 +1,7 @@ #pragma once -#include "esphome/core/component.h" #include "esphome/components/mcp23xxx_base/mcp23xxx_base.h" +#include "esphome/core/component.h" #include "esphome/core/hal.h" namespace esphome { @@ -34,10 +34,12 @@ enum MCP23X17GPIORegisters { MCP23X17_OLATB = 0x15, }; -class MCP23X17Base : public mcp23xxx_base::MCP23XXXBase { +class MCP23X17Base : public mcp23xxx_base::MCP23XXXBase<16> { public: - bool digital_read(uint8_t pin) override; - void digital_write(uint8_t pin, bool value) override; + bool digital_read_hw(uint8_t pin) override; + void digital_write_hw(uint8_t pin, bool value) override; + bool digital_read_cache(uint8_t pin) override; + void pin_mode(uint8_t pin, gpio::Flags flags) override; void pin_interrupt_mode(uint8_t pin, mcp23xxx_base::MCP23XXXInterruptMode interrupt_mode) override; @@ -46,6 +48,9 @@ class MCP23X17Base : public mcp23xxx_base::MCP23XXXBase { uint8_t olat_a_{0x00}; uint8_t olat_b_{0x00}; + + /// State read in digital_read_hw + uint16_t input_mask_{0x00}; }; } // namespace mcp23x17_base diff --git a/esphome/components/mcp23xxx_base/__init__.py b/esphome/components/mcp23xxx_base/__init__.py index 8cf0ebcd44..d6e82101ad 100644 --- a/esphome/components/mcp23xxx_base/__init__.py +++ b/esphome/components/mcp23xxx_base/__init__.py @@ -12,8 +12,9 @@ from esphome.const import ( CONF_OUTPUT, CONF_PULLUP, ) -from esphome.core import coroutine +from esphome.core import CORE, ID, coroutine +AUTO_LOAD = ["gpio_expander"] CODEOWNERS = ["@jesserockz"] mcp23xxx_base_ns = cg.esphome_ns.namespace("mcp23xxx_base") @@ -36,9 +37,11 @@ MCP23XXX_CONFIG_SCHEMA = cv.Schema( @coroutine -async def register_mcp23xxx(config): - var = cg.new_Pvariable(config[CONF_ID]) +async def register_mcp23xxx(config, num_pins): + id: ID = config[CONF_ID] + var = cg.new_Pvariable(id) await cg.register_component(var, config) + CORE.data.setdefault(CONF_MCP23XXX, {})[id.id] = num_pins cg.add(var.set_open_drain_ints(config[CONF_OPEN_DRAIN_INTERRUPT])) return var @@ -73,9 +76,12 @@ MCP23XXX_PIN_SCHEMA = pins.gpio_base_schema( @pins.PIN_SCHEMA_REGISTRY.register(CONF_MCP23XXX, MCP23XXX_PIN_SCHEMA) async def mcp23xxx_pin_to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - parent = await cg.get_variable(config[CONF_MCP23XXX]) + parent_id: ID = config[CONF_MCP23XXX] + parent = await cg.get_variable(parent_id) + num_pins = cg.TemplateArguments(CORE.data[CONF_MCP23XXX][parent_id.id]) + + var = cg.new_Pvariable(config[CONF_ID], num_pins) cg.add(var.set_parent(parent)) num = config[CONF_NUMBER] diff --git a/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp b/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp index fc49f216ee..81324e794f 100644 --- a/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp +++ b/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp @@ -1,24 +1,27 @@ #include "mcp23xxx_base.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" namespace esphome { namespace mcp23xxx_base { -float MCP23XXXBase::get_setup_priority() const { return setup_priority::IO; } - -void MCP23XXXGPIOPin::setup() { - pin_mode(flags_); +template void MCP23XXXGPIOPin::setup() { + this->pin_mode(flags_); this->parent_->pin_interrupt_mode(this->pin_, this->interrupt_mode_); } - -void MCP23XXXGPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } -bool MCP23XXXGPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } -void MCP23XXXGPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } -std::string MCP23XXXGPIOPin::dump_summary() const { - char buffer[32]; - snprintf(buffer, sizeof(buffer), "%u via MCP23XXX", pin_); - return buffer; +template void MCP23XXXGPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } +template bool MCP23XXXGPIOPin::digital_read() { + return this->parent_->digital_read(this->pin_) != this->inverted_; } +template void MCP23XXXGPIOPin::digital_write(bool value) { + this->parent_->digital_write(this->pin_, value != this->inverted_); +} +template std::string MCP23XXXGPIOPin::dump_summary() const { + return str_snprintf("%u via MCP23XXX", 15, pin_); +} + +template class MCP23XXXGPIOPin<8>; +template class MCP23XXXGPIOPin<16>; } // namespace mcp23xxx_base } // namespace esphome diff --git a/esphome/components/mcp23xxx_base/mcp23xxx_base.h b/esphome/components/mcp23xxx_base/mcp23xxx_base.h index 9686c9fd33..cf0ef5d41c 100644 --- a/esphome/components/mcp23xxx_base/mcp23xxx_base.h +++ b/esphome/components/mcp23xxx_base/mcp23xxx_base.h @@ -1,5 +1,6 @@ #pragma once +#include "esphome/components/gpio_expander/cached_gpio.h" #include "esphome/core/component.h" #include "esphome/core/hal.h" @@ -8,28 +9,28 @@ namespace mcp23xxx_base { enum MCP23XXXInterruptMode : uint8_t { MCP23XXX_NO_INTERRUPT = 0, MCP23XXX_CHANGE, MCP23XXX_RISING, MCP23XXX_FALLING }; -class MCP23XXXBase : public Component { +template class MCP23XXXBase : public Component, public gpio_expander::CachedGpioExpander { public: - virtual bool digital_read(uint8_t pin); - virtual void digital_write(uint8_t pin, bool value); virtual void pin_mode(uint8_t pin, gpio::Flags flags); virtual void pin_interrupt_mode(uint8_t pin, MCP23XXXInterruptMode interrupt_mode); void set_open_drain_ints(const bool value) { this->open_drain_ints_ = value; } - float get_setup_priority() const override; + float get_setup_priority() const override { return setup_priority::IO; } + + void loop() override { this->reset_pin_cache_(); } protected: // read a given register - virtual bool read_reg(uint8_t reg, uint8_t *value); + virtual bool read_reg(uint8_t reg, uint8_t *value) = 0; // write a value to a given register - virtual bool write_reg(uint8_t reg, uint8_t value); + virtual bool write_reg(uint8_t reg, uint8_t value) = 0; // update registers with given pin value. - virtual void update_reg(uint8_t pin, bool pin_value, uint8_t reg_a); + virtual void update_reg(uint8_t pin, bool pin_value, uint8_t reg_a) = 0; bool open_drain_ints_; }; -class MCP23XXXGPIOPin : public GPIOPin { +template class MCP23XXXGPIOPin : public GPIOPin { public: void setup() override; void pin_mode(gpio::Flags flags) override; @@ -37,7 +38,7 @@ class MCP23XXXGPIOPin : public GPIOPin { void digital_write(bool value) override; std::string dump_summary() const override; - void set_parent(MCP23XXXBase *parent) { parent_ = parent; } + void set_parent(MCP23XXXBase *parent) { parent_ = parent; } void set_pin(uint8_t pin) { pin_ = pin; } void set_inverted(bool inverted) { inverted_ = inverted; } void set_flags(gpio::Flags flags) { flags_ = flags; } @@ -46,7 +47,7 @@ class MCP23XXXGPIOPin : public GPIOPin { gpio::Flags get_flags() const override { return this->flags_; } protected: - MCP23XXXBase *parent_; + MCP23XXXBase *parent_; uint8_t pin_; bool inverted_; gpio::Flags flags_; diff --git a/esphome/components/mcp2515/mcp2515.cpp b/esphome/components/mcp2515/mcp2515.cpp index 23104f5aeb..1a17715315 100644 --- a/esphome/components/mcp2515/mcp2515.cpp +++ b/esphome/components/mcp2515/mcp2515.cpp @@ -20,6 +20,23 @@ bool MCP2515::setup_internal() { return false; if (this->set_bitrate_(this->bit_rate_, this->mcp_clock_) != canbus::ERROR_OK) return false; + + // setup hardware filter RXF0 accepting all standard CAN IDs + if (this->set_filter_(RXF::RXF0, false, 0) != canbus::ERROR_OK) { + return false; + } + if (this->set_filter_mask_(MASK::MASK0, false, 0) != canbus::ERROR_OK) { + return false; + } + + // setup hardware filter RXF1 accepting all extended CAN IDs + if (this->set_filter_(RXF::RXF1, true, 0) != canbus::ERROR_OK) { + return false; + } + if (this->set_filter_mask_(MASK::MASK1, true, 0) != canbus::ERROR_OK) { + return false; + } + if (this->set_mode_(this->mcp_mode_) != canbus::ERROR_OK) return false; uint8_t err_flags = this->get_error_flags_(); @@ -155,7 +172,7 @@ void MCP2515::prepare_id_(uint8_t *buffer, const bool extended, const uint32_t i canid = (uint16_t) (id >> 16); buffer[MCP_SIDL] = (uint8_t) (canid & 0x03); buffer[MCP_SIDL] += (uint8_t) ((canid & 0x1C) << 3); - buffer[MCP_SIDL] |= TXB_EXIDE_MASK; + buffer[MCP_SIDL] |= SIDL_EXIDE_MASK; buffer[MCP_SIDH] = (uint8_t) (canid >> 5); } else { buffer[MCP_SIDH] = (uint8_t) (canid >> 3); @@ -258,7 +275,7 @@ canbus::Error MCP2515::send_message(struct canbus::CanFrame *frame) { } } - return canbus::ERROR_FAILTX; + return canbus::ERROR_ALLTXBUSY; } canbus::Error MCP2515::read_message_(RXBn rxbn, struct canbus::CanFrame *frame) { @@ -272,7 +289,7 @@ canbus::Error MCP2515::read_message_(RXBn rxbn, struct canbus::CanFrame *frame) bool use_extended_id = false; bool remote_transmission_request = false; - if ((tbufdata[MCP_SIDL] & TXB_EXIDE_MASK) == TXB_EXIDE_MASK) { + if ((tbufdata[MCP_SIDL] & SIDL_EXIDE_MASK) == SIDL_EXIDE_MASK) { id = (id << 2) + (tbufdata[MCP_SIDL] & 0x03); id = (id << 8) + tbufdata[MCP_EID8]; id = (id << 8) + tbufdata[MCP_EID0]; @@ -315,6 +332,17 @@ canbus::Error MCP2515::read_message(struct canbus::CanFrame *frame) { rc = canbus::ERROR_NOMSG; } +#ifdef ESPHOME_LOG_HAS_DEBUG + uint8_t err = get_error_flags_(); + // The receive flowchart in the datasheet says that if rollover is set (BUKT), RX1OVR flag will be set + // once both buffers are full. However, the RX0OVR flag is actually set instead. + // We can just check for both though because it doesn't break anything. + if (err & (EFLG_RX0OVR | EFLG_RX1OVR)) { + ESP_LOGD(TAG, "receive buffer overrun"); + clear_rx_n_ovr_flags_(); + } +#endif + return rc; } diff --git a/esphome/components/mcp2515/mcp2515_defs.h b/esphome/components/mcp2515/mcp2515_defs.h index 2f5cf2a238..b33adcbba6 100644 --- a/esphome/components/mcp2515/mcp2515_defs.h +++ b/esphome/components/mcp2515/mcp2515_defs.h @@ -130,7 +130,9 @@ static const uint8_t CANSTAT_ICOD = 0x0E; static const uint8_t CNF3_SOF = 0x80; -static const uint8_t TXB_EXIDE_MASK = 0x08; +// applies to RXBn_SIDL, TXBn_SIDL and RXFn_SIDL +static const uint8_t SIDL_EXIDE_MASK = 0x08; + static const uint8_t DLC_MASK = 0x0F; static const uint8_t RTR_MASK = 0x40; diff --git a/esphome/components/mcp3204/mcp3204.cpp b/esphome/components/mcp3204/mcp3204.cpp index 4bb0cbed76..f0dd171a14 100644 --- a/esphome/components/mcp3204/mcp3204.cpp +++ b/esphome/components/mcp3204/mcp3204.cpp @@ -16,16 +16,21 @@ void MCP3204::dump_config() { ESP_LOGCONFIG(TAG, " Reference Voltage: %.2fV", this->reference_voltage_); } -float MCP3204::read_data(uint8_t pin) { - uint8_t adc_primary_config = 0b00000110 | (pin >> 2); - uint8_t adc_secondary_config = pin << 6; +float MCP3204::read_data(uint8_t pin, bool differential) { + uint8_t command, b0, b1; + + command = (1 << 6) | // start bit + ((differential ? 0 : 1) << 5) | // single or differential bit + ((pin & 0x07) << 2); // pin + this->enable(); - this->transfer_byte(adc_primary_config); - uint8_t adc_primary_byte = this->transfer_byte(adc_secondary_config); - uint8_t adc_secondary_byte = this->transfer_byte(0x00); + this->transfer_byte(command); + b0 = this->transfer_byte(0x00); + b1 = this->transfer_byte(0x00); this->disable(); - uint16_t digital_value = (adc_primary_byte << 8 | adc_secondary_byte) & 0b111111111111; - return float(digital_value) / 4096.000 * this->reference_voltage_; + + uint16_t digital_value = encode_uint16(b0, b1) >> 4; + return float(digital_value) / 4096.000 * this->reference_voltage_; // in V } } // namespace mcp3204 diff --git a/esphome/components/mcp3204/mcp3204.h b/esphome/components/mcp3204/mcp3204.h index 27261aa373..6287263a2a 100644 --- a/esphome/components/mcp3204/mcp3204.h +++ b/esphome/components/mcp3204/mcp3204.h @@ -18,7 +18,7 @@ class MCP3204 : public Component, void setup() override; void dump_config() override; float get_setup_priority() const override; - float read_data(uint8_t pin); + float read_data(uint8_t pin, bool differential); protected: float reference_voltage_; diff --git a/esphome/components/mcp3204/sensor/__init__.py b/esphome/components/mcp3204/sensor/__init__.py index a4b177cbcf..5f9aa9fdb6 100644 --- a/esphome/components/mcp3204/sensor/__init__.py +++ b/esphome/components/mcp3204/sensor/__init__.py @@ -13,6 +13,7 @@ MCP3204Sensor = mcp3204_ns.class_( "MCP3204Sensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler ) CONF_MCP3204_ID = "mcp3204_id" +CONF_DIFF_MODE = "diff_mode" CONFIG_SCHEMA = ( sensor.sensor_schema(MCP3204Sensor) @@ -20,6 +21,7 @@ CONFIG_SCHEMA = ( { cv.GenerateID(CONF_MCP3204_ID): cv.use_id(MCP3204), cv.Required(CONF_NUMBER): cv.int_range(min=0, max=7), + cv.Optional(CONF_DIFF_MODE, default=False): cv.boolean, } ) .extend(cv.polling_component_schema("60s")) @@ -30,6 +32,7 @@ async def to_code(config): var = cg.new_Pvariable( config[CONF_ID], config[CONF_NUMBER], + config[CONF_DIFF_MODE], ) await cg.register_parented(var, config[CONF_MCP3204_ID]) await cg.register_component(var, config) diff --git a/esphome/components/mcp3204/sensor/mcp3204_sensor.cpp b/esphome/components/mcp3204/sensor/mcp3204_sensor.cpp index ce0fd25462..4c4abef4a7 100644 --- a/esphome/components/mcp3204/sensor/mcp3204_sensor.cpp +++ b/esphome/components/mcp3204/sensor/mcp3204_sensor.cpp @@ -7,16 +7,15 @@ namespace mcp3204 { static const char *const TAG = "mcp3204.sensor"; -MCP3204Sensor::MCP3204Sensor(uint8_t pin) : pin_(pin) {} - float MCP3204Sensor::get_setup_priority() const { return setup_priority::DATA; } void MCP3204Sensor::dump_config() { LOG_SENSOR("", "MCP3204 Sensor", this); ESP_LOGCONFIG(TAG, " Pin: %u", this->pin_); + ESP_LOGCONFIG(TAG, " Differential Mode: %s", YESNO(this->differential_mode_)); LOG_UPDATE_INTERVAL(this); } -float MCP3204Sensor::sample() { return this->parent_->read_data(this->pin_); } +float MCP3204Sensor::sample() { return this->parent_->read_data(this->pin_, this->differential_mode_); } void MCP3204Sensor::update() { this->publish_state(this->sample()); } } // namespace mcp3204 diff --git a/esphome/components/mcp3204/sensor/mcp3204_sensor.h b/esphome/components/mcp3204/sensor/mcp3204_sensor.h index 21c45590ab..5665b80b98 100644 --- a/esphome/components/mcp3204/sensor/mcp3204_sensor.h +++ b/esphome/components/mcp3204/sensor/mcp3204_sensor.h @@ -15,7 +15,7 @@ class MCP3204Sensor : public PollingComponent, public sensor::Sensor, public voltage_sampler::VoltageSampler { public: - MCP3204Sensor(uint8_t pin); + MCP3204Sensor(uint8_t pin, bool differential_mode) : pin_(pin), differential_mode_(differential_mode) {} void update() override; void dump_config() override; @@ -24,6 +24,7 @@ class MCP3204Sensor : public PollingComponent, protected: uint8_t pin_; + bool differential_mode_; }; } // namespace mcp3204 diff --git a/esphome/components/mcp3221/__init__.py b/esphome/components/mcp3221/__init__.py new file mode 100644 index 0000000000..677bb78c35 --- /dev/null +++ b/esphome/components/mcp3221/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@philippderdiedas"] diff --git a/esphome/components/mcp3221/mcp3221_sensor.cpp b/esphome/components/mcp3221/mcp3221_sensor.cpp new file mode 100644 index 0000000000..c04b1c0b93 --- /dev/null +++ b/esphome/components/mcp3221/mcp3221_sensor.cpp @@ -0,0 +1,31 @@ +#include "mcp3221_sensor.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace mcp3221 { + +static const char *const TAG = "mcp3221"; + +float MCP3221Sensor::sample() { + uint8_t data[2]; + if (this->read(data, 2) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Read failed"); + this->status_set_warning(); + return NAN; + } + this->status_clear_warning(); + + uint16_t value = encode_uint16(data[0], data[1]); + float voltage = value * this->reference_voltage_ / 4096.0f; + + return voltage; +} + +void MCP3221Sensor::update() { + float v = this->sample(); + this->publish_state(v); +} + +} // namespace mcp3221 +} // namespace esphome diff --git a/esphome/components/mcp3221/mcp3221_sensor.h b/esphome/components/mcp3221/mcp3221_sensor.h new file mode 100644 index 0000000000..c83caccabf --- /dev/null +++ b/esphome/components/mcp3221/mcp3221_sensor.h @@ -0,0 +1,28 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/voltage_sampler/voltage_sampler.h" + +#include + +namespace esphome { +namespace mcp3221 { + +class MCP3221Sensor : public sensor::Sensor, + public PollingComponent, + public voltage_sampler::VoltageSampler, + public i2c::I2CDevice { + public: + void set_reference_voltage(float reference_voltage) { this->reference_voltage_ = reference_voltage; } + void update() override; + float sample() override; + + protected: + float reference_voltage_; +}; + +} // namespace mcp3221 +} // namespace esphome diff --git a/esphome/components/mcp3221/sensor.py b/esphome/components/mcp3221/sensor.py new file mode 100644 index 0000000000..993876c2c8 --- /dev/null +++ b/esphome/components/mcp3221/sensor.py @@ -0,0 +1,49 @@ +import esphome.codegen as cg +from esphome.components import i2c, sensor, voltage_sampler +import esphome.config_validation as cv +from esphome.const import ( + CONF_REFERENCE_VOLTAGE, + DEVICE_CLASS_VOLTAGE, + ICON_SCALE, + STATE_CLASS_MEASUREMENT, + UNIT_VOLT, +) + +AUTO_LOAD = ["voltage_sampler"] +DEPENDENCIES = ["i2c"] + + +mcp3221_ns = cg.esphome_ns.namespace("mcp3221") +MCP3221Sensor = mcp3221_ns.class_( + "MCP3221Sensor", + sensor.Sensor, + voltage_sampler.VoltageSampler, + cg.PollingComponent, + i2c.I2CDevice, +) + + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + MCP3221Sensor, + icon=ICON_SCALE, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_VOLTAGE, + unit_of_measurement=UNIT_VOLT, + ) + .extend( + { + cv.Optional(CONF_REFERENCE_VOLTAGE, default="3.3V"): cv.voltage, + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x48)) +) + + +async def to_code(config): + var = await sensor.new_sensor(config) + cg.add(var.set_reference_voltage(config[CONF_REFERENCE_VOLTAGE])) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/mcp4461/mcp4461.cpp b/esphome/components/mcp4461/mcp4461.cpp index 6634c5057e..2f2c75e05a 100644 --- a/esphome/components/mcp4461/mcp4461.cpp +++ b/esphome/components/mcp4461/mcp4461.cpp @@ -122,7 +122,7 @@ uint8_t Mcp4461Component::get_status_register_() { uint8_t addr = static_cast(Mcp4461Addresses::MCP4461_STATUS); uint8_t reg = addr | static_cast(Mcp4461Commands::READ); uint16_t buf; - if (!this->read_byte_16(reg, &buf)) { + if (!this->read_16_(reg, &buf)) { this->error_code_ = MCP4461_STATUS_REGISTER_ERROR; this->mark_failed(); return 0; @@ -148,6 +148,20 @@ void Mcp4461Component::read_status_register_to_log() { ((status_register_value >> 3) & 0x01), ((status_register_value >> 2) & 0x01), ((status_register_value >> 1) & 0x01), ((status_register_value >> 0) & 0x01)); } +bool Mcp4461Component::read_16_(uint8_t address, uint16_t *buf) { + // read 16 bits and convert from big endian to host, + // Do this as two separate operations to ensure a stop condition between the write and read + i2c::ErrorCode err = this->write(&address, 1); + if (err != i2c::ERROR_OK) { + return false; + } + err = this->read(reinterpret_cast(buf), 2); + if (err != i2c::ERROR_OK) { + return false; + } + *buf = convert_big_endian(*buf); + return true; +} uint8_t Mcp4461Component::get_wiper_address_(uint8_t wiper) { uint8_t addr; @@ -198,14 +212,14 @@ uint16_t Mcp4461Component::get_wiper_level_(Mcp4461WiperIdx wiper) { uint16_t Mcp4461Component::read_wiper_level_(uint8_t wiper_idx) { uint8_t addr = this->get_wiper_address_(wiper_idx); - uint8_t reg = addr | static_cast(Mcp4461Commands::INCREMENT); + uint8_t reg = addr | static_cast(Mcp4461Commands::READ); if (wiper_idx > 3) { if (!this->is_eeprom_ready_for_writing_(true)) { return 0; } } uint16_t buf = 0; - if (!(this->read_byte_16(reg, &buf))) { + if (!(this->read_16_(reg, &buf))) { this->error_code_ = MCP4461_STATUS_I2C_ERROR; this->status_set_warning(); ESP_LOGW(TAG, "Error fetching %swiper %u value", (wiper_idx > 3) ? "nonvolatile " : "", wiper_idx); @@ -328,7 +342,7 @@ bool Mcp4461Component::increase_wiper_(Mcp4461WiperIdx wiper) { ESP_LOGV(TAG, "Increasing wiper %u", wiper_idx); uint8_t addr = this->get_wiper_address_(wiper_idx); uint8_t reg = addr | static_cast(Mcp4461Commands::INCREMENT); - auto err = this->write(&this->address_, reg, sizeof(reg)); + auto err = this->write(&this->address_, reg); if (err != i2c::ERROR_OK) { this->error_code_ = MCP4461_STATUS_I2C_ERROR; this->status_set_warning(); @@ -359,7 +373,7 @@ bool Mcp4461Component::decrease_wiper_(Mcp4461WiperIdx wiper) { ESP_LOGV(TAG, "Decreasing wiper %u", wiper_idx); uint8_t addr = this->get_wiper_address_(wiper_idx); uint8_t reg = addr | static_cast(Mcp4461Commands::DECREMENT); - auto err = this->write(&this->address_, reg, sizeof(reg)); + auto err = this->write(&this->address_, reg); if (err != i2c::ERROR_OK) { this->error_code_ = MCP4461_STATUS_I2C_ERROR; this->status_set_warning(); @@ -392,7 +406,7 @@ uint8_t Mcp4461Component::get_terminal_register_(Mcp4461TerminalIdx terminal_con : static_cast(Mcp4461Addresses::MCP4461_TCON1); reg |= static_cast(Mcp4461Commands::READ); uint16_t buf; - if (this->read_byte_16(reg, &buf)) { + if (this->read_16_(reg, &buf)) { return static_cast(buf & 0x00ff); } else { this->error_code_ = MCP4461_STATUS_I2C_ERROR; @@ -517,7 +531,7 @@ uint16_t Mcp4461Component::get_eeprom_value(Mcp4461EepromLocation location) { if (!this->is_eeprom_ready_for_writing_(true)) { return 0; } - if (!this->read_byte_16(reg, &buf)) { + if (!this->read_16_(reg, &buf)) { this->error_code_ = MCP4461_STATUS_I2C_ERROR; this->status_set_warning(); ESP_LOGW(TAG, "Error fetching EEPROM location value"); diff --git a/esphome/components/mcp4461/mcp4461.h b/esphome/components/mcp4461/mcp4461.h index 9b7f60f201..59f6358a56 100644 --- a/esphome/components/mcp4461/mcp4461.h +++ b/esphome/components/mcp4461/mcp4461.h @@ -96,6 +96,7 @@ class Mcp4461Component : public Component, public i2c::I2CDevice { protected: friend class Mcp4461Wiper; + bool read_16_(uint8_t address, uint16_t *buf); void update_write_protection_status_(); uint8_t get_wiper_address_(uint8_t wiper); uint16_t read_wiper_level_(uint8_t wiper); diff --git a/esphome/components/md5/md5.cpp b/esphome/components/md5/md5.cpp index 980cb98699..866f00eda4 100644 --- a/esphome/components/md5/md5.cpp +++ b/esphome/components/md5/md5.cpp @@ -1,4 +1,3 @@ -#include #include #include "md5.h" #ifdef USE_MD5 @@ -40,30 +39,6 @@ void MD5Digest::add(const uint8_t *data, size_t len) { br_md5_update(&this->ctx_ void MD5Digest::calculate() { br_md5_out(&this->ctx_, this->digest_); } #endif // USE_RP2040 -void MD5Digest::get_bytes(uint8_t *output) { memcpy(output, this->digest_, 16); } - -void MD5Digest::get_hex(char *output) { - for (size_t i = 0; i < 16; i++) { - sprintf(output + i * 2, "%02x", this->digest_[i]); - } -} - -bool MD5Digest::equals_bytes(const uint8_t *expected) { - for (size_t i = 0; i < 16; i++) { - if (expected[i] != this->digest_[i]) { - return false; - } - } - return true; -} - -bool MD5Digest::equals_hex(const char *expected) { - uint8_t parsed[16]; - if (!parse_hex(expected, parsed, 16)) - return false; - return equals_bytes(parsed); -} - } // namespace md5 } // namespace esphome #endif diff --git a/esphome/components/md5/md5.h b/esphome/components/md5/md5.h index be1df40423..b0da2c0a3b 100644 --- a/esphome/components/md5/md5.h +++ b/esphome/components/md5/md5.h @@ -3,6 +3,8 @@ #include "esphome/core/defines.h" #ifdef USE_MD5 +#include "esphome/core/hash_base.h" + #ifdef USE_ESP32 #include "esp_rom_md5.h" #define MD5_CTX_TYPE md5_context_t @@ -26,38 +28,26 @@ namespace esphome { namespace md5 { -class MD5Digest { +class MD5Digest : public HashBase { public: MD5Digest() = default; - ~MD5Digest() = default; + ~MD5Digest() override = default; /// Initialize a new MD5 digest computation. - void init(); + void init() override; /// Add bytes of data for the digest. - void add(const uint8_t *data, size_t len); - void add(const char *data, size_t len) { this->add((const uint8_t *) data, len); } + void add(const uint8_t *data, size_t len) override; + using HashBase::add; // Bring base class overload into scope /// Compute the digest, based on the provided data. - void calculate(); + void calculate() override; - /// Retrieve the MD5 digest as bytes. - /// The output must be able to hold 16 bytes or more. - void get_bytes(uint8_t *output); - - /// Retrieve the MD5 digest as hex characters. - /// The output must be able to hold 32 bytes or more. - void get_hex(char *output); - - /// Compare the digest against a provided byte-encoded digest (16 bytes). - bool equals_bytes(const uint8_t *expected); - - /// Compare the digest against a provided hex-encoded digest (32 bytes). - bool equals_hex(const char *expected); + /// Get the size of the hash in bytes (16 for MD5) + size_t get_size() const override { return 16; } protected: MD5_CTX_TYPE ctx_{}; - uint8_t digest_[16]; }; } // namespace md5 diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index e32d39cede..4776bef22f 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -1,6 +1,6 @@ import esphome.codegen as cg from esphome.components.esp32 import add_idf_component -from esphome.config_helpers import filter_source_files_from_platform +from esphome.config_helpers import filter_source_files_from_platform, get_logger_level import esphome.config_validation as cv from esphome.const import ( CONF_DISABLED, @@ -11,11 +11,18 @@ from esphome.const import ( CONF_SERVICES, PlatformFramework, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, Lambda, coroutine_with_priority +from esphome.coroutine import CoroPriority +from esphome.types import ConfigType CODEOWNERS = ["@esphome/core"] DEPENDENCIES = ["network"] +# Components that create mDNS services at runtime +# IMPORTANT: If you add a new component here, you must also update the corresponding +# #ifdef blocks in mdns_component.cpp compile_records_() method +COMPONENTS_WITH_MDNS_SERVICES = ("api", "prometheus", "web_server") + mdns_ns = cg.esphome_ns.namespace("mdns") MDNSComponent = mdns_ns.class_("MDNSComponent", cg.Component) MDNSTXTRecord = mdns_ns.struct("MDNSTXTRecord") @@ -40,6 +47,19 @@ SERVICE_SCHEMA = cv.Schema( } ) + +def _consume_mdns_sockets(config: ConfigType) -> ConfigType: + """Register socket needs for mDNS component.""" + if config.get(CONF_DISABLED): + return config + + from esphome.components import socket + + # mDNS needs 2 sockets (IPv4 + IPv6 multicast) + socket.consume_sockets(2, "mdns")(config) + return config + + CONFIG_SCHEMA = cv.All( cv.Schema( { @@ -49,30 +69,89 @@ CONFIG_SCHEMA = cv.All( } ), _remove_id_if_disabled, + _consume_mdns_sockets, ) -def mdns_txt_record(key: str, value: str): - return cg.StructInitializer( - MDNSTXTRecord, - ("key", key), - ("value", value), +def mdns_txt_record(key: str, value: str) -> cg.RawExpression: + """Create a mDNS TXT record. + + Public API for external components. Do not remove. + + Args: + key: The TXT record key + value: The TXT record value (static string only) + + Returns: + A RawExpression representing a MDNSTXTRecord struct + """ + return cg.RawExpression( + f"{{MDNS_STR({cg.safe_exp(key)}), MDNS_STR({cg.safe_exp(value)})}}" ) +async def _mdns_txt_record_templated( + mdns_comp: cg.Pvariable, key: str, value: Lambda | str +) -> cg.RawExpression: + """Create a mDNS TXT record with support for templated values. + + Internal helper function. + + Args: + mdns_comp: The MDNSComponent instance (from cg.get_variable()) + key: The TXT record key + value: The TXT record value (can be a static string or a lambda template) + + Returns: + A RawExpression representing a MDNSTXTRecord struct + """ + if not cg.is_template(value): + # It's a static string - use directly in flash, no need to store in vector + return mdns_txt_record(key, value) + # It's a lambda - evaluate and store using helper + templated_value = await cg.templatable(value, [], cg.std_string) + safe_key = cg.safe_exp(key) + dynamic_call = f"{mdns_comp}->add_dynamic_txt_value(({templated_value})())" + return cg.RawExpression(f"{{MDNS_STR({safe_key}), MDNS_STR({dynamic_call})}}") + + def mdns_service( - service: str, proto: str, port: int, txt_records: list[dict[str, str]] -): + service: str, proto: str, port: int, txt_records: list[cg.RawExpression] +) -> cg.StructInitializer: + """Create a mDNS service. + + Public API for external components. Do not remove. + + Args: + service: Service name (e.g., "_http") + proto: Protocol (e.g., "_tcp" or "_udp") + port: Port number + txt_records: List of MDNSTXTRecord expressions + + Returns: + A StructInitializer representing a MDNSService struct + """ return cg.StructInitializer( MDNSService, - ("service_type", service), - ("proto", proto), + ("service_type", cg.RawExpression(f"MDNS_STR({cg.safe_exp(service)})")), + ("proto", cg.RawExpression(f"MDNS_STR({cg.safe_exp(proto)})")), ("port", port), ("txt_records", txt_records), ) -@coroutine_with_priority(55.0) +def enable_mdns_storage(): + """Enable persistent storage of mDNS services in the MDNSComponent. + + Called by external components (like OpenThread) that need access to + services after setup() completes via get_services(). + + Public API for external components. Do not remove. + """ + cg.add_define("USE_MDNS_STORE_SERVICES") + + +@coroutine_with_priority(CoroPriority.NETWORK_SERVICES) async def to_code(config): if config[CONF_DISABLED] is True: return @@ -90,23 +169,54 @@ async def to_code(config): cg.add_define("USE_MDNS") + # Calculate compile-time service count + service_count = sum( + 1 for key in COMPONENTS_WITH_MDNS_SERVICES if key in CORE.config + ) + len(config[CONF_SERVICES]) + + if config[CONF_SERVICES]: + cg.add_define("USE_MDNS_EXTRA_SERVICES") + # Extra services need to be stored persistently + enable_mdns_storage() + + # Ensure at least 1 service (fallback service) + cg.add_define("MDNS_SERVICE_COUNT", max(1, service_count)) + + # Calculate compile-time dynamic TXT value count + # Dynamic values are those that cannot be stored in flash at compile time + dynamic_txt_count = 0 + if "api" in CORE.config: + # Always: get_mac_address() + dynamic_txt_count += 1 + # User-provided templatable TXT values (only lambdas, not static strings) + dynamic_txt_count += sum( + 1 + for service in config[CONF_SERVICES] + for txt_value in service[CONF_TXT].values() + if cg.is_template(txt_value) + ) + + # Ensure at least 1 to avoid zero-size array + cg.add_define("MDNS_DYNAMIC_TXT_COUNT", max(1, dynamic_txt_count)) + + # Enable storage if verbose logging is enabled (for dump_config) + if get_logger_level() in ("VERBOSE", "VERY_VERBOSE"): + enable_mdns_storage() + var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) for service in config[CONF_SERVICES]: - txt = [ - cg.StructInitializer( - MDNSTXTRecord, - ("key", txt_key), - ("value", await cg.templatable(txt_value, [], cg.std_string)), - ) + txt_records = [ + await _mdns_txt_record_templated(var, txt_key, txt_value) for txt_key, txt_value in service[CONF_TXT].items() ] + exp = mdns_service( service[CONF_SERVICE], service[CONF_PROTOCOL], await cg.templatable(service[CONF_PORT], [], cg.uint16), - txt, + txt_records, ) cg.add(var.add_extra_service(exp)) diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index 06ca99b402..4655907983 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -5,6 +5,15 @@ #include "esphome/core/version.h" #include "mdns_component.h" +#ifdef USE_ESP8266 +#include +// Macro to define strings in PROGMEM on ESP8266, regular memory on other platforms +#define MDNS_STATIC_CONST_CHAR(name, value) static const char name[] PROGMEM = value +#else +// On non-ESP8266 platforms, use regular const char* +#define MDNS_STATIC_CONST_CHAR(name, value) static constexpr const char name[] = value +#endif + #ifdef USE_API #include "esphome/components/api/api_server.h" #endif @@ -12,8 +21,7 @@ #include "esphome/components/dashboard_import/dashboard_import.h" #endif -namespace esphome { -namespace mdns { +namespace esphome::mdns { static const char *const TAG = "mdns"; @@ -21,121 +29,164 @@ static const char *const TAG = "mdns"; #define USE_WEBSERVER_PORT 80 // NOLINT #endif -void MDNSComponent::compile_records_() { - this->hostname_ = App.get_name(); +// Define all constant strings using the macro +MDNS_STATIC_CONST_CHAR(SERVICE_TCP, "_tcp"); + +// Wrap build-time defines into flash storage +MDNS_STATIC_CONST_CHAR(VALUE_VERSION, ESPHOME_VERSION); + +void MDNSComponent::compile_records_(StaticVector &services) { + // IMPORTANT: The #ifdef blocks below must match COMPONENTS_WITH_MDNS_SERVICES + // in mdns/__init__.py. If you add a new service here, update both locations. - this->services_.clear(); #ifdef USE_API - if (api::global_api_server != nullptr) { - MDNSService service{}; - service.service_type = "_esphomelib"; - service.proto = "_tcp"; - service.port = api::global_api_server->get_port(); - if (!App.get_friendly_name().empty()) { - service.txt_records.push_back({"friendly_name", App.get_friendly_name()}); - } - service.txt_records.push_back({"version", ESPHOME_VERSION}); - service.txt_records.push_back({"mac", get_mac_address()}); - const char *platform = nullptr; -#ifdef USE_ESP8266 - platform = "ESP8266"; -#endif -#ifdef USE_ESP32 - platform = "ESP32"; -#endif -#ifdef USE_RP2040 - platform = "RP2040"; -#endif -#ifdef USE_LIBRETINY - platform = lt_cpu_get_model_name(); -#endif - if (platform != nullptr) { - service.txt_records.push_back({"platform", platform}); - } + MDNS_STATIC_CONST_CHAR(SERVICE_ESPHOMELIB, "_esphomelib"); + MDNS_STATIC_CONST_CHAR(TXT_FRIENDLY_NAME, "friendly_name"); + MDNS_STATIC_CONST_CHAR(TXT_VERSION, "version"); + MDNS_STATIC_CONST_CHAR(TXT_MAC, "mac"); + MDNS_STATIC_CONST_CHAR(TXT_PLATFORM, "platform"); + MDNS_STATIC_CONST_CHAR(TXT_BOARD, "board"); + MDNS_STATIC_CONST_CHAR(TXT_NETWORK, "network"); + MDNS_STATIC_CONST_CHAR(VALUE_BOARD, ESPHOME_BOARD); - service.txt_records.push_back({"board", ESPHOME_BOARD}); + if (api::global_api_server != nullptr) { + auto &service = services.emplace_next(); + service.service_type = MDNS_STR(SERVICE_ESPHOMELIB); + service.proto = MDNS_STR(SERVICE_TCP); + service.port = api::global_api_server->get_port(); + + const std::string &friendly_name = App.get_friendly_name(); + bool friendly_name_empty = friendly_name.empty(); + + // Calculate exact capacity for txt_records + size_t txt_count = 3; // version, mac, board (always present) + if (!friendly_name_empty) { + txt_count++; // friendly_name + } +#if defined(USE_ESP8266) || defined(USE_ESP32) || defined(USE_RP2040) || defined(USE_LIBRETINY) + txt_count++; // platform +#endif +#if defined(USE_WIFI) || defined(USE_ETHERNET) || defined(USE_OPENTHREAD) + txt_count++; // network +#endif +#ifdef USE_API_NOISE + txt_count++; // api_encryption or api_encryption_supported +#endif +#ifdef ESPHOME_PROJECT_NAME + txt_count += 2; // project_name and project_version +#endif +#ifdef USE_DASHBOARD_IMPORT + txt_count++; // package_import_url +#endif + + auto &txt_records = service.txt_records; + txt_records.init(txt_count); + + if (!friendly_name_empty) { + txt_records.push_back({MDNS_STR(TXT_FRIENDLY_NAME), MDNS_STR(friendly_name.c_str())}); + } + txt_records.push_back({MDNS_STR(TXT_VERSION), MDNS_STR(VALUE_VERSION)}); + txt_records.push_back({MDNS_STR(TXT_MAC), MDNS_STR(this->add_dynamic_txt_value(get_mac_address()))}); + +#ifdef USE_ESP8266 + MDNS_STATIC_CONST_CHAR(PLATFORM_ESP8266, "ESP8266"); + txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(PLATFORM_ESP8266)}); +#elif defined(USE_ESP32) + MDNS_STATIC_CONST_CHAR(PLATFORM_ESP32, "ESP32"); + txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(PLATFORM_ESP32)}); +#elif defined(USE_RP2040) + MDNS_STATIC_CONST_CHAR(PLATFORM_RP2040, "RP2040"); + txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(PLATFORM_RP2040)}); +#elif defined(USE_LIBRETINY) + txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(lt_cpu_get_model_name())}); +#endif + + txt_records.push_back({MDNS_STR(TXT_BOARD), MDNS_STR(VALUE_BOARD)}); #if defined(USE_WIFI) - service.txt_records.push_back({"network", "wifi"}); + MDNS_STATIC_CONST_CHAR(NETWORK_WIFI, "wifi"); + txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(NETWORK_WIFI)}); #elif defined(USE_ETHERNET) - service.txt_records.push_back({"network", "ethernet"}); + MDNS_STATIC_CONST_CHAR(NETWORK_ETHERNET, "ethernet"); + txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(NETWORK_ETHERNET)}); #elif defined(USE_OPENTHREAD) - service.txt_records.push_back({"network", "thread"}); + MDNS_STATIC_CONST_CHAR(NETWORK_THREAD, "thread"); + txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(NETWORK_THREAD)}); #endif #ifdef USE_API_NOISE - if (api::global_api_server->get_noise_ctx()->has_psk()) { - service.txt_records.push_back({"api_encryption", "Noise_NNpsk0_25519_ChaChaPoly_SHA256"}); - } else { - service.txt_records.push_back({"api_encryption_supported", "Noise_NNpsk0_25519_ChaChaPoly_SHA256"}); - } + MDNS_STATIC_CONST_CHAR(TXT_API_ENCRYPTION, "api_encryption"); + MDNS_STATIC_CONST_CHAR(TXT_API_ENCRYPTION_SUPPORTED, "api_encryption_supported"); + MDNS_STATIC_CONST_CHAR(NOISE_ENCRYPTION, "Noise_NNpsk0_25519_ChaChaPoly_SHA256"); + bool has_psk = api::global_api_server->get_noise_ctx().has_psk(); + const char *encryption_key = has_psk ? TXT_API_ENCRYPTION : TXT_API_ENCRYPTION_SUPPORTED; + txt_records.push_back({MDNS_STR(encryption_key), MDNS_STR(NOISE_ENCRYPTION)}); #endif #ifdef ESPHOME_PROJECT_NAME - service.txt_records.push_back({"project_name", ESPHOME_PROJECT_NAME}); - service.txt_records.push_back({"project_version", ESPHOME_PROJECT_VERSION}); + MDNS_STATIC_CONST_CHAR(TXT_PROJECT_NAME, "project_name"); + MDNS_STATIC_CONST_CHAR(TXT_PROJECT_VERSION, "project_version"); + MDNS_STATIC_CONST_CHAR(VALUE_PROJECT_NAME, ESPHOME_PROJECT_NAME); + MDNS_STATIC_CONST_CHAR(VALUE_PROJECT_VERSION, ESPHOME_PROJECT_VERSION); + txt_records.push_back({MDNS_STR(TXT_PROJECT_NAME), MDNS_STR(VALUE_PROJECT_NAME)}); + txt_records.push_back({MDNS_STR(TXT_PROJECT_VERSION), MDNS_STR(VALUE_PROJECT_VERSION)}); #endif // ESPHOME_PROJECT_NAME #ifdef USE_DASHBOARD_IMPORT - service.txt_records.push_back({"package_import_url", dashboard_import::get_package_import_url()}); + MDNS_STATIC_CONST_CHAR(TXT_PACKAGE_IMPORT_URL, "package_import_url"); + txt_records.push_back({MDNS_STR(TXT_PACKAGE_IMPORT_URL), MDNS_STR(dashboard_import::get_package_import_url())}); #endif - - this->services_.push_back(service); } #endif // USE_API #ifdef USE_PROMETHEUS - { - MDNSService service{}; - service.service_type = "_prometheus-http"; - service.proto = "_tcp"; - service.port = USE_WEBSERVER_PORT; - this->services_.push_back(service); - } + MDNS_STATIC_CONST_CHAR(SERVICE_PROMETHEUS, "_prometheus-http"); + + auto &prom_service = services.emplace_next(); + prom_service.service_type = MDNS_STR(SERVICE_PROMETHEUS); + prom_service.proto = MDNS_STR(SERVICE_TCP); + prom_service.port = USE_WEBSERVER_PORT; #endif #ifdef USE_WEBSERVER - { - MDNSService service{}; - service.service_type = "_http"; - service.proto = "_tcp"; - service.port = USE_WEBSERVER_PORT; - this->services_.push_back(service); - } + MDNS_STATIC_CONST_CHAR(SERVICE_HTTP, "_http"); + + auto &web_service = services.emplace_next(); + web_service.service_type = MDNS_STR(SERVICE_HTTP); + web_service.proto = MDNS_STR(SERVICE_TCP); + web_service.port = USE_WEBSERVER_PORT; #endif - this->services_.insert(this->services_.end(), this->services_extra_.begin(), this->services_extra_.end()); +#if !defined(USE_API) && !defined(USE_PROMETHEUS) && !defined(USE_WEBSERVER) && !defined(USE_MDNS_EXTRA_SERVICES) + MDNS_STATIC_CONST_CHAR(SERVICE_HTTP, "_http"); + MDNS_STATIC_CONST_CHAR(TXT_VERSION, "version"); - if (this->services_.empty()) { - // Publish "http" service if not using native API - // This is just to have *some* mDNS service so that .local resolution works - MDNSService service{}; - service.service_type = "_http"; - service.proto = "_tcp"; - service.port = USE_WEBSERVER_PORT; - service.txt_records.push_back({"version", ESPHOME_VERSION}); - this->services_.push_back(service); - } + // Publish "http" service if not using native API or any other services + // This is just to have *some* mDNS service so that .local resolution works + auto &fallback_service = services.emplace_next(); + fallback_service.service_type = MDNS_STR(SERVICE_HTTP); + fallback_service.proto = MDNS_STR(SERVICE_TCP); + fallback_service.port = USE_WEBSERVER_PORT; + fallback_service.txt_records = {{MDNS_STR(TXT_VERSION), MDNS_STR(VALUE_VERSION)}}; +#endif } void MDNSComponent::dump_config() { ESP_LOGCONFIG(TAG, "mDNS:\n" " Hostname: %s", - this->hostname_.c_str()); + App.get_name().c_str()); +#ifdef USE_MDNS_STORE_SERVICES ESP_LOGV(TAG, " Services:"); for (const auto &service : this->services_) { - ESP_LOGV(TAG, " - %s, %s, %d", service.service_type.c_str(), service.proto.c_str(), + ESP_LOGV(TAG, " - %s, %s, %d", MDNS_STR_ARG(service.service_type), MDNS_STR_ARG(service.proto), const_cast &>(service.port).value()); for (const auto &record : service.txt_records) { - ESP_LOGV(TAG, " TXT: %s = %s", record.key.c_str(), - const_cast &>(record.value).value().c_str()); + ESP_LOGV(TAG, " TXT: %s = %s", MDNS_STR_ARG(record.key), MDNS_STR_ARG(record.value)); } } +#endif } -std::vector MDNSComponent::get_services() { return this->services_; } - -} // namespace mdns -} // namespace esphome +} // namespace esphome::mdns #endif diff --git a/esphome/components/mdns/mdns_component.h b/esphome/components/mdns/mdns_component.h index 93a16f40d2..691c45b7df 100644 --- a/esphome/components/mdns/mdns_component.h +++ b/esphome/components/mdns/mdns_component.h @@ -2,27 +2,42 @@ #include "esphome/core/defines.h" #ifdef USE_MDNS #include -#include #include "esphome/core/automation.h" #include "esphome/core/component.h" +#include "esphome/core/helpers.h" -namespace esphome { -namespace mdns { +namespace esphome::mdns { + +// Helper struct that identifies strings that may be stored in flash storage (similar to LogString) +struct MDNSString; + +// Macro to cast string literals to MDNSString* (works on all platforms) +#define MDNS_STR(name) (reinterpret_cast(name)) + +#ifdef USE_ESP8266 +#include +#define MDNS_STR_ARG(s) ((PGM_P) (s)) +#else +#define MDNS_STR_ARG(s) (reinterpret_cast(s)) +#endif + +// Service count is calculated at compile time by Python codegen +// MDNS_SERVICE_COUNT will always be defined struct MDNSTXTRecord { - std::string key; - TemplatableValue value; + const MDNSString *key; + const MDNSString *value; }; struct MDNSService { // service name _including_ underscore character prefix // as defined in RFC6763 Section 7 - std::string service_type; + const MDNSString *service_type; // second label indicating protocol _including_ underscore character prefix // as defined in RFC6763 Section 7, like "_tcp" or "_udp" - std::string proto; + const MDNSString *proto; TemplatableValue port; - std::vector txt_records; + FixedVector txt_records; }; class MDNSComponent : public Component { @@ -35,19 +50,33 @@ class MDNSComponent : public Component { #endif float get_setup_priority() const override { return setup_priority::AFTER_CONNECTION; } - void add_extra_service(MDNSService service) { services_extra_.push_back(std::move(service)); } +#ifdef USE_MDNS_EXTRA_SERVICES + void add_extra_service(MDNSService service) { this->services_.emplace_next() = std::move(service); } +#endif - std::vector get_services(); +#ifdef USE_MDNS_STORE_SERVICES + const StaticVector &get_services() const { return this->services_; } +#endif void on_shutdown() override; + /// Add a dynamic TXT value and return pointer to it for use in MDNSTXTRecord + const char *add_dynamic_txt_value(const std::string &value) { + this->dynamic_txt_values_.push_back(value); + return this->dynamic_txt_values_[this->dynamic_txt_values_.size() - 1].c_str(); + } + + /// Storage for runtime-generated TXT values (MAC address, user lambdas) + /// Pre-sized at compile time via MDNS_DYNAMIC_TXT_COUNT to avoid heap allocations. + /// Static/compile-time values (version, board, etc.) are stored directly in flash and don't use this. + StaticVector dynamic_txt_values_; + protected: - std::vector services_extra_{}; - std::vector services_{}; - std::string hostname_; - void compile_records_(); +#ifdef USE_MDNS_STORE_SERVICES + StaticVector services_{}; +#endif + void compile_records_(StaticVector &services); }; -} // namespace mdns -} // namespace esphome +} // namespace esphome::mdns #endif diff --git a/esphome/components/mdns/mdns_esp32.cpp b/esphome/components/mdns/mdns_esp32.cpp index ffd86afec1..5547a2524b 100644 --- a/esphome/components/mdns/mdns_esp32.cpp +++ b/esphome/components/mdns/mdns_esp32.cpp @@ -2,18 +2,23 @@ #if defined(USE_ESP32) && defined(USE_MDNS) #include -#include +#include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" #include "mdns_component.h" -namespace esphome { -namespace mdns { +namespace esphome::mdns { static const char *const TAG = "mdns"; void MDNSComponent::setup() { - this->compile_records_(); +#ifdef USE_MDNS_STORE_SERVICES + this->compile_records_(this->services_); + const auto &services = this->services_; +#else + StaticVector services; + this->compile_records_(services); +#endif esp_err_t err = mdns_init(); if (err != ESP_OK) { @@ -22,30 +27,25 @@ void MDNSComponent::setup() { return; } - mdns_hostname_set(this->hostname_.c_str()); - mdns_instance_name_set(this->hostname_.c_str()); + const char *hostname = App.get_name().c_str(); + mdns_hostname_set(hostname); + mdns_instance_name_set(hostname); - for (const auto &service : this->services_) { - std::vector txt_records; - for (const auto &record : service.txt_records) { - mdns_txt_item_t it{}; - // dup strings to ensure the pointer is valid even after the record loop - it.key = strdup(record.key.c_str()); - it.value = strdup(const_cast &>(record.value).value().c_str()); - txt_records.push_back(it); + for (const auto &service : services) { + auto txt_records = std::make_unique(service.txt_records.size()); + for (size_t i = 0; i < service.txt_records.size(); i++) { + const auto &record = service.txt_records[i]; + // key and value are either compile-time string literals in flash or pointers to dynamic_txt_values_ + // Both remain valid for the lifetime of this function, and ESP-IDF makes internal copies + txt_records[i].key = MDNS_STR_ARG(record.key); + txt_records[i].value = MDNS_STR_ARG(record.value); } uint16_t port = const_cast &>(service.port).value(); - err = mdns_service_add(nullptr, service.service_type.c_str(), service.proto.c_str(), port, txt_records.data(), - txt_records.size()); - - // free records - for (const auto &it : txt_records) { - delete it.key; // NOLINT(cppcoreguidelines-owning-memory) - delete it.value; // NOLINT(cppcoreguidelines-owning-memory) - } + err = mdns_service_add(nullptr, MDNS_STR_ARG(service.service_type), MDNS_STR_ARG(service.proto), port, + txt_records.get(), service.txt_records.size()); if (err != ESP_OK) { - ESP_LOGW(TAG, "Failed to register service %s: %s", service.service_type.c_str(), esp_err_to_name(err)); + ESP_LOGW(TAG, "Failed to register service %s: %s", MDNS_STR_ARG(service.service_type), esp_err_to_name(err)); } } } @@ -55,7 +55,6 @@ void MDNSComponent::on_shutdown() { delay(40); // Allow the mdns packets announcing service removal to be sent } -} // namespace mdns -} // namespace esphome +} // namespace esphome::mdns #endif // USE_ESP32 diff --git a/esphome/components/mdns/mdns_esp8266.cpp b/esphome/components/mdns/mdns_esp8266.cpp index 2c90d57021..06f905884c 100644 --- a/esphome/components/mdns/mdns_esp8266.cpp +++ b/esphome/components/mdns/mdns_esp8266.cpp @@ -4,36 +4,42 @@ #include #include "esphome/components/network/ip_address.h" #include "esphome/components/network/util.h" +#include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" #include "mdns_component.h" -namespace esphome { -namespace mdns { +namespace esphome::mdns { void MDNSComponent::setup() { - this->compile_records_(); +#ifdef USE_MDNS_STORE_SERVICES + this->compile_records_(this->services_); + const auto &services = this->services_; +#else + StaticVector services; + this->compile_records_(services); +#endif - MDNS.begin(this->hostname_.c_str()); + MDNS.begin(App.get_name().c_str()); - for (const auto &service : this->services_) { + for (const auto &service : services) { // Strip the leading underscore from the proto and service_type. While it is // part of the wire protocol to have an underscore, and for example ESP-IDF // expects the underscore to be there, the ESP8266 implementation always adds // the underscore itself. - auto *proto = service.proto.c_str(); - while (*proto == '_') { + auto *proto = MDNS_STR_ARG(service.proto); + while (progmem_read_byte((const uint8_t *) proto) == '_') { proto++; } - auto *service_type = service.service_type.c_str(); - while (*service_type == '_') { + auto *service_type = MDNS_STR_ARG(service.service_type); + while (progmem_read_byte((const uint8_t *) service_type) == '_') { service_type++; } uint16_t port = const_cast &>(service.port).value(); - MDNS.addService(service_type, proto, port); + MDNS.addService(FPSTR(service_type), FPSTR(proto), port); for (const auto &record : service.txt_records) { - MDNS.addServiceTxt(service_type, proto, record.key.c_str(), - const_cast &>(record.value).value().c_str()); + MDNS.addServiceTxt(FPSTR(service_type), FPSTR(proto), FPSTR(MDNS_STR_ARG(record.key)), + FPSTR(MDNS_STR_ARG(record.value))); } } } @@ -45,7 +51,6 @@ void MDNSComponent::on_shutdown() { delay(10); } -} // namespace mdns -} // namespace esphome +} // namespace esphome::mdns #endif diff --git a/esphome/components/mdns/mdns_host.cpp b/esphome/components/mdns/mdns_host.cpp index 78767ed136..64b8c8f54b 100644 --- a/esphome/components/mdns/mdns_host.cpp +++ b/esphome/components/mdns/mdns_host.cpp @@ -6,14 +6,14 @@ #include "esphome/core/log.h" #include "mdns_component.h" -namespace esphome { -namespace mdns { +namespace esphome::mdns { -void MDNSComponent::setup() { this->compile_records_(); } +void MDNSComponent::setup() { + // Host platform doesn't have actual mDNS implementation +} void MDNSComponent::on_shutdown() {} -} // namespace mdns -} // namespace esphome +} // namespace esphome::mdns #endif diff --git a/esphome/components/mdns/mdns_libretiny.cpp b/esphome/components/mdns/mdns_libretiny.cpp index 7a41ec9dce..a049fe2109 100644 --- a/esphome/components/mdns/mdns_libretiny.cpp +++ b/esphome/components/mdns/mdns_libretiny.cpp @@ -3,44 +3,48 @@ #include "esphome/components/network/ip_address.h" #include "esphome/components/network/util.h" +#include "esphome/core/application.h" #include "esphome/core/log.h" #include "mdns_component.h" #include -namespace esphome { -namespace mdns { +namespace esphome::mdns { void MDNSComponent::setup() { - this->compile_records_(); +#ifdef USE_MDNS_STORE_SERVICES + this->compile_records_(this->services_); + const auto &services = this->services_; +#else + StaticVector services; + this->compile_records_(services); +#endif - MDNS.begin(this->hostname_.c_str()); + MDNS.begin(App.get_name().c_str()); - for (const auto &service : this->services_) { + for (const auto &service : services) { // Strip the leading underscore from the proto and service_type. While it is // part of the wire protocol to have an underscore, and for example ESP-IDF // expects the underscore to be there, the ESP8266 implementation always adds // the underscore itself. - auto *proto = service.proto.c_str(); + auto *proto = MDNS_STR_ARG(service.proto); while (*proto == '_') { proto++; } - auto *service_type = service.service_type.c_str(); + auto *service_type = MDNS_STR_ARG(service.service_type); while (*service_type == '_') { service_type++; } uint16_t port_ = const_cast &>(service.port).value(); MDNS.addService(service_type, proto, port_); for (const auto &record : service.txt_records) { - MDNS.addServiceTxt(service_type, proto, record.key.c_str(), - const_cast &>(record.value).value().c_str()); + MDNS.addServiceTxt(service_type, proto, MDNS_STR_ARG(record.key), MDNS_STR_ARG(record.value)); } } } void MDNSComponent::on_shutdown() {} -} // namespace mdns -} // namespace esphome +} // namespace esphome::mdns #endif diff --git a/esphome/components/mdns/mdns_rp2040.cpp b/esphome/components/mdns/mdns_rp2040.cpp index 95894323f4..a102e0b6c3 100644 --- a/esphome/components/mdns/mdns_rp2040.cpp +++ b/esphome/components/mdns/mdns_rp2040.cpp @@ -3,37 +3,42 @@ #include "esphome/components/network/ip_address.h" #include "esphome/components/network/util.h" +#include "esphome/core/application.h" #include "esphome/core/log.h" #include "mdns_component.h" #include -namespace esphome { -namespace mdns { +namespace esphome::mdns { void MDNSComponent::setup() { - this->compile_records_(); +#ifdef USE_MDNS_STORE_SERVICES + this->compile_records_(this->services_); + const auto &services = this->services_; +#else + StaticVector services; + this->compile_records_(services); +#endif - MDNS.begin(this->hostname_.c_str()); + MDNS.begin(App.get_name().c_str()); - for (const auto &service : this->services_) { + for (const auto &service : services) { // Strip the leading underscore from the proto and service_type. While it is // part of the wire protocol to have an underscore, and for example ESP-IDF // expects the underscore to be there, the ESP8266 implementation always adds // the underscore itself. - auto *proto = service.proto.c_str(); + auto *proto = MDNS_STR_ARG(service.proto); while (*proto == '_') { proto++; } - auto *service_type = service.service_type.c_str(); + auto *service_type = MDNS_STR_ARG(service.service_type); while (*service_type == '_') { service_type++; } uint16_t port = const_cast &>(service.port).value(); MDNS.addService(service_type, proto, port); for (const auto &record : service.txt_records) { - MDNS.addServiceTxt(service_type, proto, record.key.c_str(), - const_cast &>(record.value).value().c_str()); + MDNS.addServiceTxt(service_type, proto, MDNS_STR_ARG(record.key), MDNS_STR_ARG(record.value)); } } } @@ -45,7 +50,6 @@ void MDNSComponent::on_shutdown() { delay(40); } -} // namespace mdns -} // namespace esphome +} // namespace esphome::mdns #endif diff --git a/esphome/components/media_player/__init__.py b/esphome/components/media_player/__init__.py index d288e70cba..c6ffe50d79 100644 --- a/esphome/components/media_player/__init__.py +++ b/esphome/components/media_player/__init__.py @@ -14,7 +14,7 @@ from esphome.const import ( ) from esphome.core import CORE from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity -from esphome.coroutine import coroutine_with_priority +from esphome.coroutine import CoroPriority, coroutine_with_priority from esphome.cpp_generator import MockObjClass CODEOWNERS = ["@jesserockz"] @@ -192,10 +192,6 @@ def media_player_schema( return _MEDIA_PLAYER_SCHEMA.extend(schema) -# Remove before 2025.11.0 -MEDIA_PLAYER_SCHEMA = media_player_schema(MediaPlayer) -MEDIA_PLAYER_SCHEMA.add_extra(cv.deprecated_schema_constant("media_player")) - MEDIA_PLAYER_ACTION_SCHEMA = automation.maybe_simple_id( cv.Schema( { @@ -303,7 +299,7 @@ async def media_player_volume_set_action(config, action_id, template_arg, args): return var -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(media_player_ns.using) cg.add_define("USE_MEDIA_PLAYER") diff --git a/esphome/components/media_player/automation.h b/esphome/components/media_player/automation.h index 3af5959f32..50e7693cb5 100644 --- a/esphome/components/media_player/automation.h +++ b/esphome/components/media_player/automation.h @@ -11,7 +11,7 @@ template class MediaPlayerCommandAction : public Action, public Parented { public: TEMPLATABLE_VALUE(bool, announcement); - void play(Ts... x) override { + void play(const Ts &...x) override { this->parent_->make_call().set_command(Command).set_announcement(this->announcement_.value(x...)).perform(); } }; @@ -36,7 +36,7 @@ using TurnOffAction = MediaPlayerCommandAction class PlayMediaAction : public Action, public Parented { TEMPLATABLE_VALUE(std::string, media_url) TEMPLATABLE_VALUE(bool, announcement) - void play(Ts... x) override { + void play(const Ts &...x) override { this->parent_->make_call() .set_media_url(this->media_url_.value(x...)) .set_announcement(this->announcement_.value(x...)) @@ -46,7 +46,7 @@ template class PlayMediaAction : public Action, public Pa template class VolumeSetAction : public Action, public Parented { TEMPLATABLE_VALUE(float, volume) - void play(Ts... x) override { this->parent_->make_call().set_volume(this->volume_.value(x...)).perform(); } + void play(const Ts &...x) override { this->parent_->make_call().set_volume(this->volume_.value(x...)).perform(); } }; class StateTrigger : public Trigger<> { @@ -75,32 +75,34 @@ using OffTrigger = MediaPlayerStateTrigger class IsIdleCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_IDLE; } + bool check(const Ts &...x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_IDLE; } }; template class IsPlayingCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_PLAYING; } + bool check(const Ts &...x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_PLAYING; } }; template class IsPausedCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_PAUSED; } + bool check(const Ts &...x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_PAUSED; } }; template class IsAnnouncingCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING; } + bool check(const Ts &...x) override { + return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING; + } }; template class IsOnCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_ON; } + bool check(const Ts &...x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_ON; } }; template class IsOffCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_OFF; } + bool check(const Ts &...x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_OFF; } }; } // namespace media_player diff --git a/esphome/components/media_player/media_player.cpp b/esphome/components/media_player/media_player.cpp index 3f274bf73b..b46ec39d30 100644 --- a/esphome/components/media_player/media_player.cpp +++ b/esphome/components/media_player/media_player.cpp @@ -1,5 +1,6 @@ #include "media_player.h" - +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" namespace esphome { @@ -148,7 +149,12 @@ void MediaPlayer::add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } -void MediaPlayer::publish_state() { this->state_callback_.call(); } +void MediaPlayer::publish_state() { + this->state_callback_.call(); +#if defined(USE_MEDIA_PLAYER) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_media_player_update(this); +#endif +} } // namespace media_player } // namespace esphome diff --git a/esphome/components/mhz19/mhz19.h b/esphome/components/mhz19/mhz19.h index ec38f2cd2f..be36886d62 100644 --- a/esphome/components/mhz19/mhz19.h +++ b/esphome/components/mhz19/mhz19.h @@ -40,7 +40,7 @@ template class MHZ19CalibrateZeroAction : public Action { public: MHZ19CalibrateZeroAction(MHZ19Component *mhz19) : mhz19_(mhz19) {} - void play(Ts... x) override { this->mhz19_->calibrate_zero(); } + void play(const Ts &...x) override { this->mhz19_->calibrate_zero(); } protected: MHZ19Component *mhz19_; @@ -50,7 +50,7 @@ template class MHZ19ABCEnableAction : public Action { public: MHZ19ABCEnableAction(MHZ19Component *mhz19) : mhz19_(mhz19) {} - void play(Ts... x) override { this->mhz19_->abc_enable(); } + void play(const Ts &...x) override { this->mhz19_->abc_enable(); } protected: MHZ19Component *mhz19_; @@ -60,7 +60,7 @@ template class MHZ19ABCDisableAction : public Action { public: MHZ19ABCDisableAction(MHZ19Component *mhz19) : mhz19_(mhz19) {} - void play(Ts... x) override { this->mhz19_->abc_disable(); } + void play(const Ts &...x) override { this->mhz19_->abc_disable(); } protected: MHZ19Component *mhz19_; diff --git a/esphome/components/micro_wake_word/__init__.py b/esphome/components/micro_wake_word/__init__.py index cde8752157..575fb97799 100644 --- a/esphome/components/micro_wake_word/__init__.py +++ b/esphome/components/micro_wake_word/__init__.py @@ -7,7 +7,7 @@ from urllib.parse import urljoin from esphome import automation, external_files, git from esphome.automation import register_action, register_condition import esphome.codegen as cg -from esphome.components import esp32, microphone +from esphome.components import esp32, microphone, socket import esphome.config_validation as cv from esphome.const import ( CONF_FILE, @@ -32,6 +32,7 @@ _LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@kahrendt", "@jesserockz"] DEPENDENCIES = ["microphone"] +AUTO_LOAD = ["socket"] DOMAIN = "micro_wake_word" @@ -201,7 +202,7 @@ def _validate_manifest_version(manifest_data): else: raise cv.Invalid("Invalid manifest version") else: - raise cv.Invalid("Invalid manifest file, missing 'version' key.") + raise cv.Invalid("Invalid manifest file, missing 'version' key") def _process_http_source(config): @@ -421,7 +422,7 @@ def _feature_step_size_validate(config): if features_step_size is None: features_step_size = model_step_size elif features_step_size != model_step_size: - raise cv.Invalid("Cannot load models with different features step sizes.") + raise cv.Invalid("Cannot load models with different features step sizes") FINAL_VALIDATE_SCHEMA = cv.All( @@ -443,6 +444,10 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) + # Enable wake_loop_threadsafe() for low-latency wake word detection + # The inference task queues detection events that need immediate processing + socket.require_wake_loop_threadsafe() + mic_source = await microphone.microphone_source_to_code(config[CONF_MICROPHONE]) cg.add(var.set_microphone_source(mic_source)) diff --git a/esphome/components/micro_wake_word/automation.h b/esphome/components/micro_wake_word/automation.h index f10a4ed347..e1795a7e64 100644 --- a/esphome/components/micro_wake_word/automation.h +++ b/esphome/components/micro_wake_word/automation.h @@ -9,23 +9,23 @@ namespace micro_wake_word { template class StartAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->start(); } + void play(const Ts &...x) override { this->parent_->start(); } }; template class StopAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->stop(); } + void play(const Ts &...x) override { this->parent_->stop(); } }; template class IsRunningCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->is_running(); } + bool check(const Ts &...x) override { return this->parent_->is_running(); } }; template class EnableModelAction : public Action { public: explicit EnableModelAction(WakeWordModel *wake_word_model) : wake_word_model_(wake_word_model) {} - void play(Ts... x) override { this->wake_word_model_->enable(); } + void play(const Ts &...x) override { this->wake_word_model_->enable(); } protected: WakeWordModel *wake_word_model_; @@ -34,7 +34,7 @@ template class EnableModelAction : public Action { template class DisableModelAction : public Action { public: explicit DisableModelAction(WakeWordModel *wake_word_model) : wake_word_model_(wake_word_model) {} - void play(Ts... x) override { this->wake_word_model_->disable(); } + void play(const Ts &...x) override { this->wake_word_model_->disable(); } protected: WakeWordModel *wake_word_model_; @@ -43,7 +43,7 @@ template class DisableModelAction : public Action { template class ModelIsEnabledCondition : public Condition { public: explicit ModelIsEnabledCondition(WakeWordModel *wake_word_model) : wake_word_model_(wake_word_model) {} - bool check(Ts... x) override { return this->wake_word_model_->is_enabled(); } + bool check(const Ts &...x) override { return this->wake_word_model_->is_enabled(); } protected: WakeWordModel *wake_word_model_; diff --git a/esphome/components/micro_wake_word/micro_wake_word.cpp b/esphome/components/micro_wake_word/micro_wake_word.cpp index 6fca48a5bd..a0547b158e 100644 --- a/esphome/components/micro_wake_word/micro_wake_word.cpp +++ b/esphome/components/micro_wake_word/micro_wake_word.cpp @@ -2,6 +2,7 @@ #ifdef USE_ESP_IDF +#include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -426,6 +427,12 @@ void MicroWakeWord::process_probabilities_() { if (vad_state.detected) { #endif xQueueSend(this->detection_queue_, &wake_word_state, portMAX_DELAY); + + // Wake main loop immediately to process wake word detection +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + App.wake_loop_threadsafe(); +#endif + model->reset_probabilities(); #ifdef USE_MICRO_WAKE_WORD_VAD } else { diff --git a/esphome/components/microphone/__init__.py b/esphome/components/microphone/__init__.py index 29bdcfa3f3..1fc0df88a3 100644 --- a/esphome/components/microphone/__init__.py +++ b/esphome/components/microphone/__init__.py @@ -12,7 +12,7 @@ from esphome.const import ( CONF_TRIGGER_ID, ) from esphome.core import CORE -from esphome.coroutine import coroutine_with_priority +from esphome.coroutine import CoroPriority, coroutine_with_priority AUTO_LOAD = ["audio"] CODEOWNERS = ["@jesserockz", "@kahrendt"] @@ -213,7 +213,7 @@ automation.register_condition( )(microphone_action) -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(microphone_ns.using) cg.add_define("USE_MICROPHONE") diff --git a/esphome/components/microphone/automation.h b/esphome/components/microphone/automation.h index 5745909c46..a6c4bdae66 100644 --- a/esphome/components/microphone/automation.h +++ b/esphome/components/microphone/automation.h @@ -9,18 +9,18 @@ namespace esphome { namespace microphone { template class CaptureAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->start(); } + void play(const Ts &...x) override { this->parent_->start(); } }; template class StopCaptureAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->stop(); } + void play(const Ts &...x) override { this->parent_->stop(); } }; template class MuteAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->set_mute_state(true); } + void play(const Ts &...x) override { this->parent_->set_mute_state(true); } }; template class UnmuteAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->set_mute_state(false); } + void play(const Ts &...x) override { this->parent_->set_mute_state(false); } }; class DataTrigger : public Trigger &> { @@ -32,12 +32,12 @@ class DataTrigger : public Trigger &> { template class IsCapturingCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->is_running(); } + bool check(const Ts &...x) override { return this->parent_->is_running(); } }; template class IsMutedCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->get_mute_state(); } + bool check(const Ts &...x) override { return this->parent_->get_mute_state(); } }; } // namespace microphone diff --git a/esphome/components/midea/ac_adapter.cpp b/esphome/components/midea/ac_adapter.cpp index 2837713c35..d903db4a1b 100644 --- a/esphome/components/midea/ac_adapter.cpp +++ b/esphome/components/midea/ac_adapter.cpp @@ -8,9 +8,9 @@ namespace midea { namespace ac { const char *const Constants::TAG = "midea"; -const std::string Constants::FREEZE_PROTECTION = "freeze protection"; -const std::string Constants::SILENT = "silent"; -const std::string Constants::TURBO = "turbo"; +const char *const Constants::FREEZE_PROTECTION = "freeze protection"; +const char *const Constants::SILENT = "silent"; +const char *const Constants::TURBO = "turbo"; ClimateMode Converters::to_climate_mode(MideaMode mode) { switch (mode) { @@ -108,7 +108,7 @@ bool Converters::is_custom_midea_fan_mode(MideaFanMode mode) { } } -const std::string &Converters::to_custom_climate_fan_mode(MideaFanMode mode) { +const char *Converters::to_custom_climate_fan_mode(MideaFanMode mode) { switch (mode) { case MideaFanMode::FAN_SILENT: return Constants::SILENT; @@ -117,8 +117,8 @@ const std::string &Converters::to_custom_climate_fan_mode(MideaFanMode mode) { } } -MideaFanMode Converters::to_midea_fan_mode(const std::string &mode) { - if (mode == Constants::SILENT) +MideaFanMode Converters::to_midea_fan_mode(const char *mode) { + if (strcmp(mode, Constants::SILENT) == 0) return MideaFanMode::FAN_SILENT; return MideaFanMode::FAN_TURBO; } @@ -151,9 +151,9 @@ ClimatePreset Converters::to_climate_preset(MideaPreset preset) { bool Converters::is_custom_midea_preset(MideaPreset preset) { return preset == MideaPreset::PRESET_FREEZE_PROTECTION; } -const std::string &Converters::to_custom_climate_preset(MideaPreset preset) { return Constants::FREEZE_PROTECTION; } +const char *Converters::to_custom_climate_preset(MideaPreset preset) { return Constants::FREEZE_PROTECTION; } -MideaPreset Converters::to_midea_preset(const std::string &preset) { return MideaPreset::PRESET_FREEZE_PROTECTION; } +MideaPreset Converters::to_midea_preset(const char *preset) { return MideaPreset::PRESET_FREEZE_PROTECTION; } void Converters::to_climate_traits(ClimateTraits &traits, const dudanov::midea::ac::Capabilities &capabilities) { if (capabilities.supportAutoMode()) @@ -169,7 +169,7 @@ void Converters::to_climate_traits(ClimateTraits &traits, const dudanov::midea:: if (capabilities.supportEcoPreset()) traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_ECO); if (capabilities.supportFrostProtectionPreset()) - traits.add_supported_custom_preset(Constants::FREEZE_PROTECTION); + traits.set_supported_custom_presets({Constants::FREEZE_PROTECTION}); } } // namespace ac diff --git a/esphome/components/midea/ac_adapter.h b/esphome/components/midea/ac_adapter.h index c17894ae31..b0589a37f9 100644 --- a/esphome/components/midea/ac_adapter.h +++ b/esphome/components/midea/ac_adapter.h @@ -20,9 +20,9 @@ using MideaPreset = dudanov::midea::ac::Preset; class Constants { public: static const char *const TAG; - static const std::string FREEZE_PROTECTION; - static const std::string SILENT; - static const std::string TURBO; + static const char *const FREEZE_PROTECTION; + static const char *const SILENT; + static const char *const TURBO; }; class Converters { @@ -32,15 +32,15 @@ class Converters { static MideaSwingMode to_midea_swing_mode(ClimateSwingMode mode); static ClimateSwingMode to_climate_swing_mode(MideaSwingMode mode); static MideaPreset to_midea_preset(ClimatePreset preset); - static MideaPreset to_midea_preset(const std::string &preset); + static MideaPreset to_midea_preset(const char *preset); static bool is_custom_midea_preset(MideaPreset preset); static ClimatePreset to_climate_preset(MideaPreset preset); - static const std::string &to_custom_climate_preset(MideaPreset preset); + static const char *to_custom_climate_preset(MideaPreset preset); static MideaFanMode to_midea_fan_mode(ClimateFanMode fan_mode); - static MideaFanMode to_midea_fan_mode(const std::string &fan_mode); + static MideaFanMode to_midea_fan_mode(const char *fan_mode); static bool is_custom_midea_fan_mode(MideaFanMode fan_mode); static ClimateFanMode to_climate_fan_mode(MideaFanMode fan_mode); - static const std::string &to_custom_climate_fan_mode(MideaFanMode fan_mode); + static const char *to_custom_climate_fan_mode(MideaFanMode fan_mode); static void to_climate_traits(ClimateTraits &traits, const dudanov::midea::ac::Capabilities &capabilities); }; diff --git a/esphome/components/midea/ac_automations.h b/esphome/components/midea/ac_automations.h index e6fffa2511..760737be87 100644 --- a/esphome/components/midea/ac_automations.h +++ b/esphome/components/midea/ac_automations.h @@ -22,7 +22,7 @@ template class FollowMeAction : public MideaActionBase { TEMPLATABLE_VALUE(bool, use_fahrenheit) TEMPLATABLE_VALUE(bool, beeper) - void play(Ts... x) override { + void play(const Ts &...x) override { this->parent_->do_follow_me(this->temperature_.value(x...), this->use_fahrenheit_.value(x...), this->beeper_.value(x...)); } @@ -30,37 +30,37 @@ template class FollowMeAction : public MideaActionBase { template class SwingStepAction : public MideaActionBase { public: - void play(Ts... x) override { this->parent_->do_swing_step(); } + void play(const Ts &...x) override { this->parent_->do_swing_step(); } }; template class DisplayToggleAction : public MideaActionBase { public: - void play(Ts... x) override { this->parent_->do_display_toggle(); } + void play(const Ts &...x) override { this->parent_->do_display_toggle(); } }; template class BeeperOnAction : public MideaActionBase { public: - void play(Ts... x) override { this->parent_->do_beeper_on(); } + void play(const Ts &...x) override { this->parent_->do_beeper_on(); } }; template class BeeperOffAction : public MideaActionBase { public: - void play(Ts... x) override { this->parent_->do_beeper_off(); } + void play(const Ts &...x) override { this->parent_->do_beeper_off(); } }; template class PowerOnAction : public MideaActionBase { public: - void play(Ts... x) override { this->parent_->do_power_on(); } + void play(const Ts &...x) override { this->parent_->do_power_on(); } }; template class PowerOffAction : public MideaActionBase { public: - void play(Ts... x) override { this->parent_->do_power_off(); } + void play(const Ts &...x) override { this->parent_->do_power_off(); } }; template class PowerToggleAction : public MideaActionBase { public: - void play(Ts... x) override { this->parent_->do_power_toggle(); } + void play(const Ts &...x) override { this->parent_->do_power_toggle(); } }; } // namespace ac diff --git a/esphome/components/midea/air_conditioner.cpp b/esphome/components/midea/air_conditioner.cpp index 170a2f6a40..a6a8d52549 100644 --- a/esphome/components/midea/air_conditioner.cpp +++ b/esphome/components/midea/air_conditioner.cpp @@ -64,28 +64,30 @@ void AirConditioner::control(const ClimateCall &call) { ctrl.mode = Converters::to_midea_mode(call.get_mode().value()); if (call.get_preset().has_value()) { ctrl.preset = Converters::to_midea_preset(call.get_preset().value()); - } else if (call.get_custom_preset().has_value()) { - ctrl.preset = Converters::to_midea_preset(call.get_custom_preset().value()); + } else if (call.has_custom_preset()) { + ctrl.preset = Converters::to_midea_preset(call.get_custom_preset()); } if (call.get_fan_mode().has_value()) { ctrl.fanMode = Converters::to_midea_fan_mode(call.get_fan_mode().value()); - } else if (call.get_custom_fan_mode().has_value()) { - ctrl.fanMode = Converters::to_midea_fan_mode(call.get_custom_fan_mode().value()); + } else if (call.has_custom_fan_mode()) { + ctrl.fanMode = Converters::to_midea_fan_mode(call.get_custom_fan_mode()); } this->base_.control(ctrl); } ClimateTraits AirConditioner::traits() { auto traits = ClimateTraits(); - traits.set_supports_current_temperature(true); + traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE); traits.set_visual_min_temperature(17); traits.set_visual_max_temperature(30); traits.set_visual_temperature_step(0.5); traits.set_supported_modes(this->supported_modes_); traits.set_supported_swing_modes(this->supported_swing_modes_); traits.set_supported_presets(this->supported_presets_); - traits.set_supported_custom_presets(this->supported_custom_presets_); - traits.set_supported_custom_fan_modes(this->supported_custom_fan_modes_); + if (!this->supported_custom_presets_.empty()) + traits.set_supported_custom_presets(this->supported_custom_presets_); + if (!this->supported_custom_fan_modes_.empty()) + traits.set_supported_custom_fan_modes(this->supported_custom_fan_modes_); /* + MINIMAL SET OF CAPABILITIES */ traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_AUTO); traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_LOW); diff --git a/esphome/components/midea/air_conditioner.h b/esphome/components/midea/air_conditioner.h index e70bd34e71..70833b8bcc 100644 --- a/esphome/components/midea/air_conditioner.h +++ b/esphome/components/midea/air_conditioner.h @@ -19,6 +19,9 @@ using climate::ClimateTraits; using climate::ClimateMode; using climate::ClimateSwingMode; using climate::ClimateFanMode; +using climate::ClimateModeMask; +using climate::ClimateSwingModeMask; +using climate::ClimatePresetMask; class AirConditioner : public ApplianceBase, public climate::Climate { public: @@ -40,20 +43,20 @@ class AirConditioner : public ApplianceBase, void do_power_on() { this->base_.setPowerState(true); } void do_power_off() { this->base_.setPowerState(false); } void do_power_toggle() { this->base_.setPowerState(this->mode == ClimateMode::CLIMATE_MODE_OFF); } - void set_supported_modes(const std::set &modes) { this->supported_modes_ = modes; } - void set_supported_swing_modes(const std::set &modes) { this->supported_swing_modes_ = modes; } - void set_supported_presets(const std::set &presets) { this->supported_presets_ = presets; } - void set_custom_presets(const std::set &presets) { this->supported_custom_presets_ = presets; } - void set_custom_fan_modes(const std::set &modes) { this->supported_custom_fan_modes_ = modes; } + void set_supported_modes(ClimateModeMask modes) { this->supported_modes_ = modes; } + void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; } + void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; } + void set_custom_presets(std::initializer_list presets) { this->supported_custom_presets_ = presets; } + void set_custom_fan_modes(std::initializer_list modes) { this->supported_custom_fan_modes_ = modes; } protected: void control(const ClimateCall &call) override; ClimateTraits traits() override; - std::set supported_modes_{}; - std::set supported_swing_modes_{}; - std::set supported_presets_{}; - std::set supported_custom_presets_{}; - std::set supported_custom_fan_modes_{}; + ClimateModeMask supported_modes_{}; + ClimateSwingModeMask supported_swing_modes_{}; + ClimatePresetMask supported_presets_{}; + std::vector supported_custom_presets_{}; + std::vector supported_custom_fan_modes_{}; Sensor *outdoor_sensor_{nullptr}; Sensor *humidity_sensor_{nullptr}; Sensor *power_sensor_{nullptr}; diff --git a/esphome/components/mipi/__init__.py b/esphome/components/mipi/__init__.py index b9299bb8d7..4dbc81caa2 100644 --- a/esphome/components/mipi/__init__.py +++ b/esphome/components/mipi/__init__.py @@ -2,7 +2,7 @@ # Various configuration constants for MIPI displays # Various utility functions for MIPI DBI configuration -from typing import Any +from typing import Any, Self from esphome.components.const import CONF_COLOR_DEPTH from esphome.components.display import CONF_SHOW_TEST_CARD, display_ns @@ -11,6 +11,7 @@ from esphome.const import ( CONF_BRIGHTNESS, CONF_COLOR_ORDER, CONF_DIMENSIONS, + CONF_DISABLED, CONF_HEIGHT, CONF_INIT_SEQUENCE, CONF_INVERT_COLORS, @@ -77,6 +78,7 @@ BRIGHTNESS = 0x51 WRDISBV = 0x51 RDDISBV = 0x52 WRCTRLD = 0x53 +WCE = 0x58 SWIRE1 = 0x5A SWIRE2 = 0x5B IFMODE = 0xB0 @@ -91,6 +93,7 @@ PWCTR2 = 0xC1 PWCTR3 = 0xC2 PWCTR4 = 0xC3 PWCTR5 = 0xC4 +SPIMODESEL = 0xC4 VMCTR1 = 0xC5 IFCTR = 0xC6 VMCTR2 = 0xC7 @@ -215,12 +218,33 @@ def map_sequence(value): return tuple(value) +def flatten_sequence(sequence: tuple | list): + """ + Flatten an init sequence into a single list of bytes. + :param sequence: The list of tuples + :return: a list of bytes + """ + return sum( + tuple( + (x[1], 0xFF) if x[0] == DELAY_FLAG else (x[0], len(x) - 1) + x[1:] + for x in sequence + ), + (), + ) + + def delay(ms): return DELAY_FLAG, ms class DriverChip: - models = {} + """ + A class representing a MIPI DBI driver chip model. + The parameters supplied as defaults will be used to provide default values for the display configuration. + Setting swap_xy to cv.UNDEFINED will indicate that the model does not support swapping X and Y axes. + """ + + models: dict[str, Self] = {} def __init__( self, @@ -230,7 +254,7 @@ class DriverChip: ): name = name.upper() self.name = name - self.initsequence = initsequence or defaults.get("init_sequence") + self.initsequence = initsequence self.defaults = defaults DriverChip.models[name] = self @@ -244,6 +268,17 @@ class DriverChip: return models def extend(self, name, **kwargs) -> "DriverChip": + """ + Extend the current model with additional parameters or a modified init sequence. + Parameters supplied here will override the defaults of the current model. + if the initsequence is not provided, the current model's initsequence will be used. + If add_init_sequence is provided, it will be appended to the current initsequence. + :param name: + :param kwargs: + :return: + """ + initsequence = list(kwargs.pop("initsequence", self.initsequence)) + initsequence.extend(kwargs.pop("add_init_sequence", ())) defaults = self.defaults.copy() if ( CONF_WIDTH in defaults @@ -258,23 +293,41 @@ class DriverChip: ): defaults[CONF_NATIVE_HEIGHT] = defaults[CONF_HEIGHT] defaults.update(kwargs) - return DriverChip(name, initsequence=self.initsequence, **defaults) + return self.__class__(name, initsequence=tuple(initsequence), **defaults) def get_default(self, key, fallback: Any = False) -> Any: return self.defaults.get(key, fallback) + @property + def transforms(self) -> set[str]: + """ + Return the available transforms for this model. + """ + if self.get_default("no_transform", False): + return set() + if self.get_default(CONF_SWAP_XY) != cv.UNDEFINED: + return {CONF_MIRROR_X, CONF_MIRROR_Y, CONF_SWAP_XY} + return {CONF_MIRROR_X, CONF_MIRROR_Y} + def option(self, name, fallback=False) -> cv.Optional: return cv.Optional(name, default=self.get_default(name, fallback)) def rotation_as_transform(self, config) -> bool: """ Check if a rotation can be implemented in hardware using the MADCTL register. - A rotation of 180 is always possible, 90 and 270 are possible if the model supports swapping X and Y. + A rotation of 180 is always possible if x and y mirroring are supported, 90 and 270 are possible if the model supports swapping X and Y. """ + if config.get(CONF_TRANSFORM) == CONF_DISABLED: + return False + transforms = self.transforms rotation = config.get(CONF_ROTATION, 0) - return rotation and ( - self.get_default(CONF_SWAP_XY) != cv.UNDEFINED or rotation == 180 - ) + if rotation == 0 or not transforms: + return False + if rotation == 180: + return CONF_MIRROR_X in transforms and CONF_MIRROR_Y in transforms + if rotation == 90: + return CONF_SWAP_XY in transforms and CONF_MIRROR_X in transforms + return CONF_SWAP_XY in transforms and CONF_MIRROR_Y in transforms def get_dimensions(self, config) -> tuple[int, int, int, int]: if CONF_DIMENSIONS in config: @@ -299,16 +352,16 @@ class DriverChip: # if mirroring axes and there are offsets, also mirror the offsets to cater for situations where # the offset is asymmetric - if transform[CONF_MIRROR_X]: + if transform.get(CONF_MIRROR_X): native_width = self.get_default(CONF_NATIVE_WIDTH, width + offset_width * 2) offset_width = native_width - width - offset_width - if transform[CONF_MIRROR_Y]: + if transform.get(CONF_MIRROR_Y): native_height = self.get_default( CONF_NATIVE_HEIGHT, height + offset_height * 2 ) offset_height = native_height - height - offset_height - # Swap default dimensions if swap_xy is set - if transform[CONF_SWAP_XY] is True: + # Swap default dimensions if swap_xy is set, or if rotation is 90/270 and we are not using a buffer + if transform.get(CONF_SWAP_XY) is True: width, height = height, width offset_height, offset_width = offset_width, offset_height return width, height, offset_width, offset_height @@ -318,12 +371,19 @@ class DriverChip: transform = config.get( CONF_TRANSFORM, { - CONF_MIRROR_X: self.get_default(CONF_MIRROR_X, False), - CONF_MIRROR_Y: self.get_default(CONF_MIRROR_Y, False), - CONF_SWAP_XY: self.get_default(CONF_SWAP_XY, False), + CONF_MIRROR_X: self.get_default(CONF_MIRROR_X), + CONF_MIRROR_Y: self.get_default(CONF_MIRROR_Y), + CONF_SWAP_XY: self.get_default(CONF_SWAP_XY), }, ) - + if not isinstance(transform, dict): + # Presumably disabled + return { + CONF_MIRROR_X: False, + CONF_MIRROR_Y: False, + CONF_SWAP_XY: False, + CONF_TRANSFORM: False, + } # Can we use the MADCTL register to set the rotation? if can_transform and CONF_TRANSFORM not in config: rotation = config[CONF_ROTATION] @@ -339,6 +399,40 @@ class DriverChip: transform[CONF_TRANSFORM] = True return transform + def swap_xy_schema(self): + uses_swap = self.get_default(CONF_SWAP_XY, None) != cv.UNDEFINED + + def validator(value): + if value: + raise cv.Invalid("Axis swapping not supported by this model") + return cv.boolean(value) + + if uses_swap: + return {cv.Required(CONF_SWAP_XY): cv.boolean} + return {cv.Optional(CONF_SWAP_XY, default=False): validator} + + def add_madctl(self, sequence: list, config: dict): + # Add the MADCTL command to the sequence based on the configuration. + use_flip = config.get(CONF_USE_AXIS_FLIPS) + madctl = 0 + transform = self.get_transform(config) + if transform[CONF_MIRROR_X]: + madctl |= MADCTL_XFLIP if use_flip else MADCTL_MX + if transform[CONF_MIRROR_Y]: + madctl |= MADCTL_YFLIP if use_flip else MADCTL_MY + if transform.get(CONF_SWAP_XY) is True: # Exclude Undefined + madctl |= MADCTL_MV + if config[CONF_COLOR_ORDER] == MODE_BGR: + madctl |= MADCTL_BGR + sequence.append((MADCTL, madctl)) + return madctl + + def skip_command(self, command: str): + """ + Allow suppressing a standard command in the init sequence. + """ + return self.get_default(f"no_{command.lower()}", False) + def get_sequence(self, config) -> tuple[tuple[int, ...], int]: """ Create the init sequence for the display. @@ -361,39 +455,23 @@ class DriverChip: pixel_mode = PIXEL_MODES[pixel_mode] sequence.append((PIXFMT, pixel_mode)) - # Does the chip use the flipping bits for mirroring rather than the reverse order bits? - use_flip = config.get(CONF_USE_AXIS_FLIPS) - madctl = 0 - transform = self.get_transform(config) if self.rotation_as_transform(config): LOGGER.info("Using hardware transform to implement rotation") - if transform.get(CONF_MIRROR_X): - madctl |= MADCTL_XFLIP if use_flip else MADCTL_MX - if transform.get(CONF_MIRROR_Y): - madctl |= MADCTL_YFLIP if use_flip else MADCTL_MY - if transform.get(CONF_SWAP_XY) is True: # Exclude Undefined - madctl |= MADCTL_MV - if config[CONF_COLOR_ORDER] == MODE_BGR: - madctl |= MADCTL_BGR - sequence.append((MADCTL, madctl)) + madctl = self.add_madctl(sequence, config) if config[CONF_INVERT_COLORS]: sequence.append((INVON,)) else: sequence.append((INVOFF,)) if brightness := config.get(CONF_BRIGHTNESS, self.get_default(CONF_BRIGHTNESS)): sequence.append((BRIGHTNESS, brightness)) - sequence.append((SLPOUT,)) + # Add a SLPOUT command if required. + if not self.skip_command("SLPOUT"): + sequence.append((SLPOUT,)) sequence.append((DISPON,)) # Flatten the sequence into a list of bytes, with the length of each command # or the delay flag inserted where needed - return sum( - tuple( - (x[1], 0xFF) if x[0] == DELAY_FLAG else (x[0], len(x) - 1) + x[1:] - for x in sequence - ), - (), - ), madctl + return flatten_sequence(sequence), madctl def requires_buffer(config) -> bool: diff --git a/esphome/components/mipi_dsi/mipi_dsi.cpp b/esphome/components/mipi_dsi/mipi_dsi.cpp index fbe251de41..cae8647398 100644 --- a/esphome/components/mipi_dsi/mipi_dsi.cpp +++ b/esphome/components/mipi_dsi/mipi_dsi.cpp @@ -11,6 +11,12 @@ static bool notify_refresh_ready(esp_lcd_panel_handle_t panel, esp_lcd_dpi_panel xSemaphoreGiveFromISR(sem, &need_yield); return (need_yield == pdTRUE); } + +void MIPI_DSI::smark_failed(const LogString *message, esp_err_t err) { + ESP_LOGE(TAG, "%s: %s", LOG_STR_ARG(message), esp_err_to_name(err)); + this->mark_failed(message); +} + void MIPI_DSI::setup() { ESP_LOGCONFIG(TAG, "Running Setup"); @@ -31,7 +37,7 @@ void MIPI_DSI::setup() { }; auto err = esp_lcd_new_dsi_bus(&bus_config, &this->bus_handle_); if (err != ESP_OK) { - this->smark_failed("lcd_new_dsi_bus failed", err); + this->smark_failed(LOG_STR("lcd_new_dsi_bus failed"), err); return; } esp_lcd_dbi_io_config_t dbi_config = { @@ -41,7 +47,7 @@ void MIPI_DSI::setup() { }; err = esp_lcd_new_panel_io_dbi(this->bus_handle_, &dbi_config, &this->io_handle_); if (err != ESP_OK) { - this->smark_failed("new_panel_io_dbi failed", err); + this->smark_failed(LOG_STR("new_panel_io_dbi failed"), err); return; } auto pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB565; @@ -69,7 +75,7 @@ void MIPI_DSI::setup() { }}; err = esp_lcd_new_panel_dpi(this->bus_handle_, &dpi_config, &this->handle_); if (err != ESP_OK) { - this->smark_failed("esp_lcd_new_panel_dpi failed", err); + this->smark_failed(LOG_STR("esp_lcd_new_panel_dpi failed"), err); return; } if (this->reset_pin_ != nullptr) { @@ -86,14 +92,14 @@ void MIPI_DSI::setup() { auto when = millis() + 120; err = esp_lcd_panel_init(this->handle_); if (err != ESP_OK) { - this->smark_failed("esp_lcd_init failed", err); + this->smark_failed(LOG_STR("esp_lcd_init failed"), err); return; } size_t index = 0; auto &vec = this->init_sequence_; while (index != vec.size()) { if (vec.size() - index < 2) { - this->mark_failed("Malformed init sequence"); + this->mark_failed(LOG_STR("Malformed init sequence")); return; } uint8_t cmd = vec[index++]; @@ -104,7 +110,7 @@ void MIPI_DSI::setup() { } else { uint8_t num_args = x & 0x7F; if (vec.size() - index < num_args) { - this->mark_failed("Malformed init sequence"); + this->mark_failed(LOG_STR("Malformed init sequence")); return; } if (cmd == SLEEP_OUT) { @@ -119,7 +125,7 @@ void MIPI_DSI::setup() { format_hex_pretty(ptr, num_args, '.', false).c_str()); err = esp_lcd_panel_io_tx_param(this->io_handle_, cmd, ptr, num_args); if (err != ESP_OK) { - this->smark_failed("lcd_panel_io_tx_param failed", err); + this->smark_failed(LOG_STR("lcd_panel_io_tx_param failed"), err); return; } index += num_args; @@ -134,7 +140,7 @@ void MIPI_DSI::setup() { err = (esp_lcd_dpi_panel_register_event_callbacks(this->handle_, &cbs, this->io_lock_)); if (err != ESP_OK) { - this->smark_failed("Failed to register callbacks", err); + this->smark_failed(LOG_STR("Failed to register callbacks"), err); return; } @@ -216,7 +222,7 @@ bool MIPI_DSI::check_buffer_() { RAMAllocator allocator; this->buffer_ = allocator.allocate(this->height_ * this->width_ * bytes_per_pixel); if (this->buffer_ == nullptr) { - this->mark_failed("Could not allocate buffer for display!"); + this->mark_failed(LOG_STR("Could not allocate buffer for display!")); return false; } return true; diff --git a/esphome/components/mipi_dsi/mipi_dsi.h b/esphome/components/mipi_dsi/mipi_dsi.h index ce8a2a2236..1cffe3b178 100644 --- a/esphome/components/mipi_dsi/mipi_dsi.h +++ b/esphome/components/mipi_dsi/mipi_dsi.h @@ -62,10 +62,7 @@ class MIPI_DSI : public display::Display { void set_lanes(uint8_t lanes) { this->lanes_ = lanes; } void set_madctl(uint8_t madctl) { this->madctl_ = madctl; } - void smark_failed(const char *message, esp_err_t err) { - auto str = str_sprintf("Setup failed: %s: %s", message, esp_err_to_name(err)); - this->mark_failed(str.c_str()); - } + void smark_failed(const LogString *message, esp_err_t err); void update() override; diff --git a/esphome/components/mipi_dsi/models/guition.py b/esphome/components/mipi_dsi/models/guition.py index fd3fbf6160..cd566633f9 100644 --- a/esphome/components/mipi_dsi/models/guition.py +++ b/esphome/components/mipi_dsi/models/guition.py @@ -16,7 +16,6 @@ DriverChip( lane_bit_rate="750Mbps", swap_xy=cv.UNDEFINED, color_order="RGB", - reset_pin=27, initsequence=[ (0x30, 0x00), (0xF7, 0x49, 0x61, 0x02, 0x00), (0x30, 0x01), (0x04, 0x0C), (0x05, 0x00), (0x06, 0x00), (0x0B, 0x11), (0x17, 0x00), (0x20, 0x04), (0x1F, 0x05), (0x23, 0x00), (0x25, 0x19), (0x28, 0x18), (0x29, 0x04), (0x2A, 0x01), @@ -36,3 +35,70 @@ DriverChip( (0x10, 0x0C), (0x11, 0x0C), (0x12, 0x0C), (0x13, 0x0C), (0x30, 0x00), ], ) + + +# JC4880P443 Driver Configuration (ST7701) +# Using parameters from esp_lcd_st7701.h and the working full init sequence +# ---------------------------------------------------------------------------------------------------------------------- +# * Resolution: 480x800 +# * PCLK Frequency: 34 MHz +# * DSI Lane Bit Rate: 500 Mbps (using 2-Lane DSI configuration) +# * Horizontal Timing (hsync_pulse_width=12, hsync_back_porch=42, hsync_front_porch=42) +# * Vertical Timing (vsync_pulse_width=2, vsync_back_porch=8, vsync_front_porch=166) +# ---------------------------------------------------------------------------------------------------------------------- +DriverChip( + "JC4880P443", + width=480, + height=800, + hsync_back_porch=42, + hsync_pulse_width=12, + hsync_front_porch=42, + vsync_back_porch=8, + vsync_pulse_width=2, + vsync_front_porch=166, + pclk_frequency="34MHz", + lane_bit_rate="500Mbps", + swap_xy=cv.UNDEFINED, + color_order="RGB", + reset_pin=5, + initsequence=[ + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x13), + (0xEF, 0x08), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x10), + (0xC0, 0x63, 0x00), + (0xC1, 0x0D, 0x02), + (0xC2, 0x10, 0x08), + (0xCC, 0x10), + (0xB0, 0x80, 0x09, 0x53, 0x0C, 0xD0, 0x07, 0x0C, 0x09, 0x09, 0x28, 0x06, 0xD4, 0x13, 0x69, 0x2B, 0x71), + (0xB1, 0x80, 0x94, 0x5A, 0x10, 0xD3, 0x06, 0x0A, 0x08, 0x08, 0x25, 0x03, 0xD3, 0x12, 0x66, 0x6A, 0x0D), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x11), + (0xB0, 0x5D), + (0xB1, 0x58), + (0xB2, 0x87), + (0xB3, 0x80), + (0xB5, 0x4E), + (0xB7, 0x85), + (0xB8, 0x21), + (0xB9, 0x10, 0x1F), + (0xBB, 0x03), + (0xBC, 0x00), + (0xC1, 0x78), + (0xC2, 0x78), + (0xD0, 0x88), + (0xE0, 0x00, 0x3A, 0x02), + (0xE1, 0x04, 0xA0, 0x00, 0xA0, 0x05, 0xA0, 0x00, 0xA0, 0x00, 0x40, 0x40), + (0xE2, 0x30, 0x00, 0x40, 0x40, 0x32, 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00), + (0xE3, 0x00, 0x00, 0x33, 0x33), + (0xE4, 0x44, 0x44), + (0xE5, 0x09, 0x2E, 0xA0, 0xA0, 0x0B, 0x30, 0xA0, 0xA0, 0x05, 0x2A, 0xA0, 0xA0, 0x07, 0x2C, 0xA0, 0xA0), + (0xE6, 0x00, 0x00, 0x33, 0x33), + (0xE7, 0x44, 0x44), + (0xE8, 0x08, 0x2D, 0xA0, 0xA0, 0x0A, 0x2F, 0xA0, 0xA0, 0x04, 0x29, 0xA0, 0xA0, 0x06, 0x2B, 0xA0, 0xA0), + (0xEB, 0x00, 0x00, 0x4E, 0x4E, 0x00, 0x00, 0x00), + (0xEC, 0x08, 0x01), + (0xED, 0xB0, 0x2B, 0x98, 0xA4, 0x56, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF7, 0x65, 0x4A, 0x89, 0xB2, 0x0B), + (0xEF, 0x08, 0x08, 0x08, 0x45, 0x3F, 0x54), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x00), + ] +) +# fmt: on diff --git a/esphome/components/mipi_dsi/models/waveshare.py b/esphome/components/mipi_dsi/models/waveshare.py index 7cfd6f1645..c3d080f8b2 100644 --- a/esphome/components/mipi_dsi/models/waveshare.py +++ b/esphome/components/mipi_dsi/models/waveshare.py @@ -56,50 +56,41 @@ DriverChip( "WAVESHARE-P4-86-PANEL", height=720, width=720, - hsync_back_porch=80, + hsync_back_porch=50, hsync_pulse_width=20, - hsync_front_porch=80, - vsync_back_porch=12, + hsync_front_porch=50, + vsync_back_porch=20, vsync_pulse_width=4, - vsync_front_porch=30, - pclk_frequency="46MHz", - lane_bit_rate="1Gbps", + vsync_front_porch=20, + pclk_frequency="38MHz", + lane_bit_rate="480Mbps", swap_xy=cv.UNDEFINED, color_order="RGB", reset_pin=27, initsequence=[ (0xB9, 0xF1, 0x12, 0x83), - ( - 0xBA, 0x31, 0x81, 0x05, 0xF9, 0x0E, 0x0E, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x25, 0x00, - 0x90, 0x0A, 0x00, 0x00, 0x01, 0x4F, 0x01, 0x00, 0x00, 0x37, - ), - (0xB8, 0x25, 0x22, 0xF0, 0x63), - (0xBF, 0x02, 0x11, 0x00), + (0xB1, 0x00, 0x00, 0x00, 0xDA, 0x80), + (0xB2, 0x3C, 0x12, 0x30), (0xB3, 0x10, 0x10, 0x28, 0x28, 0x03, 0xFF, 0x00, 0x00, 0x00, 0x00), - (0xC0, 0x73, 0x73, 0x50, 0x50, 0x00, 0x00, 0x12, 0x70, 0x00), - (0xBC, 0x46), (0xCC, 0x0B), (0xB4, 0x80), (0xB2, 0x3C, 0x12, 0x30), - (0xE3, 0x07, 0x07, 0x0B, 0x0B, 0x03, 0x0B, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0xC0, 0x10,), - (0xC1, 0x36, 0x00, 0x32, 0x32, 0x77, 0xF1, 0xCC, 0xCC, 0x77, 0x77, 0x33, 0x33), + (0xB4, 0x80), (0xB5, 0x0A, 0x0A), - (0xB6, 0xB2, 0xB2), - ( - 0xE9, 0xC8, 0x10, 0x0A, 0x10, 0x0F, 0xA1, 0x80, 0x12, 0x31, 0x23, 0x47, 0x86, 0xA1, 0x80, - 0x47, 0x08, 0x00, 0x00, 0x0D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, 0x00, 0x00, 0x00, 0x48, - 0x02, 0x8B, 0xAF, 0x46, 0x02, 0x88, 0x88, 0x88, 0x88, 0x88, 0x48, 0x13, 0x8B, 0xAF, 0x57, - 0x13, 0x88, 0x88, 0x88, 0x88, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - ), - ( - 0xEA, 0x96, 0x12, 0x01, 0x01, 0x01, 0x78, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4F, 0x31, - 0x8B, 0xA8, 0x31, 0x75, 0x88, 0x88, 0x88, 0x88, 0x88, 0x4F, 0x20, 0x8B, 0xA8, 0x20, 0x64, - 0x88, 0x88, 0x88, 0x88, 0x88, 0x23, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xA1, 0x80, 0x00, 0x00, - 0x00, 0x00, - ), - ( - 0xE0, 0x00, 0x0A, 0x0F, 0x29, 0x3B, 0x3F, 0x42, 0x39, 0x06, 0x0D, 0x10, 0x13, 0x15, 0x14, - 0x15, 0x10, 0x17, 0x00, 0x0A, 0x0F, 0x29, 0x3B, 0x3F, 0x42, 0x39, 0x06, 0x0D, 0x10, 0x13, - 0x15, 0x14, 0x15, 0x10, 0x17, - ), + (0xB6, 0x97, 0x97), + (0xB8, 0x26, 0x22, 0xF0, 0x13), + (0xBA, 0x31, 0x81, 0x0F, 0xF9, 0x0E, 0x06, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x25, 0x00, 0x90, 0x0A, 0x00, 0x00, 0x01, 0x4F, 0x01, 0x00, 0x00, 0x37), + (0xBC, 0x47), + (0xBF, 0x02, 0x11, 0x00), + (0xC0, 0x73, 0x73, 0x50, 0x50, 0x00, 0x00, 0x12, 0x70, 0x00), + (0xC1, 0x25, 0x00, 0x32, 0x32, 0x77, 0xE4, 0xFF, 0xFF, 0xCC, 0xCC, 0x77, 0x77), + (0xC6, 0x82, 0x00, 0xBF, 0xFF, 0x00, 0xFF), + (0xC7, 0xB8, 0x00, 0x0A, 0x10, 0x01, 0x09), + (0xC8, 0x10, 0x40, 0x1E, 0x02), + (0xCC, 0x0B), + (0xE0, 0x00, 0x0B, 0x10, 0x2C, 0x3D, 0x3F, 0x42, 0x3A, 0x07, 0x0D, 0x0F, 0x13, 0x15, 0x13, 0x14, 0x0F, 0x16, 0x00, 0x0B, 0x10, 0x2C, 0x3D, 0x3F, 0x42, 0x3A, 0x07, 0x0D, 0x0F, 0x13, 0x15, 0x13, 0x14, 0x0F, 0x16), + (0xE3, 0x07, 0x07, 0x0B, 0x0B, 0x0B, 0x0B, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0xC0, 0x10), + (0xE9, 0xC8, 0x10, 0x0A, 0x00, 0x00, 0x80, 0x81, 0x12, 0x31, 0x23, 0x4F, 0x86, 0xA0, 0x00, 0x47, 0x08, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x98, 0x02, 0x8B, 0xAF, 0x46, 0x02, 0x88, 0x88, 0x88, 0x88, 0x88, 0x98, 0x13, 0x8B, 0xAF, 0x57, 0x13, 0x88, 0x88, 0x88, 0x88, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00), + (0xEA, 0x97, 0x0C, 0x09, 0x09, 0x09, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9F, 0x31, 0x8B, 0xA8, 0x31, 0x75, 0x88, 0x88, 0x88, 0x88, 0x88, 0x9F, 0x20, 0x8B, 0xA8, 0x20, 0x64, 0x88, 0x88, 0x88, 0x88, 0x88, 0x23, 0x00, 0x00, 0x02, 0x71, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x80, 0x81, 0x00, 0x00, 0x00, 0x00), + (0xEF, 0xFF, 0xFF, 0x01), + (0x11, 0x00), + (0x29, 0x00), ], ) diff --git a/esphome/components/mipi_rgb/__init__.py b/esphome/components/mipi_rgb/__init__.py new file mode 100644 index 0000000000..4f9972c6e0 --- /dev/null +++ b/esphome/components/mipi_rgb/__init__.py @@ -0,0 +1,2 @@ +CODEOWNERS = ["@clydebarrow"] +DOMAIN = "mipi_rgb" diff --git a/esphome/components/mipi_rgb/display.py b/esphome/components/mipi_rgb/display.py new file mode 100644 index 0000000000..9d6b1fa729 --- /dev/null +++ b/esphome/components/mipi_rgb/display.py @@ -0,0 +1,325 @@ +import importlib +import pkgutil + +from esphome import pins +import esphome.codegen as cg +from esphome.components import display, spi +from esphome.components.const import ( + BYTE_ORDER_BIG, + BYTE_ORDER_LITTLE, + CONF_BYTE_ORDER, + CONF_DRAW_ROUNDING, +) +from esphome.components.display import CONF_SHOW_TEST_CARD +from esphome.components.esp32 import const, only_on_variant +from esphome.components.mipi import ( + COLOR_ORDERS, + CONF_DE_PIN, + CONF_HSYNC_BACK_PORCH, + CONF_HSYNC_FRONT_PORCH, + CONF_HSYNC_PULSE_WIDTH, + CONF_PCLK_PIN, + CONF_PIXEL_MODE, + CONF_USE_AXIS_FLIPS, + CONF_VSYNC_BACK_PORCH, + CONF_VSYNC_FRONT_PORCH, + CONF_VSYNC_PULSE_WIDTH, + MODE_BGR, + PIXEL_MODE_16BIT, + PIXEL_MODE_18BIT, + DriverChip, + dimension_schema, + map_sequence, + power_of_two, + requires_buffer, +) +from esphome.components.rpi_dpi_rgb.display import ( + CONF_PCLK_FREQUENCY, + CONF_PCLK_INVERTED, +) +import esphome.config_validation as cv +from esphome.const import ( + CONF_BLUE, + CONF_COLOR_ORDER, + CONF_CS_PIN, + CONF_DATA_PINS, + CONF_DATA_RATE, + CONF_DC_PIN, + CONF_DIMENSIONS, + CONF_DISABLED, + CONF_ENABLE_PIN, + CONF_GREEN, + CONF_HSYNC_PIN, + CONF_ID, + CONF_IGNORE_STRAPPING_WARNING, + CONF_INIT_SEQUENCE, + CONF_INVERT_COLORS, + CONF_LAMBDA, + CONF_MIRROR_X, + CONF_MIRROR_Y, + CONF_MODEL, + CONF_NUMBER, + CONF_RED, + CONF_RESET_PIN, + CONF_ROTATION, + CONF_SPI_ID, + CONF_SWAP_XY, + CONF_TRANSFORM, + CONF_VSYNC_PIN, + CONF_WIDTH, +) +from esphome.final_validate import full_config + +from ..spi import CONF_SPI_MODE, SPI_DATA_RATE_SCHEMA, SPI_MODE_OPTIONS, SPIComponent +from . import models + +DEPENDENCIES = ["esp32", "psram"] + +mipi_rgb_ns = cg.esphome_ns.namespace("mipi_rgb") +mipi_rgb = mipi_rgb_ns.class_("MipiRgb", display.Display, cg.Component) +mipi_rgb_spi = mipi_rgb_ns.class_( + "MipiRgbSpi", mipi_rgb, display.Display, cg.Component, spi.SPIDevice +) +ColorOrder = display.display_ns.enum("ColorMode") + +DATA_PIN_SCHEMA = pins.internal_gpio_output_pin_schema + +DriverChip("CUSTOM") + +# Import all models dynamically from the models package + +for module_info in pkgutil.iter_modules(models.__path__): + importlib.import_module(f".models.{module_info.name}", package=__package__) + +MODELS = DriverChip.get_models() + + +def data_pin_validate(value): + """ + It is safe to use strapping pins as RGB output data bits, as they are outputs only, + and not initialised until after boot. + """ + if not isinstance(value, dict): + try: + return DATA_PIN_SCHEMA( + {CONF_NUMBER: value, CONF_IGNORE_STRAPPING_WARNING: True} + ) + except cv.Invalid: + pass + return DATA_PIN_SCHEMA(value) + + +def data_pin_set(length): + return cv.All( + [data_pin_validate], + cv.Length(min=length, max=length, msg=f"Exactly {length} data pins required"), + ) + + +def model_schema(config): + model = MODELS[config[CONF_MODEL].upper()] + transform = cv.Any( + cv.Schema( + { + cv.Required(CONF_MIRROR_X): cv.boolean, + cv.Required(CONF_MIRROR_Y): cv.boolean, + **model.swap_xy_schema(), + } + ), + cv.one_of(CONF_DISABLED, lower=True), + ) + # RPI model does not use an init sequence, indicates with empty list + if model.initsequence is None: + # Custom model requires an init sequence + iseqconf = cv.Required(CONF_INIT_SEQUENCE) + uses_spi = True + else: + iseqconf = cv.Optional(CONF_INIT_SEQUENCE) + uses_spi = CONF_INIT_SEQUENCE in config or len(model.initsequence) != 0 + # Dimensions are optional if the model has a default width and the x-y transform is not overridden + transform_config = config.get(CONF_TRANSFORM, {}) + is_swapped = ( + isinstance(transform_config, dict) + and transform_config.get(CONF_SWAP_XY, False) is True + ) + cv_dimensions = ( + cv.Optional if model.get_default(CONF_WIDTH) and not is_swapped else cv.Required + ) + + pixel_modes = (PIXEL_MODE_16BIT, PIXEL_MODE_18BIT, "16", "18") + schema = display.FULL_DISPLAY_SCHEMA.extend( + { + model.option(CONF_RESET_PIN, cv.UNDEFINED): pins.gpio_output_pin_schema, + cv.GenerateID(): cv.declare_id(mipi_rgb_spi if uses_spi else mipi_rgb), + cv_dimensions(CONF_DIMENSIONS): dimension_schema( + model.get_default(CONF_DRAW_ROUNDING, 1) + ), + model.option(CONF_ENABLE_PIN, cv.UNDEFINED): cv.ensure_list( + pins.gpio_output_pin_schema + ), + model.option(CONF_COLOR_ORDER, MODE_BGR): cv.enum(COLOR_ORDERS, upper=True), + model.option(CONF_DRAW_ROUNDING, 2): power_of_two, + model.option(CONF_PIXEL_MODE, PIXEL_MODE_16BIT): cv.one_of( + *pixel_modes, lower=True + ), + cv.Optional(CONF_TRANSFORM): transform, + cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True), + model.option(CONF_INVERT_COLORS, False): cv.boolean, + model.option(CONF_USE_AXIS_FLIPS, True): cv.boolean, + model.option(CONF_PCLK_FREQUENCY, "40MHz"): cv.All( + cv.frequency, cv.Range(min=4e6, max=100e6) + ), + model.option(CONF_PCLK_INVERTED, True): cv.boolean, + iseqconf: cv.ensure_list(map_sequence), + model.option(CONF_BYTE_ORDER, BYTE_ORDER_BIG): cv.one_of( + BYTE_ORDER_LITTLE, BYTE_ORDER_BIG, lower=True + ), + model.option(CONF_HSYNC_PULSE_WIDTH): cv.int_, + model.option(CONF_HSYNC_BACK_PORCH): cv.int_, + model.option(CONF_HSYNC_FRONT_PORCH): cv.int_, + model.option(CONF_VSYNC_PULSE_WIDTH): cv.int_, + model.option(CONF_VSYNC_BACK_PORCH): cv.int_, + model.option(CONF_VSYNC_FRONT_PORCH): cv.int_, + model.option(CONF_DATA_PINS): cv.Any( + data_pin_set(16), + cv.Schema( + { + cv.Required(CONF_RED): data_pin_set(5), + cv.Required(CONF_GREEN): data_pin_set(6), + cv.Required(CONF_BLUE): data_pin_set(5), + } + ), + ), + model.option( + CONF_DE_PIN, cv.UNDEFINED + ): pins.internal_gpio_output_pin_schema, + model.option(CONF_PCLK_PIN): pins.internal_gpio_output_pin_schema, + model.option(CONF_HSYNC_PIN): pins.internal_gpio_output_pin_schema, + model.option(CONF_VSYNC_PIN): pins.internal_gpio_output_pin_schema, + model.option(CONF_RESET_PIN, cv.UNDEFINED): pins.gpio_output_pin_schema, + } + ) + if uses_spi: + schema = schema.extend( + { + cv.GenerateID(CONF_SPI_ID): cv.use_id(SPIComponent), + model.option(CONF_DC_PIN, cv.UNDEFINED): pins.gpio_output_pin_schema, + model.option(CONF_DATA_RATE, "1MHz"): SPI_DATA_RATE_SCHEMA, + model.option(CONF_SPI_MODE, "MODE0"): cv.enum( + SPI_MODE_OPTIONS, upper=True + ), + model.option(CONF_CS_PIN, cv.UNDEFINED): pins.gpio_output_pin_schema, + } + ) + return schema + + +def _config_schema(config): + config = cv.Schema( + { + cv.Required(CONF_MODEL): cv.one_of(*MODELS, upper=True), + }, + extra=cv.ALLOW_EXTRA, + )(config) + schema = model_schema(config) + return cv.All( + schema, + only_on_variant(supported=[const.VARIANT_ESP32S3]), + cv.only_with_esp_idf, + )(config) + + +CONFIG_SCHEMA = _config_schema + + +def _final_validate(config): + global_config = full_config.get() + + from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN + + if not requires_buffer(config) and LVGL_DOMAIN not in global_config: + # If no drawing methods are configured, and LVGL is not enabled, show a test card + config[CONF_SHOW_TEST_CARD] = True + if CONF_SPI_ID in config: + config = spi.final_validate_device_schema( + "mipi_rgb", require_miso=False, require_mosi=True + )(config) + return config + + +FINAL_VALIDATE_SCHEMA = _final_validate + + +async def to_code(config): + model = MODELS[config[CONF_MODEL].upper()] + width, height, _offset_width, _offset_height = model.get_dimensions(config) + var = cg.new_Pvariable(config[CONF_ID], width, height) + cg.add(var.set_model(model.name)) + if enable_pin := config.get(CONF_ENABLE_PIN): + enable = [await cg.gpio_pin_expression(pin) for pin in enable_pin] + cg.add(var.set_enable_pins(enable)) + + if CONF_SPI_ID in config: + await spi.register_spi_device(var, config) + sequence, madctl = model.get_sequence(config) + cg.add(var.set_init_sequence(sequence)) + cg.add(var.set_madctl(madctl)) + + cg.add(var.set_color_mode(COLOR_ORDERS[config[CONF_COLOR_ORDER]])) + cg.add(var.set_invert_colors(config[CONF_INVERT_COLORS])) + cg.add(var.set_hsync_pulse_width(config[CONF_HSYNC_PULSE_WIDTH])) + cg.add(var.set_hsync_back_porch(config[CONF_HSYNC_BACK_PORCH])) + cg.add(var.set_hsync_front_porch(config[CONF_HSYNC_FRONT_PORCH])) + cg.add(var.set_vsync_pulse_width(config[CONF_VSYNC_PULSE_WIDTH])) + cg.add(var.set_vsync_back_porch(config[CONF_VSYNC_BACK_PORCH])) + cg.add(var.set_vsync_front_porch(config[CONF_VSYNC_FRONT_PORCH])) + cg.add(var.set_pclk_inverted(config[CONF_PCLK_INVERTED])) + cg.add(var.set_pclk_frequency(config[CONF_PCLK_FREQUENCY])) + dpins = [] + if CONF_RED in config[CONF_DATA_PINS]: + red_pins = config[CONF_DATA_PINS][CONF_RED] + green_pins = config[CONF_DATA_PINS][CONF_GREEN] + blue_pins = config[CONF_DATA_PINS][CONF_BLUE] + if config[CONF_COLOR_ORDER] == "BGR": + dpins.extend(red_pins) + dpins.extend(green_pins) + dpins.extend(blue_pins) + else: + dpins.extend(blue_pins) + dpins.extend(green_pins) + dpins.extend(red_pins) + # swap bytes to match big-endian format + dpins = dpins[8:16] + dpins[0:8] + else: + dpins = config[CONF_DATA_PINS] + for index, pin in enumerate(dpins): + data_pin = await cg.gpio_pin_expression(pin) + cg.add(var.add_data_pin(data_pin, index)) + + if dc_pin := config.get(CONF_DC_PIN): + dc = await cg.gpio_pin_expression(dc_pin) + cg.add(var.set_dc_pin(dc)) + + if reset_pin := config.get(CONF_RESET_PIN): + reset = await cg.gpio_pin_expression(reset_pin) + cg.add(var.set_reset_pin(reset)) + + if model.rotation_as_transform(config): + config[CONF_ROTATION] = 0 + + if de_pin := config.get(CONF_DE_PIN): + pin = await cg.gpio_pin_expression(de_pin) + cg.add(var.set_de_pin(pin)) + pin = await cg.gpio_pin_expression(config[CONF_PCLK_PIN]) + cg.add(var.set_pclk_pin(pin)) + pin = await cg.gpio_pin_expression(config[CONF_HSYNC_PIN]) + cg.add(var.set_hsync_pin(pin)) + pin = await cg.gpio_pin_expression(config[CONF_VSYNC_PIN]) + cg.add(var.set_vsync_pin(pin)) + + await display.register_display(var, config) + if lamb := config.get(CONF_LAMBDA): + lambda_ = await cg.process_lambda( + lamb, [(display.DisplayRef, "it")], return_type=cg.void + ) + cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/mipi_rgb/mipi_rgb.cpp b/esphome/components/mipi_rgb/mipi_rgb.cpp new file mode 100644 index 0000000000..74eedae4f4 --- /dev/null +++ b/esphome/components/mipi_rgb/mipi_rgb.cpp @@ -0,0 +1,389 @@ +#ifdef USE_ESP32_VARIANT_ESP32S3 +#include "mipi_rgb.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" +#include "esp_lcd_panel_rgb.h" + +namespace esphome { +namespace mipi_rgb { + +static const uint8_t DELAY_FLAG = 0xFF; +static constexpr uint8_t MADCTL_MY = 0x80; // Bit 7 Bottom to top +static constexpr uint8_t MADCTL_MX = 0x40; // Bit 6 Right to left +static constexpr uint8_t MADCTL_MV = 0x20; // Bit 5 Swap axes +static constexpr uint8_t MADCTL_ML = 0x10; // Bit 4 Refresh bottom to top +static constexpr uint8_t MADCTL_BGR = 0x08; // Bit 3 Blue-Green-Red pixel order +static constexpr uint8_t MADCTL_XFLIP = 0x02; // Mirror the display horizontally +static constexpr uint8_t MADCTL_YFLIP = 0x01; // Mirror the display vertically + +void MipiRgb::setup_enables_() { + if (!this->enable_pins_.empty()) { + for (auto *pin : this->enable_pins_) { + pin->setup(); + pin->digital_write(true); + } + delay(10); + } + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + this->reset_pin_->digital_write(true); + delay(5); + this->reset_pin_->digital_write(false); + delay(5); + this->reset_pin_->digital_write(true); + } +} + +#ifdef USE_SPI +void MipiRgbSpi::setup() { + this->setup_enables_(); + this->spi_setup(); + this->write_init_sequence_(); + this->common_setup_(); +} +void MipiRgbSpi::write_command_(uint8_t value) { + this->enable(); + if (this->dc_pin_ == nullptr) { + this->write(value, 9); + } else { + this->dc_pin_->digital_write(false); + this->write_byte(value); + this->dc_pin_->digital_write(true); + } + this->disable(); +} + +void MipiRgbSpi::write_data_(uint8_t value) { + this->enable(); + if (this->dc_pin_ == nullptr) { + this->write(value | 0x100, 9); + } else { + this->dc_pin_->digital_write(true); + this->write_byte(value); + } + this->disable(); +} + +/** + * this relies upon the init sequence being well-formed, which is guaranteed by the Python init code. + */ + +void MipiRgbSpi::write_init_sequence_() { + size_t index = 0; + auto &vec = this->init_sequence_; + while (index != vec.size()) { + if (vec.size() - index < 2) { + this->mark_failed(LOG_STR("Malformed init sequence")); + return; + } + uint8_t cmd = vec[index++]; + uint8_t x = vec[index++]; + if (x == DELAY_FLAG) { + ESP_LOGD(TAG, "Delay %dms", cmd); + delay(cmd); + } else { + uint8_t num_args = x & 0x7F; + if (vec.size() - index < num_args) { + this->mark_failed(LOG_STR("Malformed init sequence")); + return; + } + if (cmd == SLEEP_OUT) { + delay(120); // NOLINT + } + const auto *ptr = vec.data() + index; + ESP_LOGD(TAG, "Write command %02X, length %d, byte(s) %s", cmd, num_args, + format_hex_pretty(ptr, num_args, '.', false).c_str()); + index += num_args; + this->write_command_(cmd); + while (num_args-- != 0) + this->write_data_(*ptr++); + if (cmd == SLEEP_OUT) + delay(10); + } + } + // this->spi_teardown(); // SPI not needed after this + this->init_sequence_.clear(); + delay(10); +} + +void MipiRgbSpi::dump_config() { + MipiRgb::dump_config(); + LOG_PIN(" CS Pin: ", this->cs_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + ESP_LOGCONFIG(TAG, + " SPI Data rate: %uMHz" + "\n Mirror X: %s" + "\n Mirror Y: %s" + "\n Swap X/Y: %s" + "\n Color Order: %s", + (unsigned) (this->data_rate_ / 1000000), YESNO(this->madctl_ & (MADCTL_XFLIP | MADCTL_MX)), + YESNO(this->madctl_ & (MADCTL_YFLIP | MADCTL_MY | MADCTL_ML)), YESNO(this->madctl_ & MADCTL_MV), + this->madctl_ & MADCTL_BGR ? "BGR" : "RGB"); +} + +#endif // USE_SPI + +void MipiRgb::setup() { + this->setup_enables_(); + this->common_setup_(); +} + +void MipiRgb::common_setup_() { + esp_lcd_rgb_panel_config_t config{}; + config.flags.fb_in_psram = 1; + config.bounce_buffer_size_px = this->width_ * 10; + config.num_fbs = 1; + config.timings.h_res = this->width_; + config.timings.v_res = this->height_; + config.timings.hsync_pulse_width = this->hsync_pulse_width_; + config.timings.hsync_back_porch = this->hsync_back_porch_; + config.timings.hsync_front_porch = this->hsync_front_porch_; + config.timings.vsync_pulse_width = this->vsync_pulse_width_; + config.timings.vsync_back_porch = this->vsync_back_porch_; + config.timings.vsync_front_porch = this->vsync_front_porch_; + config.timings.flags.pclk_active_neg = this->pclk_inverted_; + config.timings.pclk_hz = this->pclk_frequency_; + config.clk_src = LCD_CLK_SRC_PLL160M; + size_t data_pin_count = sizeof(this->data_pins_) / sizeof(this->data_pins_[0]); + for (size_t i = 0; i != data_pin_count; i++) { + config.data_gpio_nums[i] = this->data_pins_[i]->get_pin(); + } + config.data_width = data_pin_count; + config.disp_gpio_num = -1; + config.hsync_gpio_num = this->hsync_pin_->get_pin(); + config.vsync_gpio_num = this->vsync_pin_->get_pin(); + if (this->de_pin_) { + config.de_gpio_num = this->de_pin_->get_pin(); + } else { + config.de_gpio_num = -1; + } + config.pclk_gpio_num = this->pclk_pin_->get_pin(); + esp_err_t err = esp_lcd_new_rgb_panel(&config, &this->handle_); + if (err == ESP_OK) + err = esp_lcd_panel_reset(this->handle_); + if (err == ESP_OK) + err = esp_lcd_panel_init(this->handle_); + if (err != ESP_OK) { + ESP_LOGE(TAG, "lcd setup failed: %s", esp_err_to_name(err)); + this->mark_failed(LOG_STR("lcd setup failed")); + } + ESP_LOGCONFIG(TAG, "MipiRgb setup complete"); +} + +void MipiRgb::loop() { + if (this->handle_ != nullptr) + esp_lcd_rgb_panel_restart(this->handle_); +} + +void MipiRgb::update() { + if (this->is_failed()) + return; + if (this->auto_clear_enabled_) { + this->clear(); + } + if (this->show_test_card_) { + this->test_card(); + } else if (this->page_ != nullptr) { + this->page_->get_writer()(*this); + } else if (this->writer_.has_value()) { + (*this->writer_)(*this); + } else { + this->stop_poller(); + } + if (this->buffer_ == nullptr || this->x_low_ > this->x_high_ || this->y_low_ > this->y_high_) + return; + ESP_LOGV(TAG, "x_low %d, y_low %d, x_high %d, y_high %d", this->x_low_, this->y_low_, this->x_high_, this->y_high_); + int w = this->x_high_ - this->x_low_ + 1; + int h = this->y_high_ - this->y_low_ + 1; + this->write_to_display_(this->x_low_, this->y_low_, w, h, reinterpret_cast(this->buffer_), + this->x_low_, this->y_low_, this->width_ - w - this->x_low_); + // invalidate watermarks + this->x_low_ = this->width_; + this->y_low_ = this->height_; + this->x_high_ = 0; + this->y_high_ = 0; +} + +void MipiRgb::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order, + display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) { + if (w <= 0 || h <= 0 || this->is_failed()) + return; + // if color mapping is required, pass the buck. + // note that endianness is not considered here - it is assumed to match! + if (bitness != display::COLOR_BITNESS_565) { + Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad); + this->write_to_display_(x_start, y_start, w, h, reinterpret_cast(this->buffer_), x_start, y_start, + this->width_ - w - x_start); + } else { + this->write_to_display_(x_start, y_start, w, h, ptr, x_offset, y_offset, x_pad); + } +} + +void MipiRgb::write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset, int y_offset, + int x_pad) { + esp_err_t err = ESP_OK; + auto stride = (x_offset + w + x_pad) * 2; + ptr += y_offset * stride + x_offset * 2; // skip to the first pixel + // x_ and y_offset are offsets into the source buffer, unrelated to our own offsets into the display. + if (x_offset == 0 && x_pad == 0) { + err = esp_lcd_panel_draw_bitmap(this->handle_, x_start, y_start, x_start + w, y_start + h, ptr); + } else { + // draw line by line + for (int y = 0; y != h; y++) { + err = esp_lcd_panel_draw_bitmap(this->handle_, x_start, y + y_start, x_start + w, y + y_start + 1, ptr); + if (err != ESP_OK) + break; + ptr += stride; // next line + } + } + if (err != ESP_OK) + ESP_LOGE(TAG, "lcd_lcd_panel_draw_bitmap failed: %s", esp_err_to_name(err)); +} + +bool MipiRgb::check_buffer_() { + if (this->is_failed()) + return false; + if (this->buffer_ != nullptr) + return true; + // this is dependent on the enum values. + RAMAllocator allocator; + this->buffer_ = allocator.allocate(this->height_ * this->width_); + if (this->buffer_ == nullptr) { + this->mark_failed(LOG_STR("Could not allocate buffer for display!")); + return false; + } + return true; +} + +void MipiRgb::draw_pixel_at(int x, int y, Color color) { + if (!this->get_clipping().inside(x, y) || this->is_failed()) + return; + + switch (this->rotation_) { + case display::DISPLAY_ROTATION_0_DEGREES: + break; + case display::DISPLAY_ROTATION_90_DEGREES: + std::swap(x, y); + x = this->width_ - x - 1; + break; + case display::DISPLAY_ROTATION_180_DEGREES: + x = this->width_ - x - 1; + y = this->height_ - y - 1; + break; + case display::DISPLAY_ROTATION_270_DEGREES: + std::swap(x, y); + y = this->height_ - y - 1; + break; + } + if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) { + return; + } + if (!this->check_buffer_()) + return; + size_t pos = (y * this->width_) + x; + uint8_t hi_byte = static_cast(color.r & 0xF8) | (color.g >> 5); + uint8_t lo_byte = static_cast((color.g & 0x1C) << 3) | (color.b >> 3); + uint16_t new_color = hi_byte | (lo_byte << 8); // big endian + if (this->buffer_[pos] == new_color) + return; + this->buffer_[pos] = new_color; + // low and high watermark may speed up drawing from buffer + if (x < this->x_low_) + this->x_low_ = x; + if (y < this->y_low_) + this->y_low_ = y; + if (x > this->x_high_) + this->x_high_ = x; + if (y > this->y_high_) + this->y_high_ = y; +} +void MipiRgb::fill(Color color) { + if (!this->check_buffer_()) + return; + auto *ptr_16 = reinterpret_cast(this->buffer_); + uint8_t hi_byte = static_cast(color.r & 0xF8) | (color.g >> 5); + uint8_t lo_byte = static_cast((color.g & 0x1C) << 3) | (color.b >> 3); + uint16_t new_color = lo_byte | (hi_byte << 8); // little endian + std::fill_n(ptr_16, this->width_ * this->height_, new_color); +} + +int MipiRgb::get_width() { + switch (this->rotation_) { + case display::DISPLAY_ROTATION_90_DEGREES: + case display::DISPLAY_ROTATION_270_DEGREES: + return this->get_height_internal(); + case display::DISPLAY_ROTATION_0_DEGREES: + case display::DISPLAY_ROTATION_180_DEGREES: + default: + return this->get_width_internal(); + } +} + +int MipiRgb::get_height() { + switch (this->rotation_) { + case display::DISPLAY_ROTATION_0_DEGREES: + case display::DISPLAY_ROTATION_180_DEGREES: + return this->get_height_internal(); + case display::DISPLAY_ROTATION_90_DEGREES: + case display::DISPLAY_ROTATION_270_DEGREES: + default: + return this->get_width_internal(); + } +} + +static std::string get_pin_name(GPIOPin *pin) { + if (pin == nullptr) + return "None"; + return pin->dump_summary(); +} + +void MipiRgb::dump_pins_(uint8_t start, uint8_t end, const char *name, uint8_t offset) { + for (uint8_t i = start; i != end; i++) { + ESP_LOGCONFIG(TAG, " %s pin %d: %s", name, offset++, this->data_pins_[i]->dump_summary().c_str()); + } +} + +void MipiRgb::dump_config() { + ESP_LOGCONFIG(TAG, + "MIPI_RGB LCD" + "\n Model: %s" + "\n Width: %u" + "\n Height: %u" + "\n Rotation: %d degrees" + "\n PCLK Inverted: %s" + "\n HSync Pulse Width: %u" + "\n HSync Back Porch: %u" + "\n HSync Front Porch: %u" + "\n VSync Pulse Width: %u" + "\n VSync Back Porch: %u" + "\n VSync Front Porch: %u" + "\n Invert Colors: %s" + "\n Pixel Clock: %uMHz" + "\n Reset Pin: %s" + "\n DE Pin: %s" + "\n PCLK Pin: %s" + "\n HSYNC Pin: %s" + "\n VSYNC Pin: %s", + this->model_, this->width_, this->height_, this->rotation_, YESNO(this->pclk_inverted_), + this->hsync_pulse_width_, this->hsync_back_porch_, this->hsync_front_porch_, this->vsync_pulse_width_, + this->vsync_back_porch_, this->vsync_front_porch_, YESNO(this->invert_colors_), + (unsigned) (this->pclk_frequency_ / 1000000), get_pin_name(this->reset_pin_).c_str(), + get_pin_name(this->de_pin_).c_str(), get_pin_name(this->pclk_pin_).c_str(), + get_pin_name(this->hsync_pin_).c_str(), get_pin_name(this->vsync_pin_).c_str()); + + if (this->madctl_ & MADCTL_BGR) { + this->dump_pins_(8, 13, "Blue", 0); + this->dump_pins_(13, 16, "Green", 0); + this->dump_pins_(0, 3, "Green", 3); + this->dump_pins_(3, 8, "Red", 0); + } else { + this->dump_pins_(8, 13, "Red", 0); + this->dump_pins_(13, 16, "Green", 0); + this->dump_pins_(0, 3, "Green", 3); + this->dump_pins_(3, 8, "Blue", 0); + } +} + +} // namespace mipi_rgb +} // namespace esphome +#endif // USE_ESP32_VARIANT_ESP32S3 diff --git a/esphome/components/mipi_rgb/mipi_rgb.h b/esphome/components/mipi_rgb/mipi_rgb.h new file mode 100644 index 0000000000..173e23752d --- /dev/null +++ b/esphome/components/mipi_rgb/mipi_rgb.h @@ -0,0 +1,127 @@ +#pragma once + +#ifdef USE_ESP32_VARIANT_ESP32S3 +#include "esphome/core/gpio.h" +#include "esphome/components/display/display.h" +#include "esp_lcd_panel_ops.h" +#ifdef USE_SPI +#include "esphome/components/spi/spi.h" +#endif + +namespace esphome { +namespace mipi_rgb { + +constexpr static const char *const TAG = "display.mipi_rgb"; +const uint8_t SW_RESET_CMD = 0x01; +const uint8_t SLEEP_OUT = 0x11; +const uint8_t SDIR_CMD = 0xC7; +const uint8_t MADCTL_CMD = 0x36; +const uint8_t INVERT_OFF = 0x20; +const uint8_t INVERT_ON = 0x21; +const uint8_t DISPLAY_ON = 0x29; +const uint8_t CMD2_BKSEL = 0xFF; +const uint8_t CMD2_BK0[5] = {0x77, 0x01, 0x00, 0x00, 0x10}; + +class MipiRgb : public display::Display { + public: + MipiRgb(int width, int height) : width_(width), height_(height) {} + void setup() override; + void loop() override; + void update() override; + void fill(Color color); + void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order, + display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override; + void write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset, int y_offset, + int x_pad); + bool check_buffer_(); + + display::ColorOrder get_color_mode() { return this->color_mode_; } + void set_color_mode(display::ColorOrder color_mode) { this->color_mode_ = color_mode; } + void set_invert_colors(bool invert_colors) { this->invert_colors_ = invert_colors; } + void set_madctl(uint8_t madctl) { this->madctl_ = madctl; } + + void add_data_pin(InternalGPIOPin *data_pin, size_t index) { this->data_pins_[index] = data_pin; }; + void set_de_pin(InternalGPIOPin *de_pin) { this->de_pin_ = de_pin; } + void set_pclk_pin(InternalGPIOPin *pclk_pin) { this->pclk_pin_ = pclk_pin; } + void set_vsync_pin(InternalGPIOPin *vsync_pin) { this->vsync_pin_ = vsync_pin; } + void set_hsync_pin(InternalGPIOPin *hsync_pin) { this->hsync_pin_ = hsync_pin; } + void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } + void set_width(uint16_t width) { this->width_ = width; } + void set_pclk_frequency(uint32_t pclk_frequency) { this->pclk_frequency_ = pclk_frequency; } + void set_pclk_inverted(bool inverted) { this->pclk_inverted_ = inverted; } + void set_model(const char *model) { this->model_ = model; } + int get_width() override; + int get_height() override; + void set_hsync_back_porch(uint16_t hsync_back_porch) { this->hsync_back_porch_ = hsync_back_porch; } + void set_hsync_front_porch(uint16_t hsync_front_porch) { this->hsync_front_porch_ = hsync_front_porch; } + void set_hsync_pulse_width(uint16_t hsync_pulse_width) { this->hsync_pulse_width_ = hsync_pulse_width; } + void set_vsync_pulse_width(uint16_t vsync_pulse_width) { this->vsync_pulse_width_ = vsync_pulse_width; } + void set_vsync_back_porch(uint16_t vsync_back_porch) { this->vsync_back_porch_ = vsync_back_porch; } + void set_vsync_front_porch(uint16_t vsync_front_porch) { this->vsync_front_porch_ = vsync_front_porch; } + void set_enable_pins(std::vector enable_pins) { this->enable_pins_ = std::move(enable_pins); } + display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; } + int get_width_internal() override { return this->width_; } + int get_height_internal() override { return this->height_; } + void dump_pins_(uint8_t start, uint8_t end, const char *name, uint8_t offset); + void dump_config() override; + void draw_pixel_at(int x, int y, Color color) override; + + // this will be horribly slow. + protected: + void setup_enables_(); + void common_setup_(); + InternalGPIOPin *de_pin_{nullptr}; + InternalGPIOPin *pclk_pin_{nullptr}; + InternalGPIOPin *hsync_pin_{nullptr}; + InternalGPIOPin *vsync_pin_{nullptr}; + GPIOPin *reset_pin_{nullptr}; + InternalGPIOPin *data_pins_[16] = {}; + uint16_t hsync_pulse_width_ = 10; + uint16_t hsync_back_porch_ = 10; + uint16_t hsync_front_porch_ = 20; + uint16_t vsync_pulse_width_ = 10; + uint16_t vsync_back_porch_ = 10; + uint16_t vsync_front_porch_ = 10; + uint32_t pclk_frequency_ = 16 * 1000 * 1000; + bool pclk_inverted_{true}; + uint8_t madctl_{}; + const char *model_{"Unknown"}; + bool invert_colors_{}; + display::ColorOrder color_mode_{display::COLOR_ORDER_BGR}; + size_t width_; + size_t height_; + uint16_t *buffer_{nullptr}; + std::vector enable_pins_{}; + uint16_t x_low_{1}; + uint16_t y_low_{1}; + uint16_t x_high_{0}; + uint16_t y_high_{0}; + + esp_lcd_panel_handle_t handle_{}; +}; + +#ifdef USE_SPI +class MipiRgbSpi : public MipiRgb, + public spi::SPIDevice { + public: + MipiRgbSpi(int width, int height) : MipiRgb(width, height) {} + + void set_init_sequence(const std::vector &init_sequence) { this->init_sequence_ = init_sequence; } + void set_dc_pin(GPIOPin *dc_pin) { this->dc_pin_ = dc_pin; } + void setup() override; + + protected: + void write_command_(uint8_t value); + void write_data_(uint8_t value); + void write_init_sequence_(); + void dump_config(); + + GPIOPin *dc_pin_{nullptr}; + std::vector init_sequence_; +}; +#endif + +} // namespace mipi_rgb +} // namespace esphome +#endif diff --git a/esphome/components/mipi_rgb/models/guition.py b/esphome/components/mipi_rgb/models/guition.py new file mode 100644 index 0000000000..915b8beda0 --- /dev/null +++ b/esphome/components/mipi_rgb/models/guition.py @@ -0,0 +1,25 @@ +from .st7701s import st7701s + +st7701s.extend( + "GUITION-4848S040", + width=480, + height=480, + data_rate="2MHz", + cs_pin=39, + de_pin=18, + hsync_pin=16, + vsync_pin=17, + pclk_pin=21, + pclk_frequency="12MHz", + pclk_inverted=False, + pixel_mode="18bit", + mirror_x=True, + mirror_y=True, + data_pins={ + "red": [11, 12, 13, 14, 0], + "green": [8, 20, 3, 46, 9, 10], + "blue": [4, 5, 6, 7, 15], + }, + # Additional configuration for Guition 4848S040, 16 bit bus config + add_init_sequence=((0xCD, 0x00),), +) diff --git a/esphome/components/mipi_rgb/models/lilygo.py b/esphome/components/mipi_rgb/models/lilygo.py index e69de29bb2..109dc42af6 100644 --- a/esphome/components/mipi_rgb/models/lilygo.py +++ b/esphome/components/mipi_rgb/models/lilygo.py @@ -0,0 +1,228 @@ +from esphome.config_validation import UNDEFINED + +from .st7701s import ST7701S + +# fmt: off +ST7701S( + "T-PANEL-S3", + width=480, + height=480, + color_order="BGR", + invert_colors=False, + swap_xy=UNDEFINED, + spi_mode="MODE3", + cs_pin={"xl9535": None, "number": 17}, + reset_pin={"xl9535": None, "number": 5}, + hsync_pin=39, + vsync_pin=40, + pclk_pin=41, + data_pins={ + "red": [12, 13, 42, 46, 45], + "green": [6, 7, 8, 9, 10, 11], + "blue": [1, 2, 3, 4, 5], + }, + hsync_front_porch=20, + hsync_back_porch=0, + hsync_pulse_width=2, + vsync_front_porch=30, + vsync_back_porch=1, + vsync_pulse_width=8, + pclk_frequency="6MHz", + pclk_inverted=False, + initsequence=( + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x13), (0xEF, 0x08), (0xFF, 0x77, 0x01, 0x00, 0x00, 0x10), + (0xC0, 0x3B, 0x00), (0xC1, 0x0B, 0x02), (0xC2, 0x30, 0x02, 0x37), (0xCC, 0x10), + (0xB0, 0x00, 0x0F, 0x16, 0x0E, 0x11, 0x07, 0x09, 0x09, 0x08, 0x23, 0x05, 0x11, 0x0F, 0x28, 0x2D, 0x18), + (0xB1, 0x00, 0x0F, 0x16, 0x0E, 0x11, 0x07, 0x09, 0x08, 0x09, 0x23, 0x05, 0x11, 0x0F, 0x28, 0x2D, 0x18), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x11), + (0xB0, 0x4D), (0xB1, 0x33), (0xB2, 0x87), (0xB5, 0x4B), (0xB7, 0x8C), (0xB8, 0x20), (0xC1, 0x78), + (0xC2, 0x78), (0xD0, 0x88), (0xE0, 0x00, 0x00, 0x02), + (0xE1, 0x02, 0xF0, 0x00, 0x00, 0x03, 0xF0, 0x00, 0x00, 0x00, 0x44, 0x44), + (0xE2, 0x10, 0x10, 0x40, 0x40, 0xF2, 0xF0, 0x00, 0x00, 0xF2, 0xF0, 0x00, 0x00), + (0xE3, 0x00, 0x00, 0x11, 0x11), (0xE4, 0x44, 0x44), + (0xE5, 0x07, 0xEF, 0xF0, 0xF0, 0x09, 0xF1, 0xF0, 0xF0, 0x03, 0xF3, 0xF0, 0xF0, 0x05, 0xED, 0xF0, 0xF0), + (0xE6, 0x00, 0x00, 0x11, 0x11), (0xE7, 0x44, 0x44), + (0xE8, 0x08, 0xF0, 0xF0, 0xF0, 0x0A, 0xF2, 0xF0, 0xF0, 0x04, 0xF4, 0xF0, 0xF0, 0x06, 0xEE, 0xF0, 0xF0), + (0xEB, 0x00, 0x00, 0xE4, 0xE4, 0x44, 0x88, 0x40), + (0xEC, 0x78, 0x00), + (0xED, 0x20, 0xF9, 0x87, 0x76, 0x65, 0x54, 0x4F, 0xFF, 0xFF, 0xF4, 0x45, 0x56, 0x67, 0x78, 0x9F, 0x02), + (0xEF, 0x10, 0x0D, 0x04, 0x08, 0x3F, 0x1F), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x10), + ), +) + + +t_rgb = ST7701S( + "T-RGB-2.1", + width=480, + height=480, + color_order="BGR", + pixel_mode="18bit", + invert_colors=False, + swap_xy=UNDEFINED, + spi_mode="MODE3", + cs_pin={"xl9535": None, "number": 3}, + de_pin=45, + hsync_pin=47, + vsync_pin=41, + pclk_pin=42, + data_pins={ + "red": [7, 6, 5, 3, 2], + "green": [14, 13, 12, 11, 10, 9], + "blue": [21, 18, 17, 16, 15], + }, + hsync_front_porch=50, + hsync_pulse_width=1, + hsync_back_porch=30, + vsync_front_porch=20, + vsync_pulse_width=1, + vsync_back_porch=30, + pclk_frequency="12MHz", + pclk_inverted=False, + initsequence=( + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x10), + + (0xC0, 0x3B, 0x00), + (0xC1, 0x0B, 0x02), + (0xC2, 0x07, 0x02), + (0xCC, 0x10), + (0xCD, 0x08), + + (0xB0, + 0x00, 0x11, 0x16, 0x0e, + 0x11, 0x06, 0x05, 0x09, + 0x08, 0x21, 0x06, 0x13, + 0x10, 0x29, 0x31, 0x18), + + (0xB1, + 0x00, 0x11, 0x16, 0x0e, + 0x11, 0x07, 0x05, 0x09, + 0x09, 0x21, 0x05, 0x13, + 0x11, 0x2a, 0x31, 0x18), + + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x11), + + (0xB0, 0x6D), + (0xB1, 0x37), + (0xB2, 0x81), + (0xB3, 0x80), + (0xB5, 0x43), + (0xB7, 0x85), + (0xB8, 0x20), + + (0xC1, 0x78), + (0xC2, 0x78), + (0xC3, 0x8C), + + (0xD0, 0x88), + + (0xE0, 0x00, 0x00, 0x02), + (0xE1, + 0x03, 0xA0, 0x00, 0x00, + 0x04, 0xA0, 0x00, 0x00, + 0x00, 0x20, 0x20), + + (0xE2, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00), + + (0xE3, 0x00, 0x00, 0x11, 0x00), + (0xE4, 0x22, 0x00), + + (0xE5, + 0x05, 0xEC, 0xA0, 0xA0, + 0x07, 0xEE, 0xA0, 0xA0, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00), + + (0xE6, 0x00, 0x00, 0x11, 0x00), + (0xE7, 0x22, 0x00), + + (0xE8, + 0x06, 0xED, 0xA0, 0xA0, + 0x08, 0xEF, 0xA0, 0xA0, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00), + + (0xEB, 0x00, 0x00, 0x40, 0x40, 0x00, 0x00, 0x10), + + (0xED, + 0xFF, 0xFF, 0xFF, 0xBA, + 0x0A, 0xBF, 0x45, 0xFF, + 0xFF, 0x54, 0xFB, 0xA0, + 0xAB, 0xFF, 0xFF, 0xFF), + + (0xEF, 0x10, 0x0D, 0x04, 0x08, 0x3F, 0x1F), + + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x13), + (0xEF, 0x08), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x10) + ) + +) +t_rgb.extend( + "T-RGB-2.8", + initsequence=( + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x13), + (0xEF, 0x08), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x10), + (0xC0, 0x3B, 0x00), + (0xC1, 0x10, 0x0C), + (0xC2, 0x07, 0x0A), + (0xC7, 0x00), + (0xC7, 0x10), + (0xCD, 0x08), + (0xB0, + 0x05, 0x12, 0x98, 0x0e, 0x0F, + 0x07, 0x07, 0x09, 0x09, 0x23, + 0x05, 0x52, 0x0F, 0x67, 0x2C, 0x11), + (0xB1, + 0x0B, 0x11, 0x97, 0x0C, 0x12, + 0x06, 0x06, 0x08, 0x08, 0x22, + 0x03, 0x51, 0x11, 0x66, 0x2B, 0x0F), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x11), + (0xB0, 0x5D), + (0xB1, 0x2D), + (0xB2, 0x81), + (0xB3, 0x80), + (0xB5, 0x4E), + (0xB7, 0x85), + (0xB8, 0x20), + (0xC1, 0x78), + (0xC2, 0x78), + (0xD0, 0x88), + (0xE0, 0x00, 0x00, 0x02), + (0xE1, + 0x06, 0x30, 0x08, 0x30, 0x05, + 0x30, 0x07, 0x30, 0x00, 0x33, + 0x33), + (0xE2, + 0x11, 0x11, 0x33, 0x33, 0xf4, + 0x00, 0x00, 0x00, 0xf4, 0x00, + 0x00, 0x00), + (0xE3, 0x00, 0x00, 0x11, 0x11), + (0xE4, 0x44, 0x44), + (0xE5, + 0x0d, 0xf5, 0x30, 0xf0, 0x0f, + 0xf7, 0x30, 0xf0, 0x09, 0xf1, + 0x30, 0xf0, 0x0b, 0xf3, 0x30, 0xf0), + (0xE6, 0x00, 0x00, 0x11, 0x11), + (0xE7, 0x44, 0x44), + (0xE8, + 0x0c, 0xf4, 0x30, 0xf0, + 0x0e, 0xf6, 0x30, 0xf0, + 0x08, 0xf0, 0x30, 0xf0, + 0x0a, 0xf2, 0x30, 0xf0), + (0xe9, 0x36), + (0xEB, 0x00, 0x01, 0xe4, 0xe4, 0x44, 0x88, 0x40), + (0xED, + 0xff, 0x10, 0xaf, 0x76, + 0x54, 0x2b, 0xcf, 0xff, + 0xff, 0xfc, 0xb2, 0x45, + 0x67, 0xfa, 0x01, 0xff), + (0xEF, 0x08, 0x08, 0x08, 0x45, 0x3f, 0x54), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x10), + ) +) diff --git a/esphome/components/mipi_rgb/models/rpi.py b/esphome/components/mipi_rgb/models/rpi.py new file mode 100644 index 0000000000..076d96b658 --- /dev/null +++ b/esphome/components/mipi_rgb/models/rpi.py @@ -0,0 +1,9 @@ +from esphome.components.mipi import DriverChip +from esphome.config_validation import UNDEFINED + +# A driver chip for Raspberry Pi MIPI RGB displays. These require no init sequence +DriverChip( + "RPI", + swap_xy=UNDEFINED, + initsequence=(), +) diff --git a/esphome/components/mipi_rgb/models/st7701s.py b/esphome/components/mipi_rgb/models/st7701s.py new file mode 100644 index 0000000000..0b0a9548ca --- /dev/null +++ b/esphome/components/mipi_rgb/models/st7701s.py @@ -0,0 +1,216 @@ +from esphome.components.mipi import ( + MADCTL, + MADCTL_ML, + MADCTL_XFLIP, + MODE_BGR, + DriverChip, +) +from esphome.config_validation import UNDEFINED +from esphome.const import CONF_COLOR_ORDER, CONF_HEIGHT, CONF_MIRROR_X, CONF_MIRROR_Y + +SDIR_CMD = 0xC7 + + +class ST7701S(DriverChip): + # The ST7701s does not use the standard MADCTL bits for x/y mirroring + def add_madctl(self, sequence: list, config: dict): + transform = self.get_transform(config) + madctl = 0x00 + if config[CONF_COLOR_ORDER] == MODE_BGR: + madctl |= 0x08 + if transform.get(CONF_MIRROR_Y): + madctl |= MADCTL_ML + sequence.append((MADCTL, madctl)) + sdir = 0 + if transform.get(CONF_MIRROR_X): + sdir |= 0x04 + # XFLIP doesn't do anything in the ST7701S, + # it's set in the madctl byte just so it can be reported at runtime by logconfig + madctl |= MADCTL_XFLIP + sequence.append((SDIR_CMD, sdir)) + return madctl + + @property + def transforms(self) -> set[str]: + """ + The ST7701 never supports axis swapping, and mirroring the y-axis only works for full height. + """ + if self.get_default(CONF_HEIGHT) != 864: + return {CONF_MIRROR_X} + return {CONF_MIRROR_X, CONF_MIRROR_Y} + + +# fmt: off +st7701s = ST7701S( + "ST7701S", + width=480, + height=864, + swap_xy=UNDEFINED, + hsync_front_porch=20, + hsync_back_porch=10, + hsync_pulse_width=10, + vsync_front_porch=10, + vsync_back_porch=10, + vsync_pulse_width=10, + pclk_frequency="16MHz", + pclk_inverted=True, + initsequence=( + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x10), # Page 0 + (0xC0, 0x3B, 0x00), (0xC1, 0x0D, 0x02), (0xC2, 0x31, 0x05), + (0xB0, 0x00, 0x11, 0x18, 0x0E, 0x11, 0x06, 0x07, 0x08, 0x07, 0x22, 0x04, 0x12, 0x0F, 0xAA, 0x31, 0x18,), + (0xB1, 0x00, 0x11, 0x19, 0x0E, 0x12, 0x07, 0x08, 0x08, 0x08, 0x22, 0x04, 0x11, 0x11, 0xA9, 0x32, 0x18,), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x11), # page 1 + (0xB0, 0x60), (0xB1, 0x32), (0xB2, 0x07), (0xB3, 0x80), (0xB5, 0x49), (0xB7, 0x85), (0xB8, 0x21), (0xC1, 0x78), + (0xC2, 0x78), (0xE0, 0x00, 0x1B, 0x02), + (0xE1, 0x08, 0xA0, 0x00, 0x00, 0x07, 0xA0, 0x00, 0x00, 0x00, 0x44, 0x44), + (0xE2, 0x11, 0x11, 0x44, 0x44, 0xED, 0xA0, 0x00, 0x00, 0xEC, 0xA0, 0x00, 0x00), + (0xE3, 0x00, 0x00, 0x11, 0x11), + (0xE4, 0x44, 0x44), + (0xE5, 0x0A, 0xE9, 0xD8, 0xA0, 0x0C, 0xEB, 0xD8, 0xA0, 0x0E, 0xED, 0xD8, 0xA0, 0x10, 0xEF, 0xD8, 0xA0,), + (0xE6, 0x00, 0x00, 0x11, 0x11), (0xE7, 0x44, 0x44), + (0xE8, 0x09, 0xE8, 0xD8, 0xA0, 0x0B, 0xEA, 0xD8, 0xA0, 0x0D, 0xEC, 0xD8, 0xA0, 0x0F, 0xEE, 0xD8, 0xA0,), + (0xEB, 0x02, 0x00, 0xE4, 0xE4, 0x88, 0x00, 0x40), (0xEC, 0x3C, 0x00), + (0xED, 0xAB, 0x89, 0x76, 0x54, 0x02, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x20, 0x45, 0x67, 0x98, 0xBA,), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x13), # Page 3 + (0xE5, 0xE4), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x10), # Page 0 + (0xCD, 0x08), + ) +) + +st7701s.extend( + "MAKERFABS-4", + width=480, + height=480, + color_order="RGB", + invert_colors=True, + pixel_mode="18bit", + cs_pin=1, + de_pin={ + "number": 45, + "ignore_strapping_warning": True + }, + hsync_pin=5, + vsync_pin=4, + pclk_pin=21, + data_pins={ + "red": [39, 40, 41, 42, 2], + "green": [0, 9, 14, 47, 48, 3], + "blue": [6, 7, 15, 16, 8] + } +) + +st7701s.extend( + "SEEED-INDICATOR-D1", + width=480, + height=480, + mirror_x=True, + mirror_y=True, + invert_colors=True, + pixel_mode="18bit", + spi_mode="MODE3", + data_rate="2MHz", + hsync_front_porch=10, + hsync_pulse_width=8, + hsync_back_porch=50, + vsync_front_porch=10, + vsync_pulse_width=8, + vsync_back_porch=20, + cs_pin={"pca9554": None, "number": 4}, + de_pin=18, + hsync_pin=16, + vsync_pin=17, + pclk_pin=21, + pclk_inverted=False, + data_pins={ + "red": [4, 3, 2, 1, 0], + "green": [10, 9, 8, 7, 6, 5], + "blue": [15, 14, 13, 12, 11] + }, +) + +st7701s.extend( + "UEDX48480021-MD80ET", + width=480, + height=480, + pixel_mode="18bit", + cs_pin=18, + reset_pin=8, + de_pin=17, + vsync_pin={"number": 3, "ignore_strapping_warning": True}, + hsync_pin={"number": 46, "ignore_strapping_warning": True}, + pclk_pin=9, + data_pins={ + "red": [40, 41, 42, 2, 1], + "green": [21, 47, 48, 45, 38, 39], + "blue": [10, 11, {"number": 12, "allow_other_uses": True}, {"number": 13, "allow_other_uses": True}, 14] + }, + initsequence=( + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x13), (0xEF, 0x08), (0xFF, 0x77, 0x01, 0x00, 0x00, 0x10), + (0xC0, 0x3B, 0x00), (0xC1, 0x0B, 0x02), (0xC2, 0x07, 0x02), (0xC7, 0x00), (0xCC, 0x10), (0xCD, 0x08), + (0xB0, 0x00, 0x11, 0x16, 0x0E, 0x11, 0x06, 0x05, 0x09, 0x08, 0x21, 0x06, 0x13, 0x10, 0x29, 0x31, 0x18), + (0xB1, 0x00, 0x11, 0x16, 0x0E, 0x11, 0x07, 0x05, 0x09, 0x09, 0x21, 0x05, 0x13, 0x11, 0x2A, 0x31, 0x18), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x11), + (0xB0, 0x6D), (0xB1, 0x37), (0xB2, 0x8B), (0xB3, 0x80), (0xB5, 0x43), (0xB7, 0x85), + (0xB8, 0x20), (0xC0, 0x09), (0xC1, 0x78), (0xC2, 0x78), (0xD0, 0x88), + (0xE0, 0x00, 0x00, 0x02), + (0xE1, 0x03, 0xA0, 0x00, 0x00, 0x04, 0xA0, 0x00, 0x00, 0x00, 0x20, 0x20), + (0xE2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00), + (0xE3, 0x00, 0x00, 0x11, 0x00), + (0xE4, 0x22, 0x00), + (0xE5, 0x05, 0xEC, 0xF6, 0xCA, 0x07, 0xEE, 0xF6, 0xCA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00), + (0xE6, 0x00, 0x00, 0x11, 0x00), + (0xE7, 0x22, 0x00), + (0xE8, 0x06, 0xED, 0xF6, 0xCA, 0x08, 0xEF, 0xF6, 0xCA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00), + (0xE9, 0x36, 0x00), + (0xEB, 0x00, 0x00, 0x40, 0x40, 0x00, 0x00, 0x00), + (0xED, 0xFF, 0xFF, 0xFF, 0xBA, 0x0A, 0xFF, 0x45, 0xFF, 0xFF, 0x54, 0xFF, 0xA0, 0xAB, 0xFF, 0xFF, 0xFF), + (0xEF, 0x08, 0x08, 0x08, 0x45, 0x3F, 0x54), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x13), (0xE8, 0x00, 0x0E), (0xFF, 0x77, 0x01, 0x00, 0x00, 0x00), + (0x11, 0x00), (0xFF, 0x77, 0x01, 0x00, 0x00, 0x13), (0xE8, 0x00, 0x0C), + (0xE8, 0x00, 0x00), (0xFF, 0x77, 0x01, 0x00, 0x00, 0x00) + ) +) + +st7701s.extend( + "ZX2D10GE01R-V4848", + width=480, + height=480, + pixel_mode="18bit", + cs_pin=21, + de_pin=39, + vsync_pin=48, + hsync_pin=40, + pclk_pin={"number": 45, "ignore_strapping_warning": True}, + pclk_frequency="15MHz", + pclk_inverted=True, + hsync_pulse_width=10, + hsync_back_porch=10, + hsync_front_porch=10, + vsync_pulse_width=2, + vsync_back_porch=12, + vsync_front_porch=14, + data_pins={ + "red": [10, 16, 9, 15, 46], + "green": [8, 13, 18, 12, 11, 17], + "blue": [{"number": 47, "allow_other_uses": True}, {"number": 41, "allow_other_uses": True}, 0, 42, 14] + }, + initsequence=( + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x13), (0xEF, 0x08), (0xFF, 0x77, 0x01, 0x00, 0x00, 0x10), (0xC0, 0x3B, 0x00), + (0xC1, 0x0B, 0x02), (0xC2, 0x07, 0x02), (0xCC, 0x10), (0xCD, 0x08), + (0xB0, 0x00, 0x11, 0x16, 0x0e, 0x11, 0x06, 0x05, 0x09, 0x08, 0x21, 0x06, 0x13, 0x10, 0x29, 0x31, 0x18), + (0xB1, 0x00, 0x11, 0x16, 0x0e, 0x11, 0x07, 0x05, 0x09, 0x09, 0x21, 0x05, 0x13, 0x11, 0x2a, 0x31, 0x18), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x11), (0xB0, 0x6d), (0xB1, 0x37), (0xB2, 0x81), (0xB3, 0x80), (0xB5, 0x43), + (0xB7, 0x85), (0xB8, 0x20), (0xC1, 0x78), (0xC2, 0x78), (0xD0, 0x88), (0xE0, 0x00, 0x00, 0x02), + (0xE1, 0x03, 0xA0, 0x00, 0x00, 0x04, 0xA0, 0x00, 0x00, 0x00, 0x20, 0x20), + (0xE2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00), + (0xE3, 0x00, 0x00, 0x11, 0x00), (0xE4, 0x22, 0x00), + (0xE5, 0x05, 0xEC, 0xA0, 0xA0, 0x07, 0xEE, 0xA0, 0xA0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00), + (0xE6, 0x00, 0x00, 0x11, 0x00), (0xE7, 0x22, 0x00), + (0xE8, 0x06, 0xED, 0xA0, 0xA0, 0x08, 0xEF, 0xA0, 0xA0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00), + (0xEB, 0x00, 0x00, 0x40, 0x40, 0x00, 0x00, 0x00), + (0xED, 0xFF, 0xFF, 0xFF, 0xBA, 0x0A, 0xBF, 0x45, 0xFF, 0xFF, 0x54, 0xFB, 0xA0, 0xAB, 0xFF, 0xFF, 0xFF), + (0xEF, 0x10, 0x0D, 0x04, 0x08, 0x3F, 0x1F), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x13), (0xEF, 0x08), (0xFF, 0x77, 0x01, 0x00, 0x00, 0x00) + ) +) diff --git a/esphome/components/mipi_rgb/models/waveshare.py b/esphome/components/mipi_rgb/models/waveshare.py new file mode 100644 index 0000000000..0fc765fd52 --- /dev/null +++ b/esphome/components/mipi_rgb/models/waveshare.py @@ -0,0 +1,78 @@ +from esphome.components.mipi import DriverChip +from esphome.config_validation import UNDEFINED + +from .st7701s import st7701s + +wave_4_3 = DriverChip( + "ESP32-S3-TOUCH-LCD-4.3", + swap_xy=UNDEFINED, + initsequence=(), + color_order="RGB", + width=800, + height=480, + pclk_frequency="16MHz", + reset_pin={"ch422g": None, "number": 3}, + enable_pin={"ch422g": None, "number": 2}, + de_pin=5, + hsync_pin={"number": 46, "ignore_strapping_warning": True}, + vsync_pin={"number": 3, "ignore_strapping_warning": True}, + pclk_pin=7, + pclk_inverted=True, + hsync_front_porch=210, + hsync_pulse_width=30, + hsync_back_porch=30, + vsync_front_porch=4, + vsync_pulse_width=4, + vsync_back_porch=4, + data_pins={ + "red": [1, 2, 42, 41, 40], + "green": [39, 0, 45, 48, 47, 21], + "blue": [14, 38, 18, 17, 10], + }, +) + +wave_4_3.extend( + "WAVESHARE-5-1024X600", + width=1024, + height=600, + hsync_back_porch=145, + hsync_front_porch=170, + hsync_pulse_width=30, + vsync_back_porch=23, + vsync_front_porch=12, + vsync_pulse_width=2, +) + +wave_4_3.extend( + "ESP32-S3-TOUCH-LCD-7-800X480", + enable_pin=[{"ch422g": None, "number": 2}, {"ch422g": None, "number": 6}], + hsync_back_porch=8, + hsync_front_porch=8, + hsync_pulse_width=4, + vsync_back_porch=16, + vsync_front_porch=16, + vsync_pulse_width=4, +) + +st7701s.extend( + "WAVESHARE-4-480x480", + data_rate="2MHz", + spi_mode="MODE3", + color_order="BGR", + pixel_mode="18bit", + width=480, + height=480, + invert_colors=True, + cs_pin=42, + de_pin=40, + hsync_pin=38, + vsync_pin=39, + pclk_pin=41, + pclk_frequency="12MHz", + pclk_inverted=False, + data_pins={ + "red": [46, 3, 8, 18, 17], + "green": [14, 13, 12, 11, 10, 9], + "blue": [5, 45, 48, 47, 21], + }, +) diff --git a/esphome/components/mipi_spi/display.py b/esphome/components/mipi_spi/display.py index e891e2daad..50ea826eab 100644 --- a/esphome/components/mipi_spi/display.py +++ b/esphome/components/mipi_spi/display.py @@ -37,6 +37,7 @@ from esphome.const import ( CONF_DATA_RATE, CONF_DC_PIN, CONF_DIMENSIONS, + CONF_DISABLED, CONF_ENABLE_PIN, CONF_ID, CONF_INIT_SEQUENCE, @@ -130,28 +131,18 @@ def denominator(config): ) from StopIteration -def swap_xy_schema(model): - uses_swap = model.get_default(CONF_SWAP_XY, None) != cv.UNDEFINED - - def validator(value): - if value: - raise cv.Invalid("Axis swapping not supported by this model") - return cv.boolean(value) - - if uses_swap: - return {cv.Required(CONF_SWAP_XY): cv.boolean} - return {cv.Optional(CONF_SWAP_XY, default=False): validator} - - def model_schema(config): model = MODELS[config[CONF_MODEL]] bus_mode = config[CONF_BUS_MODE] - transform = cv.Schema( - { - cv.Required(CONF_MIRROR_X): cv.boolean, - cv.Required(CONF_MIRROR_Y): cv.boolean, - **swap_xy_schema(model), - } + transform = cv.Any( + cv.Schema( + { + cv.Required(CONF_MIRROR_X): cv.boolean, + cv.Required(CONF_MIRROR_Y): cv.boolean, + **model.swap_xy_schema(), + } + ), + cv.one_of(CONF_DISABLED, lower=True), ) # CUSTOM model will need to provide a custom init sequence iseqconf = ( @@ -160,7 +151,11 @@ def model_schema(config): else cv.Optional(CONF_INIT_SEQUENCE) ) # Dimensions are optional if the model has a default width and the x-y transform is not overridden - is_swapped = config.get(CONF_TRANSFORM, {}).get(CONF_SWAP_XY) is True + transform_config = config.get(CONF_TRANSFORM, {}) + is_swapped = ( + isinstance(transform_config, dict) + and transform_config.get(CONF_SWAP_XY, False) is True + ) cv_dimensions = ( cv.Optional if model.get_default(CONF_WIDTH) and not is_swapped else cv.Required ) @@ -192,9 +187,7 @@ def model_schema(config): .extend( { cv.GenerateID(): cv.declare_id(MipiSpi), - cv_dimensions(CONF_DIMENSIONS): dimension_schema( - model.get_default(CONF_DRAW_ROUNDING, 1) - ), + cv_dimensions(CONF_DIMENSIONS): dimension_schema(1), model.option(CONF_ENABLE_PIN, cv.UNDEFINED): cv.ensure_list( pins.gpio_output_pin_schema ), @@ -380,25 +373,42 @@ def get_instance(config): bus_type = BusTypes[bus_type] buffer_type = cg.uint8 if color_depth == 8 else cg.uint16 frac = denominator(config) - rotation = DISPLAY_ROTATIONS[ + rotation = ( 0 if model.rotation_as_transform(config) else config.get(CONF_ROTATION, 0) - ] + ) templateargs = [ buffer_type, bufferpixels, config[CONF_BYTE_ORDER] == "big_endian", display_pixel_mode, bus_type, - width, - height, - offset_width, - offset_height, ] # If a buffer is required, use MipiSpiBuffer, otherwise use MipiSpi if requires_buffer(config): - templateargs.append(rotation) - templateargs.append(frac) + templateargs.extend( + [ + width, + height, + offset_width, + offset_height, + DISPLAY_ROTATIONS[rotation], + frac, + config[CONF_DRAW_ROUNDING], + ] + ) return MipiSpiBuffer, templateargs + # Swap height and width if the display is rotated 90 or 270 degrees in software + if rotation in (90, 270): + width, height = height, width + offset_width, offset_height = offset_height, offset_width + templateargs.extend( + [ + width, + height, + offset_width, + offset_height, + ] + ) return MipiSpi, templateargs @@ -415,7 +425,6 @@ async def to_code(config): else: config[CONF_ROTATION] = 0 cg.add(var.set_model(config[CONF_MODEL])) - cg.add(var.set_draw_rounding(config[CONF_DRAW_ROUNDING])) if enable_pin := config.get(CONF_ENABLE_PIN): enable = [await cg.gpio_pin_expression(pin) for pin in enable_pin] cg.add(var.set_enable_pins(enable)) diff --git a/esphome/components/mipi_spi/mipi_spi.h b/esphome/components/mipi_spi/mipi_spi.h index 00b861f71b..1953aef035 100644 --- a/esphome/components/mipi_spi/mipi_spi.h +++ b/esphome/components/mipi_spi/mipi_spi.h @@ -38,7 +38,7 @@ static constexpr uint8_t MADCTL_BGR = 0x08; // Bit 3 Blue-Green-Red pixel ord static constexpr uint8_t MADCTL_XFLIP = 0x02; // Mirror the display horizontally static constexpr uint8_t MADCTL_YFLIP = 0x01; // Mirror the display vertically -static const uint8_t DELAY_FLAG = 0xFF; +static constexpr uint8_t DELAY_FLAG = 0xFF; // store a 16 bit value in a buffer, big endian. static inline void put16_be(uint8_t *buf, uint16_t value) { buf[0] = value >> 8; @@ -79,7 +79,7 @@ class MipiSpi : public display::Display, public spi::SPIDevice { public: - MipiSpi() {} + MipiSpi() = default; void update() override { this->stop_poller(); } void draw_pixel_at(int x, int y, Color color) override {} void set_model(const char *model) { this->model_ = model; } @@ -99,7 +99,6 @@ class MipiSpi : public display::Display, int get_width_internal() override { return WIDTH; } int get_height_internal() override { return HEIGHT; } void set_init_sequence(const std::vector &sequence) { this->init_sequence_ = sequence; } - void set_draw_rounding(unsigned rounding) { this->draw_rounding_ = rounding; } // reset the display, and write the init sequence void setup() override { @@ -326,6 +325,7 @@ class MipiSpi : public display::Display, /** * Writes a buffer to the display. + * @param ptr The pointer to the pixel data * @param w Width of each line in bytes * @param h Height of the buffer in rows * @param pad Padding in bytes after each line @@ -340,7 +340,7 @@ class MipiSpi : public display::Display, this->write_cmd_addr_data(0, 0, 0, 0, ptr, w * h, 8); } } else { - for (size_t y = 0; y != h; y++) { + for (size_t y = 0; y != static_cast(h); y++) { if constexpr (BUS_TYPE == BUS_TYPE_SINGLE || BUS_TYPE == BUS_TYPE_SINGLE_16) { this->write_array(ptr, w); } else if constexpr (BUS_TYPE == BUS_TYPE_QUAD) { @@ -372,8 +372,8 @@ class MipiSpi : public display::Display, uint8_t dbuffer[DISPLAYPIXEL * 48]; uint8_t *dptr = dbuffer; auto stride = x_offset + w + x_pad; // stride in pixels - for (size_t y = 0; y != h; y++) { - for (size_t x = 0; x != w; x++) { + for (size_t y = 0; y != static_cast(h); y++) { + for (size_t x = 0; x != static_cast(w); x++) { auto color_val = ptr[y * stride + x]; if constexpr (DISPLAYPIXEL == PIXEL_MODE_18 && BUFFERPIXEL == PIXEL_MODE_16) { // 16 to 18 bit conversion @@ -424,7 +424,6 @@ class MipiSpi : public display::Display, // other properties set by configuration bool invert_colors_{}; - unsigned draw_rounding_{2}; optional brightness_{}; const char *model_{"Unknown"}; std::vector init_sequence_{}; @@ -444,12 +443,20 @@ class MipiSpi : public display::Display, * @tparam OFFSET_WIDTH The x-offset of the display in pixels * @tparam OFFSET_HEIGHT The y-offset of the display in pixels * @tparam FRACTION The fraction of the display size to use for the buffer (e.g. 4 means a 1/4 buffer). + * @tparam ROUNDING The alignment requirement for drawing operations (e.g. 2 means that x coordinates must be even) */ template + uint16_t WIDTH, uint16_t HEIGHT, int OFFSET_WIDTH, int OFFSET_HEIGHT, display::DisplayRotation ROTATION, + int FRACTION, unsigned ROUNDING> class MipiSpiBuffer : public MipiSpi { public: + // these values define the buffer size needed to write in accordance with the chip pixel alignment + // requirements. If the required rounding does not divide the width and height, we round up to the next multiple and + // ignore the extra columns and rows when drawing, but use them to write to the display. + static constexpr unsigned BUFFER_WIDTH = (WIDTH + ROUNDING - 1) / ROUNDING * ROUNDING; + static constexpr unsigned BUFFER_HEIGHT = (HEIGHT + ROUNDING - 1) / ROUNDING * ROUNDING; + MipiSpiBuffer() { this->rotation_ = ROTATION; } void dump_config() override { @@ -461,17 +468,17 @@ class MipiSpiBuffer : public MipiSpirotation_, BUFFERPIXEL * 8, FRACTION, sizeof(BUFFERTYPE) * WIDTH * HEIGHT / FRACTION, - this->draw_rounding_); + this->rotation_, BUFFERPIXEL * 8, FRACTION, + sizeof(BUFFERTYPE) * BUFFER_WIDTH * BUFFER_HEIGHT / FRACTION, ROUNDING); } void setup() override { MipiSpi::setup(); RAMAllocator allocator{}; - this->buffer_ = allocator.allocate(WIDTH * HEIGHT / FRACTION); + this->buffer_ = allocator.allocate(BUFFER_WIDTH * BUFFER_HEIGHT / FRACTION); if (this->buffer_ == nullptr) { - this->mark_failed("Buffer allocation failed"); + this->mark_failed(LOG_STR("Buffer allocation failed")); } } @@ -508,15 +515,14 @@ class MipiSpiBuffer : public MipiSpix_low_, this->y_low_, this->x_high_, this->y_high_); // Some chips require that the drawing window be aligned on certain boundaries - auto dr = this->draw_rounding_; - this->x_low_ = this->x_low_ / dr * dr; - this->y_low_ = this->y_low_ / dr * dr; - this->x_high_ = (this->x_high_ + dr) / dr * dr - 1; - this->y_high_ = (this->y_high_ + dr) / dr * dr - 1; + this->x_low_ = this->x_low_ / ROUNDING * ROUNDING; + this->y_low_ = this->y_low_ / ROUNDING * ROUNDING; + this->x_high_ = (this->x_high_ + ROUNDING) / ROUNDING * ROUNDING - 1; + this->y_high_ = (this->y_high_ + ROUNDING) / ROUNDING * ROUNDING - 1; int w = this->x_high_ - this->x_low_ + 1; int h = this->y_high_ - this->y_low_ + 1; this->write_to_display_(this->x_low_, this->y_low_, w, h, this->buffer_, this->x_low_, - this->y_low_ - this->start_line_, WIDTH - w); + this->y_low_ - this->start_line_, BUFFER_WIDTH - w); // invalidate watermarks this->x_low_ = WIDTH; this->y_low_ = HEIGHT; @@ -536,10 +542,10 @@ class MipiSpiBuffer : public MipiSpiget_clipping().inside(x, y)) return; - rotate_coordinates_(x, y); + rotate_coordinates(x, y); if (x < 0 || x >= WIDTH || y < this->start_line_ || y >= this->end_line_) return; - this->buffer_[(y - this->start_line_) * WIDTH + x] = convert_color_(color); + this->buffer_[(y - this->start_line_) * BUFFER_WIDTH + x] = convert_color(color); if (x < this->x_low_) { this->x_low_ = x; } @@ -560,7 +566,7 @@ class MipiSpiBuffer : public MipiSpiy_low_ = this->start_line_; this->x_high_ = WIDTH - 1; this->y_high_ = this->end_line_ - 1; - std::fill_n(this->buffer_, HEIGHT * WIDTH / FRACTION, convert_color_(color)); + std::fill_n(this->buffer_, HEIGHT * BUFFER_WIDTH / FRACTION, convert_color(color)); } int get_width() override { @@ -577,7 +583,7 @@ class MipiSpiBuffer : public MipiSpi> 3 | color.b >> 6; } else if constexpr (BUFFERPIXEL == PIXEL_MODE_16) { diff --git a/esphome/components/mipi_spi/models/amoled.py b/esphome/components/mipi_spi/models/amoled.py index 6fe882b584..4d6c8da4b0 100644 --- a/esphome/components/mipi_spi/models/amoled.py +++ b/esphome/components/mipi_spi/models/amoled.py @@ -5,10 +5,13 @@ from esphome.components.mipi import ( PAGESEL, PIXFMT, SLPOUT, + SPIMODESEL, SWIRE1, SWIRE2, TEON, + WCE, WRAM, + WRCTRLD, DriverChip, delay, ) @@ -24,7 +27,8 @@ DriverChip( bus_mode=TYPE_QUAD, brightness=0xD0, color_order=MODE_RGB, - initsequence=(SLPOUT,), # Requires early SLPOUT + no_slpout=True, # SLPOUT is in the init sequence, early + initsequence=(SLPOUT,), ) DriverChip( @@ -87,4 +91,20 @@ T4_S3_AMOLED = RM690B0.extend( bus_mode=TYPE_QUAD, ) +CO5300 = DriverChip( + "CO5300", + brightness=0xD0, + color_order=MODE_RGB, + bus_mode=TYPE_QUAD, + no_slpout=True, + initsequence=( + (SLPOUT,), # Requires early SLPOUT + (PAGESEL, 0x00), + (SPIMODESEL, 0x80), + (WRCTRLD, 0x20), + (WCE, 0x00), + ), +) + + models = {} diff --git a/esphome/components/mipi_spi/models/jc.py b/esphome/components/mipi_spi/models/jc.py index f1f046a427..5dbf049ded 100644 --- a/esphome/components/mipi_spi/models/jc.py +++ b/esphome/components/mipi_spi/models/jc.py @@ -255,4 +255,233 @@ DriverChip( ), ) +DriverChip( + "JC3636W518V2", + height=360, + width=360, + offset_height=1, + draw_rounding=1, + cs_pin=10, + reset_pin=47, + invert_colors=True, + color_order=MODE_RGB, + bus_mode=TYPE_QUAD, + data_rate="40MHz", + initsequence=( + (0xF0, 0x28), + (0xF2, 0x28), + (0x73, 0xF0), + (0x7C, 0xD1), + (0x83, 0xE0), + (0x84, 0x61), + (0xF2, 0x82), + (0xF0, 0x00), + (0xF0, 0x01), + (0xF1, 0x01), + (0xB0, 0x56), + (0xB1, 0x4D), + (0xB2, 0x24), + (0xB4, 0x87), + (0xB5, 0x44), + (0xB6, 0x8B), + (0xB7, 0x40), + (0xB8, 0x86), + (0xBA, 0x00), + (0xBB, 0x08), + (0xBC, 0x08), + (0xBD, 0x00), + (0xC0, 0x80), + (0xC1, 0x10), + (0xC2, 0x37), + (0xC3, 0x80), + (0xC4, 0x10), + (0xC5, 0x37), + (0xC6, 0xA9), + (0xC7, 0x41), + (0xC8, 0x01), + (0xC9, 0xA9), + (0xCA, 0x41), + (0xCB, 0x01), + (0xD0, 0x91), + (0xD1, 0x68), + (0xD2, 0x68), + (0xF5, 0x00, 0xA5), + (0xDD, 0x4F), + (0xDE, 0x4F), + (0xF1, 0x10), + (0xF0, 0x00), + (0xF0, 0x02), + ( + 0xE0, + 0xF0, + 0x0A, + 0x10, + 0x09, + 0x09, + 0x36, + 0x35, + 0x33, + 0x4A, + 0x29, + 0x15, + 0x15, + 0x2E, + 0x34, + ), + ( + 0xE1, + 0xF0, + 0x0A, + 0x0F, + 0x08, + 0x08, + 0x05, + 0x34, + 0x33, + 0x4A, + 0x39, + 0x15, + 0x15, + 0x2D, + 0x33, + ), + (0xF0, 0x10), + (0xF3, 0x10), + (0xE0, 0x07), + (0xE1, 0x00), + (0xE2, 0x00), + (0xE3, 0x00), + (0xE4, 0xE0), + (0xE5, 0x06), + (0xE6, 0x21), + (0xE7, 0x01), + (0xE8, 0x05), + (0xE9, 0x02), + (0xEA, 0xDA), + (0xEB, 0x00), + (0xEC, 0x00), + (0xED, 0x0F), + (0xEE, 0x00), + (0xEF, 0x00), + (0xF8, 0x00), + (0xF9, 0x00), + (0xFA, 0x00), + (0xFB, 0x00), + (0xFC, 0x00), + (0xFD, 0x00), + (0xFE, 0x00), + (0xFF, 0x00), + (0x60, 0x40), + (0x61, 0x04), + (0x62, 0x00), + (0x63, 0x42), + (0x64, 0xD9), + (0x65, 0x00), + (0x66, 0x00), + (0x67, 0x00), + (0x68, 0x00), + (0x69, 0x00), + (0x6A, 0x00), + (0x6B, 0x00), + (0x70, 0x40), + (0x71, 0x03), + (0x72, 0x00), + (0x73, 0x42), + (0x74, 0xD8), + (0x75, 0x00), + (0x76, 0x00), + (0x77, 0x00), + (0x78, 0x00), + (0x79, 0x00), + (0x7A, 0x00), + (0x7B, 0x00), + (0x80, 0x48), + (0x81, 0x00), + (0x82, 0x06), + (0x83, 0x02), + (0x84, 0xD6), + (0x85, 0x04), + (0x86, 0x00), + (0x87, 0x00), + (0x88, 0x48), + (0x89, 0x00), + (0x8A, 0x08), + (0x8B, 0x02), + (0x8C, 0xD8), + (0x8D, 0x04), + (0x8E, 0x00), + (0x8F, 0x00), + (0x90, 0x48), + (0x91, 0x00), + (0x92, 0x0A), + (0x93, 0x02), + (0x94, 0xDA), + (0x95, 0x04), + (0x96, 0x00), + (0x97, 0x00), + (0x98, 0x48), + (0x99, 0x00), + (0x9A, 0x0C), + (0x9B, 0x02), + (0x9C, 0xDC), + (0x9D, 0x04), + (0x9E, 0x00), + (0x9F, 0x00), + (0xA0, 0x48), + (0xA1, 0x00), + (0xA2, 0x05), + (0xA3, 0x02), + (0xA4, 0xD5), + (0xA5, 0x04), + (0xA6, 0x00), + (0xA7, 0x00), + (0xA8, 0x48), + (0xA9, 0x00), + (0xAA, 0x07), + (0xAB, 0x02), + (0xAC, 0xD7), + (0xAD, 0x04), + (0xAE, 0x00), + (0xAF, 0x00), + (0xB0, 0x48), + (0xB1, 0x00), + (0xB2, 0x09), + (0xB3, 0x02), + (0xB4, 0xD9), + (0xB5, 0x04), + (0xB6, 0x00), + (0xB7, 0x00), + (0xB8, 0x48), + (0xB9, 0x00), + (0xBA, 0x0B), + (0xBB, 0x02), + (0xBC, 0xDB), + (0xBD, 0x04), + (0xBE, 0x00), + (0xBF, 0x00), + (0xC0, 0x10), + (0xC1, 0x47), + (0xC2, 0x56), + (0xC3, 0x65), + (0xC4, 0x74), + (0xC5, 0x88), + (0xC6, 0x99), + (0xC7, 0x01), + (0xC8, 0xBB), + (0xC9, 0xAA), + (0xD0, 0x10), + (0xD1, 0x47), + (0xD2, 0x56), + (0xD3, 0x65), + (0xD4, 0x74), + (0xD5, 0x88), + (0xD6, 0x99), + (0xD7, 0x01), + (0xD8, 0xBB), + (0xD9, 0xAA), + (0xF3, 0x01), + (0xF0, 0x00), + ), +) + models = {} diff --git a/esphome/components/mipi_spi/models/waveshare.py b/esphome/components/mipi_spi/models/waveshare.py index 002f81f3a6..e4e090da2e 100644 --- a/esphome/components/mipi_spi/models/waveshare.py +++ b/esphome/components/mipi_spi/models/waveshare.py @@ -1,7 +1,9 @@ from esphome.components.mipi import DriverChip import esphome.config_validation as cv +from .amoled import CO5300 from .ili import ILI9488_A +from .jc import AXS15231 DriverChip( "WAVESHARE-4-TFT", @@ -140,3 +142,23 @@ ILI9488_A.extend( data_rate="20MHz", invert_colors=True, ) + +CO5300.extend( + "WAVESHARE-ESP32-S3-TOUCH-AMOLED-1.75", + width=466, + height=466, + pixel_mode="16bit", + offset_height=0, + offset_width=6, + cs_pin=12, + reset_pin=39, +) + +AXS15231.extend( + "WAVESHARE-ESP32-S3-TOUCH-LCD-3.49", + width=172, + height=640, + data_rate="80MHz", + cs_pin=9, + reset_pin=21, +) diff --git a/esphome/components/mitsubishi/mitsubishi.cpp b/esphome/components/mitsubishi/mitsubishi.cpp index 3d9207dd96..10ab4f3b5c 100644 --- a/esphome/components/mitsubishi/mitsubishi.cpp +++ b/esphome/components/mitsubishi/mitsubishi.cpp @@ -52,8 +52,9 @@ const uint8_t MITSUBISHI_BYTE16 = 0x00; climate::ClimateTraits MitsubishiClimate::traits() { auto traits = climate::ClimateTraits(); - traits.set_supports_current_temperature(this->sensor_ != nullptr); - traits.set_supports_action(false); + if (this->sensor_ != nullptr) { + traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE); + } traits.set_visual_min_temperature(MITSUBISHI_TEMP_MIN); traits.set_visual_max_temperature(MITSUBISHI_TEMP_MAX); traits.set_visual_temperature_step(1.0f); diff --git a/esphome/components/mixer/speaker/automation.h b/esphome/components/mixer/speaker/automation.h index b688fa2c1e..2234936628 100644 --- a/esphome/components/mixer/speaker/automation.h +++ b/esphome/components/mixer/speaker/automation.h @@ -9,7 +9,7 @@ namespace mixer_speaker { template class DuckingApplyAction : public Action, public Parented { TEMPLATABLE_VALUE(uint8_t, decibel_reduction) TEMPLATABLE_VALUE(uint32_t, duration) - void play(Ts... x) override { + void play(const Ts &...x) override { this->parent_->apply_ducking(this->decibel_reduction_.value(x...), this->duration_.value(x...)); } }; diff --git a/esphome/components/mixer/speaker/mixer_speaker.cpp b/esphome/components/mixer/speaker/mixer_speaker.cpp index fc0517c7be..043b629cf1 100644 --- a/esphome/components/mixer/speaker/mixer_speaker.cpp +++ b/esphome/components/mixer/speaker/mixer_speaker.cpp @@ -78,19 +78,20 @@ void SourceSpeaker::loop() { } else { switch (err) { case ESP_ERR_NO_MEM: - this->status_set_error("Failed to start mixer: not enough memory"); + this->status_set_error(LOG_STR("Failed to start mixer: not enough memory")); break; case ESP_ERR_NOT_SUPPORTED: - this->status_set_error("Failed to start mixer: unsupported bits per sample"); + this->status_set_error(LOG_STR("Failed to start mixer: unsupported bits per sample")); break; case ESP_ERR_INVALID_ARG: - this->status_set_error("Failed to start mixer: audio stream isn't compatible with the other audio stream."); + this->status_set_error( + LOG_STR("Failed to start mixer: audio stream isn't compatible with the other audio stream.")); break; case ESP_ERR_INVALID_STATE: - this->status_set_error("Failed to start mixer: mixer task failed to start"); + this->status_set_error(LOG_STR("Failed to start mixer: mixer task failed to start")); break; default: - this->status_set_error("Failed to start mixer"); + this->status_set_error(LOG_STR("Failed to start mixer")); break; } @@ -317,7 +318,7 @@ void MixerSpeaker::loop() { xEventGroupClearBits(this->event_group_, MixerEventGroupBits::STATE_STARTING); } if (event_group_bits & MixerEventGroupBits::ERR_ESP_NO_MEM) { - this->status_set_error("Failed to allocate the mixer's internal buffer"); + this->status_set_error(LOG_STR("Failed to allocate the mixer's internal buffer")); xEventGroupClearBits(this->event_group_, MixerEventGroupBits::ERR_ESP_NO_MEM); } if (event_group_bits & MixerEventGroupBits::STATE_RUNNING) { @@ -572,7 +573,7 @@ void MixerSpeaker::audio_mixer_task(void *params) { } } else { // Determine how many frames to mix - for (int i = 0; i < transfer_buffers_with_data.size(); ++i) { + for (size_t i = 0; i < transfer_buffers_with_data.size(); ++i) { const uint32_t frames_available_in_buffer = speakers_with_data[i]->get_audio_stream_info().bytes_to_frames(transfer_buffers_with_data[i]->available()); frames_to_mix = std::min(frames_to_mix, frames_available_in_buffer); @@ -581,7 +582,7 @@ void MixerSpeaker::audio_mixer_task(void *params) { audio::AudioStreamInfo primary_stream_info = speakers_with_data[0]->get_audio_stream_info(); // Mix two streams together - for (int i = 1; i < transfer_buffers_with_data.size(); ++i) { + for (size_t i = 1; i < transfer_buffers_with_data.size(); ++i) { mix_audio_samples(primary_buffer, primary_stream_info, reinterpret_cast(transfer_buffers_with_data[i]->get_buffer_start()), speakers_with_data[i]->get_audio_stream_info(), @@ -596,7 +597,7 @@ void MixerSpeaker::audio_mixer_task(void *params) { } // Update source transfer buffer lengths and add new audio durations to the source speaker pending playbacks - for (int i = 0; i < transfer_buffers_with_data.size(); ++i) { + for (size_t i = 0; i < transfer_buffers_with_data.size(); ++i) { transfer_buffers_with_data[i]->decrease_buffer_length( speakers_with_data[i]->get_audio_stream_info().frames_to_bytes(frames_to_mix)); speakers_with_data[i]->pending_playback_frames_ += frames_to_mix; diff --git a/esphome/components/mlx90614/mlx90614.cpp b/esphome/components/mlx90614/mlx90614.cpp index 2e711baf9a..8e53b9e3c3 100644 --- a/esphome/components/mlx90614/mlx90614.cpp +++ b/esphome/components/mlx90614/mlx90614.cpp @@ -50,28 +50,13 @@ bool MLX90614Component::write_emissivity_() { return true; } -uint8_t MLX90614Component::crc8_pec_(const uint8_t *data, uint8_t len) { - uint8_t crc = 0; - for (uint8_t i = 0; i < len; i++) { - uint8_t in = data[i]; - for (uint8_t j = 0; j < 8; j++) { - bool carry = (crc ^ in) & 0x80; - crc <<= 1; - if (carry) - crc ^= 0x07; - in <<= 1; - } - } - return crc; -} - bool MLX90614Component::write_bytes_(uint8_t reg, uint16_t data) { uint8_t buf[5]; buf[0] = this->address_ << 1; buf[1] = reg; buf[2] = data & 0xFF; buf[3] = data >> 8; - buf[4] = this->crc8_pec_(buf, 4); + buf[4] = crc8(buf, 4, 0x00, 0x07, true); return this->write_bytes(reg, buf + 2, 3); } @@ -90,18 +75,18 @@ float MLX90614Component::get_setup_priority() const { return setup_priority::DAT void MLX90614Component::update() { uint8_t emissivity[3]; - if (this->read_register(MLX90614_EMISSIVITY, emissivity, 3, false) != i2c::ERROR_OK) { + if (this->read_register(MLX90614_EMISSIVITY, emissivity, 3) != i2c::ERROR_OK) { this->status_set_warning(); return; } uint8_t raw_object[3]; - if (this->read_register(MLX90614_TEMPERATURE_OBJECT_1, raw_object, 3, false) != i2c::ERROR_OK) { + if (this->read_register(MLX90614_TEMPERATURE_OBJECT_1, raw_object, 3) != i2c::ERROR_OK) { this->status_set_warning(); return; } uint8_t raw_ambient[3]; - if (this->read_register(MLX90614_TEMPERATURE_AMBIENT, raw_ambient, 3, false) != i2c::ERROR_OK) { + if (this->read_register(MLX90614_TEMPERATURE_AMBIENT, raw_ambient, 3) != i2c::ERROR_OK) { this->status_set_warning(); return; } diff --git a/esphome/components/mlx90614/mlx90614.h b/esphome/components/mlx90614/mlx90614.h index b6bd44172d..fa6fb523bb 100644 --- a/esphome/components/mlx90614/mlx90614.h +++ b/esphome/components/mlx90614/mlx90614.h @@ -22,7 +22,6 @@ class MLX90614Component : public PollingComponent, public i2c::I2CDevice { protected: bool write_emissivity_(); - uint8_t crc8_pec_(const uint8_t *data, uint8_t len); bool write_bytes_(uint8_t reg, uint16_t data); sensor::Sensor *ambient_sensor_{nullptr}; diff --git a/esphome/components/mmc5603/mmc5603.cpp b/esphome/components/mmc5603/mmc5603.cpp index d712e2401d..f0d1044f3f 100644 --- a/esphome/components/mmc5603/mmc5603.cpp +++ b/esphome/components/mmc5603/mmc5603.cpp @@ -128,21 +128,21 @@ void MMC5603Component::update() { raw_x |= buffer[1] << 4; raw_x |= buffer[2] << 0; - const float x = 0.0625 * (raw_x - 524288); + const float x = 0.00625 * (raw_x - 524288); int32_t raw_y = 0; raw_y |= buffer[3] << 12; raw_y |= buffer[4] << 4; raw_y |= buffer[5] << 0; - const float y = 0.0625 * (raw_y - 524288); + const float y = 0.00625 * (raw_y - 524288); int32_t raw_z = 0; raw_z |= buffer[6] << 12; raw_z |= buffer[7] << 4; raw_z |= buffer[8] << 0; - const float z = 0.0625 * (raw_z - 524288); + const float z = 0.00625 * (raw_z - 524288); const float heading = atan2f(0.0f - x, y) * 180.0f / M_PI; ESP_LOGD(TAG, "Got x=%0.02fµT y=%0.02fµT z=%0.02fµT heading=%0.01f°", x, y, z, heading); diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index 6350f43ef6..20271b4bdb 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -66,7 +66,10 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { uint8_t data_offset = 3; // Per https://modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf Ch 5 User-Defined function codes - if (((function_code >= 65) && (function_code <= 72)) || ((function_code >= 100) && (function_code <= 110))) { + if (((function_code >= FUNCTION_CODE_USER_DEFINED_SPACE_1_INIT) && + (function_code <= FUNCTION_CODE_USER_DEFINED_SPACE_1_END)) || + ((function_code >= FUNCTION_CODE_USER_DEFINED_SPACE_2_INIT) && + (function_code <= FUNCTION_CODE_USER_DEFINED_SPACE_2_END))) { // Handle user-defined function, since we don't know how big this ought to be, // ideally we should delegate the entire length detection to whatever handler is // installed, but wait, there is the CRC, and if we get a hit there is a good @@ -91,10 +94,14 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { } else { // data starts at 2 and length is 4 for read registers commands if (this->role == ModbusRole::SERVER) { - if (function_code == 0x1 || function_code == 0x3 || function_code == 0x4 || function_code == 0x6) { + if (function_code == ModbusFunctionCode::READ_COILS || + function_code == ModbusFunctionCode::READ_DISCRETE_INPUTS || + function_code == ModbusFunctionCode::READ_HOLDING_REGISTERS || + function_code == ModbusFunctionCode::READ_INPUT_REGISTERS || + function_code == ModbusFunctionCode::WRITE_SINGLE_REGISTER) { data_offset = 2; data_len = 4; - } else if (function_code == 0x10) { + } else if (function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) { if (at < 6) { return true; } @@ -104,7 +111,10 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { } } else { // the response for write command mirrors the requests and data starts at offset 2 instead of 3 for read commands - if (function_code == 0x5 || function_code == 0x06 || function_code == 0xF || function_code == 0x10) { + if (function_code == ModbusFunctionCode::WRITE_SINGLE_COIL || + function_code == ModbusFunctionCode::WRITE_SINGLE_REGISTER || + function_code == ModbusFunctionCode::WRITE_MULTIPLE_COILS || + function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) { data_offset = 2; data_len = 4; } @@ -112,7 +122,7 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { // Error ( msb indicates error ) // response format: Byte[0] = device address, Byte[1] function code | 0x80 , Byte[2] exception code, Byte[3-4] crc - if ((function_code & 0x80) == 0x80) { + if ((function_code & FUNCTION_CODE_EXCEPTION_MASK) == FUNCTION_CODE_EXCEPTION_MASK) { data_offset = 2; data_len = 1; } @@ -143,10 +153,10 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { if (device->address_ == address) { found = true; // Is it an error response? - if ((function_code & 0x80) == 0x80) { + if ((function_code & FUNCTION_CODE_EXCEPTION_MASK) == FUNCTION_CODE_EXCEPTION_MASK) { ESP_LOGD(TAG, "Modbus error function code: 0x%X exception: %d", function_code, raw[2]); if (waiting_for_response != 0) { - device->on_modbus_error(function_code & 0x7F, raw[2]); + device->on_modbus_error(function_code & FUNCTION_CODE_MASK, raw[2]); } else { // Ignore modbus exception not related to a pending command ESP_LOGD(TAG, "Ignoring Modbus error - not expecting a response"); @@ -154,12 +164,14 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { continue; } if (this->role == ModbusRole::SERVER) { - if (function_code == 0x3 || function_code == 0x4) { + if (function_code == ModbusFunctionCode::READ_HOLDING_REGISTERS || + function_code == ModbusFunctionCode::READ_INPUT_REGISTERS) { device->on_modbus_read_registers(function_code, uint16_t(data[1]) | (uint16_t(data[0]) << 8), uint16_t(data[3]) | (uint16_t(data[2]) << 8)); continue; } - if (function_code == 0x6 || function_code == 0x10) { + if (function_code == ModbusFunctionCode::WRITE_SINGLE_REGISTER || + function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) { device->on_modbus_write_registers(function_code, data); continue; } @@ -199,7 +211,7 @@ void Modbus::send(uint8_t address, uint8_t function_code, uint16_t start_address // Only check max number of registers for standard function codes // Some devices use non standard codes like 0x43 - if (number_of_entities > MAX_VALUES && function_code <= 0x10) { + if (number_of_entities > MAX_VALUES && function_code <= ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) { ESP_LOGE(TAG, "send too many values %d max=%zu", number_of_entities, MAX_VALUES); return; } @@ -210,15 +222,17 @@ void Modbus::send(uint8_t address, uint8_t function_code, uint16_t start_address if (this->role == ModbusRole::CLIENT) { data.push_back(start_address >> 8); data.push_back(start_address >> 0); - if (function_code != 0x5 && function_code != 0x6) { + if (function_code != ModbusFunctionCode::WRITE_SINGLE_COIL && + function_code != ModbusFunctionCode::WRITE_SINGLE_REGISTER) { data.push_back(number_of_entities >> 8); data.push_back(number_of_entities >> 0); } } if (payload != nullptr) { - if (this->role == ModbusRole::SERVER || function_code == 0xF || function_code == 0x10) { // Write multiple - data.push_back(payload_len); // Byte count is required for write + if (this->role == ModbusRole::SERVER || function_code == ModbusFunctionCode::WRITE_MULTIPLE_COILS || + function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) { // Write multiple + data.push_back(payload_len); // Byte count is required for write } else { payload_len = 2; // Write single register or coil } diff --git a/esphome/components/modbus/modbus.h b/esphome/components/modbus/modbus.h index ec35612690..fac74aaadf 100644 --- a/esphome/components/modbus/modbus.h +++ b/esphome/components/modbus/modbus.h @@ -3,6 +3,8 @@ #include "esphome/core/component.h" #include "esphome/components/uart/uart.h" +#include "esphome/components/modbus/modbus_definitions.h" + #include namespace esphome { @@ -65,12 +67,12 @@ class ModbusDevice { this->parent_->send(this->address_, function, start_address, number_of_entities, payload_len, payload); } void send_raw(const std::vector &payload) { this->parent_->send_raw(payload); } - void send_error(uint8_t function_code, uint8_t exception_code) { + void send_error(uint8_t function_code, ModbusExceptionCode exception_code) { std::vector error_response; error_response.reserve(3); error_response.push_back(this->address_); - error_response.push_back(function_code | 0x80); - error_response.push_back(exception_code); + error_response.push_back(function_code | FUNCTION_CODE_EXCEPTION_MASK); + error_response.push_back(static_cast(exception_code)); this->send_raw(error_response); } // If more than one device is connected block sending a new command before a response is received diff --git a/esphome/components/modbus/modbus_definitions.h b/esphome/components/modbus/modbus_definitions.h new file mode 100644 index 0000000000..07f101ae4c --- /dev/null +++ b/esphome/components/modbus/modbus_definitions.h @@ -0,0 +1,86 @@ +#pragma once + +#include "esphome/core/component.h" + +namespace esphome { +namespace modbus { + +/// Modbus definitions from specs: +/// https://modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf +// 5 Function Code Categories +const uint8_t FUNCTION_CODE_USER_DEFINED_SPACE_1_INIT = 65; // 0x41 +const uint8_t FUNCTION_CODE_USER_DEFINED_SPACE_1_END = 72; // 0x48 + +const uint8_t FUNCTION_CODE_USER_DEFINED_SPACE_2_INIT = 100; // 0x64 +const uint8_t FUNCTION_CODE_USER_DEFINED_SPACE_2_END = 110; // 0x6E + +enum class ModbusFunctionCode : uint8_t { + CUSTOM = 0x00, + READ_COILS = 0x01, + READ_DISCRETE_INPUTS = 0x02, + READ_HOLDING_REGISTERS = 0x03, + READ_INPUT_REGISTERS = 0x04, + WRITE_SINGLE_COIL = 0x05, + WRITE_SINGLE_REGISTER = 0x06, + READ_EXCEPTION_STATUS = 0x07, // not implemented + DIAGNOSTICS = 0x08, // not implemented + GET_COMM_EVENT_COUNTER = 0x0B, // not implemented + GET_COMM_EVENT_LOG = 0x0C, // not implemented + WRITE_MULTIPLE_COILS = 0x0F, + WRITE_MULTIPLE_REGISTERS = 0x10, + REPORT_SERVER_ID = 0x11, // not implemented + READ_FILE_RECORD = 0x14, // not implemented + WRITE_FILE_RECORD = 0x15, // not implemented + MASK_WRITE_REGISTER = 0x16, // not implemented + READ_WRITE_MULTIPLE_REGISTERS = 0x17, // not implemented + READ_FIFO_QUEUE = 0x18, // not implemented +}; + +/*Allow comparison operators between ModbusFunctionCode and uint8_t*/ +inline bool operator==(ModbusFunctionCode lhs, uint8_t rhs) { return static_cast(lhs) == rhs; } +inline bool operator==(uint8_t lhs, ModbusFunctionCode rhs) { return lhs == static_cast(rhs); } +inline bool operator!=(ModbusFunctionCode lhs, uint8_t rhs) { return !(static_cast(lhs) == rhs); } +inline bool operator!=(uint8_t lhs, ModbusFunctionCode rhs) { return !(lhs == static_cast(rhs)); } +inline bool operator<(ModbusFunctionCode lhs, uint8_t rhs) { return static_cast(lhs) < rhs; } +inline bool operator<(uint8_t lhs, ModbusFunctionCode rhs) { return lhs < static_cast(rhs); } +inline bool operator<=(ModbusFunctionCode lhs, uint8_t rhs) { return static_cast(lhs) <= rhs; } +inline bool operator<=(uint8_t lhs, ModbusFunctionCode rhs) { return lhs <= static_cast(rhs); } +inline bool operator>(ModbusFunctionCode lhs, uint8_t rhs) { return static_cast(lhs) > rhs; } +inline bool operator>(uint8_t lhs, ModbusFunctionCode rhs) { return lhs > static_cast(rhs); } +inline bool operator>=(ModbusFunctionCode lhs, uint8_t rhs) { return static_cast(lhs) >= rhs; } +inline bool operator>=(uint8_t lhs, ModbusFunctionCode rhs) { return lhs >= static_cast(rhs); } + +// 4.3 MODBUS Data model +enum class ModbusRegisterType : uint8_t { + CUSTOM = 0x00, + COIL = 0x01, + DISCRETE_INPUT = 0x02, + HOLDING = 0x03, + READ = 0x04, +}; + +// 7 MODBUS Exception Responses: +const uint8_t FUNCTION_CODE_MASK = 0x7F; +const uint8_t FUNCTION_CODE_EXCEPTION_MASK = 0x80; + +enum class ModbusExceptionCode : uint8_t { + ILLEGAL_FUNCTION = 0x01, + ILLEGAL_DATA_ADDRESS = 0x02, + ILLEGAL_DATA_VALUE = 0x03, + SERVICE_DEVICE_FAILURE = 0x04, + ACKNOWLEDGE = 0x05, + SERVER_DEVICE_BUSY = 0x06, + MEMORY_PARITY_ERROR = 0x08, + GATEWAY_PATH_UNAVAILABLE = 0x0A, + GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND = 0x0B, +}; + +// 6.12 16 (0x10) Write Multiple registers: +const uint8_t MAX_NUM_OF_REGISTERS_TO_WRITE = 123; // 0x7B + +// 6.3 03 (0x03) Read Holding Registers +// 6.4 04 (0x04) Read Input Registers +const uint8_t MAX_NUM_OF_REGISTERS_TO_READ = 125; // 0x7D +/// End of Modbus definitions +} // namespace modbus +} // namespace esphome diff --git a/esphome/components/modbus_controller/__init__.py b/esphome/components/modbus_controller/__init__.py index 5ab82f5e17..1c23783ce3 100644 --- a/esphome/components/modbus_controller/__init__.py +++ b/esphome/components/modbus_controller/__init__.py @@ -3,6 +3,7 @@ import binascii from esphome import automation import esphome.codegen as cg from esphome.components import modbus +from esphome.components.const import CONF_ENABLED import esphome.config_validation as cv from esphome.const import ( CONF_ADDRESS, @@ -28,8 +29,11 @@ from .const import ( CONF_ON_OFFLINE, CONF_ON_ONLINE, CONF_REGISTER_COUNT, + CONF_REGISTER_LAST_ADDRESS, CONF_REGISTER_TYPE, + CONF_REGISTER_VALUE, CONF_RESPONSE_SIZE, + CONF_SERVER_COURTESY_RESPONSE, CONF_SKIP_UPDATES, CONF_VALUE_TYPE, ) @@ -49,6 +53,7 @@ ModbusController = modbus_controller_ns.class_( ) SensorItem = modbus_controller_ns.struct("SensorItem") +ServerCourtesyResponse = modbus_controller_ns.struct("ServerCourtesyResponse") ServerRegister = modbus_controller_ns.struct("ServerRegister") ModbusFunctionCode_ns = modbus_controller_ns.namespace("ModbusFunctionCode") @@ -143,6 +148,14 @@ ModbusOfflineTrigger = modbus_controller_ns.class_( _LOGGER = logging.getLogger(__name__) +SERVER_COURTESY_RESPONSE_SCHEMA = cv.Schema( + { + cv.Optional(CONF_ENABLED, default=False): cv.boolean, + cv.Optional(CONF_REGISTER_LAST_ADDRESS, default=0xFFFF): cv.hex_uint16_t, + cv.Optional(CONF_REGISTER_VALUE, default=0): cv.hex_uint16_t, + } +) + ModbusServerRegisterSchema = cv.Schema( { cv.GenerateID(): cv.declare_id(ServerRegister), @@ -162,6 +175,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional( CONF_COMMAND_THROTTLE, default="0ms" ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_SERVER_COURTESY_RESPONSE): SERVER_COURTESY_RESPONSE_SCHEMA, cv.Optional(CONF_MAX_CMD_RETRIES, default=4): cv.positive_int, cv.Optional(CONF_OFFLINE_SKIP_UPDATES, default=0): cv.positive_int, cv.Optional( @@ -232,7 +246,7 @@ def validate_modbus_register(config): def _final_validate(config): - if CONF_SERVER_REGISTERS in config: + if CONF_SERVER_COURTESY_RESPONSE in config or CONF_SERVER_REGISTERS in config: return modbus.final_validate_modbus_device("modbus_controller", role="server")( config ) @@ -299,6 +313,20 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) cg.add(var.set_allow_duplicate_commands(config[CONF_ALLOW_DUPLICATE_COMMANDS])) cg.add(var.set_command_throttle(config[CONF_COMMAND_THROTTLE])) + if server_courtesy_response := config.get(CONF_SERVER_COURTESY_RESPONSE): + cg.add( + var.set_server_courtesy_response( + cg.StructInitializer( + ServerCourtesyResponse, + ("enabled", server_courtesy_response[CONF_ENABLED]), + ( + "register_last_address", + server_courtesy_response[CONF_REGISTER_LAST_ADDRESS], + ), + ("register_value", server_courtesy_response[CONF_REGISTER_VALUE]), + ) + ) + ) cg.add(var.set_max_cmd_retries(config[CONF_MAX_CMD_RETRIES])) cg.add(var.set_offline_skip_updates(config[CONF_OFFLINE_SKIP_UPDATES])) if CONF_SERVER_REGISTERS in config: diff --git a/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.h b/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.h index 3a017c6f88..119f4fdd5a 100644 --- a/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.h +++ b/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.h @@ -33,8 +33,8 @@ class ModbusBinarySensor : public Component, public binary_sensor::BinarySensor, void dump_config() override; - using transform_func_t = std::function(ModbusBinarySensor *, bool, const std::vector &)>; - void set_template(transform_func_t &&f) { this->transform_func_ = f; } + using transform_func_t = optional (*)(ModbusBinarySensor *, bool, const std::vector &); + void set_template(transform_func_t f) { this->transform_func_ = f; } protected: optional transform_func_{nullopt}; diff --git a/esphome/components/modbus_controller/const.py b/esphome/components/modbus_controller/const.py index 4d39e48dcd..c689d84576 100644 --- a/esphome/components/modbus_controller/const.py +++ b/esphome/components/modbus_controller/const.py @@ -13,8 +13,11 @@ CONF_ON_ONLINE = "on_online" CONF_ON_OFFLINE = "on_offline" CONF_RAW_ENCODE = "raw_encode" CONF_REGISTER_COUNT = "register_count" +CONF_REGISTER_LAST_ADDRESS = "register_last_address" CONF_REGISTER_TYPE = "register_type" +CONF_REGISTER_VALUE = "register_value" CONF_RESPONSE_SIZE = "response_size" +CONF_SERVER_COURTESY_RESPONSE = "server_courtesy_response" CONF_SKIP_UPDATES = "skip_updates" CONF_USE_WRITE_MULTIPLE = "use_write_multiple" CONF_VALUE_TYPE = "value_type" diff --git a/esphome/components/modbus_controller/modbus_controller.cpp b/esphome/components/modbus_controller/modbus_controller.cpp index 0f3ddf920d..50bd9f45cb 100644 --- a/esphome/components/modbus_controller/modbus_controller.cpp +++ b/esphome/components/modbus_controller/modbus_controller.cpp @@ -112,6 +112,12 @@ void ModbusController::on_modbus_read_registers(uint8_t function_code, uint16_t "0x%X.", this->address_, function_code, start_address, number_of_registers); + if (number_of_registers == 0 || number_of_registers > modbus::MAX_NUM_OF_REGISTERS_TO_READ) { + ESP_LOGW(TAG, "Invalid number of registers %d. Sending exception response.", number_of_registers); + this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_ADDRESS); + return; + } + std::vector sixteen_bit_response; for (uint16_t current_address = start_address; current_address < start_address + number_of_registers;) { bool found = false; @@ -136,9 +142,21 @@ void ModbusController::on_modbus_read_registers(uint8_t function_code, uint16_t } if (!found) { - ESP_LOGW(TAG, "Could not match any register to address %02X. Sending exception response.", current_address); - send_error(function_code, 0x02); - return; + if (this->server_courtesy_response_.enabled && + (current_address <= this->server_courtesy_response_.register_last_address)) { + ESP_LOGD(TAG, + "Could not match any register to address 0x%02X, but default allowed. " + "Returning default value: %d.", + current_address, this->server_courtesy_response_.register_value); + sixteen_bit_response.push_back(this->server_courtesy_response_.register_value); + current_address += 1; // Just increment by 1, as the default response is a single register + } else { + ESP_LOGW(TAG, + "Could not match any register to address 0x%02X and default not allowed. Sending exception response.", + current_address); + this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_ADDRESS); + return; + } } } @@ -156,27 +174,27 @@ void ModbusController::on_modbus_write_registers(uint8_t function_code, const st uint16_t number_of_registers; uint16_t payload_offset; - if (function_code == 0x10) { + if (function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) { number_of_registers = uint16_t(data[3]) | (uint16_t(data[2]) << 8); - if (number_of_registers == 0 || number_of_registers > 0x7B) { + if (number_of_registers == 0 || number_of_registers > modbus::MAX_NUM_OF_REGISTERS_TO_WRITE) { ESP_LOGW(TAG, "Invalid number of registers %d. Sending exception response.", number_of_registers); - send_error(function_code, 3); + this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE); return; } uint16_t payload_size = data[4]; if (payload_size != number_of_registers * 2) { ESP_LOGW(TAG, "Payload size of %d bytes is not 2 times the number of registers (%d). Sending exception response.", payload_size, number_of_registers); - send_error(function_code, 3); + this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE); return; } payload_offset = 5; - } else if (function_code == 0x06) { + } else if (function_code == ModbusFunctionCode::WRITE_SINGLE_REGISTER) { number_of_registers = 1; payload_offset = 2; } else { ESP_LOGW(TAG, "Invalid function code 0x%X. Sending exception response.", function_code); - send_error(function_code, 1); + this->send_error(function_code, ModbusExceptionCode::ILLEGAL_FUNCTION); return; } @@ -211,7 +229,7 @@ void ModbusController::on_modbus_write_registers(uint8_t function_code, const st if (!for_each_register([](ServerRegister *server_register, uint16_t offset) -> bool { return server_register->write_lambda != nullptr; })) { - send_error(function_code, 1); + this->send_error(function_code, ModbusExceptionCode::ILLEGAL_FUNCTION); return; } @@ -220,7 +238,7 @@ void ModbusController::on_modbus_write_registers(uint8_t function_code, const st int64_t number = payload_to_number(data, server_register->value_type, offset, 0xFFFFFFFF); return server_register->write_lambda(number); })) { - send_error(function_code, 4); + this->send_error(function_code, ModbusExceptionCode::SERVICE_DEVICE_FAILURE); return; } @@ -431,8 +449,15 @@ void ModbusController::dump_config() { "ModbusController:\n" " Address: 0x%02X\n" " Max Command Retries: %d\n" - " Offline Skip Updates: %d", - this->address_, this->max_cmd_retries_, this->offline_skip_updates_); + " Offline Skip Updates: %d\n" + " Server Courtesy Response:\n" + " Enabled: %s\n" + " Register Last Address: 0x%02X\n" + " Register Value: %d", + this->address_, this->max_cmd_retries_, this->offline_skip_updates_, + this->server_courtesy_response_.enabled ? "true" : "false", + this->server_courtesy_response_.register_last_address, this->server_courtesy_response_.register_value); + #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE ESP_LOGCONFIG(TAG, "sensormap"); for (auto &it : this->sensorset_) { diff --git a/esphome/components/modbus_controller/modbus_controller.h b/esphome/components/modbus_controller/modbus_controller.h index a86ad1ccb5..6ed05715cb 100644 --- a/esphome/components/modbus_controller/modbus_controller.h +++ b/esphome/components/modbus_controller/modbus_controller.h @@ -16,35 +16,9 @@ namespace modbus_controller { class ModbusController; -enum class ModbusFunctionCode { - CUSTOM = 0x00, - READ_COILS = 0x01, - READ_DISCRETE_INPUTS = 0x02, - READ_HOLDING_REGISTERS = 0x03, - READ_INPUT_REGISTERS = 0x04, - WRITE_SINGLE_COIL = 0x05, - WRITE_SINGLE_REGISTER = 0x06, - READ_EXCEPTION_STATUS = 0x07, // not implemented - DIAGNOSTICS = 0x08, // not implemented - GET_COMM_EVENT_COUNTER = 0x0B, // not implemented - GET_COMM_EVENT_LOG = 0x0C, // not implemented - WRITE_MULTIPLE_COILS = 0x0F, - WRITE_MULTIPLE_REGISTERS = 0x10, - REPORT_SERVER_ID = 0x11, // not implemented - READ_FILE_RECORD = 0x14, // not implemented - WRITE_FILE_RECORD = 0x15, // not implemented - MASK_WRITE_REGISTER = 0x16, // not implemented - READ_WRITE_MULTIPLE_REGISTERS = 0x17, // not implemented - READ_FIFO_QUEUE = 0x18, // not implemented -}; - -enum class ModbusRegisterType : uint8_t { - CUSTOM = 0x0, - COIL = 0x01, - DISCRETE_INPUT = 0x02, - HOLDING = 0x03, - READ = 0x04, -}; +using modbus::ModbusFunctionCode; +using modbus::ModbusRegisterType; +using modbus::ModbusExceptionCode; enum class SensorValueType : uint8_t { RAW = 0x00, // variable length @@ -256,6 +230,12 @@ class SensorItem { bool force_new_range{false}; }; +struct ServerCourtesyResponse { + bool enabled{false}; + uint16_t register_last_address{0xFFFF}; + uint16_t register_value{0}; +}; + class ServerRegister { using ReadLambda = std::function; using WriteLambda = std::function; @@ -530,6 +510,12 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice { void set_max_cmd_retries(uint8_t max_cmd_retries) { this->max_cmd_retries_ = max_cmd_retries; } /// get how many times a command will be (re)sent if no response is received uint8_t get_max_cmd_retries() { return this->max_cmd_retries_; } + /// Called by esphome generated code to set the server courtesy response object + void set_server_courtesy_response(const ServerCourtesyResponse &server_courtesy_response) { + this->server_courtesy_response_ = server_courtesy_response; + } + /// Get the server courtesy response object + ServerCourtesyResponse get_server_courtesy_response() const { return this->server_courtesy_response_; } protected: /// parse sensormap_ and create range of sequential addresses @@ -572,6 +558,9 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice { CallbackManager online_callback_{}; /// Server offline callback CallbackManager offline_callback_{}; + /// Server courtesy response + ServerCourtesyResponse server_courtesy_response_{ + .enabled = false, .register_last_address = 0xFFFF, .register_value = 0}; }; /** Convert vector response payload to float. diff --git a/esphome/components/modbus_controller/number/modbus_number.h b/esphome/components/modbus_controller/number/modbus_number.h index 8f77b2e014..169f85ff36 100644 --- a/esphome/components/modbus_controller/number/modbus_number.h +++ b/esphome/components/modbus_controller/number/modbus_number.h @@ -31,10 +31,10 @@ class ModbusNumber : public number::Number, public Component, public SensorItem void set_parent(ModbusController *parent) { this->parent_ = parent; } void set_write_multiply(float factor) { this->multiply_by_ = factor; } - using transform_func_t = std::function(ModbusNumber *, float, const std::vector &)>; - using write_transform_func_t = std::function(ModbusNumber *, float, std::vector &)>; - void set_template(transform_func_t &&f) { this->transform_func_ = f; } - void set_write_template(write_transform_func_t &&f) { this->write_transform_func_ = f; } + using transform_func_t = optional (*)(ModbusNumber *, float, const std::vector &); + using write_transform_func_t = optional (*)(ModbusNumber *, float, std::vector &); + void set_template(transform_func_t f) { this->transform_func_ = f; } + void set_write_template(write_transform_func_t f) { this->write_transform_func_ = f; } void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; } protected: diff --git a/esphome/components/modbus_controller/output/modbus_output.h b/esphome/components/modbus_controller/output/modbus_output.h index bceb97affb..0fb4bb89ea 100644 --- a/esphome/components/modbus_controller/output/modbus_output.h +++ b/esphome/components/modbus_controller/output/modbus_output.h @@ -29,8 +29,8 @@ class ModbusFloatOutput : public output::FloatOutput, public Component, public S // Do nothing void parse_and_publish(const std::vector &data) override{}; - using write_transform_func_t = std::function(ModbusFloatOutput *, float, std::vector &)>; - void set_write_template(write_transform_func_t &&f) { this->write_transform_func_ = f; } + using write_transform_func_t = optional (*)(ModbusFloatOutput *, float, std::vector &); + void set_write_template(write_transform_func_t f) { this->write_transform_func_ = f; } void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; } protected: @@ -60,8 +60,8 @@ class ModbusBinaryOutput : public output::BinaryOutput, public Component, public // Do nothing void parse_and_publish(const std::vector &data) override{}; - using write_transform_func_t = std::function(ModbusBinaryOutput *, bool, std::vector &)>; - void set_write_template(write_transform_func_t &&f) { this->write_transform_func_ = f; } + using write_transform_func_t = optional (*)(ModbusBinaryOutput *, bool, std::vector &); + void set_write_template(write_transform_func_t f) { this->write_transform_func_ = f; } void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; } protected: diff --git a/esphome/components/modbus_controller/select/modbus_select.cpp b/esphome/components/modbus_controller/select/modbus_select.cpp index 56b8c783ed..853f4215c3 100644 --- a/esphome/components/modbus_controller/select/modbus_select.cpp +++ b/esphome/components/modbus_controller/select/modbus_select.cpp @@ -28,8 +28,9 @@ void ModbusSelect::parse_and_publish(const std::vector &data) { if (map_it != this->mapping_.cend()) { size_t idx = std::distance(this->mapping_.cbegin(), map_it); - new_state = this->traits.get_options()[idx]; - ESP_LOGV(TAG, "Found option %s for value %lld", new_state->c_str(), value); + ESP_LOGV(TAG, "Found option %s for value %lld", this->option_at(idx), value); + this->publish_state(idx); + return; } else { ESP_LOGE(TAG, "No option found for mapping %lld", value); } @@ -40,17 +41,16 @@ void ModbusSelect::parse_and_publish(const std::vector &data) { } } -void ModbusSelect::control(const std::string &value) { - auto options = this->traits.get_options(); - auto opt_it = std::find(options.cbegin(), options.cend(), value); - size_t idx = std::distance(options.cbegin(), opt_it); - optional mapval = this->mapping_[idx]; - ESP_LOGD(TAG, "Found value %lld for option '%s'", *mapval, value.c_str()); +void ModbusSelect::control(size_t index) { + optional mapval = this->mapping_[index]; + const char *option = this->option_at(index); + ESP_LOGD(TAG, "Found value %lld for option '%s'", *mapval, option); std::vector data; if (this->write_transform_func_.has_value()) { - auto val = (*this->write_transform_func_)(this, value, *mapval, data); + // Transform func requires string parameter for backward compatibility + auto val = (*this->write_transform_func_)(this, std::string(option), *mapval, data); if (val.has_value()) { mapval = *val; ESP_LOGV(TAG, "write_lambda returned mapping value %lld", *mapval); @@ -83,7 +83,7 @@ void ModbusSelect::control(const std::string &value) { this->parent_->queue_command(write_cmd); if (this->optimistic_) - this->publish_state(value); + this->publish_state(index); } } // namespace modbus_controller diff --git a/esphome/components/modbus_controller/select/modbus_select.h b/esphome/components/modbus_controller/select/modbus_select.h index 55fb2107dd..fde441f2bc 100644 --- a/esphome/components/modbus_controller/select/modbus_select.h +++ b/esphome/components/modbus_controller/select/modbus_select.h @@ -26,20 +26,19 @@ class ModbusSelect : public Component, public select::Select, public SensorItem this->mapping_ = std::move(mapping); } - using transform_func_t = - std::function(ModbusSelect *const, int64_t, const std::vector &)>; - using write_transform_func_t = - std::function(ModbusSelect *const, const std::string &, int64_t, std::vector &)>; + using transform_func_t = optional (*)(ModbusSelect *const, int64_t, const std::vector &); + using write_transform_func_t = optional (*)(ModbusSelect *const, const std::string &, int64_t, + std::vector &); void set_parent(ModbusController *const parent) { this->parent_ = parent; } void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; } void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } - void set_template(transform_func_t &&f) { this->transform_func_ = f; } - void set_write_template(write_transform_func_t &&f) { this->write_transform_func_ = f; } + void set_template(transform_func_t f) { this->transform_func_ = f; } + void set_write_template(write_transform_func_t f) { this->write_transform_func_ = f; } void dump_config() override; void parse_and_publish(const std::vector &data) override; - void control(const std::string &value) override; + void control(size_t index) override; protected: std::vector mapping_{}; diff --git a/esphome/components/modbus_controller/sensor/modbus_sensor.h b/esphome/components/modbus_controller/sensor/modbus_sensor.h index 65eb487c1c..ba943c873c 100644 --- a/esphome/components/modbus_controller/sensor/modbus_sensor.h +++ b/esphome/components/modbus_controller/sensor/modbus_sensor.h @@ -25,9 +25,9 @@ class ModbusSensor : public Component, public sensor::Sensor, public SensorItem void parse_and_publish(const std::vector &data) override; void dump_config() override; - using transform_func_t = std::function(ModbusSensor *, float, const std::vector &)>; + using transform_func_t = optional (*)(ModbusSensor *, float, const std::vector &); - void set_template(transform_func_t &&f) { this->transform_func_ = f; } + void set_template(transform_func_t f) { this->transform_func_ = f; } protected: optional transform_func_{nullopt}; diff --git a/esphome/components/modbus_controller/switch/modbus_switch.h b/esphome/components/modbus_controller/switch/modbus_switch.h index 0098076ef4..301c2bf548 100644 --- a/esphome/components/modbus_controller/switch/modbus_switch.h +++ b/esphome/components/modbus_controller/switch/modbus_switch.h @@ -34,10 +34,10 @@ class ModbusSwitch : public Component, public switch_::Switch, public SensorItem void parse_and_publish(const std::vector &data) override; void set_parent(ModbusController *parent) { this->parent_ = parent; } - using transform_func_t = std::function(ModbusSwitch *, bool, const std::vector &)>; - using write_transform_func_t = std::function(ModbusSwitch *, bool, std::vector &)>; - void set_template(transform_func_t &&f) { this->publish_transform_func_ = f; } - void set_write_template(write_transform_func_t &&f) { this->write_transform_func_ = f; } + using transform_func_t = optional (*)(ModbusSwitch *, bool, const std::vector &); + using write_transform_func_t = optional (*)(ModbusSwitch *, bool, std::vector &); + void set_template(transform_func_t f) { this->publish_transform_func_ = f; } + void set_write_template(write_transform_func_t f) { this->write_transform_func_ = f; } void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; } protected: diff --git a/esphome/components/modbus_controller/text_sensor/modbus_textsensor.h b/esphome/components/modbus_controller/text_sensor/modbus_textsensor.h index d6eb5fd230..6666aea976 100644 --- a/esphome/components/modbus_controller/text_sensor/modbus_textsensor.h +++ b/esphome/components/modbus_controller/text_sensor/modbus_textsensor.h @@ -30,9 +30,8 @@ class ModbusTextSensor : public Component, public text_sensor::TextSensor, publi void dump_config() override; void parse_and_publish(const std::vector &data) override; - using transform_func_t = - std::function(ModbusTextSensor *, std::string, const std::vector &)>; - void set_template(transform_func_t &&f) { this->transform_func_ = f; } + using transform_func_t = optional (*)(ModbusTextSensor *, std::string, const std::vector &); + void set_template(transform_func_t f) { this->transform_func_ = f; } protected: optional transform_func_{nullopt}; diff --git a/esphome/components/mopeka_std_check/mopeka_std_check.cpp b/esphome/components/mopeka_std_check/mopeka_std_check.cpp index 6685a23c41..0d8340f95f 100644 --- a/esphome/components/mopeka_std_check/mopeka_std_check.cpp +++ b/esphome/components/mopeka_std_check/mopeka_std_check.cpp @@ -72,7 +72,7 @@ bool MopekaStdCheck::parse_device(const esp32_ble_tracker::ESPBTDevice &device) const u_int8_t hardware_id = mopeka_data->data_1 & 0xCF; if (static_cast(hardware_id) != STANDARD && static_cast(hardware_id) != XL && - static_cast(hardware_id) != ETRAILER) { + static_cast(hardware_id) != ETRAILER && static_cast(hardware_id) != STANDARD_ALT) { ESP_LOGE(TAG, "[%s] Unsupported Sensor Type (0x%X)", device.address_str().c_str(), hardware_id); return false; } diff --git a/esphome/components/mopeka_std_check/mopeka_std_check.h b/esphome/components/mopeka_std_check/mopeka_std_check.h index b92445df34..897b5414ed 100644 --- a/esphome/components/mopeka_std_check/mopeka_std_check.h +++ b/esphome/components/mopeka_std_check/mopeka_std_check.h @@ -15,6 +15,7 @@ namespace mopeka_std_check { enum SensorType { STANDARD = 0x02, XL = 0x03, + STANDARD_ALT = 0x44, ETRAILER = 0x46, }; diff --git a/esphome/components/mpl3115a2/mpl3115a2.cpp b/esphome/components/mpl3115a2/mpl3115a2.cpp index 9e8467a29b..a689149c89 100644 --- a/esphome/components/mpl3115a2/mpl3115a2.cpp +++ b/esphome/components/mpl3115a2/mpl3115a2.cpp @@ -10,7 +10,7 @@ static const char *const TAG = "mpl3115a2"; void MPL3115A2Component::setup() { uint8_t whoami = 0xFF; - if (!this->read_byte(MPL3115A2_WHOAMI, &whoami, false)) { + if (!this->read_byte(MPL3115A2_WHOAMI, &whoami)) { this->error_code_ = COMMUNICATION_FAILED; this->mark_failed(); return; @@ -54,24 +54,24 @@ void MPL3115A2Component::dump_config() { void MPL3115A2Component::update() { uint8_t mode = MPL3115A2_CTRL_REG1_OS128; - this->write_byte(MPL3115A2_CTRL_REG1, mode, true); + this->write_byte(MPL3115A2_CTRL_REG1, mode); // Trigger a new reading mode |= MPL3115A2_CTRL_REG1_OST; if (this->altitude_ != nullptr) mode |= MPL3115A2_CTRL_REG1_ALT; - this->write_byte(MPL3115A2_CTRL_REG1, mode, true); + this->write_byte(MPL3115A2_CTRL_REG1, mode); // Wait until status shows reading available uint8_t status = 0; - if (!this->read_byte(MPL3115A2_REGISTER_STATUS, &status, false) || (status & MPL3115A2_REGISTER_STATUS_PDR) == 0) { + if (!this->read_byte(MPL3115A2_REGISTER_STATUS, &status) || (status & MPL3115A2_REGISTER_STATUS_PDR) == 0) { delay(10); - if (!this->read_byte(MPL3115A2_REGISTER_STATUS, &status, false) || (status & MPL3115A2_REGISTER_STATUS_PDR) == 0) { + if (!this->read_byte(MPL3115A2_REGISTER_STATUS, &status) || (status & MPL3115A2_REGISTER_STATUS_PDR) == 0) { return; } } uint8_t buffer[5] = {0, 0, 0, 0, 0}; - this->read_register(MPL3115A2_REGISTER_PRESSURE_MSB, buffer, 5, false); + this->read_register(MPL3115A2_REGISTER_PRESSURE_MSB, buffer, 5); float altitude = 0, pressure = 0; if (this->altitude_ != nullptr) { diff --git a/esphome/components/mpr121/mpr121.cpp b/esphome/components/mpr121/mpr121.cpp index 074bc79ea2..5a8a8e7205 100644 --- a/esphome/components/mpr121/mpr121.cpp +++ b/esphome/components/mpr121/mpr121.cpp @@ -11,47 +11,49 @@ namespace mpr121 { static const char *const TAG = "mpr121"; void MPR121Component::setup() { + this->disable_loop(); // soft reset device this->write_byte(MPR121_SOFTRESET, 0x63); - delay(100); // NOLINT - if (!this->write_byte(MPR121_ECR, 0x0)) { - this->error_code_ = COMMUNICATION_FAILED; - this->mark_failed(); - return; - } + this->set_timeout(100, [this]() { + if (!this->write_byte(MPR121_ECR, 0x0)) { + this->error_code_ = COMMUNICATION_FAILED; + this->mark_failed(); + return; + } + // set touch sensitivity for all 12 channels + for (auto *channel : this->channels_) { + channel->setup(); + } + this->write_byte(MPR121_MHDR, 0x01); + this->write_byte(MPR121_NHDR, 0x01); + this->write_byte(MPR121_NCLR, 0x0E); + this->write_byte(MPR121_FDLR, 0x00); - // set touch sensitivity for all 12 channels - for (auto *channel : this->channels_) { - channel->setup(); - } - this->write_byte(MPR121_MHDR, 0x01); - this->write_byte(MPR121_NHDR, 0x01); - this->write_byte(MPR121_NCLR, 0x0E); - this->write_byte(MPR121_FDLR, 0x00); + this->write_byte(MPR121_MHDF, 0x01); + this->write_byte(MPR121_NHDF, 0x05); + this->write_byte(MPR121_NCLF, 0x01); + this->write_byte(MPR121_FDLF, 0x00); - this->write_byte(MPR121_MHDF, 0x01); - this->write_byte(MPR121_NHDF, 0x05); - this->write_byte(MPR121_NCLF, 0x01); - this->write_byte(MPR121_FDLF, 0x00); + this->write_byte(MPR121_NHDT, 0x00); + this->write_byte(MPR121_NCLT, 0x00); + this->write_byte(MPR121_FDLT, 0x00); - this->write_byte(MPR121_NHDT, 0x00); - this->write_byte(MPR121_NCLT, 0x00); - this->write_byte(MPR121_FDLT, 0x00); + this->write_byte(MPR121_DEBOUNCE, 0); + // default, 16uA charge current + this->write_byte(MPR121_CONFIG1, 0x10); + // 0.5uS encoding, 1ms period + this->write_byte(MPR121_CONFIG2, 0x20); - this->write_byte(MPR121_DEBOUNCE, 0); - // default, 16uA charge current - this->write_byte(MPR121_CONFIG1, 0x10); - // 0.5uS encoding, 1ms period - this->write_byte(MPR121_CONFIG2, 0x20); + // Write the Electrode Configuration Register + // * Highest 2 bits is "Calibration Lock", which we set to a value corresponding to 5 bits. + // * The 2 bits below is "Proximity Enable" and are left at 0. + // * The 4 least significant bits control how many electrodes are enabled. Electrodes are enabled + // as a range, starting at 0 up to the highest channel index used. + this->write_byte(MPR121_ECR, 0x80 | (this->max_touch_channel_ + 1)); - // Write the Electrode Configuration Register - // * Highest 2 bits is "Calibration Lock", which we set to a value corresponding to 5 bits. - // * The 2 bits below is "Proximity Enable" and are left at 0. - // * The 4 least significant bits control how many electrodes are enabled. Electrodes are enabled - // as a range, starting at 0 up to the highest channel index used. - this->write_byte(MPR121_ECR, 0x80 | (this->max_touch_channel_ + 1)); - - this->flush_gpio_(); + this->flush_gpio_(); + this->enable_loop(); + }); } void MPR121Component::set_touch_debounce(uint8_t debounce) { @@ -73,9 +75,6 @@ void MPR121Component::dump_config() { case COMMUNICATION_FAILED: ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); break; - case WRONG_CHIP_STATE: - ESP_LOGE(TAG, "MPR121 has wrong default value for CONFIG2?"); - break; case NONE: default: break; diff --git a/esphome/components/mpr121/mpr121.h b/esphome/components/mpr121/mpr121.h index eb2e2edc57..6dd2c38309 100644 --- a/esphome/components/mpr121/mpr121.h +++ b/esphome/components/mpr121/mpr121.h @@ -88,7 +88,6 @@ class MPR121Component : public Component, public i2c::I2CDevice { enum ErrorCode { NONE = 0, COMMUNICATION_FAILED, - WRONG_CHIP_STATE, } error_code_{NONE}; bool flush_gpio_(); diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 52d3181780..1fc0c30db1 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -3,7 +3,7 @@ import re from esphome import automation from esphome.automation import Condition import esphome.codegen as cg -from esphome.components import logger +from esphome.components import logger, socket from esphome.components.esp32 import add_idf_sdkconfig_option from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv @@ -57,7 +57,8 @@ from esphome.const import ( PLATFORM_ESP8266, PlatformFramework, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority +from esphome.types import ConfigType DEPENDENCIES = ["network"] @@ -65,6 +66,9 @@ DEPENDENCIES = ["network"] def AUTO_LOAD(): if CORE.is_esp8266 or CORE.is_libretiny: return ["async_tcp", "json"] + # ESP32 needs socket for wake_loop_threadsafe() + if CORE.is_esp32: + return ["json", "socket"] return ["json"] @@ -210,6 +214,13 @@ def validate_fingerprint(value): return value +def _consume_mqtt_sockets(config: ConfigType) -> ConfigType: + """Register socket needs for MQTT component.""" + # MQTT needs 1 socket for the broker connection + socket.consume_sockets(1, "mqtt")(config) + return config + + CONFIG_SCHEMA = cv.All( cv.Schema( { @@ -306,6 +317,7 @@ CONFIG_SCHEMA = cv.All( ), validate_config, cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX]), + _consume_mqtt_sockets, ) @@ -321,7 +333,7 @@ def exp_mqtt_message(config): ) -@coroutine_with_priority(40.0) +@coroutine_with_priority(CoroPriority.WEB) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) @@ -330,6 +342,11 @@ async def to_code(config): # https://github.com/heman/async-mqtt-client/blob/master/library.json cg.add_library("heman/AsyncMqttClient-esphome", "2.0.0") + # MQTT on ESP32 uses wake_loop_threadsafe() to wake the main loop from the MQTT event handler + # This enables low-latency MQTT event processing instead of waiting for select() timeout + if CORE.is_esp32: + socket.require_wake_loop_threadsafe() + cg.add_define("USE_MQTT") cg.add_global(mqtt_ns.using) diff --git a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp index 94460c31a7..dd3df5f8aa 100644 --- a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp +++ b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp @@ -36,7 +36,7 @@ void MQTTAlarmControlPanelComponent::setup() { } else if (strcasecmp(payload.c_str(), "TRIGGERED") == 0) { call.triggered(); } else { - ESP_LOGW(TAG, "'%s': Received unknown command payload %s", this->friendly_name().c_str(), payload.c_str()); + ESP_LOGW(TAG, "'%s': Received unknown command payload %s", this->friendly_name_().c_str(), payload.c_str()); } call.perform(); }); diff --git a/esphome/components/mqtt/mqtt_backend_esp32.cpp b/esphome/components/mqtt/mqtt_backend_esp32.cpp index 623206a0cd..dcc51ed60e 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.cpp +++ b/esphome/components/mqtt/mqtt_backend_esp32.cpp @@ -190,6 +190,11 @@ void MQTTBackendESP32::mqtt_event_handler(void *handler_args, esp_event_base_t b if (instance) { auto event = *static_cast(event_data); instance->mqtt_events_.emplace(event); + + // Wake main loop immediately to process MQTT event instead of waiting for select() timeout +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + App.wake_loop_threadsafe(); +#endif } } diff --git a/esphome/components/mqtt/mqtt_binary_sensor.cpp b/esphome/components/mqtt/mqtt_binary_sensor.cpp index 2ce4928574..479cee205a 100644 --- a/esphome/components/mqtt/mqtt_binary_sensor.cpp +++ b/esphome/components/mqtt/mqtt_binary_sensor.cpp @@ -30,9 +30,12 @@ MQTTBinarySensorComponent::MQTTBinarySensorComponent(binary_sensor::BinarySensor } void MQTTBinarySensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { - // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - if (!this->binary_sensor_->get_device_class().empty()) - root[MQTT_DEVICE_CLASS] = this->binary_sensor_->get_device_class(); + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + const auto device_class = this->binary_sensor_->get_device_class_ref(); + if (!device_class.empty()) { + root[MQTT_DEVICE_CLASS] = device_class; + } + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) if (this->binary_sensor_->is_status_binary_sensor()) root[MQTT_PAYLOAD_ON] = mqtt::global_mqtt_client->get_availability().payload_available; if (this->binary_sensor_->is_status_binary_sensor()) diff --git a/esphome/components/mqtt/mqtt_button.cpp b/esphome/components/mqtt/mqtt_button.cpp index b3435edf38..f8eb0eab2d 100644 --- a/esphome/components/mqtt/mqtt_button.cpp +++ b/esphome/components/mqtt/mqtt_button.cpp @@ -20,7 +20,7 @@ void MQTTButtonComponent::setup() { if (payload == "PRESS") { this->button_->press(); } else { - ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name().c_str(), payload.c_str()); + ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name_().c_str(), payload.c_str()); this->status_momentary_warning("state", 5000); } }); @@ -33,8 +33,9 @@ void MQTTButtonComponent::dump_config() { void MQTTButtonComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson config.state_topic = false; - if (!this->button_->get_device_class().empty()) { - root[MQTT_DEVICE_CLASS] = this->button_->get_device_class(); + const auto device_class = this->button_->get_device_class_ref(); + if (!device_class.empty()) { + root[MQTT_DEVICE_CLASS] = device_class; } // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) } diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 7675280f1a..a810d98adf 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -29,7 +29,8 @@ static const char *const TAG = "mqtt"; MQTTClientComponent::MQTTClientComponent() { global_mqtt_client = this; - this->credentials_.client_id = App.get_name() + "-" + get_mac_address(); + const std::string mac_addr = get_mac_address(); + this->credentials_.client_id = make_name_with_suffix(App.get_name(), '-', mac_addr.c_str(), mac_addr.size()); } // Connection @@ -139,11 +140,8 @@ void MQTTClientComponent::send_device_info_() { #endif #ifdef USE_API_NOISE - if (api::global_api_server->get_noise_ctx()->has_psk()) { - root["api_encryption"] = "Noise_NNpsk0_25519_ChaChaPoly_SHA256"; - } else { - root["api_encryption_supported"] = "Noise_NNpsk0_25519_ChaChaPoly_SHA256"; - } + root[api::global_api_server->get_noise_ctx().has_psk() ? "api_encryption" : "api_encryption_supported"] = + "Noise_NNpsk0_25519_ChaChaPoly_SHA256"; #endif }, 2, this->discovery_info_.retain); @@ -491,7 +489,7 @@ bool MQTTClientComponent::publish(const std::string &topic, const std::string &p bool MQTTClientComponent::publish(const std::string &topic, const char *payload, size_t payload_length, uint8_t qos, bool retain) { - return publish({.topic = topic, .payload = payload, .qos = qos, .retain = retain}); + return publish({.topic = topic, .payload = std::string(payload, payload_length), .qos = qos, .retain = retain}); } bool MQTTClientComponent::publish(const MQTTMessage &message) { diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index 325ca56f4b..79383ee857 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -389,7 +389,7 @@ template class MQTTPublishAction : public Action { TEMPLATABLE_VALUE(uint8_t, qos) TEMPLATABLE_VALUE(bool, retain) - void play(Ts... x) override { + void play(const Ts &...x) override { this->parent_->publish(this->topic_.value(x...), this->payload_.value(x...), this->qos_.value(x...), this->retain_.value(x...)); } @@ -407,7 +407,7 @@ template class MQTTPublishJsonAction : public Action { void set_payload(std::function payload) { this->payload_ = payload; } - void play(Ts... x) override { + void play(const Ts &...x) override { auto f = std::bind(&MQTTPublishJsonAction::encode_, this, x..., std::placeholders::_1); auto topic = this->topic_.value(x...); auto qos = this->qos_.value(x...); @@ -424,7 +424,7 @@ template class MQTTPublishJsonAction : public Action { template class MQTTConnectedCondition : public Condition { public: MQTTConnectedCondition(MQTTClientComponent *parent) : parent_(parent) {} - bool check(Ts... x) override { return this->parent_->is_connected(); } + bool check(const Ts &...x) override { return this->parent_->is_connected(); } protected: MQTTClientComponent *parent_; @@ -434,7 +434,7 @@ template class MQTTEnableAction : public Action { public: MQTTEnableAction(MQTTClientComponent *parent) : parent_(parent) {} - void play(Ts... x) override { this->parent_->enable(); } + void play(const Ts &...x) override { this->parent_->enable(); } protected: MQTTClientComponent *parent_; @@ -444,7 +444,7 @@ template class MQTTDisableAction : public Action { public: MQTTDisableAction(MQTTClientComponent *parent) : parent_(parent) {} - void play(Ts... x) override { this->parent_->disable(); } + void play(const Ts &...x) override { this->parent_->disable(); } protected: MQTTClientComponent *parent_; diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp index e16f097812..aee2b38942 100644 --- a/esphome/components/mqtt/mqtt_climate.cpp +++ b/esphome/components/mqtt/mqtt_climate.cpp @@ -17,11 +17,11 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson auto traits = this->device_->get_traits(); // current_temperature_topic - if (traits.get_supports_current_temperature()) { + if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) { root[MQTT_CURRENT_TEMPERATURE_TOPIC] = this->get_current_temperature_state_topic(); } // current_humidity_topic - if (traits.get_supports_current_humidity()) { + if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) { root[MQTT_CURRENT_HUMIDITY_TOPIC] = this->get_current_humidity_state_topic(); } // mode_command_topic @@ -45,7 +45,8 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo if (traits.supports_mode(CLIMATE_MODE_HEAT_COOL)) modes.add("heat_cool"); - if (traits.get_supports_two_point_target_temperature()) { + if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | + climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) { // temperature_low_command_topic root[MQTT_TEMPERATURE_LOW_COMMAND_TOPIC] = this->get_target_temperature_low_command_topic(); // temperature_low_state_topic @@ -61,7 +62,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo root[MQTT_TEMPERATURE_STATE_TOPIC] = this->get_target_temperature_state_topic(); } - if (traits.get_supports_target_humidity()) { + if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) { // target_humidity_command_topic root[MQTT_TARGET_HUMIDITY_COMMAND_TOPIC] = this->get_target_humidity_command_topic(); // target_humidity_state_topic @@ -109,7 +110,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo presets.add(preset); } - if (traits.get_supports_action()) { + if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) { // action_topic root[MQTT_ACTION_TOPIC] = this->get_action_state_topic(); } @@ -174,7 +175,8 @@ void MQTTClimateComponent::setup() { call.perform(); }); - if (traits.get_supports_two_point_target_temperature()) { + if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | + climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) { this->subscribe(this->get_target_temperature_low_command_topic(), [this](const std::string &topic, const std::string &payload) { auto val = parse_number(payload); @@ -211,7 +213,7 @@ void MQTTClimateComponent::setup() { }); } - if (traits.get_supports_target_humidity()) { + if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) { this->subscribe(this->get_target_humidity_command_topic(), [this](const std::string &topic, const std::string &payload) { auto val = parse_number(payload); @@ -290,12 +292,14 @@ bool MQTTClimateComponent::publish_state_() { success = false; int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals(); int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals(); - if (traits.get_supports_current_temperature() && !std::isnan(this->device_->current_temperature)) { + if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE) && + !std::isnan(this->device_->current_temperature)) { std::string payload = value_accuracy_to_string(this->device_->current_temperature, current_accuracy); if (!this->publish(this->get_current_temperature_state_topic(), payload)) success = false; } - if (traits.get_supports_two_point_target_temperature()) { + if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | + climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) { std::string payload = value_accuracy_to_string(this->device_->target_temperature_low, target_accuracy); if (!this->publish(this->get_target_temperature_low_state_topic(), payload)) success = false; @@ -308,12 +312,14 @@ bool MQTTClimateComponent::publish_state_() { success = false; } - if (traits.get_supports_current_humidity() && !std::isnan(this->device_->current_humidity)) { + if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY) && + !std::isnan(this->device_->current_humidity)) { std::string payload = value_accuracy_to_string(this->device_->current_humidity, 0); if (!this->publish(this->get_current_humidity_state_topic(), payload)) success = false; } - if (traits.get_supports_target_humidity() && !std::isnan(this->device_->target_humidity)) { + if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY) && + !std::isnan(this->device_->target_humidity)) { std::string payload = value_accuracy_to_string(this->device_->target_humidity, 0); if (!this->publish(this->get_target_humidity_state_topic(), payload)) success = false; @@ -351,13 +357,13 @@ bool MQTTClimateComponent::publish_state_() { payload = "unknown"; } } - if (this->device_->custom_preset.has_value()) - payload = this->device_->custom_preset.value(); + if (this->device_->has_custom_preset()) + payload = this->device_->get_custom_preset(); if (!this->publish(this->get_preset_state_topic(), payload)) success = false; } - if (traits.get_supports_action()) { + if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) { const char *payload; switch (this->device_->action) { case CLIMATE_ACTION_OFF: @@ -423,8 +429,8 @@ bool MQTTClimateComponent::publish_state_() { payload = "unknown"; } } - if (this->device_->custom_fan_mode.has_value()) - payload = this->device_->custom_fan_mode.value(); + if (this->device_->has_custom_fan_mode()) + payload = this->device_->get_custom_fan_mode(); if (!this->publish(this->get_fan_mode_state_topic(), payload)) success = false; } diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index 6ceaf219ff..1cd818964e 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -64,11 +64,11 @@ bool MQTTComponent::send_discovery_() { const MQTTDiscoveryInfo &discovery_info = global_mqtt_client->get_discovery_info(); if (discovery_info.clean) { - ESP_LOGV(TAG, "'%s': Cleaning discovery", this->friendly_name().c_str()); + ESP_LOGV(TAG, "'%s': Cleaning discovery", this->friendly_name_().c_str()); return global_mqtt_client->publish(this->get_discovery_topic_(discovery_info), "", 0, this->qos_, true); } - ESP_LOGV(TAG, "'%s': Sending discovery", this->friendly_name().c_str()); + ESP_LOGV(TAG, "'%s': Sending discovery", this->friendly_name_().c_str()); // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson return global_mqtt_client->publish_json( @@ -85,24 +85,24 @@ bool MQTTComponent::send_discovery_() { } // Fields from EntityBase - if (this->get_entity()->has_own_name()) { - root[MQTT_NAME] = this->friendly_name(); - } else { - root[MQTT_NAME] = ""; - } - if (this->is_disabled_by_default()) - root[MQTT_ENABLED_BY_DEFAULT] = false; - if (!this->get_icon().empty()) - root[MQTT_ICON] = this->get_icon(); + root[MQTT_NAME] = this->get_entity()->has_own_name() ? this->friendly_name_() : ""; - switch (this->get_entity()->get_entity_category()) { + if (this->is_disabled_by_default_()) + root[MQTT_ENABLED_BY_DEFAULT] = false; + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + const auto icon_ref = this->get_icon_ref_(); + if (!icon_ref.empty()) { + root[MQTT_ICON] = icon_ref; + } + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) + + const auto entity_category = this->get_entity()->get_entity_category(); + switch (entity_category) { case ENTITY_CATEGORY_NONE: break; case ENTITY_CATEGORY_CONFIG: - root[MQTT_ENTITY_CATEGORY] = "config"; - break; case ENTITY_CATEGORY_DIAGNOSTIC: - root[MQTT_ENTITY_CATEGORY] = "diagnostic"; + root[MQTT_ENTITY_CATEGORY] = entity_category == ENTITY_CATEGORY_CONFIG ? "config" : "diagnostic"; break; } @@ -113,26 +113,20 @@ bool MQTTComponent::send_discovery_() { if (this->command_retain_) root[MQTT_COMMAND_RETAIN] = true; - if (this->availability_ == nullptr) { - if (!global_mqtt_client->get_availability().topic.empty()) { - root[MQTT_AVAILABILITY_TOPIC] = global_mqtt_client->get_availability().topic; - if (global_mqtt_client->get_availability().payload_available != "online") - root[MQTT_PAYLOAD_AVAILABLE] = global_mqtt_client->get_availability().payload_available; - if (global_mqtt_client->get_availability().payload_not_available != "offline") - root[MQTT_PAYLOAD_NOT_AVAILABLE] = global_mqtt_client->get_availability().payload_not_available; - } - } else if (!this->availability_->topic.empty()) { - root[MQTT_AVAILABILITY_TOPIC] = this->availability_->topic; - if (this->availability_->payload_available != "online") - root[MQTT_PAYLOAD_AVAILABLE] = this->availability_->payload_available; - if (this->availability_->payload_not_available != "offline") - root[MQTT_PAYLOAD_NOT_AVAILABLE] = this->availability_->payload_not_available; + const Availability &avail = + this->availability_ == nullptr ? global_mqtt_client->get_availability() : *this->availability_; + if (!avail.topic.empty()) { + root[MQTT_AVAILABILITY_TOPIC] = avail.topic; + if (avail.payload_available != "online") + root[MQTT_PAYLOAD_AVAILABLE] = avail.payload_available; + if (avail.payload_not_available != "offline") + root[MQTT_PAYLOAD_NOT_AVAILABLE] = avail.payload_not_available; } const MQTTDiscoveryInfo &discovery_info = global_mqtt_client->get_discovery_info(); 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())); + sprintf(friendly_name_hash, "%08" PRIx32, fnv1_hash(this->friendly_name_())); friendly_name_hash[8] = 0; // ensure the hash-string ends with null root[MQTT_UNIQUE_ID] = get_mac_address() + "-" + this->component_type() + "-" + friendly_name_hash; } else { @@ -145,10 +139,8 @@ bool MQTTComponent::send_discovery_() { if (discovery_info.object_id_generator == MQTT_DEVICE_NAME_OBJECT_ID_GENERATOR) root[MQTT_OBJECT_ID] = node_name + "_" + this->get_default_object_id_(); - std::string node_friendly_name = App.get_friendly_name(); - if (node_friendly_name.empty()) { - node_friendly_name = node_name; - } + const std::string &friendly_name_ref = App.get_friendly_name(); + const std::string &node_friendly_name = friendly_name_ref.empty() ? node_name : friendly_name_ref; std::string node_area = App.get_area(); JsonObject device_info = root[MQTT_DEVICE].to(); @@ -158,13 +150,9 @@ bool MQTTComponent::send_discovery_() { #ifdef ESPHOME_PROJECT_NAME device_info[MQTT_DEVICE_SW_VERSION] = ESPHOME_PROJECT_VERSION " (ESPHome " ESPHOME_VERSION ")"; const char *model = std::strchr(ESPHOME_PROJECT_NAME, '.'); - if (model == nullptr) { // must never happen but check anyway - device_info[MQTT_DEVICE_MODEL] = ESPHOME_BOARD; - device_info[MQTT_DEVICE_MANUFACTURER] = ESPHOME_PROJECT_NAME; - } else { - device_info[MQTT_DEVICE_MODEL] = model + 1; - device_info[MQTT_DEVICE_MANUFACTURER] = std::string(ESPHOME_PROJECT_NAME, model - ESPHOME_PROJECT_NAME); - } + device_info[MQTT_DEVICE_MODEL] = model == nullptr ? ESPHOME_BOARD : model + 1; + device_info[MQTT_DEVICE_MANUFACTURER] = + model == nullptr ? ESPHOME_PROJECT_NAME : std::string(ESPHOME_PROJECT_NAME, model - ESPHOME_PROJECT_NAME); #else device_info[MQTT_DEVICE_SW_VERSION] = ESPHOME_VERSION " (" + App.get_compilation_time() + ")"; device_info[MQTT_DEVICE_MODEL] = ESPHOME_BOARD; @@ -200,7 +188,7 @@ bool MQTTComponent::is_discovery_enabled() const { } std::string MQTTComponent::get_default_object_id_() const { - return str_sanitize(str_snake_case(this->friendly_name())); + return str_sanitize(str_snake_case(this->friendly_name_())); } void MQTTComponent::subscribe(const std::string &topic, mqtt_callback_t callback, uint8_t qos) { @@ -284,9 +272,9 @@ void MQTTComponent::schedule_resend_state() { this->resend_state_ = true; } bool MQTTComponent::is_connected_() const { return global_mqtt_client->is_connected(); } // Pull these properties from EntityBase if not overridden -std::string MQTTComponent::friendly_name() const { return this->get_entity()->get_name(); } -std::string MQTTComponent::get_icon() const { return this->get_entity()->get_icon(); } -bool MQTTComponent::is_disabled_by_default() const { return this->get_entity()->is_disabled_by_default(); } +std::string MQTTComponent::friendly_name_() const { return this->get_entity()->get_name(); } +StringRef MQTTComponent::get_icon_ref_() const { return this->get_entity()->get_icon_ref(); } +bool MQTTComponent::is_disabled_by_default_() const { return this->get_entity()->is_disabled_by_default(); } bool MQTTComponent::is_internal() { if (this->has_custom_state_topic_) { // If the custom state_topic is null, return true as it is internal and should not publish diff --git a/esphome/components/mqtt/mqtt_component.h b/esphome/components/mqtt/mqtt_component.h index 851fdd842c..2f8dfcf64e 100644 --- a/esphome/components/mqtt/mqtt_component.h +++ b/esphome/components/mqtt/mqtt_component.h @@ -165,13 +165,13 @@ class MQTTComponent : public Component { virtual const EntityBase *get_entity() const = 0; /// Get the friendly name of this MQTT component. - virtual std::string friendly_name() const; + std::string friendly_name_() const; - /// Get the icon field of this component - virtual std::string get_icon() const; + /// Get the icon field of this component as StringRef + StringRef get_icon_ref_() const; /// Get whether the underlying Entity is disabled by default - virtual bool is_disabled_by_default() const; + bool is_disabled_by_default_() const; /// Get the MQTT topic that new states will be shared to. std::string get_state_topic_() const; diff --git a/esphome/components/mqtt/mqtt_cover.cpp b/esphome/components/mqtt/mqtt_cover.cpp index 6fb61ee469..b63aa66d29 100644 --- a/esphome/components/mqtt/mqtt_cover.cpp +++ b/esphome/components/mqtt/mqtt_cover.cpp @@ -67,9 +67,12 @@ void MQTTCoverComponent::dump_config() { } } void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { - // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - if (!this->cover_->get_device_class().empty()) - root[MQTT_DEVICE_CLASS] = this->cover_->get_device_class(); + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + const auto device_class = this->cover_->get_device_class_ref(); + if (!device_class.empty()) { + root[MQTT_DEVICE_CLASS] = device_class; + } + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) auto traits = this->cover_->get_traits(); if (traits.get_is_assumed_state()) { diff --git a/esphome/components/mqtt/mqtt_event.cpp b/esphome/components/mqtt/mqtt_event.cpp index f972d545c6..fd095ea041 100644 --- a/esphome/components/mqtt/mqtt_event.cpp +++ b/esphome/components/mqtt/mqtt_event.cpp @@ -21,8 +21,12 @@ void MQTTEventComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConf for (const auto &event_type : this->event_->get_event_types()) event_types.add(event_type); - if (!this->event_->get_device_class().empty()) - root[MQTT_DEVICE_CLASS] = this->event_->get_device_class(); + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + const auto device_class = this->event_->get_device_class_ref(); + if (!device_class.empty()) { + root[MQTT_DEVICE_CLASS] = device_class; + } + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) config.command_topic = false; } @@ -34,8 +38,8 @@ void MQTTEventComponent::setup() { void MQTTEventComponent::dump_config() { ESP_LOGCONFIG(TAG, "MQTT Event '%s': ", this->event_->get_name().c_str()); ESP_LOGCONFIG(TAG, "Event Types: "); - for (const auto &event_type : this->event_->get_event_types()) { - ESP_LOGCONFIG(TAG, "- %s", event_type.c_str()); + for (const char *event_type : this->event_->get_event_types()) { + ESP_LOGCONFIG(TAG, "- %s", event_type); } LOG_MQTT_COMPONENT(true, true); } diff --git a/esphome/components/mqtt/mqtt_fan.cpp b/esphome/components/mqtt/mqtt_fan.cpp index 70e1ae3b4a..2aefc3a4db 100644 --- a/esphome/components/mqtt/mqtt_fan.cpp +++ b/esphome/components/mqtt/mqtt_fan.cpp @@ -24,15 +24,15 @@ void MQTTFanComponent::setup() { auto val = parse_on_off(payload.c_str()); switch (val) { case PARSE_ON: - ESP_LOGD(TAG, "'%s' Turning Fan ON.", this->friendly_name().c_str()); + ESP_LOGD(TAG, "'%s' Turning Fan ON.", this->friendly_name_().c_str()); this->state_->turn_on().perform(); break; case PARSE_OFF: - ESP_LOGD(TAG, "'%s' Turning Fan OFF.", this->friendly_name().c_str()); + ESP_LOGD(TAG, "'%s' Turning Fan OFF.", this->friendly_name_().c_str()); this->state_->turn_off().perform(); break; case PARSE_TOGGLE: - ESP_LOGD(TAG, "'%s' Toggling Fan.", this->friendly_name().c_str()); + ESP_LOGD(TAG, "'%s' Toggling Fan.", this->friendly_name_().c_str()); this->state_->toggle().perform(); break; case PARSE_NONE: @@ -48,11 +48,11 @@ void MQTTFanComponent::setup() { auto val = parse_on_off(payload.c_str(), "forward", "reverse"); switch (val) { case PARSE_ON: - ESP_LOGD(TAG, "'%s': Setting direction FORWARD", this->friendly_name().c_str()); + ESP_LOGD(TAG, "'%s': Setting direction FORWARD", this->friendly_name_().c_str()); this->state_->make_call().set_direction(fan::FanDirection::FORWARD).perform(); break; case PARSE_OFF: - ESP_LOGD(TAG, "'%s': Setting direction REVERSE", this->friendly_name().c_str()); + ESP_LOGD(TAG, "'%s': Setting direction REVERSE", this->friendly_name_().c_str()); this->state_->make_call().set_direction(fan::FanDirection::REVERSE).perform(); break; case PARSE_TOGGLE: @@ -75,11 +75,11 @@ void MQTTFanComponent::setup() { auto val = parse_on_off(payload.c_str(), "oscillate_on", "oscillate_off"); switch (val) { case PARSE_ON: - ESP_LOGD(TAG, "'%s': Setting oscillating ON", this->friendly_name().c_str()); + ESP_LOGD(TAG, "'%s': Setting oscillating ON", this->friendly_name_().c_str()); this->state_->make_call().set_oscillating(true).perform(); break; case PARSE_OFF: - ESP_LOGD(TAG, "'%s': Setting oscillating OFF", this->friendly_name().c_str()); + ESP_LOGD(TAG, "'%s': Setting oscillating OFF", this->friendly_name_().c_str()); this->state_->make_call().set_oscillating(false).perform(); break; case PARSE_TOGGLE: diff --git a/esphome/components/mqtt/mqtt_fan.h b/esphome/components/mqtt/mqtt_fan.h index fdcec0782d..78641d224f 100644 --- a/esphome/components/mqtt/mqtt_fan.h +++ b/esphome/components/mqtt/mqtt_fan.h @@ -5,7 +5,7 @@ #ifdef USE_MQTT #ifdef USE_FAN -#include "esphome/components/fan/fan_state.h" +#include "esphome/components/fan/fan.h" #include "mqtt_component.h" namespace esphome { diff --git a/esphome/components/mqtt/mqtt_light.cpp b/esphome/components/mqtt/mqtt_light.cpp index 4f5ff408a4..883b67ffc6 100644 --- a/esphome/components/mqtt/mqtt_light.cpp +++ b/esphome/components/mqtt/mqtt_light.cpp @@ -69,6 +69,12 @@ void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscovery if (traits.supports_color_capability(ColorCapability::BRIGHTNESS)) root["brightness"] = true; + if (traits.supports_color_mode(ColorMode::COLOR_TEMPERATURE) || + traits.supports_color_mode(ColorMode::COLD_WARM_WHITE)) { + root[MQTT_MIN_MIREDS] = traits.get_min_mireds(); + root[MQTT_MAX_MIREDS] = traits.get_max_mireds(); + } + if (this->state_->supports_effects()) { root["effect"] = true; JsonArray effect_list = root[MQTT_EFFECT_LIST].to(); diff --git a/esphome/components/mqtt/mqtt_lock.cpp b/esphome/components/mqtt/mqtt_lock.cpp index 0412624983..0e15377ba4 100644 --- a/esphome/components/mqtt/mqtt_lock.cpp +++ b/esphome/components/mqtt/mqtt_lock.cpp @@ -24,7 +24,7 @@ void MQTTLockComponent::setup() { } else if (strcasecmp(payload.c_str(), "OPEN") == 0) { this->lock_->open(); } else { - ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name().c_str(), payload.c_str()); + ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name_().c_str(), payload.c_str()); this->status_momentary_warning("state", 5000); } }); diff --git a/esphome/components/mqtt/mqtt_number.cpp b/esphome/components/mqtt/mqtt_number.cpp index a44632ff30..f419eac130 100644 --- a/esphome/components/mqtt/mqtt_number.cpp +++ b/esphome/components/mqtt/mqtt_number.cpp @@ -44,8 +44,11 @@ void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon root[MQTT_MIN] = traits.get_min_value(); root[MQTT_MAX] = traits.get_max_value(); root[MQTT_STEP] = traits.get_step(); - if (!this->number_->traits.get_unit_of_measurement().empty()) - root[MQTT_UNIT_OF_MEASUREMENT] = this->number_->traits.get_unit_of_measurement(); + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + const auto unit_of_measurement = this->number_->traits.get_unit_of_measurement_ref(); + if (!unit_of_measurement.empty()) { + root[MQTT_UNIT_OF_MEASUREMENT] = unit_of_measurement; + } switch (this->number_->traits.get_mode()) { case NUMBER_MODE_AUTO: break; @@ -56,8 +59,11 @@ void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon root[MQTT_MODE] = "slider"; break; } - if (!this->number_->traits.get_device_class().empty()) - root[MQTT_DEVICE_CLASS] = this->number_->traits.get_device_class(); + const auto device_class = this->number_->traits.get_device_class_ref(); + if (!device_class.empty()) { + root[MQTT_DEVICE_CLASS] = device_class; + } + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) config.command_topic = true; } diff --git a/esphome/components/mqtt/mqtt_select.cpp b/esphome/components/mqtt/mqtt_select.cpp index b851348306..e1660b07ea 100644 --- a/esphome/components/mqtt/mqtt_select.cpp +++ b/esphome/components/mqtt/mqtt_select.cpp @@ -21,7 +21,8 @@ void MQTTSelectComponent::setup() { call.set_option(state); call.perform(); }); - this->select_->add_on_state_callback([this](const std::string &state, size_t index) { this->publish_state(state); }); + this->select_->add_on_state_callback( + [this](const std::string &state, size_t index) { this->publish_state(this->select_->option_at(index)); }); } void MQTTSelectComponent::dump_config() { @@ -44,7 +45,7 @@ void MQTTSelectComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon } bool MQTTSelectComponent::send_initial_state() { if (this->select_->has_state()) { - return this->publish_state(this->select_->state); + return this->publish_state(this->select_->current_option()); } else { return true; } diff --git a/esphome/components/mqtt/mqtt_sensor.cpp b/esphome/components/mqtt/mqtt_sensor.cpp index 2e1db1908f..010ac3013e 100644 --- a/esphome/components/mqtt/mqtt_sensor.cpp +++ b/esphome/components/mqtt/mqtt_sensor.cpp @@ -44,13 +44,17 @@ void MQTTSensorComponent::set_expire_after(uint32_t expire_after) { this->expire void MQTTSensorComponent::disable_expire_after() { this->expire_after_ = 0; } void MQTTSensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { - // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - if (!this->sensor_->get_device_class().empty()) { - root[MQTT_DEVICE_CLASS] = this->sensor_->get_device_class(); + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + const auto device_class = this->sensor_->get_device_class_ref(); + if (!device_class.empty()) { + root[MQTT_DEVICE_CLASS] = device_class; } - if (!this->sensor_->get_unit_of_measurement().empty()) - root[MQTT_UNIT_OF_MEASUREMENT] = this->sensor_->get_unit_of_measurement(); + const auto unit_of_measurement = this->sensor_->get_unit_of_measurement_ref(); + if (!unit_of_measurement.empty()) { + root[MQTT_UNIT_OF_MEASUREMENT] = unit_of_measurement; + } + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) if (this->get_expire_after() > 0) root[MQTT_EXPIRE_AFTER] = this->get_expire_after() / 1000; @@ -58,8 +62,13 @@ void MQTTSensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon if (this->sensor_->get_force_update()) root[MQTT_FORCE_UPDATE] = true; - if (this->sensor_->get_state_class() != STATE_CLASS_NONE) - root[MQTT_STATE_CLASS] = state_class_to_string(this->sensor_->get_state_class()); + if (this->sensor_->get_state_class() != STATE_CLASS_NONE) { +#ifdef USE_STORE_LOG_STR_IN_FLASH + root[MQTT_STATE_CLASS] = (const __FlashStringHelper *) state_class_to_string(this->sensor_->get_state_class()); +#else + root[MQTT_STATE_CLASS] = LOG_STR_ARG(state_class_to_string(this->sensor_->get_state_class())); +#endif + } config.command_topic = false; } diff --git a/esphome/components/mqtt/mqtt_switch.cpp b/esphome/components/mqtt/mqtt_switch.cpp index 8b1323bdb2..b3a35420b9 100644 --- a/esphome/components/mqtt/mqtt_switch.cpp +++ b/esphome/components/mqtt/mqtt_switch.cpp @@ -29,7 +29,7 @@ void MQTTSwitchComponent::setup() { break; case PARSE_NONE: default: - ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name().c_str(), payload.c_str()); + ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name_().c_str(), payload.c_str()); this->status_momentary_warning("state", 5000); break; } diff --git a/esphome/components/mqtt/mqtt_text_sensor.cpp b/esphome/components/mqtt/mqtt_text_sensor.cpp index 42260ed2a8..e6e7cf04e8 100644 --- a/esphome/components/mqtt/mqtt_text_sensor.cpp +++ b/esphome/components/mqtt/mqtt_text_sensor.cpp @@ -15,10 +15,12 @@ using namespace esphome::text_sensor; MQTTTextSensor::MQTTTextSensor(TextSensor *sensor) : sensor_(sensor) {} void MQTTTextSensor::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { - // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - if (!this->sensor_->get_device_class().empty()) { - root[MQTT_DEVICE_CLASS] = this->sensor_->get_device_class(); + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + const auto device_class = this->sensor_->get_device_class_ref(); + if (!device_class.empty()) { + root[MQTT_DEVICE_CLASS] = device_class; } + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) config.command_topic = false; } void MQTTTextSensor::setup() { diff --git a/esphome/components/mqtt/mqtt_update.cpp b/esphome/components/mqtt/mqtt_update.cpp index 5d4807c7f3..20f3a69a9e 100644 --- a/esphome/components/mqtt/mqtt_update.cpp +++ b/esphome/components/mqtt/mqtt_update.cpp @@ -20,7 +20,7 @@ void MQTTUpdateComponent::setup() { if (payload == "INSTALL") { this->update_->perform(); } else { - ESP_LOGW(TAG, "'%s': Received unknown update payload: %s", this->friendly_name().c_str(), payload.c_str()); + ESP_LOGW(TAG, "'%s': Received unknown update payload: %s", this->friendly_name_().c_str(), payload.c_str()); this->status_momentary_warning("state", 5000); } }); diff --git a/esphome/components/mqtt/mqtt_valve.cpp b/esphome/components/mqtt/mqtt_valve.cpp index 551398cf42..ae60670748 100644 --- a/esphome/components/mqtt/mqtt_valve.cpp +++ b/esphome/components/mqtt/mqtt_valve.cpp @@ -49,10 +49,12 @@ void MQTTValveComponent::dump_config() { } } void MQTTValveComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { - // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - if (!this->valve_->get_device_class().empty()) { - root[MQTT_DEVICE_CLASS] = this->valve_->get_device_class(); + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + const auto device_class = this->valve_->get_device_class_ref(); + if (!device_class.empty()) { + root[MQTT_DEVICE_CLASS] = device_class; } + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) auto traits = this->valve_->get_traits(); if (traits.get_is_assumed_state()) { diff --git a/esphome/components/ms5611/ms5611.cpp b/esphome/components/ms5611/ms5611.cpp index 8f8c05eb7d..5a7622e783 100644 --- a/esphome/components/ms5611/ms5611.cpp +++ b/esphome/components/ms5611/ms5611.cpp @@ -19,13 +19,14 @@ void MS5611Component::setup() { this->mark_failed(); return; } - delay(100); // NOLINT - for (uint8_t offset = 0; offset < 6; offset++) { - if (!this->read_byte_16(MS5611_CMD_READ_PROM + (offset * 2), &this->prom_[offset])) { - this->mark_failed(); - return; + this->set_timeout(100, [this]() { + for (uint8_t offset = 0; offset < 6; offset++) { + if (!this->read_byte_16(MS5611_CMD_READ_PROM + (offset * 2), &this->prom_[offset])) { + this->mark_failed(); + return; + } } - } + }); } void MS5611Component::dump_config() { ESP_LOGCONFIG(TAG, "MS5611:"); diff --git a/esphome/components/nau7802/nau7802.cpp b/esphome/components/nau7802/nau7802.cpp index acdca03fdb..11f63a9a33 100644 --- a/esphome/components/nau7802/nau7802.cpp +++ b/esphome/components/nau7802/nau7802.cpp @@ -218,7 +218,7 @@ void NAU7802Sensor::dump_config() { void NAU7802Sensor::write_value_(uint8_t start_reg, size_t size, int32_t value) { uint8_t data[4]; - for (int i = 0; i < size; i++) { + for (size_t i = 0; i < size; i++) { data[i] = 0xFF & (value >> (size - 1 - i) * 8); } this->write_register(start_reg, data, size); @@ -228,7 +228,7 @@ int32_t NAU7802Sensor::read_value_(uint8_t start_reg, size_t size) { uint8_t data[4]; this->read_register(start_reg, data, size); int32_t result = 0; - for (int i = 0; i < size; i++) { + for (size_t i = 0; i < size; i++) { result |= data[i] << (size - 1 - i) * 8; } // extend sign bit @@ -278,7 +278,7 @@ void NAU7802Sensor::loop() { this->set_calibration_failure_(true); this->state_ = CalibrationState::INACTIVE; ESP_LOGE(TAG, "Failed to calibrate sensor"); - this->status_set_error("Calibration Failed"); + this->status_set_error(LOG_STR("Calibration Failed")); return; } diff --git a/esphome/components/nau7802/nau7802.h b/esphome/components/nau7802/nau7802.h index 17e426ccc6..05452851ca 100644 --- a/esphome/components/nau7802/nau7802.h +++ b/esphome/components/nau7802/nau7802.h @@ -101,18 +101,18 @@ class NAU7802Sensor : public sensor::Sensor, public PollingComponent, public i2c template class NAU7802CalbrateExternalOffsetAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->calibrate_external_offset(); } + void play(const Ts &...x) override { this->parent_->calibrate_external_offset(); } }; template class NAU7802CalbrateInternalOffsetAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->calibrate_internal_offset(); } + void play(const Ts &...x) override { this->parent_->calibrate_internal_offset(); } }; template class NAU7802CalbrateGainAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->calibrate_gain(); } + void play(const Ts &...x) override { this->parent_->calibrate_gain(); } }; } // namespace nau7802 diff --git a/esphome/components/neopixelbus/light.py b/esphome/components/neopixelbus/light.py index c63790e60b..0c9604e932 100644 --- a/esphome/components/neopixelbus/light.py +++ b/esphome/components/neopixelbus/light.py @@ -225,6 +225,9 @@ async def to_code(config): # https://github.com/Makuna/NeoPixelBus/blob/master/library.json # Version Listed Here: https://registry.platformio.org/libraries/makuna/NeoPixelBus/versions if CORE.is_esp32: + # disable built in rgb support as it uses the new RMT drivers and will + # conflict with NeoPixelBus which uses the legacy drivers + cg.add_build_flag("-DESP32_ARDUINO_NO_RGB_BUILTIN") cg.add_library("makuna/NeoPixelBus", "2.8.0") else: cg.add_library("makuna/NeoPixelBus", "2.7.3") diff --git a/esphome/components/network/__init__.py b/esphome/components/network/__init__.py index b04fca7a1c..d7a51fb0c6 100644 --- a/esphome/components/network/__init__.py +++ b/esphome/components/network/__init__.py @@ -1,15 +1,110 @@ +import ipaddress +import logging + import esphome.codegen as cg from esphome.components.esp32 import add_idf_sdkconfig_option +from esphome.components.psram import is_guaranteed as psram_is_guaranteed import esphome.config_validation as cv from esphome.const import CONF_ENABLE_IPV6, CONF_MIN_IPV6_ADDR_COUNT -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority CODEOWNERS = ["@esphome/core"] AUTO_LOAD = ["mdns"] +_LOGGER = logging.getLogger(__name__) + +# High performance networking tracking infrastructure +# Components can request high performance networking and this configures lwip and WiFi settings +KEY_HIGH_PERFORMANCE_NETWORKING = "high_performance_networking" +CONF_ENABLE_HIGH_PERFORMANCE = "enable_high_performance" + network_ns = cg.esphome_ns.namespace("network") IPAddress = network_ns.class_("IPAddress") + +def ip_address_literal(ip: str | int | None) -> cg.MockObj: + """Generate an IPAddress with compile-time initialization instead of runtime parsing. + + This function parses the IP address in Python during code generation and generates + a call to the 4-octet constructor (IPAddress(192, 168, 1, 1)) instead of the + string constructor (IPAddress("192.168.1.1")). This eliminates runtime string + parsing overhead and reduces flash usage on embedded systems. + + Args: + ip: IP address as string (e.g., "192.168.1.1"), ipaddress.IPv4Address, or None + + Returns: + IPAddress expression that uses 4-octet constructor for efficiency + """ + if ip is None: + return IPAddress(0, 0, 0, 0) + + try: + # Parse using Python's ipaddress module + ip_obj = ipaddress.ip_address(ip) + except (ValueError, TypeError): + pass + else: + # Only support IPv4 for now + if isinstance(ip_obj, ipaddress.IPv4Address): + # Extract octets from the packed bytes representation + octets = ip_obj.packed + # Generate call to 4-octet constructor: IPAddress(192, 168, 1, 1) + return IPAddress(octets[0], octets[1], octets[2], octets[3]) + + # Fallback to string constructor if parsing fails + return IPAddress(str(ip)) + + +def require_high_performance_networking() -> None: + """Request high performance networking for network and WiFi. + + Call this from components that need optimized network performance for streaming + or high-throughput data transfer. This enables high performance mode which + configures both lwip TCP settings and WiFi driver settings for improved + network performance. + + Settings applied (ESP-IDF only): + - lwip: Larger TCP buffers, windows, and mailbox sizes + - WiFi: Increased RX/TX buffers, AMPDU aggregation, PSRAM allocation (set by wifi component) + + Configuration is PSRAM-aware: + - With PSRAM guaranteed: Aggressive settings (512 RX buffers, 512KB TCP windows) + - Without PSRAM: Conservative optimized settings (64 buffers, 65KB TCP windows) + + Example: + from esphome.components import network + + def _request_high_performance_networking(config): + network.require_high_performance_networking() + return config + + CONFIG_SCHEMA = cv.All( + ..., + _request_high_performance_networking, + ) + """ + # Only set up once (idempotent - multiple components can call this) + if not CORE.data.get(KEY_HIGH_PERFORMANCE_NETWORKING, False): + CORE.data[KEY_HIGH_PERFORMANCE_NETWORKING] = True + + +def has_high_performance_networking() -> bool: + """Check if high performance networking mode is enabled. + + Returns True when high performance networking has been requested by a + component or explicitly enabled in the network configuration. This indicates + that lwip and WiFi will use optimized buffer sizes and settings. + + This function should be called during code generation (to_code phase) by + components that need to apply performance-related settings. + + Returns: + bool: True if high performance networking is enabled, False otherwise + """ + return CORE.data.get(KEY_HIGH_PERFORMANCE_NETWORKING, False) + + CONFIG_SCHEMA = cv.Schema( { cv.SplitDefault( @@ -18,6 +113,7 @@ CONFIG_SCHEMA = cv.Schema( esp32=False, rp2040=False, bk72xx=False, + host=False, ): cv.All( cv.boolean, cv.Any( @@ -27,29 +123,99 @@ CONFIG_SCHEMA = cv.Schema( esp8266_arduino=cv.Version(0, 0, 0), rp2040_arduino=cv.Version(0, 0, 0), bk72xx_arduino=cv.Version(1, 7, 0), + host=cv.Version(0, 0, 0), ), cv.boolean_false, ), ), cv.Optional(CONF_MIN_IPV6_ADDR_COUNT, default=0): cv.positive_int, + cv.Optional(CONF_ENABLE_HIGH_PERFORMANCE): cv.All(cv.boolean, cv.only_on_esp32), } ) -@coroutine_with_priority(201.0) +@coroutine_with_priority(CoroPriority.NETWORK) async def to_code(config): cg.add_define("USE_NETWORK") if CORE.using_arduino and CORE.is_esp32: cg.add_library("Networking", None) + + # Apply high performance networking settings + # Config can explicitly enable/disable, or default to component-driven behavior + enable_high_perf = config.get(CONF_ENABLE_HIGH_PERFORMANCE) + component_requested = CORE.data.get(KEY_HIGH_PERFORMANCE_NETWORKING, False) + + # Explicit config overrides component request + should_enable = ( + enable_high_perf if enable_high_perf is not None else component_requested + ) + + # Log when user explicitly disables but a component requested it + if enable_high_perf is False and component_requested: + _LOGGER.info( + "High performance networking disabled by user configuration (overriding component request)" + ) + + if CORE.is_esp32 and CORE.using_esp_idf and should_enable: + # Check if PSRAM is guaranteed (set by psram component during final validation) + psram_guaranteed = psram_is_guaranteed() + + if psram_guaranteed: + _LOGGER.info( + "Applying high-performance lwip settings (PSRAM guaranteed): 512KB TCP windows, 512 mailbox sizes" + ) + # PSRAM is guaranteed - use aggressive settings + # Higher maximum values are allowed because CONFIG_LWIP_WND_SCALE is set to true + # CONFIG_LWIP_WND_SCALE can only be enabled if CONFIG_SPIRAM_IGNORE_NOTFOUND isn't set + # Based on https://github.com/espressif/esp-adf/issues/297#issuecomment-783811702 + + # Enable window scaling for much larger TCP windows + add_idf_sdkconfig_option("CONFIG_LWIP_WND_SCALE", True) + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_RCV_SCALE", 3) + + # Large TCP buffers and windows (requires PSRAM) + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_SND_BUF_DEFAULT", 65534) + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_WND_DEFAULT", 512000) + + # Large mailboxes for high throughput + add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_RECVMBOX_SIZE", 512) + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_RECVMBOX_SIZE", 512) + + # TCP connection limits + add_idf_sdkconfig_option("CONFIG_LWIP_MAX_ACTIVE_TCP", 16) + add_idf_sdkconfig_option("CONFIG_LWIP_MAX_LISTENING_TCP", 16) + + # TCP optimizations + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_MAXRTX", 12) + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_SYNMAXRTX", 6) + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_MSS", 1436) + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_MSL", 60000) + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_OVERSIZE_MSS", True) + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_QUEUE_OOSEQ", True) + else: + _LOGGER.info( + "Applying optimized lwip settings: 65KB TCP windows, 64 mailbox sizes" + ) + # PSRAM not guaranteed - use more conservative, but still optimized settings + # Based on https://github.com/espressif/esp-idf/blob/release/v5.4/examples/wifi/iperf/sdkconfig.defaults.esp32 + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_SND_BUF_DEFAULT", 65534) + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_WND_DEFAULT", 65534) + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_RECVMBOX_SIZE", 64) + add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_RECVMBOX_SIZE", 64) + if (enable_ipv6 := config.get(CONF_ENABLE_IPV6, None)) is not None: cg.add_define("USE_NETWORK_IPV6", enable_ipv6) if enable_ipv6: cg.add_define( "USE_NETWORK_MIN_IPV6_ADDR_COUNT", config[CONF_MIN_IPV6_ADDR_COUNT] ) - if CORE.using_esp_idf: - add_idf_sdkconfig_option("CONFIG_LWIP_IPV6", enable_ipv6) - add_idf_sdkconfig_option("CONFIG_LWIP_IPV6_AUTOCONFIG", enable_ipv6) + if CORE.is_esp32: + if CORE.using_esp_idf: + add_idf_sdkconfig_option("CONFIG_LWIP_IPV6", enable_ipv6) + add_idf_sdkconfig_option("CONFIG_LWIP_IPV6_AUTOCONFIG", enable_ipv6) + else: + add_idf_sdkconfig_option("CONFIG_LWIP_IPV6", True) + add_idf_sdkconfig_option("CONFIG_LWIP_IPV6_AUTOCONFIG", True) elif enable_ipv6: cg.add_build_flag("-DCONFIG_LWIP_IPV6") cg.add_build_flag("-DCONFIG_LWIP_IPV6_AUTOCONFIG") diff --git a/esphome/components/network/ip_address.h b/esphome/components/network/ip_address.h index 5e6b0dbd96..3d8b062d0b 100644 --- a/esphome/components/network/ip_address.h +++ b/esphome/components/network/ip_address.h @@ -81,7 +81,12 @@ struct IPAddress { ip_addr_.type = IPADDR_TYPE_V6; } #endif /* LWIP_IPV6 */ - IPAddress(esp_ip4_addr_t *other_ip) { memcpy((void *) &ip_addr_, (void *) other_ip, sizeof(esp_ip4_addr_t)); } + IPAddress(esp_ip4_addr_t *other_ip) { + memcpy((void *) &ip_addr_, (void *) other_ip, sizeof(esp_ip4_addr_t)); +#if LWIP_IPV6 + ip_addr_.type = IPADDR_TYPE_V4; +#endif + } IPAddress(esp_ip_addr_t *other_ip) { #if LWIP_IPV6 memcpy((void *) &ip_addr_, (void *) other_ip, sizeof(ip_addr_)); @@ -118,10 +123,10 @@ struct IPAddress { operator arduino_ns::IPAddress() const { return ip_addr_get_ip4_u32(&ip_addr_); } #endif - bool is_set() { return !ip_addr_isany(&ip_addr_); } // NOLINT(readability-simplify-boolean-expr) - bool is_ip4() { return IP_IS_V4(&ip_addr_); } - bool is_ip6() { return IP_IS_V6(&ip_addr_); } - bool is_multicast() { return ip_addr_ismulticast(&ip_addr_); } + bool is_set() const { return !ip_addr_isany(&ip_addr_); } // NOLINT(readability-simplify-boolean-expr) + bool is_ip4() const { return IP_IS_V4(&ip_addr_); } + bool is_ip6() const { return IP_IS_V6(&ip_addr_); } + bool is_multicast() const { return ip_addr_ismulticast(&ip_addr_); } std::string str() const { return str_lower_case(ipaddr_ntoa(&ip_addr_)); } bool operator==(const IPAddress &other) const { return ip_addr_cmp(&ip_addr_, &other.ip_addr_); } bool operator!=(const IPAddress &other) const { return !ip_addr_cmp(&ip_addr_, &other.ip_addr_); } diff --git a/esphome/components/network/util.cpp b/esphome/components/network/util.cpp index bf76aefc30..5e741fd244 100644 --- a/esphome/components/network/util.cpp +++ b/esphome/components/network/util.cpp @@ -85,22 +85,28 @@ network::IPAddresses get_ip_addresses() { return {}; } -std::string get_use_address() { +const char *get_use_address() { + // Global component pointers are guaranteed to be set by component constructors when USE_* is defined #ifdef USE_ETHERNET - if (ethernet::global_eth_component != nullptr) - return ethernet::global_eth_component->get_use_address(); + return ethernet::global_eth_component->get_use_address(); #endif #ifdef USE_MODEM - if (modem::global_modem_component != nullptr) - return modem::global_modem_component->get_use_address(); + return modem::global_modem_component->get_use_address(); #endif #ifdef USE_WIFI - if (wifi::global_wifi_component != nullptr) - return wifi::global_wifi_component->get_use_address(); + return wifi::global_wifi_component->get_use_address(); #endif + +#ifdef USE_OPENTHREAD + return openthread::global_openthread_component->get_use_address(); +#endif + +#if !defined(USE_ETHERNET) && !defined(USE_MODEM) && !defined(USE_WIFI) && !defined(USE_OPENTHREAD) + // Fallback when no network component is defined (e.g., host platform) return ""; +#endif } } // namespace network diff --git a/esphome/components/network/util.h b/esphome/components/network/util.h index b518696e68..3dc12232aa 100644 --- a/esphome/components/network/util.h +++ b/esphome/components/network/util.h @@ -12,7 +12,7 @@ bool is_connected(); /// Return whether the network is disabled (only wifi for now) bool is_disabled(); /// Get the active network hostname -std::string get_use_address(); +const char *get_use_address(); IPAddresses get_ip_addresses(); } // namespace network diff --git a/esphome/components/nextion/automation.h b/esphome/components/nextion/automation.h index c718355af8..8e85e15823 100644 --- a/esphome/components/nextion/automation.h +++ b/esphome/components/nextion/automation.h @@ -55,7 +55,7 @@ template class NextionSetBrightnessAction : public Action TEMPLATABLE_VALUE(float, brightness) - void play(Ts... x) override { + void play(const Ts &...x) override { this->component_->set_brightness(this->brightness_.value(x...)); this->component_->set_backlight_brightness(this->brightness_.value(x...)); } @@ -74,7 +74,7 @@ template class NextionPublishFloatAction : public Action TEMPLATABLE_VALUE(bool, publish_state) TEMPLATABLE_VALUE(bool, send_to_nextion) - void play(Ts... x) override { + void play(const Ts &...x) override { this->component_->set_state(this->state_.value(x...), this->publish_state_.value(x...), this->send_to_nextion_.value(x...)); } @@ -97,7 +97,7 @@ template class NextionPublishTextAction : public Action { TEMPLATABLE_VALUE(bool, publish_state) TEMPLATABLE_VALUE(bool, send_to_nextion) - void play(Ts... x) override { + void play(const Ts &...x) override { this->component_->set_state(this->state_.value(x...), this->publish_state_.value(x...), this->send_to_nextion_.value(x...)); } @@ -120,7 +120,7 @@ template class NextionPublishBoolAction : public Action { TEMPLATABLE_VALUE(bool, publish_state) TEMPLATABLE_VALUE(bool, send_to_nextion) - void play(Ts... x) override { + void play(const Ts &...x) override { this->component_->set_state(this->state_.value(x...), this->publish_state_.value(x...), this->send_to_nextion_.value(x...)); } diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index 4254ae45fe..ed6cd93027 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -153,10 +153,10 @@ async def to_code(config): if CONF_TFT_URL in config: cg.add_define("USE_NEXTION_TFT_UPLOAD") cg.add(var.set_tft_url(config[CONF_TFT_URL])) - if CORE.is_esp32 and CORE.using_arduino: - cg.add_library("NetworkClientSecure", None) - cg.add_library("HTTPClient", None) - elif CORE.is_esp32 and CORE.using_esp_idf: + if CORE.is_esp32: + if CORE.using_arduino: + cg.add_library("NetworkClientSecure", None) + cg.add_library("HTTPClient", None) esp32.add_idf_sdkconfig_option("CONFIG_ESP_TLS_INSECURE", True) esp32.add_idf_sdkconfig_option( "CONFIG_ESP_TLS_SKIP_SERVER_CERT_VERIFY", True diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 133bd2947c..d77af510d7 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -77,7 +77,7 @@ bool Nextion::check_connect_() { this->recv_ret_string_(response, 0, false); if (!response.empty() && response[0] == 0x1A) { // Swallow invalid variable name responses that may be caused by the above commands - ESP_LOGD(TAG, "0x1A error ignored (setup)"); + ESP_LOGV(TAG, "0x1A error ignored (setup)"); return false; } if (response.empty() || response.find("comok") == std::string::npos) { @@ -323,6 +323,8 @@ void Nextion::loop() { this->set_touch_sleep_timeout(this->touch_sleep_timeout_); } + this->set_auto_wake_on_touch(this->connection_state_.auto_wake_on_touch_); + this->connection_state_.ignore_is_setup_ = false; } @@ -334,7 +336,7 @@ void Nextion::loop() { this->started_ms_ = App.get_loop_component_start_time(); if (this->started_ms_ + this->startup_override_ms_ < App.get_loop_component_start_time()) { - ESP_LOGD(TAG, "Manual ready set"); + ESP_LOGV(TAG, "Manual ready set"); this->connection_state_.nextion_reports_is_setup_ = true; } } @@ -544,7 +546,7 @@ void Nextion::process_nextion_commands_() { uint8_t page_id = to_process[0]; uint8_t component_id = to_process[1]; uint8_t touch_event = to_process[2]; // 0 -> release, 1 -> press - ESP_LOGD(TAG, "Touch %s: page %u comp %u", touch_event ? "PRESS" : "RELEASE", page_id, component_id); + ESP_LOGV(TAG, "Touch %s: page %u comp %u", touch_event ? "PRESS" : "RELEASE", page_id, component_id); for (auto *touch : this->touch_) { touch->process_touch(page_id, component_id, touch_event != 0); } @@ -559,7 +561,7 @@ void Nextion::process_nextion_commands_() { } uint8_t page_id = to_process[0]; - ESP_LOGD(TAG, "New page: %u", page_id); + ESP_LOGV(TAG, "New page: %u", page_id); this->page_callback_.call(page_id); break; } @@ -577,7 +579,7 @@ void Nextion::process_nextion_commands_() { const uint16_t x = (uint16_t(to_process[0]) << 8) | to_process[1]; const uint16_t y = (uint16_t(to_process[2]) << 8) | to_process[3]; const uint8_t touch_event = to_process[4]; // 0 -> release, 1 -> press - ESP_LOGD(TAG, "Touch %s at %u,%u", touch_event ? "PRESS" : "RELEASE", x, y); + ESP_LOGV(TAG, "Touch %s at %u,%u", touch_event ? "PRESS" : "RELEASE", x, y); break; } @@ -676,7 +678,7 @@ void Nextion::process_nextion_commands_() { } case 0x88: // system successful start up { - ESP_LOGD(TAG, "System start: %zu", to_process_length); + ESP_LOGV(TAG, "System start: %zu", to_process_length); this->connection_state_.nextion_reports_is_setup_ = true; break; } @@ -764,7 +766,8 @@ void Nextion::process_nextion_commands_() { variable_name = to_process.substr(0, index); ++index; - text_value = to_process.substr(index); + // Get variable value without terminating NUL byte. Length check above ensures substr len >= 0. + text_value = to_process.substr(index, to_process_length - index - 1); ESP_LOGN(TAG, "Text sensor: %s='%s'", variable_name.c_str(), text_value.c_str()); @@ -921,7 +924,7 @@ void Nextion::set_nextion_sensor_state(NextionQueueType queue_type, const std::s } void Nextion::set_nextion_text_state(const std::string &name, const std::string &state) { - ESP_LOGD(TAG, "State: %s='%s'", name.c_str(), state.c_str()); + ESP_LOGV(TAG, "State: %s='%s'", name.c_str(), state.c_str()); for (auto *sensor : this->textsensortype_) { if (name == sensor->get_variable_name()) { @@ -932,7 +935,7 @@ void Nextion::set_nextion_text_state(const std::string &name, const std::string } void Nextion::all_components_send_state_(bool force_update) { - ESP_LOGD(TAG, "Send states"); + ESP_LOGV(TAG, "Send states"); for (auto *binarysensortype : this->binarysensortype_) { if (force_update || binarysensortype->get_needs_to_send_update()) binarysensortype->send_state_to_nextion(); @@ -1290,9 +1293,6 @@ void Nextion::check_pending_waveform_() { void Nextion::set_writer(const nextion_writer_t &writer) { this->writer_ = writer; } -ESPDEPRECATED("set_wait_for_ack(bool) deprecated, no effect", "v1.20") -void Nextion::set_wait_for_ack(bool wait_for_ack) { ESP_LOGE(TAG, "Deprecated"); } - bool Nextion::is_updating() { return this->connection_state_.is_updating_; } } // namespace nextion diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index e2c4faa1d0..61068b52fc 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -9,6 +9,7 @@ #include "esphome/components/uart/uart.h" #include "nextion_base.h" #include "nextion_component.h" +#include "esphome/components/display/display.h" #include "esphome/components/display/display_color_utils.h" #ifdef USE_NEXTION_TFT_UPLOAD @@ -31,7 +32,7 @@ namespace nextion { class Nextion; class NextionComponentBase; -using nextion_writer_t = std::function; +using nextion_writer_t = display::DisplayWriter; static const std::string COMMAND_DELIMITER{static_cast(255), static_cast(255), static_cast(255)}; @@ -540,6 +541,23 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe */ void goto_page(uint8_t page); + /** + * Set the visibility of a component. + * + * @param component The component name. + * @param show True to show the component, false to hide it. + * + * @see show_component() + * @see hide_component() + * + * Example: + * ```cpp + * it.set_component_visibility("textview", true); // Equivalent to show_component("textview") + * it.set_component_visibility("textview", false); // Equivalent to hide_component("textview") + * ``` + */ + void set_component_visibility(const char *component, bool show) override; + /** * Hide a component. * @param component The component name. @@ -1454,7 +1472,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe CallbackManager touch_callback_{}; CallbackManager buffer_overflow_callback_{}; - optional writer_; + nextion_writer_t writer_; optional brightness_; #ifdef USE_NEXTION_CONFIG_DUMP_DEVICE_INFO diff --git a/esphome/components/nextion/nextion_base.h b/esphome/components/nextion/nextion_base.h index b88dd399f8..d46cd9a185 100644 --- a/esphome/components/nextion/nextion_base.h +++ b/esphome/components/nextion/nextion_base.h @@ -45,6 +45,7 @@ class NextionBase { virtual void set_component_pressed_font_color(const char *component, Color color) = 0; virtual void set_component_font(const char *component, uint8_t font_id) = 0; + virtual void set_component_visibility(const char *component, bool show) = 0; virtual void show_component(const char *component) = 0; virtual void hide_component(const char *component) = 0; diff --git a/esphome/components/nextion/nextion_commands.cpp b/esphome/components/nextion/nextion_commands.cpp index f3a282717b..cfaae7e3e0 100644 --- a/esphome/components/nextion/nextion_commands.cpp +++ b/esphome/components/nextion/nextion_commands.cpp @@ -201,13 +201,13 @@ void Nextion::set_component_font(const char *component, uint8_t font_id) { this->add_no_result_to_queue_with_printf_("set_component_font", "%s.font=%" PRIu8, component, font_id); } -void Nextion::hide_component(const char *component) { - this->add_no_result_to_queue_with_printf_("hide_component", "vis %s,0", component); +void Nextion::set_component_visibility(const char *component, bool show) { + this->add_no_result_to_queue_with_printf_("set_component_visibility", "vis %s,%d", component, show ? 1 : 0); } -void Nextion::show_component(const char *component) { - this->add_no_result_to_queue_with_printf_("show_component", "vis %s,1", component); -} +void Nextion::hide_component(const char *component) { this->set_component_visibility(component, false); } + +void Nextion::show_component(const char *component) { this->set_component_visibility(component, true); } void Nextion::enable_component_touch(const char *component) { this->add_no_result_to_queue_with_printf_("enable_component_touch", "tsw %s,1", component); diff --git a/esphome/components/nextion/nextion_component.cpp b/esphome/components/nextion/nextion_component.cpp index 32929d6845..324ad87372 100644 --- a/esphome/components/nextion/nextion_component.cpp +++ b/esphome/components/nextion/nextion_component.cpp @@ -81,13 +81,11 @@ void NextionComponent::update_component_settings(bool force_update) { this->component_flags_.visible_needs_update = false; - if (this->component_flags_.visible) { - this->nextion_->show_component(name_to_send.c_str()); - this->send_state_to_nextion(); - } else { - this->nextion_->hide_component(name_to_send.c_str()); + this->nextion_->set_component_visibility(name_to_send.c_str(), this->component_flags_.visible); + if (!this->component_flags_.visible) { return; } + this->send_state_to_nextion(); } if (this->component_flags_.bco_needs_update || (force_update && this->component_flags_.bco2_is_set)) { diff --git a/esphome/components/nextion/nextion_upload.cpp b/esphome/components/nextion/nextion_upload.cpp index c47b393f99..7ddd7a2f08 100644 --- a/esphome/components/nextion/nextion_upload.cpp +++ b/esphome/components/nextion/nextion_upload.cpp @@ -11,7 +11,10 @@ static const char *const TAG = "nextion.upload"; bool Nextion::upload_end_(bool successful) { if (successful) { ESP_LOGD(TAG, "Upload successful"); - delay(1500); // NOLINT + for (uint8_t i = 0; i <= 5; i++) { + delay(1000); // NOLINT + App.feed_wdt(); // Feed the watchdog timer. + } App.safe_reboot(); } else { ESP_LOGE(TAG, "Upload failed"); diff --git a/esphome/components/nextion/nextion_upload_arduino.cpp b/esphome/components/nextion/nextion_upload_arduino.cpp index b0e5d121dd..baea938729 100644 --- a/esphome/components/nextion/nextion_upload_arduino.cpp +++ b/esphome/components/nextion/nextion_upload_arduino.cpp @@ -174,9 +174,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { // Check if baud rate is supported this->original_baud_rate_ = this->parent_->get_baud_rate(); - static const std::vector SUPPORTED_BAUD_RATES = {2400, 4800, 9600, 19200, 31250, 38400, 57600, - 115200, 230400, 250000, 256000, 512000, 921600}; - if (std::find(SUPPORTED_BAUD_RATES.begin(), SUPPORTED_BAUD_RATES.end(), baud_rate) == SUPPORTED_BAUD_RATES.end()) { + if (baud_rate <= 0) { baud_rate = this->original_baud_rate_; } ESP_LOGD(TAG, "Baud rate: %" PRIu32, baud_rate); diff --git a/esphome/components/nextion/nextion_upload_idf.cpp b/esphome/components/nextion/nextion_upload_idf.cpp index 78a47f9e2c..942e3dd6c3 100644 --- a/esphome/components/nextion/nextion_upload_idf.cpp +++ b/esphome/components/nextion/nextion_upload_idf.cpp @@ -177,9 +177,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { // Check if baud rate is supported this->original_baud_rate_ = this->parent_->get_baud_rate(); - static const std::vector SUPPORTED_BAUD_RATES = {2400, 4800, 9600, 19200, 31250, 38400, 57600, - 115200, 230400, 250000, 256000, 512000, 921600}; - if (std::find(SUPPORTED_BAUD_RATES.begin(), SUPPORTED_BAUD_RATES.end(), baud_rate) == SUPPORTED_BAUD_RATES.end()) { + if (baud_rate <= 0) { baud_rate = this->original_baud_rate_; } ESP_LOGD(TAG, "Baud rate: %" PRIu32, baud_rate); diff --git a/esphome/components/npi19/npi19.cpp b/esphome/components/npi19/npi19.cpp index e8c4e8abd5..c531d2ec8f 100644 --- a/esphome/components/npi19/npi19.cpp +++ b/esphome/components/npi19/npi19.cpp @@ -33,7 +33,7 @@ float NPI19Component::get_setup_priority() const { return setup_priority::DATA; i2c::ErrorCode NPI19Component::read_(uint16_t &raw_temperature, uint16_t &raw_pressure) { // initiate data read from device - i2c::ErrorCode w_err = write(&READ_COMMAND, sizeof(READ_COMMAND), true); + i2c::ErrorCode w_err = write(&READ_COMMAND, sizeof(READ_COMMAND)); if (w_err != i2c::ERROR_OK) { return w_err; } diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py index 908a855f70..03927e8ea2 100644 --- a/esphome/components/nrf52/__init__.py +++ b/esphome/components/nrf52/__init__.py @@ -1,11 +1,16 @@ from __future__ import annotations +import asyncio +import logging from pathlib import Path +from esphome import pins import esphome.codegen as cg from esphome.components.zephyr import ( copy_files as zephyr_copy_files, zephyr_add_pm_static, + zephyr_add_prj_conf, + zephyr_data, zephyr_set_core_data, zephyr_to_code, ) @@ -18,6 +23,9 @@ import esphome.config_validation as cv from esphome.const import ( CONF_BOARD, CONF_FRAMEWORK, + CONF_ID, + CONF_RESET_PIN, + CONF_VOLTAGE, KEY_CORE, KEY_FRAMEWORK_VERSION, KEY_TARGET_FRAMEWORK, @@ -25,7 +33,7 @@ from esphome.const import ( PLATFORM_NRF52, ThreadModel, ) -from esphome.core import CORE, EsphomeError, coroutine_with_priority +from esphome.core import CORE, CoroPriority, EsphomeError, coroutine_with_priority from esphome.storage_json import StorageJSON from esphome.types import ConfigType @@ -43,6 +51,7 @@ from .gpio import nrf52_pin_to_code # noqa CODEOWNERS = ["@tomaszduda23"] AUTO_LOAD = ["zephyr"] IS_TARGET_PLATFORM = True +_LOGGER = logging.getLogger(__name__) def set_core_data(config: ConfigType) -> ConfigType: @@ -90,19 +99,63 @@ def _detect_bootloader(config: ConfigType) -> ConfigType: return config +nrf52_ns = cg.esphome_ns.namespace("nrf52") +DeviceFirmwareUpdate = nrf52_ns.class_("DeviceFirmwareUpdate", cg.Component) + +CONF_DFU = "dfu" +CONF_DCDC = "dcdc" +CONF_REG0 = "reg0" +CONF_UICR_ERASE = "uicr_erase" + +VOLTAGE_LEVELS = [1.8, 2.1, 2.4, 2.7, 3.0, 3.3] + CONFIG_SCHEMA = cv.All( + _detect_bootloader, + set_core_data, cv.Schema( { cv.Required(CONF_BOARD): cv.string_strict, cv.Optional(KEY_BOOTLOADER): cv.one_of(*BOOTLOADERS, lower=True), + cv.Optional(CONF_DFU): cv.Schema( + { + cv.GenerateID(): cv.declare_id(DeviceFirmwareUpdate), + cv.Required(CONF_RESET_PIN): pins.gpio_output_pin_schema, + } + ), + cv.Optional(CONF_DCDC, default=True): cv.boolean, + cv.Optional(CONF_REG0): cv.Schema( + { + cv.Required(CONF_VOLTAGE): cv.All( + cv.voltage, + cv.one_of(*VOLTAGE_LEVELS, float=True), + ), + cv.Optional(CONF_UICR_ERASE, default=False): cv.boolean, + } + ), } ), - _detect_bootloader, - set_core_data, ) -@coroutine_with_priority(1000) +def _validate_mcumgr(config): + bootloader = zephyr_data()[KEY_BOOTLOADER] + if bootloader == BOOTLOADER_MCUBOOT: + raise cv.Invalid(f"'{bootloader}' bootloader does not support DFU") + + +def _final_validate(config): + if CONF_DFU in config: + _validate_mcumgr(config) + if config[KEY_BOOTLOADER] == BOOTLOADER_ADAFRUIT: + _LOGGER.warning( + "Selected generic Adafruit bootloader. The board might crash. Consider settings `bootloader:`" + ) + + +FINAL_VALIDATE_SCHEMA = _final_validate + + +@coroutine_with_priority(CoroPriority.PLATFORM) async def to_code(config: ConfigType) -> None: """Convert the configuration to code.""" cg.add_platformio_option("board", config[CONF_BOARD]) @@ -119,14 +172,21 @@ async def to_code(config: ConfigType) -> None: cg.add_platformio_option( "platform_packages", [ - "platformio/framework-zephyr@https://github.com/tomaszduda23/framework-sdk-nrf/archive/refs/tags/v2.6.1-4.zip", - "platformio/toolchain-gccarmnoneeabi@https://github.com/tomaszduda23/toolchain-sdk-ng/archive/refs/tags/v0.16.1-1.zip", + "platformio/framework-zephyr@https://github.com/tomaszduda23/framework-sdk-nrf/archive/refs/tags/v2.6.1-7.zip", + "platformio/toolchain-gccarmnoneeabi@https://github.com/tomaszduda23/toolchain-sdk-ng/archive/refs/tags/v0.17.4-0.zip", ], ) if config[KEY_BOOTLOADER] == BOOTLOADER_MCUBOOT: cg.add_define("USE_BOOTLOADER_MCUBOOT") else: + if "_sd" in config[KEY_BOOTLOADER]: + bootloader = config[KEY_BOOTLOADER].split("_") + sd_id = bootloader[2][2:] + cg.add_define("USE_SOFTDEVICE_ID", int(sd_id)) + if (len(bootloader)) > 3: + sd_version = bootloader[3][1:] + cg.add_define("USE_SOFTDEVICE_VERSION", int(sd_version)) # make sure that firmware.zip is created # for Adafruit_nRF52_Bootloader cg.add_platformio_option("board_upload.protocol", "nrfutil") @@ -136,6 +196,26 @@ async def to_code(config: ConfigType) -> None: zephyr_to_code(config) + if dfu_config := config.get(CONF_DFU): + CORE.add_job(_dfu_to_code, dfu_config) + zephyr_add_prj_conf("BOARD_ENABLE_DCDC", config[CONF_DCDC]) + + if reg0_config := config.get(CONF_REG0): + value = VOLTAGE_LEVELS.index(reg0_config[CONF_VOLTAGE]) + cg.add_define("USE_NRF52_REG0_VOUT", value) + if reg0_config[CONF_UICR_ERASE]: + cg.add_define("USE_NRF52_UICR_ERASE") + + +@coroutine_with_priority(CoroPriority.DIAGNOSTICS) +async def _dfu_to_code(dfu_config): + cg.add_define("USE_NRF52_DFU") + var = cg.new_Pvariable(dfu_config[CONF_ID]) + pin = await cg.gpio_pin_expression(dfu_config[CONF_RESET_PIN]) + cg.add(var.set_reset_pin(pin)) + zephyr_add_prj_conf("CDC_ACM_DTE_RATE_CALLBACK_SUPPORT", True) + await cg.register_component(var, dfu_config) + def copy_files() -> None: """Copy files to the build directory.""" @@ -221,3 +301,20 @@ def upload_program(config: ConfigType, args, host: str) -> bool: raise EsphomeError(f"Upload failed with result: {result}") return handled + + +def show_logs(config: ConfigType, args, devices: list[str]) -> bool: + address = devices[0] + from .ble_logger import is_mac_address, logger_connect, logger_scan + + if devices[0] == "BLE": + ble_device = asyncio.run(logger_scan(CORE.config["esphome"]["name"])) + if ble_device: + address = ble_device.address + else: + return True + + if is_mac_address(address): + asyncio.run(logger_connect(address)) + return True + return False diff --git a/esphome/components/nrf52/ble_logger.py b/esphome/components/nrf52/ble_logger.py new file mode 100644 index 0000000000..f74a49ea89 --- /dev/null +++ b/esphome/components/nrf52/ble_logger.py @@ -0,0 +1,60 @@ +import asyncio +import logging +import re +from typing import Final + +from bleak import BleakClient, BleakScanner, BLEDevice +from bleak.exc import ( + BleakCharacteristicNotFoundError, + BleakDBusError, + BleakDeviceNotFoundError, +) + +_LOGGER = logging.getLogger(__name__) + + +NUS_SERVICE_UUID = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E" +NUS_TX_CHAR_UUID = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" + +MAC_ADDRESS_PATTERN: Final = re.compile( + r"([0-9A-F]{2}[:]){5}[0-9A-F]{2}$", flags=re.IGNORECASE +) + + +def is_mac_address(value: str) -> bool: + return MAC_ADDRESS_PATTERN.match(value) + + +async def logger_scan(name: str) -> BLEDevice | None: + _LOGGER.info("Scanning bluetooth for %s...", name) + device = await BleakScanner.find_device_by_name(name) + if not device: + _LOGGER.error("%s Bluetooth LE device was not found!", name) + return device + + +async def logger_connect(host: str) -> int | None: + disconnected_event = asyncio.Event() + + def handle_disconnect(client): + disconnected_event.set() + + def handle_rx(_, data: bytearray): + print(data.decode("utf-8"), end="") + + _LOGGER.info("Connecting %s...", host) + try: + async with BleakClient(host, disconnected_callback=handle_disconnect) as client: + _LOGGER.info("Connected %s...", host) + try: + await client.start_notify(NUS_TX_CHAR_UUID, handle_rx) + except BleakDBusError as e: + _LOGGER.error("Bluetooth LE logger: %s", e) + disconnected_event.set() + await disconnected_event.wait() + except BleakDeviceNotFoundError: + _LOGGER.error("Device %s not found", host) + return 1 + except BleakCharacteristicNotFoundError: + _LOGGER.error("Device %s has no NUS characteristic", host) + return 1 diff --git a/esphome/components/nrf52/boards.py b/esphome/components/nrf52/boards.py index 8e5fb2a23d..6064fe844a 100644 --- a/esphome/components/nrf52/boards.py +++ b/esphome/components/nrf52/boards.py @@ -11,10 +11,18 @@ from .const import ( BOARDS_ZEPHYR = { "adafruit_itsybitsy_nrf52840": { KEY_BOOTLOADER: [ + BOOTLOADER_ADAFRUIT_NRF52_SD140_V6, + BOOTLOADER_ADAFRUIT, + BOOTLOADER_ADAFRUIT_NRF52_SD132, + BOOTLOADER_ADAFRUIT_NRF52_SD140_V7, + ] + }, + "xiao_ble": { + KEY_BOOTLOADER: [ + BOOTLOADER_ADAFRUIT_NRF52_SD140_V7, BOOTLOADER_ADAFRUIT, BOOTLOADER_ADAFRUIT_NRF52_SD132, BOOTLOADER_ADAFRUIT_NRF52_SD140_V6, - BOOTLOADER_ADAFRUIT_NRF52_SD140_V7, ] }, } diff --git a/esphome/components/nrf52/const.py b/esphome/components/nrf52/const.py index 715d527a66..977ca2252a 100644 --- a/esphome/components/nrf52/const.py +++ b/esphome/components/nrf52/const.py @@ -2,6 +2,7 @@ BOOTLOADER_ADAFRUIT = "adafruit" BOOTLOADER_ADAFRUIT_NRF52_SD132 = "adafruit_nrf52_sd132" BOOTLOADER_ADAFRUIT_NRF52_SD140_V6 = "adafruit_nrf52_sd140_v6" BOOTLOADER_ADAFRUIT_NRF52_SD140_V7 = "adafruit_nrf52_sd140_v7" + EXTRA_ADC = [ "VDD", "VDDHDIV5", diff --git a/esphome/components/nrf52/dfu.cpp b/esphome/components/nrf52/dfu.cpp new file mode 100644 index 0000000000..9e49373467 --- /dev/null +++ b/esphome/components/nrf52/dfu.cpp @@ -0,0 +1,51 @@ +#include "dfu.h" + +#ifdef USE_NRF52_DFU + +#include +#include +#include +#include "esphome/core/log.h" + +namespace esphome { +namespace nrf52 { + +static const char *const TAG = "dfu"; + +volatile bool goto_dfu = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +static const uint32_t DFU_DBL_RESET_MAGIC = 0x5A1AD5; // SALADS + +#define DEVICE_AND_COMMA(node_id) DEVICE_DT_GET(node_id), + +static void cdc_dte_rate_callback(const struct device * /*unused*/, uint32_t rate) { + if (rate == 1200) { + goto_dfu = true; + } +} +void DeviceFirmwareUpdate::setup() { + this->reset_pin_->setup(); + const struct device *cdc_dev[] = {DT_FOREACH_STATUS_OKAY(zephyr_cdc_acm_uart, DEVICE_AND_COMMA)}; + for (auto &idx : cdc_dev) { + cdc_acm_dte_rate_callback_set(idx, cdc_dte_rate_callback); + } +} + +void DeviceFirmwareUpdate::loop() { + if (goto_dfu) { + goto_dfu = false; + volatile uint32_t *dbl_reset_mem = (volatile uint32_t *) 0x20007F7C; + (*dbl_reset_mem) = DFU_DBL_RESET_MAGIC; + this->reset_pin_->digital_write(true); + } +} + +void DeviceFirmwareUpdate::dump_config() { + ESP_LOGCONFIG(TAG, "DFU:"); + LOG_PIN(" RESET Pin: ", this->reset_pin_); +} + +} // namespace nrf52 +} // namespace esphome + +#endif diff --git a/esphome/components/nrf52/dfu.h b/esphome/components/nrf52/dfu.h new file mode 100644 index 0000000000..979a4567cf --- /dev/null +++ b/esphome/components/nrf52/dfu.h @@ -0,0 +1,24 @@ +#pragma once + +#include "esphome/core/defines.h" +#ifdef USE_NRF52_DFU +#include "esphome/core/component.h" +#include "esphome/core/gpio.h" + +namespace esphome { +namespace nrf52 { +class DeviceFirmwareUpdate : public Component { + public: + void setup() override; + void loop() override; + void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; } + void dump_config() override; + + protected: + GPIOPin *reset_pin_; +}; + +} // namespace nrf52 +} // namespace esphome + +#endif diff --git a/esphome/components/nrf52/gpio.py b/esphome/components/nrf52/gpio.py index 260114f90e..17329042b2 100644 --- a/esphome/components/nrf52/gpio.py +++ b/esphome/components/nrf52/gpio.py @@ -74,6 +74,9 @@ async def nrf52_pin_to_code(config): var = cg.new_Pvariable(config[CONF_ID]) num = config[CONF_NUMBER] cg.add(var.set_pin(num)) - cg.add(var.set_inverted(config[CONF_INVERTED])) + # Only set if true to avoid bloating setup() function + # (inverted bit in pin_flags_ bitfield is zero-initialized to false) + if config[CONF_INVERTED]: + cg.add(var.set_inverted(True)) cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) return var diff --git a/esphome/components/nrf52/uicr.cpp b/esphome/components/nrf52/uicr.cpp new file mode 100644 index 0000000000..4c0beeb503 --- /dev/null +++ b/esphome/components/nrf52/uicr.cpp @@ -0,0 +1,121 @@ +#include "esphome/core/defines.h" + +#ifdef USE_NRF52_REG0_VOUT +#include +#include +#include + +extern "C" { +void nvmc_config(uint32_t mode); +void nvmc_wait(); +nrfx_err_t nrfx_nvmc_uicr_erase(); +} + +namespace esphome::nrf52 { + +enum class StatusFlags : uint8_t { + OK = 0x00, + NEED_RESET = 0x01, + NEED_ERASE = 0x02, +}; + +constexpr StatusFlags &operator|=(StatusFlags &a, StatusFlags b) { + a = static_cast(static_cast(a) | static_cast(b)); + return a; +} + +constexpr bool operator&(StatusFlags a, StatusFlags b) { + return (static_cast(a) & static_cast(b)) != 0; +} + +static bool regout0_ok() { + return (NRF_UICR->REGOUT0 & UICR_REGOUT0_VOUT_Msk) == (USE_NRF52_REG0_VOUT << UICR_REGOUT0_VOUT_Pos); +} + +static StatusFlags set_regout0() { + /* If the board is powered from USB (high voltage mode), + * GPIO output voltage is set to 1.8 volts by default. + */ + if (!regout0_ok()) { + nvmc_config(NVMC_CONFIG_WEN_Wen); + NRF_UICR->REGOUT0 = + (NRF_UICR->REGOUT0 & ~((uint32_t) UICR_REGOUT0_VOUT_Msk)) | (USE_NRF52_REG0_VOUT << UICR_REGOUT0_VOUT_Pos); + nvmc_wait(); + nvmc_config(NVMC_CONFIG_WEN_Ren); + return regout0_ok() ? StatusFlags::NEED_RESET : StatusFlags::NEED_ERASE; + } + return StatusFlags::OK; +} + +#ifndef USE_BOOTLOADER_MCUBOOT +// https://github.com/adafruit/Adafruit_nRF52_Bootloader/blob/6a9a6a3e6d0f86918e9286188426a279976645bd/lib/sdk11/components/libraries/bootloader_dfu/dfu_types.h#L61 +constexpr uint32_t BOOTLOADER_REGION_START = 0x000F4000; +constexpr uint32_t BOOTLOADER_MBR_PARAMS_PAGE_ADDRESS = 0x000FE000; + +static bool bootloader_ok() { + return NRF_UICR->NRFFW[0] == BOOTLOADER_REGION_START && NRF_UICR->NRFFW[1] == BOOTLOADER_MBR_PARAMS_PAGE_ADDRESS; +} + +static StatusFlags fix_bootloader() { + if (!bootloader_ok()) { + nvmc_config(NVMC_CONFIG_WEN_Wen); + NRF_UICR->NRFFW[0] = BOOTLOADER_REGION_START; + NRF_UICR->NRFFW[1] = BOOTLOADER_MBR_PARAMS_PAGE_ADDRESS; + nvmc_wait(); + nvmc_config(NVMC_CONFIG_WEN_Ren); + return bootloader_ok() ? StatusFlags::NEED_RESET : StatusFlags::NEED_ERASE; + } + return StatusFlags::OK; +} +#endif + +#define BOOTLOADER_VERSION_REGISTER NRF_TIMER2->CC[0] + +static StatusFlags set_uicr() { + StatusFlags status = StatusFlags::OK; +#ifndef USE_BOOTLOADER_MCUBOOT + if (BOOTLOADER_VERSION_REGISTER <= 0x902) { +#ifdef CONFIG_PRINTK + printk("cannot control regout0 for %#x\n", BOOTLOADER_VERSION_REGISTER); +#endif + } else +#endif + { + status |= set_regout0(); + } +#ifndef USE_BOOTLOADER_MCUBOOT + status |= fix_bootloader(); +#endif + return status; +} + +static int board_esphome_init() { + StatusFlags status = set_uicr(); + +#ifdef USE_NRF52_UICR_ERASE + if (status & StatusFlags::NEED_ERASE) { + nrfx_err_t ret = nrfx_nvmc_uicr_erase(); + if (ret != NRFX_SUCCESS) { +#ifdef CONFIG_PRINTK + printk("nrfx_nvmc_uicr_erase failed %d\n", ret); +#endif + } else { + status |= set_uicr(); + } + } +#endif + + if (status & StatusFlags::NEED_RESET) { + /* a reset is required for changes to take effect */ + NVIC_SystemReset(); + } + + return 0; +} +} // namespace esphome::nrf52 + +static int board_esphome_init() { return esphome::nrf52::board_esphome_init(); } + +SYS_INIT(board_esphome_init, PRE_KERNEL_1, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT); + +#endif diff --git a/esphome/components/ntc/ntc.cpp b/esphome/components/ntc/ntc.cpp index 333dbc5a75..b08f84029b 100644 --- a/esphome/components/ntc/ntc.cpp +++ b/esphome/components/ntc/ntc.cpp @@ -11,7 +11,7 @@ void NTC::setup() { if (this->sensor_->has_state()) this->process_(this->sensor_->state); } -void NTC::dump_config() { LOG_SENSOR("", "NTC Sensor", this) } +void NTC::dump_config() { LOG_SENSOR("", "NTC Sensor", this); } float NTC::get_setup_priority() const { return setup_priority::DATA; } void NTC::process_(float value) { if (std::isnan(value)) { diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index 4a83d5fc5f..368b431d7b 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -51,6 +51,7 @@ from esphome.const import ( DEVICE_CLASS_OZONE, DEVICE_CLASS_PH, DEVICE_CLASS_PM1, + DEVICE_CLASS_PM4, DEVICE_CLASS_PM10, DEVICE_CLASS_PM25, DEVICE_CLASS_POWER, @@ -65,6 +66,7 @@ from esphome.const import ( DEVICE_CLASS_SPEED, DEVICE_CLASS_SULPHUR_DIOXIDE, DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_TEMPERATURE_DELTA, DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, DEVICE_CLASS_VOLTAGE, @@ -76,7 +78,7 @@ from esphome.const import ( DEVICE_CLASS_WIND_DIRECTION, DEVICE_CLASS_WIND_SPEED, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass @@ -116,6 +118,7 @@ DEVICE_CLASSES = [ DEVICE_CLASS_PM1, DEVICE_CLASS_PM10, DEVICE_CLASS_PM25, + DEVICE_CLASS_PM4, DEVICE_CLASS_POWER, DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_PRECIPITATION, @@ -128,6 +131,7 @@ DEVICE_CLASSES = [ DEVICE_CLASS_SPEED, DEVICE_CLASS_SULPHUR_DIOXIDE, DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_TEMPERATURE_DELTA, DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, DEVICE_CLASS_VOLTAGE, @@ -234,11 +238,6 @@ def number_schema( return _NUMBER_SCHEMA.extend(schema) -# Remove before 2025.11.0 -NUMBER_SCHEMA = number_schema(Number) -NUMBER_SCHEMA.add_extra(cv.deprecated_schema_constant("number")) - - async def setup_number_core_( var, config, *, min_value: float, max_value: float, step: float ): @@ -248,7 +247,10 @@ async def setup_number_core_( cg.add(var.traits.set_max_value(max_value)) cg.add(var.traits.set_step(step)) - cg.add(var.traits.set_mode(config[CONF_MODE])) + # Only set if non-default to avoid bloating setup() function + # (mode_ is initialized to NUMBER_MODE_AUTO in the header) + if config[CONF_MODE] != NumberMode.NUMBER_MODE_AUTO: + cg.add(var.traits.set_mode(config[CONF_MODE])) for conf in config.get(CONF_ON_VALUE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) @@ -321,7 +323,7 @@ async def number_in_range_to_code(config, condition_id, template_arg, args): return var -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(number_ns.using) diff --git a/esphome/components/number/automation.cpp b/esphome/components/number/automation.cpp index cadc6f54f6..78ffc255fe 100644 --- a/esphome/components/number/automation.cpp +++ b/esphome/components/number/automation.cpp @@ -1,8 +1,7 @@ #include "automation.h" #include "esphome/core/log.h" -namespace esphome { -namespace number { +namespace esphome::number { static const char *const TAG = "number.automation"; @@ -15,7 +14,7 @@ void ValueRangeTrigger::setup() { float local_min = this->min_.value(0.0); float local_max = this->max_.value(0.0); convert hash = {.from = (local_max - local_min)}; - uint32_t myhash = hash.to ^ this->parent_->get_object_id_hash(); + uint32_t myhash = hash.to ^ this->parent_->get_preference_hash(); this->rtc_ = global_preferences->make_preference(myhash); bool initial_state; if (this->rtc_.load(&initial_state)) { @@ -52,5 +51,4 @@ void ValueRangeTrigger::on_state_(float state) { this->rtc_.save(&in_range); } -} // namespace number -} // namespace esphome +} // namespace esphome::number diff --git a/esphome/components/number/automation.h b/esphome/components/number/automation.h index 33f0f9727e..a7cd04f083 100644 --- a/esphome/components/number/automation.h +++ b/esphome/components/number/automation.h @@ -4,8 +4,7 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" -namespace esphome { -namespace number { +namespace esphome::number { class NumberStateTrigger : public Trigger { public: @@ -19,7 +18,7 @@ template class NumberSetAction : public Action { NumberSetAction(Number *number) : number_(number) {} TEMPLATABLE_VALUE(float, value) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->number_->make_call(); call.set_value(this->value_.value(x...)); call.perform(); @@ -35,7 +34,7 @@ template class NumberOperationAction : public Action { TEMPLATABLE_VALUE(NumberOperation, operation) TEMPLATABLE_VALUE(bool, cycle) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->number_->make_call(); call.with_operation(this->operation_.value(x...)); if (this->cycle_.has_value()) { @@ -74,7 +73,7 @@ template class NumberInRangeCondition : public Condition void set_min(float min) { this->min_ = min; } void set_max(float max) { this->max_ = max; } - bool check(Ts... x) override { + bool check(const Ts &...x) override { const float state = this->parent_->state; if (std::isnan(this->min_)) { return state <= this->max_; @@ -91,5 +90,4 @@ template class NumberInRangeCondition : public Condition float max_{NAN}; }; -} // namespace number -} // namespace esphome +} // namespace esphome::number diff --git a/esphome/components/number/number.cpp b/esphome/components/number/number.cpp index b6a845b19b..992100ead0 100644 --- a/esphome/components/number/number.cpp +++ b/esphome/components/number/number.cpp @@ -1,21 +1,45 @@ #include "number.h" +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" -namespace esphome { -namespace number { +namespace esphome::number { static const char *const TAG = "number"; +// Function implementation of LOG_NUMBER macro to reduce code size +void log_number(const char *tag, const char *prefix, const char *type, Number *obj) { + if (obj == nullptr) { + return; + } + + ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str()); + + if (!obj->get_icon_ref().empty()) { + ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj->get_icon_ref().c_str()); + } + + if (!obj->traits.get_unit_of_measurement_ref().empty()) { + ESP_LOGCONFIG(tag, "%s Unit of Measurement: '%s'", prefix, obj->traits.get_unit_of_measurement_ref().c_str()); + } + + if (!obj->traits.get_device_class_ref().empty()) { + ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj->traits.get_device_class_ref().c_str()); + } +} + 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); this->state_callback_.call(state); +#if defined(USE_NUMBER) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_number_update(this); +#endif } void Number::add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } -} // namespace number -} // namespace esphome +} // namespace esphome::number diff --git a/esphome/components/number/number.h b/esphome/components/number/number.h index 49bcbb857c..472e06ad61 100644 --- a/esphome/components/number/number.h +++ b/esphome/components/number/number.h @@ -6,22 +6,12 @@ #include "number_call.h" #include "number_traits.h" -namespace esphome { -namespace number { +namespace esphome::number { -#define LOG_NUMBER(prefix, type, obj) \ - if ((obj) != nullptr) { \ - ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - if (!(obj)->get_icon().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \ - } \ - if (!(obj)->traits.get_unit_of_measurement().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Unit of Measurement: '%s'", prefix, (obj)->traits.get_unit_of_measurement().c_str()); \ - } \ - if (!(obj)->traits.get_device_class().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->traits.get_device_class().c_str()); \ - } \ - } +class Number; +void log_number(const char *tag, const char *prefix, const char *type, Number *obj); + +#define LOG_NUMBER(prefix, type, obj) log_number(TAG, prefix, LOG_STR_LITERAL(type), obj) #define SUB_NUMBER(name) \ protected: \ @@ -62,5 +52,4 @@ class Number : public EntityBase { CallbackManager state_callback_; }; -} // namespace number -} // namespace esphome +} // namespace esphome::number diff --git a/esphome/components/number/number_call.cpp b/esphome/components/number/number_call.cpp index 4219f85328..27a857c112 100644 --- a/esphome/components/number/number_call.cpp +++ b/esphome/components/number/number_call.cpp @@ -2,11 +2,21 @@ #include "number.h" #include "esphome/core/log.h" -namespace esphome { -namespace number { +namespace esphome::number { static const char *const TAG = "number"; +// Helper functions to reduce code size for logging +void NumberCall::log_perform_warning_(const LogString *message) { + ESP_LOGW(TAG, "'%s': %s", this->parent_->get_name().c_str(), LOG_STR_ARG(message)); +} + +void NumberCall::log_perform_warning_value_range_(const LogString *comparison, const LogString *limit_type, float val, + float limit) { + ESP_LOGW(TAG, "'%s': %f %s %s %f", this->parent_->get_name().c_str(), val, LOG_STR_ARG(comparison), + LOG_STR_ARG(limit_type), limit); +} + NumberCall &NumberCall::set_value(float value) { return this->with_operation(NUMBER_OP_SET).with_value(value); } NumberCall &NumberCall::number_increment(bool cycle) { @@ -42,7 +52,7 @@ void NumberCall::perform() { const auto &traits = parent->traits; if (this->operation_ == NUMBER_OP_NONE) { - ESP_LOGW(TAG, "'%s' - NumberCall performed without selecting an operation", name); + this->log_perform_warning_(LOG_STR("No operation")); return; } @@ -51,28 +61,28 @@ void NumberCall::perform() { float max_value = traits.get_max_value(); if (this->operation_ == NUMBER_OP_SET) { - ESP_LOGD(TAG, "'%s' - Setting number value", name); + ESP_LOGD(TAG, "'%s': Setting value", name); if (!this->value_.has_value() || std::isnan(*this->value_)) { - ESP_LOGW(TAG, "'%s' - No value set for NumberCall", name); + this->log_perform_warning_(LOG_STR("No value")); return; } target_value = this->value_.value(); } else if (this->operation_ == NUMBER_OP_TO_MIN) { if (std::isnan(min_value)) { - ESP_LOGW(TAG, "'%s' - Can't set to min value through NumberCall: no min_value defined", name); + this->log_perform_warning_(LOG_STR("min undefined")); } else { target_value = min_value; } } else if (this->operation_ == NUMBER_OP_TO_MAX) { if (std::isnan(max_value)) { - ESP_LOGW(TAG, "'%s' - Can't set to max value through NumberCall: no max_value defined", name); + this->log_perform_warning_(LOG_STR("max undefined")); } else { target_value = max_value; } } else if (this->operation_ == NUMBER_OP_INCREMENT) { - ESP_LOGD(TAG, "'%s' - Increment number, with%s cycling", name, this->cycle_ ? "" : "out"); + ESP_LOGD(TAG, "'%s': Increment with%s cycling", name, this->cycle_ ? "" : "out"); if (!parent->has_state()) { - ESP_LOGW(TAG, "'%s' - Can't increment number through NumberCall: no active state to modify", name); + this->log_perform_warning_(LOG_STR("Can't increment, no state")); return; } auto step = traits.get_step(); @@ -85,9 +95,9 @@ void NumberCall::perform() { } } } else if (this->operation_ == NUMBER_OP_DECREMENT) { - ESP_LOGD(TAG, "'%s' - Decrement number, with%s cycling", name, this->cycle_ ? "" : "out"); + ESP_LOGD(TAG, "'%s': Decrement with%s cycling", name, this->cycle_ ? "" : "out"); if (!parent->has_state()) { - ESP_LOGW(TAG, "'%s' - Can't decrement number through NumberCall: no active state to modify", name); + this->log_perform_warning_(LOG_STR("Can't decrement, no state")); return; } auto step = traits.get_step(); @@ -102,17 +112,16 @@ void NumberCall::perform() { } if (target_value < min_value) { - ESP_LOGW(TAG, "'%s' - Value %f must not be less than minimum %f", name, target_value, min_value); + this->log_perform_warning_value_range_(LOG_STR("<"), LOG_STR("min"), target_value, min_value); return; } if (target_value > max_value) { - ESP_LOGW(TAG, "'%s' - Value %f must not be greater than maximum %f", name, target_value, max_value); + this->log_perform_warning_value_range_(LOG_STR(">"), LOG_STR("max"), target_value, max_value); return; } - ESP_LOGD(TAG, " New number value: %f", target_value); + ESP_LOGD(TAG, " New value: %f", target_value); this->parent_->control(target_value); } -} // namespace number -} // namespace esphome +} // namespace esphome::number diff --git a/esphome/components/number/number_call.h b/esphome/components/number/number_call.h index bd50170be5..0f6889dcb6 100644 --- a/esphome/components/number/number_call.h +++ b/esphome/components/number/number_call.h @@ -1,10 +1,10 @@ #pragma once #include "esphome/core/helpers.h" +#include "esphome/core/log.h" #include "number_traits.h" -namespace esphome { -namespace number { +namespace esphome::number { class Number; @@ -33,11 +33,14 @@ class NumberCall { NumberCall &with_cycle(bool cycle); protected: + void log_perform_warning_(const LogString *message); + void log_perform_warning_value_range_(const LogString *comparison, const LogString *limit_type, float val, + float limit); + Number *const parent_; NumberOperation operation_{NUMBER_OP_NONE}; optional value_; bool cycle_; }; -} // namespace number -} // namespace esphome +} // namespace esphome::number diff --git a/esphome/components/number/number_traits.cpp b/esphome/components/number/number_traits.cpp index 89035661f5..1e4239ceca 100644 --- a/esphome/components/number/number_traits.cpp +++ b/esphome/components/number/number_traits.cpp @@ -1,10 +1,8 @@ #include "esphome/core/log.h" #include "number_traits.h" -namespace esphome { -namespace number { +namespace esphome::number { static const char *const TAG = "number"; -} // namespace number -} // namespace esphome +} // namespace esphome::number diff --git a/esphome/components/number/number_traits.h b/esphome/components/number/number_traits.h index fa68c2390a..5ccbb9ba48 100644 --- a/esphome/components/number/number_traits.h +++ b/esphome/components/number/number_traits.h @@ -3,8 +3,7 @@ #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace number { +namespace esphome::number { enum NumberMode : uint8_t { NUMBER_MODE_AUTO = 0, @@ -35,5 +34,4 @@ class NumberTraits : public EntityBase_DeviceClass, public EntityBase_UnitOfMeas NumberMode mode_{NUMBER_MODE_AUTO}; }; -} // namespace number -} // namespace esphome +} // namespace esphome::number diff --git a/esphome/components/one_wire/__init__.py b/esphome/components/one_wire/__init__.py index 6d95b8fd33..e12cca3e27 100644 --- a/esphome/components/one_wire/__init__.py +++ b/esphome/components/one_wire/__init__.py @@ -1,6 +1,6 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.const import CONF_ADDRESS +from esphome.const import CONF_ADDRESS, CONF_INDEX CODEOWNERS = ["@ssieb"] @@ -21,7 +21,8 @@ def one_wire_device_schema(): return cv.Schema( { cv.GenerateID(CONF_ONE_WIRE_ID): cv.use_id(OneWireBus), - cv.Optional(CONF_ADDRESS): cv.hex_uint64_t, + cv.Exclusive(CONF_ADDRESS, "index_or_address"): cv.hex_uint64_t, + cv.Exclusive(CONF_INDEX, "index_or_address"): cv.uint8_t, } ) @@ -37,3 +38,5 @@ async def register_one_wire_device(var, config): cg.add(var.set_one_wire_bus(parent)) if (address := config.get(CONF_ADDRESS)) is not None: cg.add(var.set_address(address)) + if (index := config.get(CONF_INDEX)) is not None: + cg.add(var.set_index(index)) diff --git a/esphome/components/one_wire/one_wire.cpp b/esphome/components/one_wire/one_wire.cpp index 96e6145f63..fd139d0ddc 100644 --- a/esphome/components/one_wire/one_wire.cpp +++ b/esphome/components/one_wire/one_wire.cpp @@ -18,10 +18,20 @@ bool OneWireDevice::send_command_(uint8_t cmd) { return true; } -bool OneWireDevice::check_address_() { +bool OneWireDevice::check_address_or_index_() { if (this->address_ != 0) return true; auto devices = this->bus_->get_devices(); + + if (this->index_ != INDEX_NOT_SET) { + if (this->index_ >= devices.size()) { + ESP_LOGE(TAG, "Index %d out of range, only %d devices found", this->index_, devices.size()); + return false; + } + this->address_ = devices[this->index_]; + return true; + } + if (devices.empty()) { ESP_LOGE(TAG, "No devices, can't auto-select address"); return false; diff --git a/esphome/components/one_wire/one_wire.h b/esphome/components/one_wire/one_wire.h index e83c6e81e8..f6a956a92c 100644 --- a/esphome/components/one_wire/one_wire.h +++ b/esphome/components/one_wire/one_wire.h @@ -17,6 +17,8 @@ class OneWireDevice { /// @param address of the device void set_address(uint64_t address) { this->address_ = address; } + void set_index(uint8_t index) { this->index_ = index; } + /// @brief store the pointer to the OneWireBus to use /// @param bus pointer to the OneWireBus object void set_one_wire_bus(OneWireBus *bus) { this->bus_ = bus; } @@ -25,13 +27,16 @@ class OneWireDevice { const std::string &get_address_name(); protected: + static constexpr uint8_t INDEX_NOT_SET = 255; + uint64_t address_{0}; + uint8_t index_{INDEX_NOT_SET}; OneWireBus *bus_{nullptr}; ///< pointer to OneWireBus instance std::string address_name_; /// @brief find an address if necessary /// should be called from setup - bool check_address_(); + bool check_address_or_index_(); /// @brief send command on the bus /// @param cmd command to send diff --git a/esphome/components/online_image/bmp_image.cpp b/esphome/components/online_image/bmp_image.cpp index f55c9f1813..676a2efca9 100644 --- a/esphome/components/online_image/bmp_image.cpp +++ b/esphome/components/online_image/bmp_image.cpp @@ -117,7 +117,8 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) { this->paint_index_++; this->current_index_ += 3; index += 3; - if (x == this->width_ - 1 && this->padding_bytes_ > 0) { + size_t last_col = static_cast(this->width_) - 1; + if (x == last_col && this->padding_bytes_ > 0) { index += this->padding_bytes_; this->current_index_ += this->padding_bytes_; } diff --git a/esphome/components/online_image/jpeg_image.cpp b/esphome/components/online_image/jpeg_image.cpp index e5ee3dd8bf..10586091d5 100644 --- a/esphome/components/online_image/jpeg_image.cpp +++ b/esphome/components/online_image/jpeg_image.cpp @@ -25,8 +25,10 @@ static int draw_callback(JPEGDRAW *jpeg) { // to avoid crashing. App.feed_wdt(); size_t position = 0; - for (size_t y = 0; y < jpeg->iHeight; y++) { - for (size_t x = 0; x < jpeg->iWidth; x++) { + size_t height = static_cast(jpeg->iHeight); + size_t width = static_cast(jpeg->iWidth); + for (size_t y = 0; y < height; y++) { + for (size_t x = 0; x < width; x++) { auto rg = decode_value(jpeg->pPixels[position++]); auto ba = decode_value(jpeg->pPixels[position++]); Color color(rg[1], rg[0], ba[1], ba[0]); diff --git a/esphome/components/online_image/online_image.h b/esphome/components/online_image/online_image.h index 3326cbe8d6..12d409ca29 100644 --- a/esphome/components/online_image/online_image.h +++ b/esphome/components/online_image/online_image.h @@ -207,7 +207,7 @@ template class OnlineImageSetUrlAction : public Action { OnlineImageSetUrlAction(OnlineImage *parent) : parent_(parent) {} TEMPLATABLE_VALUE(std::string, url) TEMPLATABLE_VALUE(bool, update) - void play(Ts... x) override { + void play(const Ts &...x) override { this->parent_->set_url(this->url_.value(x...)); if (this->update_.value(x...)) { this->parent_->update(); @@ -221,7 +221,7 @@ template class OnlineImageSetUrlAction : public Action { template class OnlineImageReleaseAction : public Action { public: OnlineImageReleaseAction(OnlineImage *parent) : parent_(parent) {} - void play(Ts... x) override { this->parent_->release(); } + void play(const Ts &...x) override { this->parent_->release(); } protected: OnlineImage *parent_; diff --git a/esphome/components/online_image/png_image.cpp b/esphome/components/online_image/png_image.cpp index 2038d09ed0..ce9d3bdc91 100644 --- a/esphome/components/online_image/png_image.cpp +++ b/esphome/components/online_image/png_image.cpp @@ -2,6 +2,7 @@ #ifdef USE_ONLINE_IMAGE_PNG_SUPPORT #include "esphome/components/display/display_buffer.h" +#include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -38,6 +39,14 @@ static void draw_callback(pngle_t *pngle, uint32_t x, uint32_t y, uint32_t w, ui PngDecoder *decoder = (PngDecoder *) pngle_get_user_data(pngle); Color color(rgba[0], rgba[1], rgba[2], rgba[3]); decoder->draw(x, y, w, h, color); + + // Feed watchdog periodically to avoid triggering during long decode operations. + // Feed every 1024 pixels to balance efficiency and responsiveness. + uint32_t pixels = w * h; + decoder->increment_pixels_decoded(pixels); + if ((decoder->get_pixels_decoded() % 1024) < pixels) { + App.feed_wdt(); + } } PngDecoder::PngDecoder(OnlineImage *image) : ImageDecoder(image) { diff --git a/esphome/components/online_image/png_image.h b/esphome/components/online_image/png_image.h index 46519f8ef4..40e85dde33 100644 --- a/esphome/components/online_image/png_image.h +++ b/esphome/components/online_image/png_image.h @@ -25,9 +25,13 @@ class PngDecoder : public ImageDecoder { int prepare(size_t download_size) override; int HOT decode(uint8_t *buffer, size_t size) override; + void increment_pixels_decoded(uint32_t count) { this->pixels_decoded_ += count; } + uint32_t get_pixels_decoded() const { return this->pixels_decoded_; } + protected: RAMAllocator allocator_; pngle_t *pngle_; + uint32_t pixels_decoded_{0}; }; } // namespace online_image diff --git a/esphome/components/opentherm/hub.h b/esphome/components/opentherm/hub.h index 80fd268820..ee0cfd104d 100644 --- a/esphome/components/opentherm/hub.h +++ b/esphome/components/opentherm/hub.h @@ -1,10 +1,10 @@ #pragma once +#include +#include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/hal.h" -#include "esphome/core/component.h" #include "esphome/core/log.h" -#include #include "opentherm.h" @@ -17,21 +17,21 @@ #endif #ifdef OPENTHERM_USE_SWITCH -#include "esphome/components/opentherm/switch/switch.h" +#include "esphome/components/opentherm/switch/opentherm_switch.h" #endif #ifdef OPENTHERM_USE_OUTPUT -#include "esphome/components/opentherm/output/output.h" +#include "esphome/components/opentherm/output/opentherm_output.h" #endif #ifdef OPENTHERM_USE_NUMBER -#include "esphome/components/opentherm/number/number.h" +#include "esphome/components/opentherm/number/opentherm_number.h" #endif +#include #include #include #include -#include #include "opentherm_macros.h" diff --git a/esphome/components/opentherm/number/number.cpp b/esphome/components/opentherm/number/opentherm_number.cpp similarity index 94% rename from esphome/components/opentherm/number/number.cpp rename to esphome/components/opentherm/number/opentherm_number.cpp index 90ab5d6490..f0c69144c8 100644 --- a/esphome/components/opentherm/number/number.cpp +++ b/esphome/components/opentherm/number/opentherm_number.cpp @@ -1,4 +1,4 @@ -#include "number.h" +#include "opentherm_number.h" namespace esphome { namespace opentherm { @@ -17,7 +17,7 @@ void OpenthermNumber::setup() { if (!this->restore_value_) { value = this->initial_value_; } else { - this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); + this->pref_ = global_preferences->make_preference(this->get_preference_hash()); if (!this->pref_.load(&value)) { if (!std::isnan(this->initial_value_)) { value = this->initial_value_; diff --git a/esphome/components/opentherm/number/number.h b/esphome/components/opentherm/number/opentherm_number.h similarity index 100% rename from esphome/components/opentherm/number/number.h rename to esphome/components/opentherm/number/opentherm_number.h diff --git a/esphome/components/opentherm/opentherm.cpp b/esphome/components/opentherm/opentherm.cpp index b2751470b2..d59b9584d1 100644 --- a/esphome/components/opentherm/opentherm.cpp +++ b/esphome/components/opentherm/opentherm.cpp @@ -7,7 +7,7 @@ #include "opentherm.h" #include "esphome/core/helpers.h" -#if defined(ESP32) || defined(USE_ESP_IDF) +#ifdef USE_ESP32 #include "driver/timer.h" #include "esp_err.h" #endif @@ -31,7 +31,7 @@ OpenTherm *OpenTherm::instance = nullptr; OpenTherm::OpenTherm(InternalGPIOPin *in_pin, InternalGPIOPin *out_pin, int32_t device_timeout) : in_pin_(in_pin), out_pin_(out_pin), -#if defined(ESP32) || defined(USE_ESP_IDF) +#ifdef USE_ESP32 timer_group_(TIMER_GROUP_0), timer_idx_(TIMER_0), #endif @@ -57,7 +57,7 @@ bool OpenTherm::initialize() { this->out_pin_->setup(); this->out_pin_->digital_write(true); -#if defined(ESP32) || defined(USE_ESP_IDF) +#ifdef USE_ESP32 return this->init_esp32_timer_(); #else return true; @@ -238,7 +238,7 @@ void IRAM_ATTR OpenTherm::write_bit_(uint8_t high, uint8_t clock) { } } -#if defined(ESP32) || defined(USE_ESP_IDF) +#ifdef USE_ESP32 bool OpenTherm::init_esp32_timer_() { // Search for a free timer. Maybe unstable, we'll see. @@ -365,7 +365,7 @@ void IRAM_ATTR OpenTherm::stop_timer_() { } } -#endif // END ESP32 +#endif // USE_ESP32 #ifdef ESP8266 // 5 kHz timer_ diff --git a/esphome/components/opentherm/opentherm.h b/esphome/components/opentherm/opentherm.h index a5822cdfe1..3996481760 100644 --- a/esphome/components/opentherm/opentherm.h +++ b/esphome/components/opentherm/opentherm.h @@ -12,7 +12,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#if defined(ESP32) || defined(USE_ESP_IDF) +#ifdef USE_ESP32 #include "driver/timer.h" #endif @@ -356,7 +356,7 @@ class OpenTherm { ISRInternalGPIOPin isr_in_pin_; ISRInternalGPIOPin isr_out_pin_; -#if defined(ESP32) || defined(USE_ESP_IDF) +#ifdef USE_ESP32 timer_group_t timer_group_; timer_idx_t timer_idx_; #endif @@ -370,7 +370,7 @@ class OpenTherm { int32_t timeout_counter_; // <0 no timeout int32_t device_timeout_; -#if defined(ESP32) || defined(USE_ESP_IDF) +#ifdef USE_ESP32 esp_err_t timer_error_ = ESP_OK; TimerErrorType timer_error_type_ = TimerErrorType::NO_TIMER_ERROR; diff --git a/esphome/components/opentherm/output/output.cpp b/esphome/components/opentherm/output/opentherm_output.cpp similarity index 95% rename from esphome/components/opentherm/output/output.cpp rename to esphome/components/opentherm/output/opentherm_output.cpp index 486aa0d4e7..ff82ddd72c 100644 --- a/esphome/components/opentherm/output/output.cpp +++ b/esphome/components/opentherm/output/opentherm_output.cpp @@ -1,5 +1,5 @@ #include "esphome/core/helpers.h" // for clamp() and lerp() -#include "output.h" +#include "opentherm_output.h" namespace esphome { namespace opentherm { diff --git a/esphome/components/opentherm/output/output.h b/esphome/components/opentherm/output/opentherm_output.h similarity index 100% rename from esphome/components/opentherm/output/output.h rename to esphome/components/opentherm/output/opentherm_output.h diff --git a/esphome/components/opentherm/switch/switch.cpp b/esphome/components/opentherm/switch/opentherm_switch.cpp similarity index 96% rename from esphome/components/opentherm/switch/switch.cpp rename to esphome/components/opentherm/switch/opentherm_switch.cpp index 228d9ac8f3..5c5d62e68e 100644 --- a/esphome/components/opentherm/switch/switch.cpp +++ b/esphome/components/opentherm/switch/opentherm_switch.cpp @@ -1,4 +1,4 @@ -#include "switch.h" +#include "opentherm_switch.h" namespace esphome { namespace opentherm { diff --git a/esphome/components/opentherm/switch/switch.h b/esphome/components/opentherm/switch/opentherm_switch.h similarity index 100% rename from esphome/components/opentherm/switch/switch.h rename to esphome/components/opentherm/switch/opentherm_switch.h diff --git a/esphome/components/openthread/__init__.py b/esphome/components/openthread/__init__.py index 2f085ebaae..e3ad3ed76c 100644 --- a/esphome/components/openthread/__init__.py +++ b/esphome/components/openthread/__init__.py @@ -4,11 +4,14 @@ from esphome.components.esp32 import ( VARIANT_ESP32H2, add_idf_sdkconfig_option, only_on_variant, + require_vfs_select, ) -from esphome.components.mdns import MDNSComponent +from esphome.components.mdns import MDNSComponent, enable_mdns_storage import esphome.config_validation as cv -from esphome.const import CONF_CHANNEL, CONF_ENABLE_IPV6, CONF_ID +from esphome.const import CONF_CHANNEL, CONF_ENABLE_IPV6, CONF_ID, CONF_USE_ADDRESS +from esphome.core import CORE, TimePeriodMilliseconds import esphome.final_validate as fv +from esphome.types import ConfigType from .const import ( CONF_DEVICE_TYPE, @@ -19,6 +22,7 @@ from .const import ( CONF_NETWORK_KEY, CONF_NETWORK_NAME, CONF_PAN_ID, + CONF_POLL_PERIOD, CONF_PSKC, CONF_SRP_ID, CONF_TLV, @@ -86,7 +90,7 @@ def set_sdkconfig_options(config): add_idf_sdkconfig_option("CONFIG_OPENTHREAD_SRP_CLIENT", True) add_idf_sdkconfig_option("CONFIG_OPENTHREAD_SRP_CLIENT_MAX_SERVICES", 5) - # TODO: Add suport for sleepy end devices + # TODO: Add suport for synchronized sleepy end devices (SSED) add_idf_sdkconfig_option(f"CONFIG_OPENTHREAD_{config.get(CONF_DEVICE_TYPE)}", True) @@ -106,6 +110,31 @@ _CONNECTION_SCHEMA = cv.Schema( } ) + +def _validate(config: ConfigType) -> ConfigType: + if CONF_USE_ADDRESS not in config: + config[CONF_USE_ADDRESS] = f"{CORE.name}.local" + device_type = config.get(CONF_DEVICE_TYPE) + poll_period = config.get(CONF_POLL_PERIOD) + if ( + device_type == "FTD" + and poll_period + and poll_period > TimePeriodMilliseconds(milliseconds=0) + ): + raise cv.Invalid( + f"{CONF_POLL_PERIOD} can only be used with {CONF_DEVICE_TYPE}: MTD" + ) + + return config + + +def _require_vfs_select(config): + """Register VFS select requirement during config validation.""" + # OpenThread uses esp_vfs_eventfd which requires VFS select support + require_vfs_select() + return config + + CONFIG_SCHEMA = cv.All( cv.Schema( { @@ -117,11 +146,15 @@ CONFIG_SCHEMA = cv.All( ), cv.Optional(CONF_FORCE_DATASET): cv.boolean, cv.Optional(CONF_TLV): cv.string_strict, + cv.Optional(CONF_USE_ADDRESS): cv.string_strict, + cv.Optional(CONF_POLL_PERIOD): cv.positive_time_period_milliseconds, } ).extend(_CONNECTION_SCHEMA), cv.has_exactly_one_key(CONF_NETWORK_KEY, CONF_TLV), cv.only_with_esp_idf, only_on_variant(supported=[VARIANT_ESP32C6, VARIANT_ESP32H2]), + _validate, + _require_vfs_select, ) @@ -141,8 +174,14 @@ FINAL_VALIDATE_SCHEMA = _final_validate async def to_code(config): cg.add_define("USE_OPENTHREAD") + # OpenThread SRP needs access to mDNS services after setup + enable_mdns_storage() + ot = cg.new_Pvariable(config[CONF_ID]) + cg.add(ot.set_use_address(config[CONF_USE_ADDRESS])) await cg.register_component(ot, config) + if (poll_period := config.get(CONF_POLL_PERIOD)) is not None: + cg.add(ot.set_poll_period(poll_period)) srp = cg.new_Pvariable(config[CONF_SRP_ID]) mdns_component = await cg.get_variable(config[CONF_MDNS_ID]) diff --git a/esphome/components/openthread/const.py b/esphome/components/openthread/const.py index 7a6ffb2df4..f0274a8c9e 100644 --- a/esphome/components/openthread/const.py +++ b/esphome/components/openthread/const.py @@ -6,6 +6,7 @@ CONF_MESH_LOCAL_PREFIX = "mesh_local_prefix" CONF_NETWORK_NAME = "network_name" CONF_NETWORK_KEY = "network_key" CONF_PAN_ID = "pan_id" +CONF_POLL_PERIOD = "poll_period" CONF_PSKC = "pskc" CONF_SRP_ID = "srp_id" CONF_TLV = "tlv" diff --git a/esphome/components/openthread/openthread.cpp b/esphome/components/openthread/openthread.cpp index 322ff43238..721ab89326 100644 --- a/esphome/components/openthread/openthread.cpp +++ b/esphome/components/openthread/openthread.cpp @@ -11,8 +11,6 @@ #include #include #include -#include -#include #include #include @@ -31,6 +29,23 @@ OpenThreadComponent *global_openthread_component = // NOLINT(cppcoreguidelines- OpenThreadComponent::OpenThreadComponent() { global_openthread_component = this; } +void OpenThreadComponent::dump_config() { + ESP_LOGCONFIG(TAG, "Open Thread:"); +#if CONFIG_OPENTHREAD_FTD + ESP_LOGCONFIG(TAG, " Device Type: FTD"); +#elif CONFIG_OPENTHREAD_MTD + ESP_LOGCONFIG(TAG, " Device Type: MTD"); + // TBD: Synchronized Sleepy End Device + if (this->poll_period > 0) { + ESP_LOGCONFIG(TAG, " Device is configured as Sleepy End Device (SED)"); + uint32_t duration = this->poll_period / 1000; + ESP_LOGCONFIG(TAG, " Poll Period: %" PRIu32 "s", duration); + } else { + ESP_LOGCONFIG(TAG, " Device is configured as Minimal End Device (MED)"); + } +#endif +} + bool OpenThreadComponent::is_connected() { auto lock = InstanceLock::try_acquire(100); if (!lock) { @@ -77,8 +92,14 @@ std::optional OpenThreadComponent::get_omr_address_(InstanceLock & return {}; } -void srp_callback(otError err, const otSrpClientHostInfo *host_info, const otSrpClientService *services, - const otSrpClientService *removed_services, void *context) { +void OpenThreadComponent::defer_factory_reset_external_callback() { + ESP_LOGD(TAG, "Defer factory_reset_external_callback_"); + this->defer([this]() { this->factory_reset_external_callback_(); }); +} + +void OpenThreadSrpComponent::srp_callback(otError err, const otSrpClientHostInfo *host_info, + const otSrpClientService *services, + const otSrpClientService *removed_services, void *context) { if (err != 0) { ESP_LOGW(TAG, "SRP client reported an error: %s", otThreadErrorToString(err)); for (const otSrpClientHostInfo *host = host_info; host; host = nullptr) { @@ -90,16 +111,30 @@ void srp_callback(otError err, const otSrpClientHostInfo *host_info, const otSrp } } -void srp_start_callback(const otSockAddr *server_socket_address, void *context) { +void OpenThreadSrpComponent::srp_start_callback(const otSockAddr *server_socket_address, void *context) { ESP_LOGI(TAG, "SRP client has started"); } +void OpenThreadSrpComponent::srp_factory_reset_callback(otError err, const otSrpClientHostInfo *host_info, + const otSrpClientService *services, + const otSrpClientService *removed_services, void *context) { + OpenThreadComponent *obj = (OpenThreadComponent *) context; + if (err == OT_ERROR_NONE && removed_services != NULL && host_info != NULL && + host_info->mState == OT_SRP_CLIENT_ITEM_STATE_REMOVED) { + ESP_LOGD(TAG, "Successful Removal SRP Host and Services"); + } else if (err != OT_ERROR_NONE) { + // Handle other SRP client events or errors + ESP_LOGW(TAG, "SRP client event/error: %s", otThreadErrorToString(err)); + } + obj->defer_factory_reset_external_callback(); +} + void OpenThreadSrpComponent::setup() { otError error; InstanceLock lock = InstanceLock::acquire(); otInstance *instance = lock.get_instance(); - otSrpClientSetCallback(instance, srp_callback, nullptr); + otSrpClientSetCallback(instance, OpenThreadSrpComponent::srp_callback, nullptr); // set the host name uint16_t size; @@ -125,11 +160,10 @@ void OpenThreadSrpComponent::setup() { return; } - // Copy the mdns services to our local instance so that the c_str pointers remain valid for the lifetime of this - // component - this->mdns_services_ = this->mdns_->get_services(); - ESP_LOGD(TAG, "Setting up SRP services. count = %d\n", this->mdns_services_.size()); - for (const auto &service : this->mdns_services_) { + // Get mdns services and copy their data (strings are copied with strdup below) + const auto &mdns_services = this->mdns_->get_services(); + ESP_LOGD(TAG, "Setting up SRP services. count = %d\n", mdns_services.size()); + for (const auto &service : mdns_services) { otSrpClientBuffersServiceEntry *entry = otSrpClientBuffersAllocateService(instance); if (!entry) { ESP_LOGW(TAG, "Failed to allocate service entry"); @@ -138,7 +172,7 @@ void OpenThreadSrpComponent::setup() { // Set service name char *string = otSrpClientBuffersGetServiceEntryServiceNameString(entry, &size); - std::string full_service = service.service_type + "." + service.proto; + std::string full_service = std::string(MDNS_STR_ARG(service.service_type)) + "." + MDNS_STR_ARG(service.proto); if (full_service.size() > size) { ESP_LOGW(TAG, "Service name too long: %s", full_service.c_str()); continue; @@ -163,10 +197,12 @@ void OpenThreadSrpComponent::setup() { entry->mService.mNumTxtEntries = service.txt_records.size(); for (size_t i = 0; i < service.txt_records.size(); i++) { const auto &txt = service.txt_records[i]; - auto value = const_cast &>(txt.value).value(); - txt_entries[i].mKey = strdup(txt.key.c_str()); - txt_entries[i].mValue = reinterpret_cast(strdup(value.c_str())); - txt_entries[i].mValueLength = value.size(); + // Value is either a compile-time string literal in flash or a pointer to dynamic_txt_values_ + // OpenThread SRP client expects the data to persist, so we strdup it + const char *value_str = MDNS_STR_ARG(txt.value); + txt_entries[i].mKey = MDNS_STR_ARG(txt.key); + txt_entries[i].mValue = reinterpret_cast(strdup(value_str)); + txt_entries[i].mValueLength = strlen(value_str); } entry->mService.mTxtEntries = txt_entries; entry->mService.mNumTxtEntries = service.txt_records.size(); @@ -179,7 +215,8 @@ void OpenThreadSrpComponent::setup() { ESP_LOGD(TAG, "Added service: %s", full_service.c_str()); } - otSrpClientEnableAutoStartMode(instance, srp_start_callback, nullptr); + otSrpClientEnableAutoStartMode(instance, OpenThreadSrpComponent::srp_start_callback, nullptr); + ESP_LOGD(TAG, "Finished SRP setup"); } void *OpenThreadSrpComponent::pool_alloc_(size_t size) { @@ -217,6 +254,27 @@ bool OpenThreadComponent::teardown() { return this->teardown_complete_; } +void OpenThreadComponent::on_factory_reset(std::function callback) { + factory_reset_external_callback_ = callback; + ESP_LOGD(TAG, "Start Removal SRP Host and Services"); + otError error; + InstanceLock lock = InstanceLock::acquire(); + otInstance *instance = lock.get_instance(); + otSrpClientSetCallback(instance, OpenThreadSrpComponent::srp_factory_reset_callback, this); + error = otSrpClientRemoveHostAndServices(instance, true, true); + if (error != OT_ERROR_NONE) { + ESP_LOGW(TAG, "Failed to Remove SRP Host and Services"); + return; + } + ESP_LOGD(TAG, "Waiting on Confirmation Removal SRP Host and Services"); +} + +// set_use_address() is guaranteed to be called during component setup by Python code generation, +// so use_address_ will always be valid when get_use_address() is called - no fallback needed. +const char *OpenThreadComponent::get_use_address() const { return this->use_address_; } + +void OpenThreadComponent::set_use_address(const char *use_address) { this->use_address_ = use_address; } + } // namespace openthread } // namespace esphome diff --git a/esphome/components/openthread/openthread.h b/esphome/components/openthread/openthread.h index a0ea1b3f3a..546128b366 100644 --- a/esphome/components/openthread/openthread.h +++ b/esphome/components/openthread/openthread.h @@ -6,6 +6,8 @@ #include "esphome/components/network/ip_address.h" #include "esphome/core/component.h" +#include +#include #include #include @@ -20,6 +22,7 @@ class OpenThreadComponent : public Component { public: OpenThreadComponent(); ~OpenThreadComponent(); + void dump_config() override; void setup() override; bool teardown() override; float get_setup_priority() const override { return setup_priority::WIFI; } @@ -28,11 +31,28 @@ class OpenThreadComponent : public Component { network::IPAddresses get_ip_addresses(); std::optional get_omr_address(); void ot_main(); + void on_factory_reset(std::function callback); + void defer_factory_reset_external_callback(); + + const char *get_use_address() const; + void set_use_address(const char *use_address); +#if CONFIG_OPENTHREAD_MTD + void set_poll_period(uint32_t poll_period) { this->poll_period = poll_period; } +#endif protected: std::optional get_omr_address_(InstanceLock &lock); bool teardown_started_{false}; bool teardown_complete_{false}; + std::function factory_reset_external_callback_; + + private: + // Stores a pointer to a string literal (static storage duration). + // ONLY set from Python-generated code with string literals - never dynamic strings. + const char *use_address_{""}; +#if CONFIG_OPENTHREAD_MTD + uint32_t poll_period{0}; +#endif }; extern OpenThreadComponent *global_openthread_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) @@ -43,10 +63,15 @@ class OpenThreadSrpComponent : public Component { // This has to run after the mdns component or else no services are available to advertise float get_setup_priority() const override { return this->mdns_->get_setup_priority() - 1.0; } void setup() override; + static void srp_callback(otError err, const otSrpClientHostInfo *host_info, const otSrpClientService *services, + const otSrpClientService *removed_services, void *context); + static void srp_start_callback(const otSockAddr *server_socket_address, void *context); + static void srp_factory_reset_callback(otError err, const otSrpClientHostInfo *host_info, + const otSrpClientService *services, const otSrpClientService *removed_services, + void *context); protected: esphome::mdns::MDNSComponent *mdns_{nullptr}; - std::vector mdns_services_; std::vector> memory_pool_; void *pool_alloc_(size_t size); }; diff --git a/esphome/components/openthread/openthread_esp.cpp b/esphome/components/openthread/openthread_esp.cpp index b11b7ad34a..72dc521091 100644 --- a/esphome/components/openthread/openthread_esp.cpp +++ b/esphome/components/openthread/openthread_esp.cpp @@ -105,6 +105,32 @@ void OpenThreadComponent::ot_main() { esp_cli_custom_command_init(); #endif // CONFIG_OPENTHREAD_CLI_ESP_EXTENSION + otLinkModeConfig link_mode_config = {0}; +#if CONFIG_OPENTHREAD_FTD + link_mode_config.mRxOnWhenIdle = true; + link_mode_config.mDeviceType = true; + link_mode_config.mNetworkData = true; +#elif CONFIG_OPENTHREAD_MTD + if (this->poll_period > 0) { + if (otLinkSetPollPeriod(esp_openthread_get_instance(), this->poll_period) != OT_ERROR_NONE) { + ESP_LOGE(TAG, "Failed to set OpenThread pollperiod."); + } + uint32_t link_polling_period = otLinkGetPollPeriod(esp_openthread_get_instance()); + ESP_LOGD(TAG, "Link Polling Period: %d", link_polling_period); + } + link_mode_config.mRxOnWhenIdle = this->poll_period == 0; + link_mode_config.mDeviceType = false; + link_mode_config.mNetworkData = false; +#endif + + if (otThreadSetLinkMode(esp_openthread_get_instance(), link_mode_config) != OT_ERROR_NONE) { + ESP_LOGE(TAG, "Failed to set OpenThread linkmode."); + } + link_mode_config = otThreadGetLinkMode(esp_openthread_get_instance()); + ESP_LOGD(TAG, "Link Mode Device Type: %s", link_mode_config.mDeviceType ? "true" : "false"); + ESP_LOGD(TAG, "Link Mode Network Data: %s", link_mode_config.mNetworkData ? "true" : "false"); + ESP_LOGD(TAG, "Link Mode RX On When Idle: %s", link_mode_config.mRxOnWhenIdle ? "true" : "false"); + // Run the main loop #if CONFIG_OPENTHREAD_CLI esp_openthread_cli_create_task(); diff --git a/esphome/components/opt3001/opt3001.cpp b/esphome/components/opt3001/opt3001.cpp index 2d65f1090d..f5f7ab9412 100644 --- a/esphome/components/opt3001/opt3001.cpp +++ b/esphome/components/opt3001/opt3001.cpp @@ -72,7 +72,7 @@ void OPT3001Sensor::read_lx_(const std::function &f) { } this->set_timeout("read", OPT3001_CONVERSION_TIME_800, [this, f]() { - if (this->write(&OPT3001_REG_CONFIGURATION, 1, true) != i2c::ERROR_OK) { + if (this->write(&OPT3001_REG_CONFIGURATION, 1) != i2c::ERROR_OK) { ESP_LOGW(TAG, "Starting configuration register read failed"); f(NAN); return; diff --git a/esphome/components/ota/__init__.py b/esphome/components/ota/__init__.py index 4d5b8a61e2..eec39668db 100644 --- a/esphome/components/ota/__init__.py +++ b/esphome/components/ota/__init__.py @@ -11,6 +11,7 @@ from esphome.const import ( PlatformFramework, ) from esphome.core import CORE, coroutine_with_priority +from esphome.coroutine import CoroPriority CODEOWNERS = ["@esphome/core"] AUTO_LOAD = ["md5", "safe_mode"] @@ -82,7 +83,7 @@ BASE_OTA_SCHEMA = cv.Schema( ) -@coroutine_with_priority(54.0) +@coroutine_with_priority(CoroPriority.OTA_UPDATES) async def to_code(config): cg.add_define("USE_OTA") diff --git a/esphome/components/ota/ota_backend.h b/esphome/components/ota/ota_backend.h index 372f24df5e..64ee0b9f7c 100644 --- a/esphome/components/ota/ota_backend.h +++ b/esphome/components/ota/ota_backend.h @@ -14,6 +14,7 @@ namespace ota { enum OTAResponseTypes { OTA_RESPONSE_OK = 0x00, OTA_RESPONSE_REQUEST_AUTH = 0x01, + OTA_RESPONSE_REQUEST_SHA256_AUTH = 0x02, OTA_RESPONSE_HEADER_OK = 0x40, OTA_RESPONSE_AUTH_OK = 0x41, diff --git a/esphome/components/output/automation.h b/esphome/components/output/automation.h index de84bb91ca..3279378129 100644 --- a/esphome/components/output/automation.h +++ b/esphome/components/output/automation.h @@ -12,7 +12,7 @@ template class TurnOffAction : public Action { public: TurnOffAction(BinaryOutput *output) : output_(output) {} - void play(Ts... x) override { this->output_->turn_off(); } + void play(const Ts &...x) override { this->output_->turn_off(); } protected: BinaryOutput *output_; @@ -22,7 +22,7 @@ template class TurnOnAction : public Action { public: TurnOnAction(BinaryOutput *output) : output_(output) {} - void play(Ts... x) override { this->output_->turn_on(); } + void play(const Ts &...x) override { this->output_->turn_on(); } protected: BinaryOutput *output_; @@ -34,7 +34,7 @@ template class SetLevelAction : public Action { TEMPLATABLE_VALUE(float, level) - void play(Ts... x) override { this->output_->set_level(this->level_.value(x...)); } + void play(const Ts &...x) override { this->output_->set_level(this->level_.value(x...)); } protected: FloatOutput *output_; @@ -46,7 +46,7 @@ template class SetMinPowerAction : public Action { TEMPLATABLE_VALUE(float, min_power) - void play(Ts... x) override { this->output_->set_min_power(this->min_power_.value(x...)); } + void play(const Ts &...x) override { this->output_->set_min_power(this->min_power_.value(x...)); } protected: FloatOutput *output_; @@ -58,7 +58,7 @@ template class SetMaxPowerAction : public Action { TEMPLATABLE_VALUE(float, max_power) - void play(Ts... x) override { this->output_->set_max_power(this->max_power_.value(x...)); } + void play(const Ts &...x) override { this->output_->set_max_power(this->max_power_.value(x...)); } protected: FloatOutput *output_; diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py index 2e7dc0e197..41cde0391b 100644 --- a/esphome/components/packages/__init__.py +++ b/esphome/components/packages/__init__.py @@ -1,3 +1,4 @@ +import logging from pathlib import Path from esphome import git, yaml_util @@ -20,18 +21,41 @@ from esphome.const import ( ) from esphome.core import EsphomeError +_LOGGER = logging.getLogger(__name__) + DOMAIN = CONF_PACKAGES -def validate_git_package(config: dict): - if CONF_URL not in config: - return config - config = BASE_SCHEMA(config) - new_config = config +def valid_package_contents(package_config: dict): + """Validates that a package_config that will be merged looks as much as possible to a valid config + to fail early on obvious mistakes.""" + if isinstance(package_config, dict): + if CONF_URL in package_config: + # If a URL key is found, then make sure the config conforms to a remote package schema: + return REMOTE_PACKAGE_SCHEMA(package_config) + + # Validate manually since Voluptuous would regenerate dicts and lose metadata + # such as ESPHomeDataBase + for k, v in package_config.items(): + if not isinstance(k, str): + raise cv.Invalid("Package content keys must be strings") + if isinstance(v, (dict, list)): + continue # e.g. script: [] or logger: {level: debug} + if v is None: + continue # e.g. web_server: + raise cv.Invalid("Invalid component content in package definition") + return package_config + + raise cv.Invalid("Package contents must be a dict") + + +def expand_file_to_files(config: dict): if CONF_FILE in config: + new_config = config new_config[CONF_FILES] = [config[CONF_FILE]] del new_config[CONF_FILE] - return new_config + return new_config + return config def validate_yaml_filename(value): @@ -45,7 +69,7 @@ def validate_yaml_filename(value): def validate_source_shorthand(value): if not isinstance(value, str): - raise cv.Invalid("Shorthand only for strings") + raise cv.Invalid("Git URL shorthand only for strings") git_file = git.GitFile.from_shorthand(value) @@ -56,10 +80,17 @@ def validate_source_shorthand(value): if git_file.ref: conf[CONF_REF] = git_file.ref - return BASE_SCHEMA(conf) + return REMOTE_PACKAGE_SCHEMA(conf) -BASE_SCHEMA = cv.All( +def deprecate_single_package(config): + _LOGGER.warning( + "Including a single package under `packages:` is deprecated. Use a list instead." + ) + return config + + +REMOTE_PACKAGE_SCHEMA = cv.All( cv.Schema( { cv.Required(CONF_URL): cv.url, @@ -90,27 +121,36 @@ BASE_SCHEMA = cv.All( } ), cv.has_at_least_one_key(CONF_FILE, CONF_FILES), + expand_file_to_files, ) -PACKAGE_SCHEMA = cv.All( - cv.Any(validate_source_shorthand, BASE_SCHEMA, dict), validate_git_package +PACKAGE_SCHEMA = cv.Any( # A package definition is either: + validate_source_shorthand, # A git URL shorthand string that expands to a remote package schema, or + REMOTE_PACKAGE_SCHEMA, # a valid remote package schema, or + valid_package_contents, # Something that at least looks like an actual package, e.g. {wifi:{ssid: xxx}} + # which will have to be fully validated later as per each component's schema. ) -CONFIG_SCHEMA = cv.Any( +CONFIG_SCHEMA = cv.Any( # under `packages:` we can have either: cv.Schema( { - str: PACKAGE_SCHEMA, + str: PACKAGE_SCHEMA, # a named dict of package definitions, or } ), - cv.ensure_list(PACKAGE_SCHEMA), + [PACKAGE_SCHEMA], # a list of package definitions, or + cv.All( # a single package definition (deprecated) + cv.ensure_list(PACKAGE_SCHEMA), deprecate_single_package + ), ) -def _process_base_package(config: dict) -> dict: +def _process_remote_package(config: dict, skip_update: bool = False) -> dict: + # When skip_update is True, use NEVER_REFRESH to prevent updates + actual_refresh = git.NEVER_REFRESH if skip_update else config[CONF_REFRESH] repo_dir, revert = git.clone_or_update( url=config[CONF_URL], ref=config.get(CONF_REF), - refresh=config[CONF_REFRESH], + refresh=actual_refresh, domain=DOMAIN, username=config.get(CONF_USERNAME), password=config.get(CONF_PASSWORD), @@ -180,16 +220,16 @@ def _process_base_package(config: dict) -> dict: return {"packages": packages} -def _process_package(package_config, config): +def _process_package(package_config, config, skip_update: bool = False): recursive_package = package_config if CONF_URL in package_config: - package_config = _process_base_package(package_config) + package_config = _process_remote_package(package_config, skip_update) if isinstance(package_config, dict): - recursive_package = do_packages_pass(package_config) + recursive_package = do_packages_pass(package_config, skip_update) return merge_config(recursive_package, config) -def do_packages_pass(config: dict): +def do_packages_pass(config: dict, skip_update: bool = False): if CONF_PACKAGES not in config: return config packages = config[CONF_PACKAGES] @@ -198,10 +238,10 @@ def do_packages_pass(config: dict): if isinstance(packages, dict): for package_name, package_config in reversed(packages.items()): with cv.prepend_path(package_name): - config = _process_package(package_config, config) + config = _process_package(package_config, config, skip_update) elif isinstance(packages, list): for package_config in reversed(packages): - config = _process_package(package_config, config) + config = _process_package(package_config, config, skip_update) else: raise cv.Invalid( f"Packages must be a key to value mapping or list, got {type(packages)} instead" diff --git a/esphome/components/packet_transport/__init__.py b/esphome/components/packet_transport/__init__.py index bfb2bbc4f8..43da7740fe 100644 --- a/esphome/components/packet_transport/__init__.py +++ b/esphome/components/packet_transport/__init__.py @@ -121,15 +121,11 @@ def transport_schema(cls): return TRANSPORT_SCHEMA.extend({cv.GenerateID(): cv.declare_id(cls)}) -# Build a list of sensors for this platform -CORE.data[DOMAIN] = {CONF_SENSORS: []} - - def get_sensors(transport_id): """Return the list of sensors for this platform.""" return ( sensor - for sensor in CORE.data[DOMAIN][CONF_SENSORS] + for sensor in CORE.data.setdefault(DOMAIN, {}).setdefault(CONF_SENSORS, []) if sensor[CONF_TRANSPORT_ID] == transport_id ) @@ -137,7 +133,8 @@ def get_sensors(transport_id): def validate_packet_transport_sensor(config): if CONF_NAME in config and CONF_INTERNAL not in config: raise cv.Invalid("Must provide internal: config when using name:") - CORE.data[DOMAIN][CONF_SENSORS].append(config) + conf_sensors = CORE.data.setdefault(DOMAIN, {}).setdefault(CONF_SENSORS, []) + conf_sensors.append(config) return config diff --git a/esphome/components/packet_transport/packet_transport.cpp b/esphome/components/packet_transport/packet_transport.cpp index b6ce24bc1b..37e5f3d9e1 100644 --- a/esphome/components/packet_transport/packet_transport.cpp +++ b/esphome/components/packet_transport/packet_transport.cpp @@ -195,8 +195,8 @@ static void add(std::vector &vec, const char *str) { void PacketTransport::setup() { this->name_ = App.get_name().c_str(); if (strlen(this->name_) > 255) { + this->status_set_error(LOG_STR("Device name exceeds 255 chars")); this->mark_failed(); - this->status_set_error("Device name exceeds 255 chars"); return; } this->resend_ping_key_ = this->ping_pong_enable_; @@ -270,6 +270,7 @@ void PacketTransport::add_binary_data_(uint8_t key, const char *id, bool data) { auto len = 1 + 1 + 1 + strlen(id); if (len + this->header_.size() + this->data_.size() > this->get_max_packet_size()) { this->flush_(); + this->init_data_(); } add(this->data_, key); add(this->data_, (uint8_t) data); @@ -284,6 +285,7 @@ void PacketTransport::add_data_(uint8_t key, const char *id, uint32_t data) { auto len = 4 + 1 + 1 + strlen(id); if (len + this->header_.size() + this->data_.size() > this->get_max_packet_size()) { this->flush_(); + this->init_data_(); } add(this->data_, key); add(this->data_, data); diff --git a/esphome/components/pca6416a/__init__.py b/esphome/components/pca6416a/__init__.py index da6c4623c9..e540edb91f 100644 --- a/esphome/components/pca6416a/__init__.py +++ b/esphome/components/pca6416a/__init__.py @@ -14,6 +14,7 @@ from esphome.const import ( CODEOWNERS = ["@Mat931"] DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["gpio_expander"] MULTI_CONF = True pca6416a_ns = cg.esphome_ns.namespace("pca6416a") diff --git a/esphome/components/pca6416a/pca6416a.cpp b/esphome/components/pca6416a/pca6416a.cpp index dc8662d1a2..c0056e780b 100644 --- a/esphome/components/pca6416a/pca6416a.cpp +++ b/esphome/components/pca6416a/pca6416a.cpp @@ -33,7 +33,7 @@ void PCA6416AComponent::setup() { } // Test to see if the device supports pull-up resistors - if (this->read_register(PCAL6416A_PULL_EN0, &value, 1, true) == i2c::ERROR_OK) { + if (this->read_register(PCAL6416A_PULL_EN0, &value, 1) == i2c::ERROR_OK) { this->has_pullup_ = true; } @@ -51,6 +51,11 @@ void PCA6416AComponent::setup() { this->status_has_error()); } +void PCA6416AComponent::loop() { + // Invalidate cache at the start of each loop + this->reset_pin_cache_(); +} + void PCA6416AComponent::dump_config() { if (this->has_pullup_) { ESP_LOGCONFIG(TAG, "PCAL6416A:"); @@ -63,15 +68,25 @@ void PCA6416AComponent::dump_config() { } } -bool PCA6416AComponent::digital_read(uint8_t pin) { - uint8_t bit = pin % 8; +bool PCA6416AComponent::digital_read_hw(uint8_t pin) { uint8_t reg_addr = pin < 8 ? PCA6416A_INPUT0 : PCA6416A_INPUT1; uint8_t value = 0; - this->read_register_(reg_addr, &value); - return value & (1 << bit); + if (!this->read_register_(reg_addr, &value)) { + return false; + } + + // Update the appropriate part of input_mask_ + if (pin < 8) { + this->input_mask_ = (this->input_mask_ & 0xFF00) | value; + } else { + this->input_mask_ = (this->input_mask_ & 0x00FF) | (uint16_t(value) << 8); + } + return true; } -void PCA6416AComponent::digital_write(uint8_t pin, bool value) { +bool PCA6416AComponent::digital_read_cache(uint8_t pin) { return this->input_mask_ & (1 << pin); } + +void PCA6416AComponent::digital_write_hw(uint8_t pin, bool value) { uint8_t reg_addr = pin < 8 ? PCA6416A_OUTPUT0 : PCA6416A_OUTPUT1; this->update_register_(pin, value, reg_addr); } @@ -105,7 +120,7 @@ bool PCA6416AComponent::read_register_(uint8_t reg, uint8_t *value) { return false; } - this->last_error_ = this->read_register(reg, value, 1, true); + this->last_error_ = this->read_register(reg, value, 1); if (this->last_error_ != i2c::ERROR_OK) { this->status_set_warning(); ESP_LOGE(TAG, "read_register_(): I2C I/O error: %d", (int) this->last_error_); @@ -122,7 +137,7 @@ bool PCA6416AComponent::write_register_(uint8_t reg, uint8_t value) { return false; } - this->last_error_ = this->write_register(reg, &value, 1, true); + this->last_error_ = this->write_register(reg, &value, 1); if (this->last_error_ != i2c::ERROR_OK) { this->status_set_warning(); ESP_LOGE(TAG, "write_register_(): I2C I/O error: %d", (int) this->last_error_); diff --git a/esphome/components/pca6416a/pca6416a.h b/esphome/components/pca6416a/pca6416a.h index 1e8015c40a..10a4a64e9b 100644 --- a/esphome/components/pca6416a/pca6416a.h +++ b/esphome/components/pca6416a/pca6416a.h @@ -3,20 +3,20 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" #include "esphome/components/i2c/i2c.h" +#include "esphome/components/gpio_expander/cached_gpio.h" namespace esphome { namespace pca6416a { -class PCA6416AComponent : public Component, public i2c::I2CDevice { +class PCA6416AComponent : public Component, + public i2c::I2CDevice, + public gpio_expander::CachedGpioExpander { public: PCA6416AComponent() = default; /// Check i2c availability and setup masks void setup() override; - /// Helper function to read the value of a pin. - bool digital_read(uint8_t pin); - /// Helper function to write the value of a pin. - void digital_write(uint8_t pin, bool value); + void loop() override; /// Helper function to set the pin mode of a pin. void pin_mode(uint8_t pin, gpio::Flags flags); @@ -25,6 +25,11 @@ class PCA6416AComponent : public Component, public i2c::I2CDevice { void dump_config() override; protected: + // Virtual methods from CachedGpioExpander + bool digital_read_hw(uint8_t pin) override; + bool digital_read_cache(uint8_t pin) override; + void digital_write_hw(uint8_t pin, bool value) override; + bool read_register_(uint8_t reg, uint8_t *value); bool write_register_(uint8_t reg, uint8_t value); void update_register_(uint8_t pin, bool pin_value, uint8_t reg_addr); @@ -32,6 +37,8 @@ class PCA6416AComponent : public Component, public i2c::I2CDevice { /// The mask to write as output state - 1 means HIGH, 0 means LOW uint8_t output_0_{0x00}; uint8_t output_1_{0x00}; + /// Cache for input values (16-bit combined for both banks) + uint16_t input_mask_{0x00}; /// Storage for last I2C error seen esphome::i2c::ErrorCode last_error_; /// Only the PCAL6416A has pull-up resistors diff --git a/esphome/components/pca9554/__init__.py b/esphome/components/pca9554/__init__.py index 05713cccda..626b08a378 100644 --- a/esphome/components/pca9554/__init__.py +++ b/esphome/components/pca9554/__init__.py @@ -11,7 +11,8 @@ from esphome.const import ( CONF_OUTPUT, ) -CODEOWNERS = ["@hwstar", "@clydebarrow"] +CODEOWNERS = ["@hwstar", "@clydebarrow", "@bdraco"] +AUTO_LOAD = ["gpio_expander"] DEPENDENCIES = ["i2c"] MULTI_CONF = True CONF_PIN_COUNT = "pin_count" diff --git a/esphome/components/pca9554/pca9554.cpp b/esphome/components/pca9554/pca9554.cpp index f77d680bec..e8d49f66e2 100644 --- a/esphome/components/pca9554/pca9554.cpp +++ b/esphome/components/pca9554/pca9554.cpp @@ -37,10 +37,9 @@ void PCA9554Component::setup() { } void PCA9554Component::loop() { - // The read_inputs_() method will cache the input values from the chip. - this->read_inputs_(); - // Clear all the previously read flags. - this->was_previously_read_ = 0x00; + // Invalidate the cache at the start of each loop. + // The actual read will happen on demand when digital_read() is called + this->reset_pin_cache_(); } void PCA9554Component::dump_config() { @@ -54,21 +53,17 @@ void PCA9554Component::dump_config() { } } -bool PCA9554Component::digital_read(uint8_t pin) { - // Note: We want to try and avoid doing any I2C bus read transactions here - // to conserve I2C bus bandwidth. So what we do is check to see if we - // have seen a read during the time esphome is running this loop. If we have, - // we do an I2C bus transaction to get the latest value. If we haven't - // we return a cached value which was read at the time loop() was called. - if (this->was_previously_read_ & (1 << pin)) - this->read_inputs_(); // Force a read of a new value - // Indicate we saw a read request for this pin in case a - // read happens later in the same loop. - this->was_previously_read_ |= (1 << pin); +bool PCA9554Component::digital_read_hw(uint8_t pin) { + // Read all pins from hardware into input_mask_ + return this->read_inputs_(); // Return true if I2C read succeeded, false on error +} + +bool PCA9554Component::digital_read_cache(uint8_t pin) { + // Return the cached pin state from input_mask_ return this->input_mask_ & (1 << pin); } -void PCA9554Component::digital_write(uint8_t pin, bool value) { +void PCA9554Component::digital_write_hw(uint8_t pin, bool value) { if (value) { this->output_mask_ |= (1 << pin); } else { @@ -96,7 +91,7 @@ bool PCA9554Component::read_inputs_() { return false; } - this->last_error_ = this->read_register(INPUT_REG * this->reg_width_, inputs, this->reg_width_, true); + this->last_error_ = this->read_register(INPUT_REG * this->reg_width_, inputs, this->reg_width_); if (this->last_error_ != i2c::ERROR_OK) { this->status_set_warning(); ESP_LOGE(TAG, "read_register_(): I2C I/O error: %d", (int) this->last_error_); @@ -114,7 +109,7 @@ bool PCA9554Component::write_register_(uint8_t reg, uint16_t value) { uint8_t outputs[2]; outputs[0] = (uint8_t) value; outputs[1] = (uint8_t) (value >> 8); - this->last_error_ = this->write_register(reg * this->reg_width_, outputs, this->reg_width_, true); + this->last_error_ = this->write_register(reg * this->reg_width_, outputs, this->reg_width_); if (this->last_error_ != i2c::ERROR_OK) { this->status_set_warning(); ESP_LOGE(TAG, "write_register_(): I2C I/O error: %d", (int) this->last_error_); @@ -127,8 +122,7 @@ bool PCA9554Component::write_register_(uint8_t reg, uint16_t value) { float PCA9554Component::get_setup_priority() const { return setup_priority::IO; } -// Run our loop() method very early in the loop, so that we cache read values before -// before other components call our digital_read() method. +// Run our loop() method early to invalidate cache before any other components access the pins float PCA9554Component::get_loop_priority() const { return 9.0f; } // Just after WIFI void PCA9554GPIOPin::setup() { pin_mode(flags_); } diff --git a/esphome/components/pca9554/pca9554.h b/esphome/components/pca9554/pca9554.h index efeec4d306..7b356b4068 100644 --- a/esphome/components/pca9554/pca9554.h +++ b/esphome/components/pca9554/pca9554.h @@ -3,22 +3,21 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" #include "esphome/components/i2c/i2c.h" +#include "esphome/components/gpio_expander/cached_gpio.h" namespace esphome { namespace pca9554 { -class PCA9554Component : public Component, public i2c::I2CDevice { +class PCA9554Component : public Component, + public i2c::I2CDevice, + public gpio_expander::CachedGpioExpander { public: PCA9554Component() = default; /// Check i2c availability and setup masks void setup() override; - /// Poll for input changes periodically + /// Invalidate cache at start of each loop void loop() override; - /// Helper function to read the value of a pin. - bool digital_read(uint8_t pin); - /// Helper function to write the value of a pin. - void digital_write(uint8_t pin, bool value); /// Helper function to set the pin mode of a pin. void pin_mode(uint8_t pin, gpio::Flags flags); @@ -32,9 +31,13 @@ class PCA9554Component : public Component, public i2c::I2CDevice { protected: bool read_inputs_(); - bool write_register_(uint8_t reg, uint16_t value); + // Virtual methods from CachedGpioExpander + bool digital_read_hw(uint8_t pin) override; + bool digital_read_cache(uint8_t pin) override; + void digital_write_hw(uint8_t pin, bool value) override; + /// number of bits the expander has size_t pin_count_{8}; /// width of registers @@ -45,8 +48,6 @@ class PCA9554Component : public Component, public i2c::I2CDevice { uint16_t output_mask_{0x00}; /// The state of the actual input pin states - 1 means HIGH, 0 means LOW uint16_t input_mask_{0x00}; - /// Flags to check if read previously during this loop - uint16_t was_previously_read_ = {0x00}; /// Storage for last I2C error seen esphome::i2c::ErrorCode last_error_; }; diff --git a/esphome/components/pcf85063/pcf85063.cpp b/esphome/components/pcf85063/pcf85063.cpp index cb987c6129..f38b60b55d 100644 --- a/esphome/components/pcf85063/pcf85063.cpp +++ b/esphome/components/pcf85063/pcf85063.cpp @@ -23,7 +23,7 @@ void PCF85063Component::dump_config() { if (this->is_failed()) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); } - ESP_LOGCONFIG(TAG, " Timezone: '%s'", this->timezone_.c_str()); + RealTimeClock::dump_config(); } float PCF85063Component::get_setup_priority() const { return setup_priority::DATA; } diff --git a/esphome/components/pcf85063/pcf85063.h b/esphome/components/pcf85063/pcf85063.h index 1a3fd704e5..b7034d4f00 100644 --- a/esphome/components/pcf85063/pcf85063.h +++ b/esphome/components/pcf85063/pcf85063.h @@ -85,12 +85,12 @@ class PCF85063Component : public time::RealTimeClock, public i2c::I2CDevice { template class WriteAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->write_time(); } + void play(const Ts &...x) override { this->parent_->write_time(); } }; template class ReadAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->read_time(); } + void play(const Ts &...x) override { this->parent_->read_time(); } }; } // namespace pcf85063 } // namespace esphome diff --git a/esphome/components/pcf8563/pcf8563.cpp b/esphome/components/pcf8563/pcf8563.cpp index 27020378a6..2090936bb6 100644 --- a/esphome/components/pcf8563/pcf8563.cpp +++ b/esphome/components/pcf8563/pcf8563.cpp @@ -23,7 +23,7 @@ void PCF8563Component::dump_config() { if (this->is_failed()) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); } - ESP_LOGCONFIG(TAG, " Timezone: '%s'", this->timezone_.c_str()); + RealTimeClock::dump_config(); } float PCF8563Component::get_setup_priority() const { return setup_priority::DATA; } diff --git a/esphome/components/pcf8563/pcf8563.h b/esphome/components/pcf8563/pcf8563.h index b6832efe72..81aa816b42 100644 --- a/esphome/components/pcf8563/pcf8563.h +++ b/esphome/components/pcf8563/pcf8563.h @@ -113,12 +113,12 @@ class PCF8563Component : public time::RealTimeClock, public i2c::I2CDevice { template class WriteAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->write_time(); } + void play(const Ts &...x) override { this->parent_->write_time(); } }; template class ReadAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->read_time(); } + void play(const Ts &...x) override { this->parent_->read_time(); } }; } // namespace pcf8563 } // namespace esphome diff --git a/esphome/components/pcf8574/__init__.py b/esphome/components/pcf8574/__init__.py index ff7c314bcd..f387d0a610 100644 --- a/esphome/components/pcf8574/__init__.py +++ b/esphome/components/pcf8574/__init__.py @@ -11,6 +11,7 @@ from esphome.const import ( CONF_OUTPUT, ) +AUTO_LOAD = ["gpio_expander"] DEPENDENCIES = ["i2c"] MULTI_CONF = True diff --git a/esphome/components/pcf8574/pcf8574.cpp b/esphome/components/pcf8574/pcf8574.cpp index 848fbed484..72d8865d7f 100644 --- a/esphome/components/pcf8574/pcf8574.cpp +++ b/esphome/components/pcf8574/pcf8574.cpp @@ -16,6 +16,10 @@ void PCF8574Component::setup() { this->write_gpio_(); this->read_gpio_(); } +void PCF8574Component::loop() { + // Invalidate the cache at the start of each loop + this->reset_pin_cache_(); +} void PCF8574Component::dump_config() { ESP_LOGCONFIG(TAG, "PCF8574:"); LOG_I2C_DEVICE(this) @@ -24,17 +28,19 @@ void PCF8574Component::dump_config() { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); } } -bool PCF8574Component::digital_read(uint8_t pin) { - this->read_gpio_(); - return this->input_mask_ & (1 << pin); +bool PCF8574Component::digital_read_hw(uint8_t pin) { + // Read all pins from hardware into input_mask_ + return this->read_gpio_(); // Return true if I2C read succeeded, false on error } -void PCF8574Component::digital_write(uint8_t pin, bool value) { + +bool PCF8574Component::digital_read_cache(uint8_t pin) { return this->input_mask_ & (1 << pin); } + +void PCF8574Component::digital_write_hw(uint8_t pin, bool value) { if (value) { this->output_mask_ |= (1 << pin); } else { this->output_mask_ &= ~(1 << pin); } - this->write_gpio_(); } void PCF8574Component::pin_mode(uint8_t pin, gpio::Flags flags) { @@ -91,6 +97,9 @@ bool PCF8574Component::write_gpio_() { } float PCF8574Component::get_setup_priority() const { return setup_priority::IO; } +// Run our loop() method early to invalidate cache before any other components access the pins +float PCF8574Component::get_loop_priority() const { return 9.0f; } // Just after WIFI + void PCF8574GPIOPin::setup() { pin_mode(flags_); } void PCF8574GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } bool PCF8574GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } diff --git a/esphome/components/pcf8574/pcf8574.h b/esphome/components/pcf8574/pcf8574.h index 6edc67fc96..fd1ea8af63 100644 --- a/esphome/components/pcf8574/pcf8574.h +++ b/esphome/components/pcf8574/pcf8574.h @@ -3,11 +3,16 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" #include "esphome/components/i2c/i2c.h" +#include "esphome/components/gpio_expander/cached_gpio.h" namespace esphome { namespace pcf8574 { -class PCF8574Component : public Component, public i2c::I2CDevice { +// PCF8574(8 pins)/PCF8575(16 pins) always read/write all pins in a single I2C transaction +// so we use uint16_t as bank type to ensure all pins are in one bank and cached together +class PCF8574Component : public Component, + public i2c::I2CDevice, + public gpio_expander::CachedGpioExpander { public: PCF8574Component() = default; @@ -15,20 +20,22 @@ class PCF8574Component : public Component, public i2c::I2CDevice { /// Check i2c availability and setup masks void setup() override; - /// Helper function to read the value of a pin. - bool digital_read(uint8_t pin); - /// Helper function to write the value of a pin. - void digital_write(uint8_t pin, bool value); + /// Invalidate cache at start of each loop + void loop() override; /// Helper function to set the pin mode of a pin. void pin_mode(uint8_t pin, gpio::Flags flags); float get_setup_priority() const override; + float get_loop_priority() const override; void dump_config() override; protected: - bool read_gpio_(); + bool digital_read_hw(uint8_t pin) override; + bool digital_read_cache(uint8_t pin) override; + void digital_write_hw(uint8_t pin, bool value) override; + bool read_gpio_(); bool write_gpio_(); /// Mask for the pin mode - 1 means output, 0 means input diff --git a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp index 18acfda934..517ca833e6 100644 --- a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp +++ b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp @@ -68,7 +68,7 @@ bool PI4IOE5V6408Component::read_gpio_outputs_() { uint8_t data; if (!this->read_byte(PI4IOE5V6408_REGISTER_OUT_SET, &data)) { - this->status_set_warning("Failed to read output register"); + this->status_set_warning(LOG_STR("Failed to read output register")); return false; } this->output_mask_ = data; @@ -82,7 +82,7 @@ bool PI4IOE5V6408Component::read_gpio_modes_() { uint8_t data; if (!this->read_byte(PI4IOE5V6408_REGISTER_IO_DIR, &data)) { - this->status_set_warning("Failed to read GPIO modes"); + this->status_set_warning(LOG_STR("Failed to read GPIO modes")); return false; } #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE @@ -99,7 +99,7 @@ bool PI4IOE5V6408Component::digital_read_hw(uint8_t pin) { uint8_t data; if (!this->read_byte(PI4IOE5V6408_REGISTER_IN_STATE, &data)) { - this->status_set_warning("Failed to read GPIO state"); + this->status_set_warning(LOG_STR("Failed to read GPIO state")); return false; } this->input_mask_ = data; @@ -117,7 +117,7 @@ void PI4IOE5V6408Component::digital_write_hw(uint8_t pin, bool value) { this->output_mask_ &= ~(1 << pin); } if (!this->write_byte(PI4IOE5V6408_REGISTER_OUT_SET, this->output_mask_)) { - this->status_set_warning("Failed to write output register"); + this->status_set_warning(LOG_STR("Failed to write output register")); return; } #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE @@ -131,15 +131,15 @@ bool PI4IOE5V6408Component::write_gpio_modes_() { return false; if (!this->write_byte(PI4IOE5V6408_REGISTER_IO_DIR, this->mode_mask_)) { - this->status_set_warning("Failed to write GPIO modes"); + this->status_set_warning(LOG_STR("Failed to write GPIO modes")); return false; } if (!this->write_byte(PI4IOE5V6408_REGISTER_PULL_SELECT, this->pull_up_down_mask_)) { - this->status_set_warning("Failed to write GPIO pullup/pulldown"); + this->status_set_warning(LOG_STR("Failed to write GPIO pullup/pulldown")); return false; } if (!this->write_byte(PI4IOE5V6408_REGISTER_PULL_ENABLE, this->pull_enable_mask_)) { - this->status_set_warning("Failed to write GPIO pull enable"); + this->status_set_warning(LOG_STR("Failed to write GPIO pull enable")); return false; } #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE diff --git a/esphome/components/pid/pid_climate.cpp b/esphome/components/pid/pid_climate.cpp index 8b3be36dcc..fd74eabd87 100644 --- a/esphome/components/pid/pid_climate.cpp +++ b/esphome/components/pid/pid_climate.cpp @@ -54,11 +54,10 @@ void PIDClimate::control(const climate::ClimateCall &call) { } climate::ClimateTraits PIDClimate::traits() { auto traits = climate::ClimateTraits(); - traits.set_supports_current_temperature(true); - traits.set_supports_two_point_target_temperature(false); + traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE | climate::CLIMATE_SUPPORTS_ACTION); if (this->humidity_sensor_ != nullptr) - traits.set_supports_current_humidity(true); + traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY); traits.set_supported_modes({climate::CLIMATE_MODE_OFF}); if (supports_cool_()) @@ -68,7 +67,6 @@ climate::ClimateTraits PIDClimate::traits() { if (supports_heat_() && supports_cool_()) traits.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL); - traits.set_supports_action(true); return traits; } void PIDClimate::dump_config() { diff --git a/esphome/components/pid/pid_climate.h b/esphome/components/pid/pid_climate.h index 1a09ffdd20..dc0a92efed 100644 --- a/esphome/components/pid/pid_climate.h +++ b/esphome/components/pid/pid_climate.h @@ -109,7 +109,7 @@ template class PIDAutotuneAction : public Action { void set_positive_output(float positive_output) { positive_output_ = positive_output; } void set_negative_output(float negative_output) { negative_output_ = negative_output; } - void play(Ts... x) { + void play(const Ts &...x) { auto tuner = make_unique(); tuner->set_noiseband(this->noiseband_); tuner->set_output_negative(this->negative_output_); @@ -128,7 +128,7 @@ template class PIDResetIntegralTermAction : public Action public: PIDResetIntegralTermAction(PIDClimate *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->reset_integral_term(); } + void play(const Ts &...x) { this->parent_->reset_integral_term(); } protected: PIDClimate *parent_; @@ -138,7 +138,7 @@ template class PIDSetControlParametersAction : public Actionkp_.value(x...); auto ki = this->ki_.value(x...); auto kd = this->kd_.value(x...); diff --git a/esphome/components/pid/pid_controller.cpp b/esphome/components/pid/pid_controller.cpp index 1a16f14542..5d7aecdb05 100644 --- a/esphome/components/pid/pid_controller.cpp +++ b/esphome/components/pid/pid_controller.cpp @@ -104,7 +104,7 @@ float PIDController::weighted_average_(std::deque &list, float new_value, list.push_front(new_value); // keep only 'samples' readings, by popping off the back of the list - while (list.size() > samples) + while (samples > 0 && list.size() > static_cast(samples)) list.pop_back(); // calculate and return the average of all values in the list diff --git a/esphome/components/pipsolar/__init__.py b/esphome/components/pipsolar/__init__.py index 1e4ea8492b..e3966aa2cc 100644 --- a/esphome/components/pipsolar/__init__.py +++ b/esphome/components/pipsolar/__init__.py @@ -26,7 +26,7 @@ CONFIG_SCHEMA = cv.All( ) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield uart.register_uart_device(var, config) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) diff --git a/esphome/components/pipsolar/binary_sensor/__init__.py b/esphome/components/pipsolar/binary_sensor/__init__.py index 625c232ed5..5bcf1f75ee 100644 --- a/esphome/components/pipsolar/binary_sensor/__init__.py +++ b/esphome/components/pipsolar/binary_sensor/__init__.py @@ -62,7 +62,7 @@ CONF_WARNING_MPPT_OVERLOAD = "warning_mppt_overload" CONF_WARNING_BATTERY_TOO_LOW_TO_CHARGE = "warning_battery_too_low_to_charge" CONF_FAULT_DC_DC_OVER_CURRENT = "fault_dc_dc_over_current" CONF_FAULT_CODE = "fault_code" -CONF_WARNUNG_LOW_PV_ENERGY = "warnung_low_pv_energy" +CONF_WARNING_LOW_PV_ENERGY = "warning_low_pv_energy" CONF_WARNING_HIGH_AC_INPUT_DURING_BUS_SOFT_START = ( "warning_high_ac_input_during_bus_soft_start" ) @@ -122,7 +122,7 @@ TYPES = [ CONF_WARNING_BATTERY_TOO_LOW_TO_CHARGE, CONF_FAULT_DC_DC_OVER_CURRENT, CONF_FAULT_CODE, - CONF_WARNUNG_LOW_PV_ENERGY, + CONF_WARNING_LOW_PV_ENERGY, CONF_WARNING_HIGH_AC_INPUT_DURING_BUS_SOFT_START, CONF_WARNING_BATTERY_EQUALIZATION, ] diff --git a/esphome/components/pipsolar/output/__init__.py b/esphome/components/pipsolar/output/__init__.py index 1eb7249119..829f8f7037 100644 --- a/esphome/components/pipsolar/output/__init__.py +++ b/esphome/components/pipsolar/output/__init__.py @@ -99,9 +99,9 @@ async def to_code(config): } ), ) -def output_pipsolar_set_level_to_code(config, action_id, template_arg, args): - paren = yield cg.get_variable(config[CONF_ID]) +async def output_pipsolar_set_level_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = yield cg.templatable(config[CONF_VALUE], args, float) + template_ = await cg.templatable(config[CONF_VALUE], args, float) cg.add(var.set_level(template_)) - yield var + return var diff --git a/esphome/components/pipsolar/output/pipsolar_output.cpp b/esphome/components/pipsolar/output/pipsolar_output.cpp index 00ec73b56a..163fbf4eb2 100644 --- a/esphome/components/pipsolar/output/pipsolar_output.cpp +++ b/esphome/components/pipsolar/output/pipsolar_output.cpp @@ -13,7 +13,7 @@ void PipsolarOutput::write_state(float state) { if (std::find(this->possible_values_.begin(), this->possible_values_.end(), state) != this->possible_values_.end()) { ESP_LOGD(TAG, "Will write: %s out of value %f / %02.0f", tmp, state, state); - this->parent_->switch_command(std::string(tmp)); + this->parent_->queue_command(std::string(tmp)); } else { ESP_LOGD(TAG, "Will not write: %s as it is not in list of allowed values", tmp); } diff --git a/esphome/components/pipsolar/output/pipsolar_output.h b/esphome/components/pipsolar/output/pipsolar_output.h index 29b2d116f2..b4b8000962 100644 --- a/esphome/components/pipsolar/output/pipsolar_output.h +++ b/esphome/components/pipsolar/output/pipsolar_output.h @@ -32,7 +32,7 @@ template class SetOutputAction : public Action { TEMPLATABLE_VALUE(float, level) - void play(Ts... x) override { this->output_->set_value(this->level_.value(x...)); } + void play(const Ts &...x) override { this->output_->set_value(this->level_.value(x...)); } protected: PipsolarOutput *output_; diff --git a/esphome/components/pipsolar/pipsolar.cpp b/esphome/components/pipsolar/pipsolar.cpp index 40405114a4..bafd5273da 100644 --- a/esphome/components/pipsolar/pipsolar.cpp +++ b/esphome/components/pipsolar/pipsolar.cpp @@ -23,24 +23,21 @@ void Pipsolar::loop() { // Read message if (this->state_ == STATE_IDLE) { this->empty_uart_buffer_(); - switch (this->send_next_command_()) { - case 0: - // no command send (empty queue) time to poll - if (millis() - this->last_poll_ > this->update_interval_) { - this->send_next_poll_(); - this->last_poll_ = millis(); - } - return; - break; - case 1: - // command send - return; - break; + + if (this->send_next_command_()) { + // command sent + return; } + + if (this->send_next_poll_()) { + // poll sent + return; + } + + return; } if (this->state_ == STATE_COMMAND_COMPLETE) { if (this->check_incoming_length_(4)) { - ESP_LOGD(TAG, "response length for command OK"); if (this->check_incoming_crc_()) { // crc ok if (this->read_buffer_[1] == 'A' && this->read_buffer_[2] == 'C' && this->read_buffer_[3] == 'K') { @@ -51,15 +48,15 @@ void Pipsolar::loop() { this->command_queue_[this->command_queue_position_] = std::string(""); this->command_queue_position_ = (command_queue_position_ + 1) % COMMAND_QUEUE_LENGTH; this->state_ = STATE_IDLE; - } else { // crc failed + // no log message necessary, check_incoming_crc_() logs this->command_queue_[this->command_queue_position_] = std::string(""); this->command_queue_position_ = (command_queue_position_ + 1) % COMMAND_QUEUE_LENGTH; this->state_ = STATE_IDLE; } } else { - ESP_LOGD(TAG, "response length for command %s not OK: with length %zu", + ESP_LOGD(TAG, "command %s response length not OK: with length %zu", this->command_queue_[this->command_queue_position_].c_str(), this->read_pos_); this->command_queue_[this->command_queue_position_] = std::string(""); this->command_queue_position_ = (command_queue_position_ + 1) % COMMAND_QUEUE_LENGTH; @@ -67,636 +64,11 @@ void Pipsolar::loop() { } } - if (this->state_ == STATE_POLL_DECODED) { - std::string mode; - switch (this->used_polling_commands_[this->last_polling_command_].identifier) { - case POLLING_QPIRI: - if (this->grid_rating_voltage_) { - this->grid_rating_voltage_->publish_state(value_grid_rating_voltage_); - } - if (this->grid_rating_current_) { - this->grid_rating_current_->publish_state(value_grid_rating_current_); - } - if (this->ac_output_rating_voltage_) { - this->ac_output_rating_voltage_->publish_state(value_ac_output_rating_voltage_); - } - if (this->ac_output_rating_frequency_) { - this->ac_output_rating_frequency_->publish_state(value_ac_output_rating_frequency_); - } - if (this->ac_output_rating_current_) { - this->ac_output_rating_current_->publish_state(value_ac_output_rating_current_); - } - if (this->ac_output_rating_apparent_power_) { - this->ac_output_rating_apparent_power_->publish_state(value_ac_output_rating_apparent_power_); - } - if (this->ac_output_rating_active_power_) { - this->ac_output_rating_active_power_->publish_state(value_ac_output_rating_active_power_); - } - if (this->battery_rating_voltage_) { - this->battery_rating_voltage_->publish_state(value_battery_rating_voltage_); - } - if (this->battery_recharge_voltage_) { - this->battery_recharge_voltage_->publish_state(value_battery_recharge_voltage_); - } - if (this->battery_under_voltage_) { - this->battery_under_voltage_->publish_state(value_battery_under_voltage_); - } - if (this->battery_bulk_voltage_) { - this->battery_bulk_voltage_->publish_state(value_battery_bulk_voltage_); - } - if (this->battery_float_voltage_) { - this->battery_float_voltage_->publish_state(value_battery_float_voltage_); - } - if (this->battery_type_) { - this->battery_type_->publish_state(value_battery_type_); - } - if (this->current_max_ac_charging_current_) { - this->current_max_ac_charging_current_->publish_state(value_current_max_ac_charging_current_); - } - if (this->current_max_charging_current_) { - this->current_max_charging_current_->publish_state(value_current_max_charging_current_); - } - if (this->input_voltage_range_) { - this->input_voltage_range_->publish_state(value_input_voltage_range_); - } - // special for input voltage range switch - if (this->input_voltage_range_switch_) { - this->input_voltage_range_switch_->publish_state(value_input_voltage_range_ == 1); - } - if (this->output_source_priority_) { - this->output_source_priority_->publish_state(value_output_source_priority_); - } - // special for output source priority switches - if (this->output_source_priority_utility_switch_) { - this->output_source_priority_utility_switch_->publish_state(value_output_source_priority_ == 0); - } - if (this->output_source_priority_solar_switch_) { - this->output_source_priority_solar_switch_->publish_state(value_output_source_priority_ == 1); - } - if (this->output_source_priority_battery_switch_) { - this->output_source_priority_battery_switch_->publish_state(value_output_source_priority_ == 2); - } - if (this->output_source_priority_hybrid_switch_) { - this->output_source_priority_hybrid_switch_->publish_state(value_output_source_priority_ == 3); - } - if (this->charger_source_priority_) { - this->charger_source_priority_->publish_state(value_charger_source_priority_); - } - if (this->parallel_max_num_) { - this->parallel_max_num_->publish_state(value_parallel_max_num_); - } - if (this->machine_type_) { - this->machine_type_->publish_state(value_machine_type_); - } - if (this->topology_) { - this->topology_->publish_state(value_topology_); - } - if (this->output_mode_) { - this->output_mode_->publish_state(value_output_mode_); - } - if (this->battery_redischarge_voltage_) { - this->battery_redischarge_voltage_->publish_state(value_battery_redischarge_voltage_); - } - if (this->pv_ok_condition_for_parallel_) { - this->pv_ok_condition_for_parallel_->publish_state(value_pv_ok_condition_for_parallel_); - } - // special for pv ok condition switch - if (this->pv_ok_condition_for_parallel_switch_) { - this->pv_ok_condition_for_parallel_switch_->publish_state(value_pv_ok_condition_for_parallel_ == 1); - } - if (this->pv_power_balance_) { - this->pv_power_balance_->publish_state(value_pv_power_balance_ == 1); - } - // special for power balance switch - if (this->pv_power_balance_switch_) { - this->pv_power_balance_switch_->publish_state(value_pv_power_balance_ == 1); - } - this->state_ = STATE_IDLE; - break; - case POLLING_QPIGS: - if (this->grid_voltage_) { - this->grid_voltage_->publish_state(value_grid_voltage_); - } - if (this->grid_frequency_) { - this->grid_frequency_->publish_state(value_grid_frequency_); - } - if (this->ac_output_voltage_) { - this->ac_output_voltage_->publish_state(value_ac_output_voltage_); - } - if (this->ac_output_frequency_) { - this->ac_output_frequency_->publish_state(value_ac_output_frequency_); - } - if (this->ac_output_apparent_power_) { - this->ac_output_apparent_power_->publish_state(value_ac_output_apparent_power_); - } - if (this->ac_output_active_power_) { - this->ac_output_active_power_->publish_state(value_ac_output_active_power_); - } - if (this->output_load_percent_) { - this->output_load_percent_->publish_state(value_output_load_percent_); - } - if (this->bus_voltage_) { - this->bus_voltage_->publish_state(value_bus_voltage_); - } - if (this->battery_voltage_) { - this->battery_voltage_->publish_state(value_battery_voltage_); - } - if (this->battery_charging_current_) { - this->battery_charging_current_->publish_state(value_battery_charging_current_); - } - if (this->battery_capacity_percent_) { - this->battery_capacity_percent_->publish_state(value_battery_capacity_percent_); - } - if (this->inverter_heat_sink_temperature_) { - this->inverter_heat_sink_temperature_->publish_state(value_inverter_heat_sink_temperature_); - } - if (this->pv_input_current_for_battery_) { - this->pv_input_current_for_battery_->publish_state(value_pv_input_current_for_battery_); - } - if (this->pv_input_voltage_) { - this->pv_input_voltage_->publish_state(value_pv_input_voltage_); - } - if (this->battery_voltage_scc_) { - this->battery_voltage_scc_->publish_state(value_battery_voltage_scc_); - } - if (this->battery_discharge_current_) { - this->battery_discharge_current_->publish_state(value_battery_discharge_current_); - } - if (this->add_sbu_priority_version_) { - this->add_sbu_priority_version_->publish_state(value_add_sbu_priority_version_); - } - if (this->configuration_status_) { - this->configuration_status_->publish_state(value_configuration_status_); - } - if (this->scc_firmware_version_) { - this->scc_firmware_version_->publish_state(value_scc_firmware_version_); - } - if (this->load_status_) { - this->load_status_->publish_state(value_load_status_); - } - if (this->battery_voltage_to_steady_while_charging_) { - this->battery_voltage_to_steady_while_charging_->publish_state( - value_battery_voltage_to_steady_while_charging_); - } - if (this->charging_status_) { - this->charging_status_->publish_state(value_charging_status_); - } - if (this->scc_charging_status_) { - this->scc_charging_status_->publish_state(value_scc_charging_status_); - } - if (this->ac_charging_status_) { - this->ac_charging_status_->publish_state(value_ac_charging_status_); - } - if (this->battery_voltage_offset_for_fans_on_) { - this->battery_voltage_offset_for_fans_on_->publish_state(value_battery_voltage_offset_for_fans_on_ / 10.0f); - } //.1 scale - if (this->eeprom_version_) { - this->eeprom_version_->publish_state(value_eeprom_version_); - } - if (this->pv_charging_power_) { - this->pv_charging_power_->publish_state(value_pv_charging_power_); - } - if (this->charging_to_floating_mode_) { - this->charging_to_floating_mode_->publish_state(value_charging_to_floating_mode_); - } - if (this->switch_on_) { - this->switch_on_->publish_state(value_switch_on_); - } - if (this->dustproof_installed_) { - this->dustproof_installed_->publish_state(value_dustproof_installed_); - } - this->state_ = STATE_IDLE; - break; - case POLLING_QMOD: - if (this->device_mode_) { - mode = value_device_mode_; - this->device_mode_->publish_state(mode); - } - this->state_ = STATE_IDLE; - break; - case POLLING_QFLAG: - if (this->silence_buzzer_open_buzzer_) { - this->silence_buzzer_open_buzzer_->publish_state(value_silence_buzzer_open_buzzer_); - } - if (this->overload_bypass_function_) { - this->overload_bypass_function_->publish_state(value_overload_bypass_function_); - } - if (this->lcd_escape_to_default_) { - this->lcd_escape_to_default_->publish_state(value_lcd_escape_to_default_); - } - if (this->overload_restart_function_) { - this->overload_restart_function_->publish_state(value_overload_restart_function_); - } - if (this->over_temperature_restart_function_) { - this->over_temperature_restart_function_->publish_state(value_over_temperature_restart_function_); - } - if (this->backlight_on_) { - this->backlight_on_->publish_state(value_backlight_on_); - } - if (this->alarm_on_when_primary_source_interrupt_) { - this->alarm_on_when_primary_source_interrupt_->publish_state(value_alarm_on_when_primary_source_interrupt_); - } - if (this->fault_code_record_) { - this->fault_code_record_->publish_state(value_fault_code_record_); - } - if (this->power_saving_) { - this->power_saving_->publish_state(value_power_saving_); - } - this->state_ = STATE_IDLE; - break; - case POLLING_QPIWS: - if (this->warnings_present_) { - this->warnings_present_->publish_state(value_warnings_present_); - } - if (this->faults_present_) { - this->faults_present_->publish_state(value_faults_present_); - } - if (this->warning_power_loss_) { - this->warning_power_loss_->publish_state(value_warning_power_loss_); - } - if (this->fault_inverter_fault_) { - this->fault_inverter_fault_->publish_state(value_fault_inverter_fault_); - } - if (this->fault_bus_over_) { - this->fault_bus_over_->publish_state(value_fault_bus_over_); - } - if (this->fault_bus_under_) { - this->fault_bus_under_->publish_state(value_fault_bus_under_); - } - if (this->fault_bus_soft_fail_) { - this->fault_bus_soft_fail_->publish_state(value_fault_bus_soft_fail_); - } - if (this->warning_line_fail_) { - this->warning_line_fail_->publish_state(value_warning_line_fail_); - } - if (this->fault_opvshort_) { - this->fault_opvshort_->publish_state(value_fault_opvshort_); - } - if (this->fault_inverter_voltage_too_low_) { - this->fault_inverter_voltage_too_low_->publish_state(value_fault_inverter_voltage_too_low_); - } - if (this->fault_inverter_voltage_too_high_) { - this->fault_inverter_voltage_too_high_->publish_state(value_fault_inverter_voltage_too_high_); - } - if (this->warning_over_temperature_) { - this->warning_over_temperature_->publish_state(value_warning_over_temperature_); - } - if (this->warning_fan_lock_) { - this->warning_fan_lock_->publish_state(value_warning_fan_lock_); - } - if (this->warning_battery_voltage_high_) { - this->warning_battery_voltage_high_->publish_state(value_warning_battery_voltage_high_); - } - if (this->warning_battery_low_alarm_) { - this->warning_battery_low_alarm_->publish_state(value_warning_battery_low_alarm_); - } - if (this->warning_battery_under_shutdown_) { - this->warning_battery_under_shutdown_->publish_state(value_warning_battery_under_shutdown_); - } - if (this->warning_battery_derating_) { - this->warning_battery_derating_->publish_state(value_warning_battery_derating_); - } - if (this->warning_over_load_) { - this->warning_over_load_->publish_state(value_warning_over_load_); - } - if (this->warning_eeprom_failed_) { - this->warning_eeprom_failed_->publish_state(value_warning_eeprom_failed_); - } - if (this->fault_inverter_over_current_) { - this->fault_inverter_over_current_->publish_state(value_fault_inverter_over_current_); - } - if (this->fault_inverter_soft_failed_) { - this->fault_inverter_soft_failed_->publish_state(value_fault_inverter_soft_failed_); - } - if (this->fault_self_test_failed_) { - this->fault_self_test_failed_->publish_state(value_fault_self_test_failed_); - } - if (this->fault_op_dc_voltage_over_) { - this->fault_op_dc_voltage_over_->publish_state(value_fault_op_dc_voltage_over_); - } - if (this->fault_battery_open_) { - this->fault_battery_open_->publish_state(value_fault_battery_open_); - } - if (this->fault_current_sensor_failed_) { - this->fault_current_sensor_failed_->publish_state(value_fault_current_sensor_failed_); - } - if (this->fault_battery_short_) { - this->fault_battery_short_->publish_state(value_fault_battery_short_); - } - if (this->warning_power_limit_) { - this->warning_power_limit_->publish_state(value_warning_power_limit_); - } - if (this->warning_pv_voltage_high_) { - this->warning_pv_voltage_high_->publish_state(value_warning_pv_voltage_high_); - } - if (this->fault_mppt_overload_) { - this->fault_mppt_overload_->publish_state(value_fault_mppt_overload_); - } - if (this->warning_mppt_overload_) { - this->warning_mppt_overload_->publish_state(value_warning_mppt_overload_); - } - if (this->warning_battery_too_low_to_charge_) { - this->warning_battery_too_low_to_charge_->publish_state(value_warning_battery_too_low_to_charge_); - } - if (this->fault_dc_dc_over_current_) { - this->fault_dc_dc_over_current_->publish_state(value_fault_dc_dc_over_current_); - } - if (this->fault_code_) { - this->fault_code_->publish_state(value_fault_code_); - } - if (this->warnung_low_pv_energy_) { - this->warnung_low_pv_energy_->publish_state(value_warnung_low_pv_energy_); - } - if (this->warning_high_ac_input_during_bus_soft_start_) { - this->warning_high_ac_input_during_bus_soft_start_->publish_state( - value_warning_high_ac_input_during_bus_soft_start_); - } - if (this->warning_battery_equalization_) { - this->warning_battery_equalization_->publish_state(value_warning_battery_equalization_); - } - this->state_ = STATE_IDLE; - break; - case POLLING_QT: - case POLLING_QMN: - this->state_ = STATE_IDLE; - break; - } - } - if (this->state_ == STATE_POLL_CHECKED) { - bool enabled = true; - std::string fc; - char tmp[PIPSOLAR_READ_BUFFER_LENGTH]; - sprintf(tmp, "%s", this->read_buffer_); - switch (this->used_polling_commands_[this->last_polling_command_].identifier) { - case POLLING_QPIRI: - ESP_LOGD(TAG, "Decode QPIRI"); - sscanf(tmp, "(%f %f %f %f %f %d %d %f %f %f %f %f %d %d %d %d %d %d %d %d %d %d %f %d %d", // NOLINT - &value_grid_rating_voltage_, &value_grid_rating_current_, &value_ac_output_rating_voltage_, // NOLINT - &value_ac_output_rating_frequency_, &value_ac_output_rating_current_, // NOLINT - &value_ac_output_rating_apparent_power_, &value_ac_output_rating_active_power_, // NOLINT - &value_battery_rating_voltage_, &value_battery_recharge_voltage_, // NOLINT - &value_battery_under_voltage_, &value_battery_bulk_voltage_, &value_battery_float_voltage_, // NOLINT - &value_battery_type_, &value_current_max_ac_charging_current_, // NOLINT - &value_current_max_charging_current_, &value_input_voltage_range_, // NOLINT - &value_output_source_priority_, &value_charger_source_priority_, &value_parallel_max_num_, // NOLINT - &value_machine_type_, &value_topology_, &value_output_mode_, // NOLINT - &value_battery_redischarge_voltage_, &value_pv_ok_condition_for_parallel_, // NOLINT - &value_pv_power_balance_); // NOLINT - if (this->last_qpiri_) { - this->last_qpiri_->publish_state(tmp); - } - this->state_ = STATE_POLL_DECODED; - break; - case POLLING_QPIGS: - ESP_LOGD(TAG, "Decode QPIGS"); - sscanf( // NOLINT - tmp, // NOLINT - "(%f %f %f %f %d %d %d %d %f %d %d %d %f %f %f %d %1d%1d%1d%1d%1d%1d%1d%1d %d %d %d %1d%1d%1d", // NOLINT - &value_grid_voltage_, &value_grid_frequency_, &value_ac_output_voltage_, // NOLINT - &value_ac_output_frequency_, // NOLINT - &value_ac_output_apparent_power_, &value_ac_output_active_power_, &value_output_load_percent_, // NOLINT - &value_bus_voltage_, &value_battery_voltage_, &value_battery_charging_current_, // NOLINT - &value_battery_capacity_percent_, &value_inverter_heat_sink_temperature_, // NOLINT - &value_pv_input_current_for_battery_, &value_pv_input_voltage_, &value_battery_voltage_scc_, // NOLINT - &value_battery_discharge_current_, &value_add_sbu_priority_version_, // NOLINT - &value_configuration_status_, &value_scc_firmware_version_, &value_load_status_, // NOLINT - &value_battery_voltage_to_steady_while_charging_, &value_charging_status_, // NOLINT - &value_scc_charging_status_, &value_ac_charging_status_, // NOLINT - &value_battery_voltage_offset_for_fans_on_, &value_eeprom_version_, &value_pv_charging_power_, // NOLINT - &value_charging_to_floating_mode_, &value_switch_on_, // NOLINT - &value_dustproof_installed_); // NOLINT - if (this->last_qpigs_) { - this->last_qpigs_->publish_state(tmp); - } - this->state_ = STATE_POLL_DECODED; - break; - case POLLING_QMOD: - ESP_LOGD(TAG, "Decode QMOD"); - this->value_device_mode_ = char(this->read_buffer_[1]); - if (this->last_qmod_) { - this->last_qmod_->publish_state(tmp); - } - this->state_ = STATE_POLL_DECODED; - break; - case POLLING_QFLAG: - ESP_LOGD(TAG, "Decode QFLAG"); - // result like:"(EbkuvxzDajy" - // get through all char: ignore first "(" Enable flag on 'E', Disable on 'D') else set the corresponding value - for (size_t i = 1; i < strlen(tmp); i++) { - switch (tmp[i]) { - case 'E': - enabled = true; - break; - case 'D': - enabled = false; - break; - case 'a': - this->value_silence_buzzer_open_buzzer_ = enabled; - break; - case 'b': - this->value_overload_bypass_function_ = enabled; - break; - case 'k': - this->value_lcd_escape_to_default_ = enabled; - break; - case 'u': - this->value_overload_restart_function_ = enabled; - break; - case 'v': - this->value_over_temperature_restart_function_ = enabled; - break; - case 'x': - this->value_backlight_on_ = enabled; - break; - case 'y': - this->value_alarm_on_when_primary_source_interrupt_ = enabled; - break; - case 'z': - this->value_fault_code_record_ = enabled; - break; - case 'j': - this->value_power_saving_ = enabled; - break; - } - } - if (this->last_qflag_) { - this->last_qflag_->publish_state(tmp); - } - this->state_ = STATE_POLL_DECODED; - break; - case POLLING_QPIWS: - ESP_LOGD(TAG, "Decode QPIWS"); - // '(00000000000000000000000000000000' - // iterate over all available flag (as not all models have all flags, but at least in the same order) - this->value_warnings_present_ = false; - this->value_faults_present_ = true; - - for (size_t i = 1; i < strlen(tmp); i++) { - enabled = tmp[i] == '1'; - switch (i) { - case 1: - this->value_warning_power_loss_ = enabled; - this->value_warnings_present_ += enabled; - break; - case 2: - this->value_fault_inverter_fault_ = enabled; - this->value_faults_present_ += enabled; - break; - case 3: - this->value_fault_bus_over_ = enabled; - this->value_faults_present_ += enabled; - break; - case 4: - this->value_fault_bus_under_ = enabled; - this->value_faults_present_ += enabled; - break; - case 5: - this->value_fault_bus_soft_fail_ = enabled; - this->value_faults_present_ += enabled; - break; - case 6: - this->value_warning_line_fail_ = enabled; - this->value_warnings_present_ += enabled; - break; - case 7: - this->value_fault_opvshort_ = enabled; - this->value_faults_present_ += enabled; - break; - case 8: - this->value_fault_inverter_voltage_too_low_ = enabled; - this->value_faults_present_ += enabled; - break; - case 9: - this->value_fault_inverter_voltage_too_high_ = enabled; - this->value_faults_present_ += enabled; - break; - case 10: - this->value_warning_over_temperature_ = enabled; - this->value_warnings_present_ += enabled; - break; - case 11: - this->value_warning_fan_lock_ = enabled; - this->value_warnings_present_ += enabled; - break; - case 12: - this->value_warning_battery_voltage_high_ = enabled; - this->value_warnings_present_ += enabled; - break; - case 13: - this->value_warning_battery_low_alarm_ = enabled; - this->value_warnings_present_ += enabled; - break; - case 15: - this->value_warning_battery_under_shutdown_ = enabled; - this->value_warnings_present_ += enabled; - break; - case 16: - this->value_warning_battery_derating_ = enabled; - this->value_warnings_present_ += enabled; - break; - case 17: - this->value_warning_over_load_ = enabled; - this->value_warnings_present_ += enabled; - break; - case 18: - this->value_warning_eeprom_failed_ = enabled; - this->value_warnings_present_ += enabled; - break; - case 19: - this->value_fault_inverter_over_current_ = enabled; - this->value_faults_present_ += enabled; - break; - case 20: - this->value_fault_inverter_soft_failed_ = enabled; - this->value_faults_present_ += enabled; - break; - case 21: - this->value_fault_self_test_failed_ = enabled; - this->value_faults_present_ += enabled; - break; - case 22: - this->value_fault_op_dc_voltage_over_ = enabled; - this->value_faults_present_ += enabled; - break; - case 23: - this->value_fault_battery_open_ = enabled; - this->value_faults_present_ += enabled; - break; - case 24: - this->value_fault_current_sensor_failed_ = enabled; - this->value_faults_present_ += enabled; - break; - case 25: - this->value_fault_battery_short_ = enabled; - this->value_faults_present_ += enabled; - break; - case 26: - this->value_warning_power_limit_ = enabled; - this->value_warnings_present_ += enabled; - break; - case 27: - this->value_warning_pv_voltage_high_ = enabled; - this->value_warnings_present_ += enabled; - break; - case 28: - this->value_fault_mppt_overload_ = enabled; - this->value_faults_present_ += enabled; - break; - case 29: - this->value_warning_mppt_overload_ = enabled; - this->value_warnings_present_ += enabled; - break; - case 30: - this->value_warning_battery_too_low_to_charge_ = enabled; - this->value_warnings_present_ += enabled; - break; - case 31: - this->value_fault_dc_dc_over_current_ = enabled; - this->value_faults_present_ += enabled; - break; - case 32: - fc = tmp[i]; - fc += tmp[i + 1]; - this->value_fault_code_ = parse_number(fc).value_or(0); - break; - case 34: - this->value_warnung_low_pv_energy_ = enabled; - this->value_warnings_present_ += enabled; - break; - case 35: - this->value_warning_high_ac_input_during_bus_soft_start_ = enabled; - this->value_warnings_present_ += enabled; - break; - case 36: - this->value_warning_battery_equalization_ = enabled; - this->value_warnings_present_ += enabled; - break; - } - } - if (this->last_qpiws_) { - this->last_qpiws_->publish_state(tmp); - } - this->state_ = STATE_POLL_DECODED; - break; - case POLLING_QT: - ESP_LOGD(TAG, "Decode QT"); - if (this->last_qt_) { - this->last_qt_->publish_state(tmp); - } - this->state_ = STATE_POLL_DECODED; - break; - case POLLING_QMN: - ESP_LOGD(TAG, "Decode QMN"); - if (this->last_qmn_) { - this->last_qmn_->publish_state(tmp); - } - this->state_ = STATE_POLL_DECODED; - break; - default: - this->state_ = STATE_IDLE; - break; - } + ESP_LOGD(TAG, "poll %s decode", this->enabled_polling_commands_[this->last_polling_command_].command); + this->handle_poll_response_(this->enabled_polling_commands_[this->last_polling_command_].identifier, + (const char *) this->read_buffer_); + this->state_ = STATE_IDLE; return; } @@ -704,13 +76,19 @@ void Pipsolar::loop() { if (this->check_incoming_crc_()) { if (this->read_buffer_[0] == '(' && this->read_buffer_[1] == 'N' && this->read_buffer_[2] == 'A' && this->read_buffer_[3] == 'K') { + ESP_LOGD(TAG, "poll %s NACK", this->enabled_polling_commands_[this->last_polling_command_].command); + this->handle_poll_error_(this->enabled_polling_commands_[this->last_polling_command_].identifier); this->state_ = STATE_IDLE; return; } // crc ok + this->enabled_polling_commands_[this->last_polling_command_].needs_update = false; this->state_ = STATE_POLL_CHECKED; return; } else { + // crc failed + // no log message necessary, check_incoming_crc_() logs + this->handle_poll_error_(this->enabled_polling_commands_[this->last_polling_command_].identifier); this->state_ = STATE_IDLE; } } @@ -720,9 +98,12 @@ void Pipsolar::loop() { uint8_t byte; this->read_byte(&byte); - if (this->read_pos_ == PIPSOLAR_READ_BUFFER_LENGTH) { + // make sure data and null terminator fit in buffer + if (this->read_pos_ >= PIPSOLAR_READ_BUFFER_LENGTH - 1) { this->read_pos_ = 0; this->empty_uart_buffer_(); + ESP_LOGW(TAG, "response data too long, discarding."); + break; } this->read_buffer_[this->read_pos_] = byte; this->read_pos_++; @@ -745,20 +126,19 @@ void Pipsolar::loop() { // command timeout const char *command = this->command_queue_[this->command_queue_position_].c_str(); this->command_start_millis_ = millis(); - ESP_LOGD(TAG, "timeout command from queue: %s", command); + ESP_LOGD(TAG, "command %s timeout", command); this->command_queue_[this->command_queue_position_] = std::string(""); this->command_queue_position_ = (command_queue_position_ + 1) % COMMAND_QUEUE_LENGTH; this->state_ = STATE_IDLE; return; - } else { } } if (this->state_ == STATE_POLL) { if (millis() - this->command_start_millis_ > esphome::pipsolar::Pipsolar::COMMAND_TIMEOUT) { // command timeout - ESP_LOGD(TAG, "timeout command to poll: %s", this->used_polling_commands_[this->last_polling_command_].command); + ESP_LOGD(TAG, "poll %s timeout", this->enabled_polling_commands_[this->last_polling_command_].command); + this->handle_poll_error_(this->enabled_polling_commands_[this->last_polling_command_].identifier); this->state_ = STATE_IDLE; - } else { } } } @@ -773,7 +153,6 @@ uint8_t Pipsolar::check_incoming_length_(uint8_t length) { uint8_t Pipsolar::check_incoming_crc_() { uint16_t crc16; crc16 = this->pipsolar_crc_(read_buffer_, read_pos_ - 3); - ESP_LOGD(TAG, "checking crc on incoming message"); if (((uint8_t) ((crc16) >> 8)) == read_buffer_[read_pos_ - 3] && ((uint8_t) ((crc16) &0xff)) == read_buffer_[read_pos_ - 2]) { ESP_LOGD(TAG, "CRC OK"); @@ -787,8 +166,8 @@ uint8_t Pipsolar::check_incoming_crc_() { return 0; } -// send next command used -uint8_t Pipsolar::send_next_command_() { +// send next command from queue +bool Pipsolar::send_next_command_() { uint16_t crc16; if (!this->command_queue_[this->command_queue_position_].empty()) { const char *command = this->command_queue_[this->command_queue_position_].c_str(); @@ -809,88 +188,583 @@ uint8_t Pipsolar::send_next_command_() { // end Byte this->write(0x0D); ESP_LOGD(TAG, "Sending command from queue: %s with length %d", command, length); - return 1; + return true; } - return 0; + return false; } -void Pipsolar::send_next_poll_() { +bool Pipsolar::send_next_poll_() { uint16_t crc16; - this->last_polling_command_ = (this->last_polling_command_ + 1) % 15; - if (this->used_polling_commands_[this->last_polling_command_].length == 0) { - this->last_polling_command_ = 0; + for (uint8_t i = 0; i < POLLING_COMMANDS_MAX; i++) { + this->last_polling_command_ = (this->last_polling_command_ + 1) % POLLING_COMMANDS_MAX; + if (this->enabled_polling_commands_[this->last_polling_command_].length == 0) { + // not enabled + continue; + } + if (!this->enabled_polling_commands_[this->last_polling_command_].needs_update) { + // no update requested + continue; + } + this->state_ = STATE_POLL; + this->command_start_millis_ = millis(); + this->empty_uart_buffer_(); + this->read_pos_ = 0; + crc16 = this->pipsolar_crc_(this->enabled_polling_commands_[this->last_polling_command_].command, + this->enabled_polling_commands_[this->last_polling_command_].length); + this->write_array(this->enabled_polling_commands_[this->last_polling_command_].command, + this->enabled_polling_commands_[this->last_polling_command_].length); + // checksum + this->write(((uint8_t) ((crc16) >> 8))); // highbyte + this->write(((uint8_t) ((crc16) &0xff))); // lowbyte + // end Byte + this->write(0x0D); + ESP_LOGD(TAG, "Sending polling command: %s with length %d", + this->enabled_polling_commands_[this->last_polling_command_].command, + this->enabled_polling_commands_[this->last_polling_command_].length); + return true; } - if (this->used_polling_commands_[this->last_polling_command_].length == 0) { - // no command specified - return; - } - this->state_ = STATE_POLL; - this->command_start_millis_ = millis(); - this->empty_uart_buffer_(); - this->read_pos_ = 0; - crc16 = this->pipsolar_crc_(this->used_polling_commands_[this->last_polling_command_].command, - this->used_polling_commands_[this->last_polling_command_].length); - this->write_array(this->used_polling_commands_[this->last_polling_command_].command, - this->used_polling_commands_[this->last_polling_command_].length); - // checksum - this->write(((uint8_t) ((crc16) >> 8))); // highbyte - this->write(((uint8_t) ((crc16) &0xff))); // lowbyte - // end Byte - this->write(0x0D); - ESP_LOGD(TAG, "Sending polling command : %s with length %d", - this->used_polling_commands_[this->last_polling_command_].command, - this->used_polling_commands_[this->last_polling_command_].length); + return false; } -void Pipsolar::queue_command_(const char *command, uint8_t length) { +void Pipsolar::queue_command(const std::string &command) { uint8_t next_position = command_queue_position_; for (uint8_t i = 0; i < COMMAND_QUEUE_LENGTH; i++) { uint8_t testposition = (next_position + i) % COMMAND_QUEUE_LENGTH; if (command_queue_[testposition].empty()) { command_queue_[testposition] = command; - ESP_LOGD(TAG, "Command queued successfully: %s with length %u at position %d", command, - command_queue_[testposition].length(), testposition); + ESP_LOGD(TAG, "Command queued successfully: %s at position %d", command.c_str(), testposition); return; } } - ESP_LOGD(TAG, "Command queue full dropping command: %s", command); + ESP_LOGD(TAG, "Command queue full dropping command: %s", command.c_str()); } -void Pipsolar::switch_command(const std::string &command) { - ESP_LOGD(TAG, "got command: %s", command.c_str()); - queue_command_(command.c_str(), command.length()); +void Pipsolar::handle_poll_response_(ENUMPollingCommand polling_command, const char *message) { + switch (polling_command) { + case POLLING_QPIRI: + handle_qpiri_(message); + break; + case POLLING_QPIGS: + handle_qpigs_(message); + break; + case POLLING_QMOD: + handle_qmod_(message); + break; + case POLLING_QFLAG: + handle_qflag_(message); + break; + case POLLING_QPIWS: + handle_qpiws_(message); + break; + case POLLING_QT: + handle_qt_(message); + break; + case POLLING_QMN: + handle_qmn_(message); + break; + default: + break; + } } -void Pipsolar::dump_config() { - ESP_LOGCONFIG(TAG, "Pipsolar:\n" - "used commands:"); - for (auto &used_polling_command : this->used_polling_commands_) { - if (used_polling_command.length != 0) { - ESP_LOGCONFIG(TAG, "%s", used_polling_command.command); +void Pipsolar::handle_poll_error_(ENUMPollingCommand polling_command) { + // handlers are designed in a way that an empty message sets all sensors to unknown + this->handle_poll_response_(polling_command, ""); +} + +void Pipsolar::handle_qpiri_(const char *message) { + if (this->last_qpiri_) { + this->last_qpiri_->publish_state(message); + } + + size_t pos = 0; + this->skip_start_(message, &pos); + + this->read_float_sensor_(message, &pos, this->grid_rating_voltage_); + this->read_float_sensor_(message, &pos, this->grid_rating_current_); + this->read_float_sensor_(message, &pos, this->ac_output_rating_voltage_); + this->read_float_sensor_(message, &pos, this->ac_output_rating_frequency_); + this->read_float_sensor_(message, &pos, this->ac_output_rating_current_); + + this->read_int_sensor_(message, &pos, this->ac_output_rating_apparent_power_); + this->read_int_sensor_(message, &pos, this->ac_output_rating_active_power_); + + this->read_float_sensor_(message, &pos, this->battery_rating_voltage_); + this->read_float_sensor_(message, &pos, this->battery_recharge_voltage_); + this->read_float_sensor_(message, &pos, this->battery_under_voltage_); + this->read_float_sensor_(message, &pos, this->battery_bulk_voltage_); + this->read_float_sensor_(message, &pos, this->battery_float_voltage_); + + this->read_int_sensor_(message, &pos, this->battery_type_); + this->read_int_sensor_(message, &pos, this->current_max_ac_charging_current_); + this->read_int_sensor_(message, &pos, this->current_max_charging_current_); + + esphome::optional input_voltage_range = parse_number(this->read_field_(message, &pos)); + esphome::optional output_source_priority = parse_number(this->read_field_(message, &pos)); + + this->read_int_sensor_(message, &pos, this->charger_source_priority_); + this->read_int_sensor_(message, &pos, this->parallel_max_num_); + this->read_int_sensor_(message, &pos, this->machine_type_); + this->read_int_sensor_(message, &pos, this->topology_); + this->read_int_sensor_(message, &pos, this->output_mode_); + + this->read_float_sensor_(message, &pos, this->battery_redischarge_voltage_); + + esphome::optional pv_ok_condition_for_parallel = parse_number(this->read_field_(message, &pos)); + esphome::optional pv_power_balance = parse_number(this->read_field_(message, &pos)); + + if (this->input_voltage_range_) { + this->input_voltage_range_->publish_state(input_voltage_range.value_or(NAN)); + } + // special for input voltage range switch + if (this->input_voltage_range_switch_ && input_voltage_range.has_value()) { + this->input_voltage_range_switch_->publish_state(input_voltage_range.value() == 1); + } + + if (this->output_source_priority_) { + this->output_source_priority_->publish_state(output_source_priority.value_or(NAN)); + } + // special for output source priority switches + if (this->output_source_priority_utility_switch_ && output_source_priority.has_value()) { + this->output_source_priority_utility_switch_->publish_state(output_source_priority.value() == 0); + } + if (this->output_source_priority_solar_switch_ && output_source_priority.has_value()) { + this->output_source_priority_solar_switch_->publish_state(output_source_priority.value() == 1); + } + if (this->output_source_priority_battery_switch_ && output_source_priority.has_value()) { + this->output_source_priority_battery_switch_->publish_state(output_source_priority.value() == 2); + } + if (this->output_source_priority_hybrid_switch_ && output_source_priority.has_value()) { + this->output_source_priority_hybrid_switch_->publish_state(output_source_priority.value() == 3); + } + + if (this->pv_ok_condition_for_parallel_) { + this->pv_ok_condition_for_parallel_->publish_state(pv_ok_condition_for_parallel.value_or(NAN)); + } + // special for pv ok condition switch + if (this->pv_ok_condition_for_parallel_switch_ && pv_ok_condition_for_parallel.has_value()) { + this->pv_ok_condition_for_parallel_switch_->publish_state(pv_ok_condition_for_parallel.value() == 1); + } + + if (this->pv_power_balance_) { + this->pv_power_balance_->publish_state(pv_power_balance.value_or(NAN)); + } + // special for power balance switch + if (this->pv_power_balance_switch_ && pv_power_balance.has_value()) { + this->pv_power_balance_switch_->publish_state(pv_power_balance.value() == 1); + } +} + +void Pipsolar::handle_qpigs_(const char *message) { + if (this->last_qpigs_) { + this->last_qpigs_->publish_state(message); + } + + size_t pos = 0; + this->skip_start_(message, &pos); + + this->read_float_sensor_(message, &pos, this->grid_voltage_); + this->read_float_sensor_(message, &pos, this->grid_frequency_); + this->read_float_sensor_(message, &pos, this->ac_output_voltage_); + this->read_float_sensor_(message, &pos, this->ac_output_frequency_); + + this->read_int_sensor_(message, &pos, this->ac_output_apparent_power_); + this->read_int_sensor_(message, &pos, this->ac_output_active_power_); + this->read_int_sensor_(message, &pos, this->output_load_percent_); + this->read_int_sensor_(message, &pos, this->bus_voltage_); + + this->read_float_sensor_(message, &pos, this->battery_voltage_); + + this->read_int_sensor_(message, &pos, this->battery_charging_current_); + this->read_int_sensor_(message, &pos, this->battery_capacity_percent_); + this->read_int_sensor_(message, &pos, this->inverter_heat_sink_temperature_); + + this->read_float_sensor_(message, &pos, this->pv_input_current_for_battery_); + this->read_float_sensor_(message, &pos, this->pv_input_voltage_); + this->read_float_sensor_(message, &pos, this->battery_voltage_scc_); + + this->read_int_sensor_(message, &pos, this->battery_discharge_current_); + + std::string device_status_1 = this->read_field_(message, &pos); + this->publish_binary_sensor_(this->get_bit_(device_status_1, 0), this->add_sbu_priority_version_); + this->publish_binary_sensor_(this->get_bit_(device_status_1, 1), this->configuration_status_); + this->publish_binary_sensor_(this->get_bit_(device_status_1, 2), this->scc_firmware_version_); + this->publish_binary_sensor_(this->get_bit_(device_status_1, 3), this->load_status_); + this->publish_binary_sensor_(this->get_bit_(device_status_1, 4), this->battery_voltage_to_steady_while_charging_); + this->publish_binary_sensor_(this->get_bit_(device_status_1, 5), this->charging_status_); + this->publish_binary_sensor_(this->get_bit_(device_status_1, 6), this->scc_charging_status_); + this->publish_binary_sensor_(this->get_bit_(device_status_1, 7), this->ac_charging_status_); + + esphome::optional battery_voltage_offset_for_fans_on = parse_number(this->read_field_(message, &pos)); + if (this->battery_voltage_offset_for_fans_on_) { + this->battery_voltage_offset_for_fans_on_->publish_state(battery_voltage_offset_for_fans_on.value_or(NAN) / 10.0f); + } + this->read_int_sensor_(message, &pos, this->eeprom_version_); + this->read_int_sensor_(message, &pos, this->pv_charging_power_); + + std::string device_status_2 = this->read_field_(message, &pos); + this->publish_binary_sensor_(this->get_bit_(device_status_2, 0), this->charging_to_floating_mode_); + this->publish_binary_sensor_(this->get_bit_(device_status_2, 1), this->switch_on_); + this->publish_binary_sensor_(this->get_bit_(device_status_2, 2), this->dustproof_installed_); +} + +void Pipsolar::handle_qmod_(const char *message) { + std::string mode; + char device_mode = char(message[1]); + if (this->last_qmod_) { + this->last_qmod_->publish_state(message); + } + if (this->device_mode_) { + mode = device_mode; + this->device_mode_->publish_state(mode); + } +} + +void Pipsolar::handle_qflag_(const char *message) { + // result like:"(EbkuvxzDajy" + // get through all char: ignore first "(" Enable flag on 'E', Disable on 'D') else set the corresponding value + if (this->last_qflag_) { + this->last_qflag_->publish_state(message); + } + + QFLAGValues values = QFLAGValues(); + bool enabled = true; + for (size_t i = 1; i < strlen(message); i++) { + switch (message[i]) { + case 'E': + enabled = true; + break; + case 'D': + enabled = false; + break; + case 'a': + values.silence_buzzer_open_buzzer = enabled; + break; + case 'b': + values.overload_bypass_function = enabled; + break; + case 'k': + values.lcd_escape_to_default = enabled; + break; + case 'u': + values.overload_restart_function = enabled; + break; + case 'v': + values.over_temperature_restart_function = enabled; + break; + case 'x': + values.backlight_on = enabled; + break; + case 'y': + values.alarm_on_when_primary_source_interrupt = enabled; + break; + case 'z': + values.fault_code_record = enabled; + break; + case 'j': + values.power_saving = enabled; + break; + } + } + + this->publish_binary_sensor_(values.silence_buzzer_open_buzzer, this->silence_buzzer_open_buzzer_); + this->publish_binary_sensor_(values.overload_bypass_function, this->overload_bypass_function_); + this->publish_binary_sensor_(values.lcd_escape_to_default, this->lcd_escape_to_default_); + this->publish_binary_sensor_(values.overload_restart_function, this->overload_restart_function_); + this->publish_binary_sensor_(values.over_temperature_restart_function, this->over_temperature_restart_function_); + this->publish_binary_sensor_(values.backlight_on, this->backlight_on_); + this->publish_binary_sensor_(values.alarm_on_when_primary_source_interrupt, + this->alarm_on_when_primary_source_interrupt_); + this->publish_binary_sensor_(values.fault_code_record, this->fault_code_record_); + this->publish_binary_sensor_(values.power_saving, this->power_saving_); +} + +void Pipsolar::handle_qpiws_(const char *message) { + // '(00000000000000000000000000000000' + // iterate over all available flag (as not all models have all flags, but at least in the same order) + if (this->last_qpiws_) { + this->last_qpiws_->publish_state(message); + } + + size_t pos = 0; + this->skip_start_(message, &pos); + std::string flags = this->read_field_(message, &pos); + + esphome::optional enabled; + bool value_warnings_present = false; + bool value_faults_present = false; + + for (size_t i = 0; i < 36; i++) { + if (i == 31 || i == 32) { + // special case for fault code + continue; + } + enabled = this->get_bit_(flags, i); + switch (i) { + case 0: + this->publish_binary_sensor_(enabled, this->warning_power_loss_); + value_warnings_present |= enabled.value_or(false); + break; + case 1: + this->publish_binary_sensor_(enabled, this->fault_inverter_fault_); + value_faults_present |= enabled.value_or(false); + break; + case 2: + this->publish_binary_sensor_(enabled, this->fault_bus_over_); + value_faults_present |= enabled.value_or(false); + break; + case 3: + this->publish_binary_sensor_(enabled, this->fault_bus_under_); + value_faults_present |= enabled.value_or(false); + break; + case 4: + this->publish_binary_sensor_(enabled, this->fault_bus_soft_fail_); + value_faults_present |= enabled.value_or(false); + break; + case 5: + this->publish_binary_sensor_(enabled, this->warning_line_fail_); + value_warnings_present |= enabled.value_or(false); + break; + case 6: + this->publish_binary_sensor_(enabled, this->fault_opvshort_); + value_faults_present |= enabled.value_or(false); + break; + case 7: + this->publish_binary_sensor_(enabled, this->fault_inverter_voltage_too_low_); + value_faults_present |= enabled.value_or(false); + break; + case 8: + this->publish_binary_sensor_(enabled, this->fault_inverter_voltage_too_high_); + value_faults_present |= enabled.value_or(false); + break; + case 9: + this->publish_binary_sensor_(enabled, this->warning_over_temperature_); + value_warnings_present |= enabled.value_or(false); + break; + case 10: + this->publish_binary_sensor_(enabled, this->warning_fan_lock_); + value_warnings_present |= enabled.value_or(false); + break; + case 11: + this->publish_binary_sensor_(enabled, this->warning_battery_voltage_high_); + value_warnings_present |= enabled.value_or(false); + break; + case 12: + this->publish_binary_sensor_(enabled, this->warning_battery_low_alarm_); + value_warnings_present |= enabled.value_or(false); + break; + case 14: + this->publish_binary_sensor_(enabled, this->warning_battery_under_shutdown_); + value_warnings_present |= enabled.value_or(false); + break; + case 15: + this->publish_binary_sensor_(enabled, this->warning_battery_derating_); + value_warnings_present |= enabled.value_or(false); + break; + case 16: + this->publish_binary_sensor_(enabled, this->warning_over_load_); + value_warnings_present |= enabled.value_or(false); + break; + case 17: + this->publish_binary_sensor_(enabled, this->warning_eeprom_failed_); + value_warnings_present |= enabled.value_or(false); + break; + case 18: + this->publish_binary_sensor_(enabled, this->fault_inverter_over_current_); + value_faults_present |= enabled.value_or(false); + break; + case 19: + this->publish_binary_sensor_(enabled, this->fault_inverter_soft_failed_); + value_faults_present |= enabled.value_or(false); + break; + case 20: + this->publish_binary_sensor_(enabled, this->fault_self_test_failed_); + value_faults_present |= enabled.value_or(false); + break; + case 21: + this->publish_binary_sensor_(enabled, this->fault_op_dc_voltage_over_); + value_faults_present |= enabled.value_or(false); + break; + case 22: + this->publish_binary_sensor_(enabled, this->fault_battery_open_); + value_faults_present |= enabled.value_or(false); + break; + case 23: + this->publish_binary_sensor_(enabled, this->fault_current_sensor_failed_); + value_faults_present |= enabled.value_or(false); + break; + case 24: + this->publish_binary_sensor_(enabled, this->fault_battery_short_); + value_faults_present |= enabled.value_or(false); + break; + case 25: + this->publish_binary_sensor_(enabled, this->warning_power_limit_); + value_warnings_present |= enabled.value_or(false); + break; + case 26: + this->publish_binary_sensor_(enabled, this->warning_pv_voltage_high_); + value_warnings_present |= enabled.value_or(false); + break; + case 27: + this->publish_binary_sensor_(enabled, this->fault_mppt_overload_); + value_faults_present |= enabled.value_or(false); + break; + case 28: + this->publish_binary_sensor_(enabled, this->warning_mppt_overload_); + value_warnings_present |= enabled.value_or(false); + break; + case 29: + this->publish_binary_sensor_(enabled, this->warning_battery_too_low_to_charge_); + value_warnings_present |= enabled.value_or(false); + break; + case 30: + this->publish_binary_sensor_(enabled, this->fault_dc_dc_over_current_); + value_faults_present |= enabled.value_or(false); + break; + case 33: + this->publish_binary_sensor_(enabled, this->warning_low_pv_energy_); + value_warnings_present |= enabled.value_or(false); + break; + case 34: + this->publish_binary_sensor_(enabled, this->warning_high_ac_input_during_bus_soft_start_); + value_warnings_present |= enabled.value_or(false); + case 35: + this->publish_binary_sensor_(enabled, this->warning_battery_equalization_); + value_warnings_present |= enabled.value_or(false); + break; + } + } + + this->publish_binary_sensor_(value_warnings_present, this->warnings_present_); + this->publish_binary_sensor_(value_faults_present, this->faults_present_); + + if (this->fault_code_) { + if (flags.length() < 33) { + this->fault_code_->publish_state(NAN); + } else { + std::string fc(flags, 31, 2); + this->fault_code_->publish_state(parse_number(fc).value_or(NAN)); + } + } +} + +void Pipsolar::handle_qt_(const char *message) { + if (this->last_qt_) { + this->last_qt_->publish_state(message); + } +} + +void Pipsolar::handle_qmn_(const char *message) { + if (this->last_qmn_) { + this->last_qmn_->publish_state(message); + } +} + +void Pipsolar::skip_start_(const char *message, size_t *pos) { + if (message[*pos] == '(') { + (*pos)++; + } +} +void Pipsolar::skip_field_(const char *message, size_t *pos) { + // find delimiter or end of string + while (message[*pos] != '\0' && message[*pos] != ' ') { + (*pos)++; + } + if (message[*pos] != '\0') { + // skip delimiter after this field if there is one + (*pos)++; + } +} +std::string Pipsolar::read_field_(const char *message, size_t *pos) { + size_t begin = *pos; + // find delimiter or end of string + while (message[*pos] != '\0' && message[*pos] != ' ') { + (*pos)++; + } + if (*pos == begin) { + return ""; + } + + std::string field(message, begin, *pos - begin); + + if (message[*pos] != '\0') { + // skip delimiter after this field if there is one + (*pos)++; + } + + return field; +} + +void Pipsolar::read_float_sensor_(const char *message, size_t *pos, sensor::Sensor *sensor) { + if (sensor != nullptr) { + std::string field = this->read_field_(message, pos); + sensor->publish_state(parse_number(field).value_or(NAN)); + } else { + this->skip_field_(message, pos); + } +} +void Pipsolar::read_int_sensor_(const char *message, size_t *pos, sensor::Sensor *sensor) { + if (sensor != nullptr) { + std::string field = this->read_field_(message, pos); + esphome::optional parsed = parse_number(field); + sensor->publish_state(parsed.has_value() ? parsed.value() : NAN); + } else { + this->skip_field_(message, pos); + } +} + +void Pipsolar::publish_binary_sensor_(esphome::optional b, binary_sensor::BinarySensor *sensor) { + if (sensor) { + if (b.has_value()) { + sensor->publish_state(b.value()); + } else { + sensor->invalidate_state(); + } + } +} + +esphome::optional Pipsolar::get_bit_(std::string bits, uint8_t bit_pos) { + if (bit_pos >= bits.length()) { + return {}; + } + return bits[bit_pos] == '1'; +} + +void Pipsolar::dump_config() { + ESP_LOGCONFIG(TAG, "Pipsolar:\n" + "enabled polling commands:"); + for (auto &enabled_polling_command : this->enabled_polling_commands_) { + if (enabled_polling_command.length != 0) { + ESP_LOGCONFIG(TAG, "%s", enabled_polling_command.command); + } + } +} +void Pipsolar::update() { + for (auto &enabled_polling_command : this->enabled_polling_commands_) { + if (enabled_polling_command.length != 0) { + enabled_polling_command.needs_update = true; } } } -void Pipsolar::update() {} void Pipsolar::add_polling_command_(const char *command, ENUMPollingCommand polling_command) { - for (auto &used_polling_command : this->used_polling_commands_) { - if (used_polling_command.length == strlen(command)) { + for (auto &enabled_polling_command : this->enabled_polling_commands_) { + if (enabled_polling_command.length == strlen(command)) { uint8_t len = strlen(command); - if (memcmp(used_polling_command.command, command, len) == 0) { + if (memcmp(enabled_polling_command.command, command, len) == 0) { return; } } - if (used_polling_command.length == 0) { - size_t length = strlen(command) + 1; - const char *beg = command; - const char *end = command + length; - used_polling_command.command = new uint8_t[length]; // NOLINT(cppcoreguidelines-owning-memory) - size_t i = 0; - for (; beg != end; ++beg, ++i) { - used_polling_command.command[i] = (uint8_t) (*beg); + if (enabled_polling_command.length == 0) { + size_t length = strlen(command); + + enabled_polling_command.command = new uint8_t[length + 1]; // NOLINT(cppcoreguidelines-owning-memory) + for (size_t i = 0; i < length + 1; i++) { + enabled_polling_command.command[i] = (uint8_t) command[i]; } - used_polling_command.errors = 0; - used_polling_command.identifier = polling_command; - used_polling_command.length = length - 1; + enabled_polling_command.errors = 0; + enabled_polling_command.identifier = polling_command; + enabled_polling_command.length = length; + enabled_polling_command.needs_update = true; return; } } diff --git a/esphome/components/pipsolar/pipsolar.h b/esphome/components/pipsolar/pipsolar.h index 373911b2d7..beae67a4e0 100644 --- a/esphome/components/pipsolar/pipsolar.h +++ b/esphome/components/pipsolar/pipsolar.h @@ -7,6 +7,7 @@ #include "esphome/components/uart/uart.h" #include "esphome/core/automation.h" #include "esphome/core/component.h" +#include "esphome/core/helpers.h" namespace esphome { namespace pipsolar { @@ -25,12 +26,20 @@ struct PollingCommand { uint8_t length = 0; uint8_t errors; ENUMPollingCommand identifier; + bool needs_update; }; -#define PIPSOLAR_VALUED_ENTITY_(type, name, polling_command, value_type) \ - protected: \ - value_type value_##name##_; \ - PIPSOLAR_ENTITY_(type, name, polling_command) +struct QFLAGValues { + esphome::optional silence_buzzer_open_buzzer; + esphome::optional overload_bypass_function; + esphome::optional lcd_escape_to_default; + esphome::optional overload_restart_function; + esphome::optional over_temperature_restart_function; + esphome::optional backlight_on; + esphome::optional alarm_on_when_primary_source_interrupt; + esphome::optional fault_code_record; + esphome::optional power_saving; +}; #define PIPSOLAR_ENTITY_(type, name, polling_command) \ protected: \ @@ -42,126 +51,123 @@ struct PollingCommand { this->add_polling_command_(#polling_command, POLLING_##polling_command); \ } -#define PIPSOLAR_SENSOR(name, polling_command, value_type) \ - PIPSOLAR_VALUED_ENTITY_(sensor::Sensor, name, polling_command, value_type) +#define PIPSOLAR_SENSOR(name, polling_command) PIPSOLAR_ENTITY_(sensor::Sensor, name, polling_command) #define PIPSOLAR_SWITCH(name, polling_command) PIPSOLAR_ENTITY_(switch_::Switch, name, polling_command) -#define PIPSOLAR_BINARY_SENSOR(name, polling_command, value_type) \ - PIPSOLAR_VALUED_ENTITY_(binary_sensor::BinarySensor, name, polling_command, value_type) -#define PIPSOLAR_VALUED_TEXT_SENSOR(name, polling_command, value_type) \ - PIPSOLAR_VALUED_ENTITY_(text_sensor::TextSensor, name, polling_command, value_type) +#define PIPSOLAR_BINARY_SENSOR(name, polling_command) \ + PIPSOLAR_ENTITY_(binary_sensor::BinarySensor, name, polling_command) #define PIPSOLAR_TEXT_SENSOR(name, polling_command) PIPSOLAR_ENTITY_(text_sensor::TextSensor, name, polling_command) class Pipsolar : public uart::UARTDevice, public PollingComponent { // QPIGS values - PIPSOLAR_SENSOR(grid_voltage, QPIGS, float) - PIPSOLAR_SENSOR(grid_frequency, QPIGS, float) - PIPSOLAR_SENSOR(ac_output_voltage, QPIGS, float) - PIPSOLAR_SENSOR(ac_output_frequency, QPIGS, float) - PIPSOLAR_SENSOR(ac_output_apparent_power, QPIGS, int) - PIPSOLAR_SENSOR(ac_output_active_power, QPIGS, int) - PIPSOLAR_SENSOR(output_load_percent, QPIGS, int) - PIPSOLAR_SENSOR(bus_voltage, QPIGS, int) - PIPSOLAR_SENSOR(battery_voltage, QPIGS, float) - PIPSOLAR_SENSOR(battery_charging_current, QPIGS, int) - PIPSOLAR_SENSOR(battery_capacity_percent, QPIGS, int) - PIPSOLAR_SENSOR(inverter_heat_sink_temperature, QPIGS, int) - PIPSOLAR_SENSOR(pv_input_current_for_battery, QPIGS, float) - PIPSOLAR_SENSOR(pv_input_voltage, QPIGS, float) - PIPSOLAR_SENSOR(battery_voltage_scc, QPIGS, float) - PIPSOLAR_SENSOR(battery_discharge_current, QPIGS, int) - PIPSOLAR_BINARY_SENSOR(add_sbu_priority_version, QPIGS, int) - PIPSOLAR_BINARY_SENSOR(configuration_status, QPIGS, int) - PIPSOLAR_BINARY_SENSOR(scc_firmware_version, QPIGS, int) - PIPSOLAR_BINARY_SENSOR(load_status, QPIGS, int) - PIPSOLAR_BINARY_SENSOR(battery_voltage_to_steady_while_charging, QPIGS, int) - PIPSOLAR_BINARY_SENSOR(charging_status, QPIGS, int) - PIPSOLAR_BINARY_SENSOR(scc_charging_status, QPIGS, int) - PIPSOLAR_BINARY_SENSOR(ac_charging_status, QPIGS, int) - PIPSOLAR_SENSOR(battery_voltage_offset_for_fans_on, QPIGS, int) //.1 scale - PIPSOLAR_SENSOR(eeprom_version, QPIGS, int) - PIPSOLAR_SENSOR(pv_charging_power, QPIGS, int) - PIPSOLAR_BINARY_SENSOR(charging_to_floating_mode, QPIGS, int) - PIPSOLAR_BINARY_SENSOR(switch_on, QPIGS, int) - PIPSOLAR_BINARY_SENSOR(dustproof_installed, QPIGS, int) + PIPSOLAR_SENSOR(grid_voltage, QPIGS) + PIPSOLAR_SENSOR(grid_frequency, QPIGS) + PIPSOLAR_SENSOR(ac_output_voltage, QPIGS) + PIPSOLAR_SENSOR(ac_output_frequency, QPIGS) + PIPSOLAR_SENSOR(ac_output_apparent_power, QPIGS) + PIPSOLAR_SENSOR(ac_output_active_power, QPIGS) + PIPSOLAR_SENSOR(output_load_percent, QPIGS) + PIPSOLAR_SENSOR(bus_voltage, QPIGS) + PIPSOLAR_SENSOR(battery_voltage, QPIGS) + PIPSOLAR_SENSOR(battery_charging_current, QPIGS) + PIPSOLAR_SENSOR(battery_capacity_percent, QPIGS) + PIPSOLAR_SENSOR(inverter_heat_sink_temperature, QPIGS) + PIPSOLAR_SENSOR(pv_input_current_for_battery, QPIGS) + PIPSOLAR_SENSOR(pv_input_voltage, QPIGS) + PIPSOLAR_SENSOR(battery_voltage_scc, QPIGS) + PIPSOLAR_SENSOR(battery_discharge_current, QPIGS) + PIPSOLAR_BINARY_SENSOR(add_sbu_priority_version, QPIGS) + PIPSOLAR_BINARY_SENSOR(configuration_status, QPIGS) + PIPSOLAR_BINARY_SENSOR(scc_firmware_version, QPIGS) + PIPSOLAR_BINARY_SENSOR(load_status, QPIGS) + PIPSOLAR_BINARY_SENSOR(battery_voltage_to_steady_while_charging, QPIGS) + PIPSOLAR_BINARY_SENSOR(charging_status, QPIGS) + PIPSOLAR_BINARY_SENSOR(scc_charging_status, QPIGS) + PIPSOLAR_BINARY_SENSOR(ac_charging_status, QPIGS) + PIPSOLAR_SENSOR(battery_voltage_offset_for_fans_on, QPIGS) //.1 scale + PIPSOLAR_SENSOR(eeprom_version, QPIGS) + PIPSOLAR_SENSOR(pv_charging_power, QPIGS) + PIPSOLAR_BINARY_SENSOR(charging_to_floating_mode, QPIGS) + PIPSOLAR_BINARY_SENSOR(switch_on, QPIGS) + PIPSOLAR_BINARY_SENSOR(dustproof_installed, QPIGS) // QPIRI values - PIPSOLAR_SENSOR(grid_rating_voltage, QPIRI, float) - PIPSOLAR_SENSOR(grid_rating_current, QPIRI, float) - PIPSOLAR_SENSOR(ac_output_rating_voltage, QPIRI, float) - PIPSOLAR_SENSOR(ac_output_rating_frequency, QPIRI, float) - PIPSOLAR_SENSOR(ac_output_rating_current, QPIRI, float) - PIPSOLAR_SENSOR(ac_output_rating_apparent_power, QPIRI, int) - PIPSOLAR_SENSOR(ac_output_rating_active_power, QPIRI, int) - PIPSOLAR_SENSOR(battery_rating_voltage, QPIRI, float) - PIPSOLAR_SENSOR(battery_recharge_voltage, QPIRI, float) - PIPSOLAR_SENSOR(battery_under_voltage, QPIRI, float) - PIPSOLAR_SENSOR(battery_bulk_voltage, QPIRI, float) - PIPSOLAR_SENSOR(battery_float_voltage, QPIRI, float) - PIPSOLAR_SENSOR(battery_type, QPIRI, int) - PIPSOLAR_SENSOR(current_max_ac_charging_current, QPIRI, int) - PIPSOLAR_SENSOR(current_max_charging_current, QPIRI, int) - PIPSOLAR_SENSOR(input_voltage_range, QPIRI, int) - PIPSOLAR_SENSOR(output_source_priority, QPIRI, int) - PIPSOLAR_SENSOR(charger_source_priority, QPIRI, int) - PIPSOLAR_SENSOR(parallel_max_num, QPIRI, int) - PIPSOLAR_SENSOR(machine_type, QPIRI, int) - PIPSOLAR_SENSOR(topology, QPIRI, int) - PIPSOLAR_SENSOR(output_mode, QPIRI, int) - PIPSOLAR_SENSOR(battery_redischarge_voltage, QPIRI, float) - PIPSOLAR_SENSOR(pv_ok_condition_for_parallel, QPIRI, int) - PIPSOLAR_SENSOR(pv_power_balance, QPIRI, int) + PIPSOLAR_SENSOR(grid_rating_voltage, QPIRI) + PIPSOLAR_SENSOR(grid_rating_current, QPIRI) + PIPSOLAR_SENSOR(ac_output_rating_voltage, QPIRI) + PIPSOLAR_SENSOR(ac_output_rating_frequency, QPIRI) + PIPSOLAR_SENSOR(ac_output_rating_current, QPIRI) + PIPSOLAR_SENSOR(ac_output_rating_apparent_power, QPIRI) + PIPSOLAR_SENSOR(ac_output_rating_active_power, QPIRI) + PIPSOLAR_SENSOR(battery_rating_voltage, QPIRI) + PIPSOLAR_SENSOR(battery_recharge_voltage, QPIRI) + PIPSOLAR_SENSOR(battery_under_voltage, QPIRI) + PIPSOLAR_SENSOR(battery_bulk_voltage, QPIRI) + PIPSOLAR_SENSOR(battery_float_voltage, QPIRI) + PIPSOLAR_SENSOR(battery_type, QPIRI) + PIPSOLAR_SENSOR(current_max_ac_charging_current, QPIRI) + PIPSOLAR_SENSOR(current_max_charging_current, QPIRI) + PIPSOLAR_SENSOR(input_voltage_range, QPIRI) + PIPSOLAR_SENSOR(output_source_priority, QPIRI) + PIPSOLAR_SENSOR(charger_source_priority, QPIRI) + PIPSOLAR_SENSOR(parallel_max_num, QPIRI) + PIPSOLAR_SENSOR(machine_type, QPIRI) + PIPSOLAR_SENSOR(topology, QPIRI) + PIPSOLAR_SENSOR(output_mode, QPIRI) + PIPSOLAR_SENSOR(battery_redischarge_voltage, QPIRI) + PIPSOLAR_SENSOR(pv_ok_condition_for_parallel, QPIRI) + PIPSOLAR_SENSOR(pv_power_balance, QPIRI) // QMOD values - PIPSOLAR_VALUED_TEXT_SENSOR(device_mode, QMOD, char) + PIPSOLAR_TEXT_SENSOR(device_mode, QMOD) // QFLAG values - PIPSOLAR_BINARY_SENSOR(silence_buzzer_open_buzzer, QFLAG, int) - PIPSOLAR_BINARY_SENSOR(overload_bypass_function, QFLAG, int) - PIPSOLAR_BINARY_SENSOR(lcd_escape_to_default, QFLAG, int) - PIPSOLAR_BINARY_SENSOR(overload_restart_function, QFLAG, int) - PIPSOLAR_BINARY_SENSOR(over_temperature_restart_function, QFLAG, int) - PIPSOLAR_BINARY_SENSOR(backlight_on, QFLAG, int) - PIPSOLAR_BINARY_SENSOR(alarm_on_when_primary_source_interrupt, QFLAG, int) - PIPSOLAR_BINARY_SENSOR(fault_code_record, QFLAG, int) - PIPSOLAR_BINARY_SENSOR(power_saving, QFLAG, int) + PIPSOLAR_BINARY_SENSOR(silence_buzzer_open_buzzer, QFLAG) + PIPSOLAR_BINARY_SENSOR(overload_bypass_function, QFLAG) + PIPSOLAR_BINARY_SENSOR(lcd_escape_to_default, QFLAG) + PIPSOLAR_BINARY_SENSOR(overload_restart_function, QFLAG) + PIPSOLAR_BINARY_SENSOR(over_temperature_restart_function, QFLAG) + PIPSOLAR_BINARY_SENSOR(backlight_on, QFLAG) + PIPSOLAR_BINARY_SENSOR(alarm_on_when_primary_source_interrupt, QFLAG) + PIPSOLAR_BINARY_SENSOR(fault_code_record, QFLAG) + PIPSOLAR_BINARY_SENSOR(power_saving, QFLAG) // QPIWS values - PIPSOLAR_BINARY_SENSOR(warnings_present, QPIWS, bool) - PIPSOLAR_BINARY_SENSOR(faults_present, QPIWS, bool) - PIPSOLAR_BINARY_SENSOR(warning_power_loss, QPIWS, bool) - PIPSOLAR_BINARY_SENSOR(fault_inverter_fault, QPIWS, bool) - PIPSOLAR_BINARY_SENSOR(fault_bus_over, QPIWS, bool) - PIPSOLAR_BINARY_SENSOR(fault_bus_under, QPIWS, bool) - PIPSOLAR_BINARY_SENSOR(fault_bus_soft_fail, QPIWS, bool) - PIPSOLAR_BINARY_SENSOR(warning_line_fail, QPIWS, bool) - PIPSOLAR_BINARY_SENSOR(fault_opvshort, QPIWS, bool) - PIPSOLAR_BINARY_SENSOR(fault_inverter_voltage_too_low, QPIWS, bool) - PIPSOLAR_BINARY_SENSOR(fault_inverter_voltage_too_high, QPIWS, bool) - PIPSOLAR_BINARY_SENSOR(warning_over_temperature, QPIWS, bool) - PIPSOLAR_BINARY_SENSOR(warning_fan_lock, QPIWS, bool) - PIPSOLAR_BINARY_SENSOR(warning_battery_voltage_high, QPIWS, bool) - PIPSOLAR_BINARY_SENSOR(warning_battery_low_alarm, QPIWS, bool) - PIPSOLAR_BINARY_SENSOR(warning_battery_under_shutdown, QPIWS, bool) - PIPSOLAR_BINARY_SENSOR(warning_battery_derating, QPIWS, bool) - PIPSOLAR_BINARY_SENSOR(warning_over_load, QPIWS, bool) - PIPSOLAR_BINARY_SENSOR(warning_eeprom_failed, QPIWS, bool) - PIPSOLAR_BINARY_SENSOR(fault_inverter_over_current, QPIWS, bool) - PIPSOLAR_BINARY_SENSOR(fault_inverter_soft_failed, QPIWS, bool) - PIPSOLAR_BINARY_SENSOR(fault_self_test_failed, QPIWS, bool) - PIPSOLAR_BINARY_SENSOR(fault_op_dc_voltage_over, QPIWS, bool) - PIPSOLAR_BINARY_SENSOR(fault_battery_open, QPIWS, bool) - PIPSOLAR_BINARY_SENSOR(fault_current_sensor_failed, QPIWS, bool) - PIPSOLAR_BINARY_SENSOR(fault_battery_short, QPIWS, bool) - PIPSOLAR_BINARY_SENSOR(warning_power_limit, QPIWS, bool) - PIPSOLAR_BINARY_SENSOR(warning_pv_voltage_high, QPIWS, bool) - PIPSOLAR_BINARY_SENSOR(fault_mppt_overload, QPIWS, bool) - PIPSOLAR_BINARY_SENSOR(warning_mppt_overload, QPIWS, bool) - PIPSOLAR_BINARY_SENSOR(warning_battery_too_low_to_charge, QPIWS, bool) - PIPSOLAR_BINARY_SENSOR(fault_dc_dc_over_current, QPIWS, bool) - PIPSOLAR_BINARY_SENSOR(fault_code, QPIWS, int) - PIPSOLAR_BINARY_SENSOR(warnung_low_pv_energy, QPIWS, bool) - PIPSOLAR_BINARY_SENSOR(warning_high_ac_input_during_bus_soft_start, QPIWS, bool) - PIPSOLAR_BINARY_SENSOR(warning_battery_equalization, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(warnings_present, QPIWS) + PIPSOLAR_BINARY_SENSOR(faults_present, QPIWS) + PIPSOLAR_BINARY_SENSOR(warning_power_loss, QPIWS) + PIPSOLAR_BINARY_SENSOR(fault_inverter_fault, QPIWS) + PIPSOLAR_BINARY_SENSOR(fault_bus_over, QPIWS) + PIPSOLAR_BINARY_SENSOR(fault_bus_under, QPIWS) + PIPSOLAR_BINARY_SENSOR(fault_bus_soft_fail, QPIWS) + PIPSOLAR_BINARY_SENSOR(warning_line_fail, QPIWS) + PIPSOLAR_BINARY_SENSOR(fault_opvshort, QPIWS) + PIPSOLAR_BINARY_SENSOR(fault_inverter_voltage_too_low, QPIWS) + PIPSOLAR_BINARY_SENSOR(fault_inverter_voltage_too_high, QPIWS) + PIPSOLAR_BINARY_SENSOR(warning_over_temperature, QPIWS) + PIPSOLAR_BINARY_SENSOR(warning_fan_lock, QPIWS) + PIPSOLAR_BINARY_SENSOR(warning_battery_voltage_high, QPIWS) + PIPSOLAR_BINARY_SENSOR(warning_battery_low_alarm, QPIWS) + PIPSOLAR_BINARY_SENSOR(warning_battery_under_shutdown, QPIWS) + PIPSOLAR_BINARY_SENSOR(warning_battery_derating, QPIWS) + PIPSOLAR_BINARY_SENSOR(warning_over_load, QPIWS) + PIPSOLAR_BINARY_SENSOR(warning_eeprom_failed, QPIWS) + PIPSOLAR_BINARY_SENSOR(fault_inverter_over_current, QPIWS) + PIPSOLAR_BINARY_SENSOR(fault_inverter_soft_failed, QPIWS) + PIPSOLAR_BINARY_SENSOR(fault_self_test_failed, QPIWS) + PIPSOLAR_BINARY_SENSOR(fault_op_dc_voltage_over, QPIWS) + PIPSOLAR_BINARY_SENSOR(fault_battery_open, QPIWS) + PIPSOLAR_BINARY_SENSOR(fault_current_sensor_failed, QPIWS) + PIPSOLAR_BINARY_SENSOR(fault_battery_short, QPIWS) + PIPSOLAR_BINARY_SENSOR(warning_power_limit, QPIWS) + PIPSOLAR_BINARY_SENSOR(warning_pv_voltage_high, QPIWS) + PIPSOLAR_BINARY_SENSOR(fault_mppt_overload, QPIWS) + PIPSOLAR_BINARY_SENSOR(warning_mppt_overload, QPIWS) + PIPSOLAR_BINARY_SENSOR(warning_battery_too_low_to_charge, QPIWS) + PIPSOLAR_BINARY_SENSOR(fault_dc_dc_over_current, QPIWS) + PIPSOLAR_BINARY_SENSOR(fault_code, QPIWS) + PIPSOLAR_BINARY_SENSOR(warning_low_pv_energy, QPIWS) + PIPSOLAR_BINARY_SENSOR(warning_high_ac_input_during_bus_soft_start, QPIWS) + PIPSOLAR_BINARY_SENSOR(warning_battery_equalization, QPIWS) PIPSOLAR_TEXT_SENSOR(last_qpigs, QPIGS) PIPSOLAR_TEXT_SENSOR(last_qpiri, QPIRI) @@ -179,25 +185,47 @@ class Pipsolar : public uart::UARTDevice, public PollingComponent { PIPSOLAR_SWITCH(pv_ok_condition_for_parallel_switch, QPIRI) PIPSOLAR_SWITCH(pv_power_balance_switch, QPIRI) - void switch_command(const std::string &command); + void queue_command(const std::string &command); void setup() override; void loop() override; void dump_config() override; void update() override; protected: - static const size_t PIPSOLAR_READ_BUFFER_LENGTH = 110; // maximum supported answer length + static const size_t PIPSOLAR_READ_BUFFER_LENGTH = 128; // maximum supported answer length static const size_t COMMAND_QUEUE_LENGTH = 10; static const size_t COMMAND_TIMEOUT = 5000; - uint32_t last_poll_ = 0; + static const size_t POLLING_COMMANDS_MAX = 15; void add_polling_command_(const char *command, ENUMPollingCommand polling_command); void empty_uart_buffer_(); uint8_t check_incoming_crc_(); uint8_t check_incoming_length_(uint8_t length); uint16_t pipsolar_crc_(uint8_t *msg, uint8_t len); - uint8_t send_next_command_(); - void send_next_poll_(); - void queue_command_(const char *command, uint8_t length); + bool send_next_command_(); + bool send_next_poll_(); + + void handle_poll_response_(ENUMPollingCommand polling_command, const char *message); + void handle_poll_error_(ENUMPollingCommand polling_command); + // these handlers are designed in a way that an empty message sets all sensors to unknown + void handle_qpiri_(const char *message); + void handle_qpigs_(const char *message); + void handle_qmod_(const char *message); + void handle_qflag_(const char *message); + void handle_qpiws_(const char *message); + void handle_qt_(const char *message); + void handle_qmn_(const char *message); + + void skip_start_(const char *message, size_t *pos); + void skip_field_(const char *message, size_t *pos); + std::string read_field_(const char *message, size_t *pos); + + void read_float_sensor_(const char *message, size_t *pos, sensor::Sensor *sensor); + void read_int_sensor_(const char *message, size_t *pos, sensor::Sensor *sensor); + + void publish_binary_sensor_(esphome::optional b, binary_sensor::BinarySensor *sensor); + + esphome::optional get_bit_(std::string bits, uint8_t bit_pos); + std::string command_queue_[COMMAND_QUEUE_LENGTH]; uint8_t command_queue_position_ = 0; uint8_t read_buffer_[PIPSOLAR_READ_BUFFER_LENGTH]; @@ -212,11 +240,10 @@ class Pipsolar : public uart::UARTDevice, public PollingComponent { STATE_POLL_COMPLETE = 3, STATE_COMMAND_COMPLETE = 4, STATE_POLL_CHECKED = 5, - STATE_POLL_DECODED = 6, }; uint8_t last_polling_command_ = 0; - PollingCommand used_polling_commands_[15]; + PollingCommand enabled_polling_commands_[POLLING_COMMANDS_MAX]; }; } // namespace pipsolar diff --git a/esphome/components/pipsolar/sensor/__init__.py b/esphome/components/pipsolar/sensor/__init__.py index 929865b480..d08a877b55 100644 --- a/esphome/components/pipsolar/sensor/__init__.py +++ b/esphome/components/pipsolar/sensor/__init__.py @@ -4,11 +4,18 @@ import esphome.config_validation as cv from esphome.const import ( CONF_BATTERY_VOLTAGE, CONF_BUS_VOLTAGE, + DEVICE_CLASS_APPARENT_POWER, + DEVICE_CLASS_BATTERY, DEVICE_CLASS_CURRENT, + DEVICE_CLASS_FREQUENCY, DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, + ICON_BATTERY, ICON_CURRENT_AC, + ICON_FLASH, + ICON_GAUGE, + STATE_CLASS_MEASUREMENT, UNIT_AMPERE, UNIT_CELSIUS, UNIT_HERTZ, @@ -22,6 +29,10 @@ from .. import CONF_PIPSOLAR_ID, PIPSOLAR_COMPONENT_SCHEMA DEPENDENCIES = ["uart"] +ICON_SOLAR_POWER = "mdi:solar-power" +ICON_SOLAR_PANEL = "mdi:solar-panel" +ICON_CURRENT_DC = "mdi:current-dc" + # QPIRI sensors CONF_GRID_RATING_VOLTAGE = "grid_rating_voltage" CONF_GRID_RATING_CURRENT = "grid_rating_current" @@ -75,16 +86,19 @@ TYPES = { unit_of_measurement=UNIT_VOLT, accuracy_decimals=1, device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_GRID_RATING_CURRENT: sensor.sensor_schema( unit_of_measurement=UNIT_AMPERE, accuracy_decimals=1, device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_AC_OUTPUT_RATING_VOLTAGE: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, accuracy_decimals=1, device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_AC_OUTPUT_RATING_FREQUENCY: sensor.sensor_schema( unit_of_measurement=UNIT_HERTZ, @@ -98,11 +112,12 @@ TYPES = { ), CONF_AC_OUTPUT_RATING_APPARENT_POWER: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT_AMPS, - accuracy_decimals=1, + accuracy_decimals=0, + device_class=DEVICE_CLASS_APPARENT_POWER, ), CONF_AC_OUTPUT_RATING_ACTIVE_POWER: sensor.sensor_schema( unit_of_measurement=UNIT_WATT, - accuracy_decimals=1, + accuracy_decimals=0, device_class=DEVICE_CLASS_POWER, ), CONF_BATTERY_RATING_VOLTAGE: sensor.sensor_schema( @@ -131,124 +146,151 @@ TYPES = { device_class=DEVICE_CLASS_VOLTAGE, ), CONF_BATTERY_TYPE: sensor.sensor_schema( - accuracy_decimals=1, + accuracy_decimals=0, ), CONF_CURRENT_MAX_AC_CHARGING_CURRENT: sensor.sensor_schema( unit_of_measurement=UNIT_AMPERE, - accuracy_decimals=1, + accuracy_decimals=0, device_class=DEVICE_CLASS_CURRENT, ), CONF_CURRENT_MAX_CHARGING_CURRENT: sensor.sensor_schema( unit_of_measurement=UNIT_AMPERE, - accuracy_decimals=1, + accuracy_decimals=0, device_class=DEVICE_CLASS_CURRENT, ), CONF_INPUT_VOLTAGE_RANGE: sensor.sensor_schema( - accuracy_decimals=1, + accuracy_decimals=0, ), CONF_OUTPUT_SOURCE_PRIORITY: sensor.sensor_schema( - accuracy_decimals=1, + accuracy_decimals=0, ), CONF_CHARGER_SOURCE_PRIORITY: sensor.sensor_schema( - accuracy_decimals=1, + accuracy_decimals=0, ), CONF_PARALLEL_MAX_NUM: sensor.sensor_schema( - accuracy_decimals=1, + accuracy_decimals=0, ), CONF_MACHINE_TYPE: sensor.sensor_schema( - accuracy_decimals=1, + accuracy_decimals=0, ), CONF_TOPOLOGY: sensor.sensor_schema( - accuracy_decimals=1, + accuracy_decimals=0, ), CONF_OUTPUT_MODE: sensor.sensor_schema( - accuracy_decimals=1, + accuracy_decimals=0, ), CONF_BATTERY_REDISCHARGE_VOLTAGE: sensor.sensor_schema( accuracy_decimals=1, ), CONF_PV_OK_CONDITION_FOR_PARALLEL: sensor.sensor_schema( - accuracy_decimals=1, + accuracy_decimals=0, ), CONF_PV_POWER_BALANCE: sensor.sensor_schema( - accuracy_decimals=1, + accuracy_decimals=0, ), CONF_GRID_VOLTAGE: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, accuracy_decimals=1, device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_GRID_FREQUENCY: sensor.sensor_schema( unit_of_measurement=UNIT_HERTZ, icon=ICON_CURRENT_AC, accuracy_decimals=1, + device_class=DEVICE_CLASS_FREQUENCY, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_AC_OUTPUT_VOLTAGE: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, accuracy_decimals=1, device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_AC_OUTPUT_FREQUENCY: sensor.sensor_schema( unit_of_measurement=UNIT_HERTZ, icon=ICON_CURRENT_AC, accuracy_decimals=1, + device_class=DEVICE_CLASS_FREQUENCY, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_AC_OUTPUT_APPARENT_POWER: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT_AMPS, - accuracy_decimals=1, + accuracy_decimals=0, + device_class=DEVICE_CLASS_APPARENT_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_AC_OUTPUT_ACTIVE_POWER: sensor.sensor_schema( unit_of_measurement=UNIT_WATT, - accuracy_decimals=1, + accuracy_decimals=0, device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_OUTPUT_LOAD_PERCENT: sensor.sensor_schema( unit_of_measurement=UNIT_PERCENT, - accuracy_decimals=1, + icon=ICON_GAUGE, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_BUS_VOLTAGE: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, - accuracy_decimals=1, + icon=ICON_FLASH, + accuracy_decimals=0, device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_BATTERY_VOLTAGE: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, - accuracy_decimals=1, + icon=ICON_BATTERY, + accuracy_decimals=2, device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_BATTERY_CHARGING_CURRENT: sensor.sensor_schema( unit_of_measurement=UNIT_AMPERE, - accuracy_decimals=1, + icon=ICON_CURRENT_DC, + accuracy_decimals=0, device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_BATTERY_CAPACITY_PERCENT: sensor.sensor_schema( unit_of_measurement=UNIT_PERCENT, - accuracy_decimals=1, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_INVERTER_HEAT_SINK_TEMPERATURE: sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, - accuracy_decimals=1, + accuracy_decimals=0, device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_PV_INPUT_CURRENT_FOR_BATTERY: sensor.sensor_schema( unit_of_measurement=UNIT_AMPERE, + icon=ICON_SOLAR_PANEL, accuracy_decimals=1, device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_PV_INPUT_VOLTAGE: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, + icon=ICON_SOLAR_PANEL, accuracy_decimals=1, device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_BATTERY_VOLTAGE_SCC: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, - accuracy_decimals=1, + accuracy_decimals=2, device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_BATTERY_DISCHARGE_CURRENT: sensor.sensor_schema( unit_of_measurement=UNIT_AMPERE, - accuracy_decimals=1, + icon=ICON_CURRENT_DC, + accuracy_decimals=0, device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_BATTERY_VOLTAGE_OFFSET_FOR_FANS_ON: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, @@ -256,12 +298,14 @@ TYPES = { device_class=DEVICE_CLASS_VOLTAGE, ), CONF_EEPROM_VERSION: sensor.sensor_schema( - accuracy_decimals=1, + accuracy_decimals=0, ), CONF_PV_CHARGING_POWER: sensor.sensor_schema( unit_of_measurement=UNIT_WATT, - accuracy_decimals=1, + icon=ICON_SOLAR_POWER, + accuracy_decimals=0, device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), } diff --git a/esphome/components/pipsolar/switch/pipsolar_switch.cpp b/esphome/components/pipsolar/switch/pipsolar_switch.cpp index be7763226b..649d951618 100644 --- a/esphome/components/pipsolar/switch/pipsolar_switch.cpp +++ b/esphome/components/pipsolar/switch/pipsolar_switch.cpp @@ -11,11 +11,11 @@ void PipsolarSwitch::dump_config() { LOG_SWITCH("", "Pipsolar Switch", this); } void PipsolarSwitch::write_state(bool state) { if (state) { if (!this->on_command_.empty()) { - this->parent_->switch_command(this->on_command_); + this->parent_->queue_command(this->on_command_); } } else { if (!this->off_command_.empty()) { - this->parent_->switch_command(this->off_command_); + this->parent_->queue_command(this->off_command_); } } } diff --git a/esphome/components/pmwcs3/pmwcs3.h b/esphome/components/pmwcs3/pmwcs3.h index d60f9d1f61..d63c516586 100644 --- a/esphome/components/pmwcs3/pmwcs3.h +++ b/esphome/components/pmwcs3/pmwcs3.h @@ -38,7 +38,7 @@ template class PMWCS3AirCalibrationAction : public Action public: PMWCS3AirCalibrationAction(PMWCS3Component *parent) : parent_(parent) {} - void play(Ts... x) override { this->parent_->air_calibration(); } + void play(const Ts &...x) override { this->parent_->air_calibration(); } protected: PMWCS3Component *parent_; @@ -48,7 +48,7 @@ template class PMWCS3WaterCalibrationAction : public Actionparent_->water_calibration(); } + void play(const Ts &...x) override { this->parent_->water_calibration(); } protected: PMWCS3Component *parent_; @@ -59,7 +59,7 @@ template class PMWCS3NewI2cAddressAction : public Action PMWCS3NewI2cAddressAction(PMWCS3Component *parent) : parent_(parent) {} TEMPLATABLE_VALUE(int, new_address) - void play(Ts... x) override { this->parent_->new_i2c_address(this->new_address_.value(x...)); } + void play(const Ts &...x) override { this->parent_->new_i2c_address(this->new_address_.value(x...)); } protected: PMWCS3Component *parent_; diff --git a/esphome/components/pn532/pn532.h b/esphome/components/pn532/pn532.h index c8e9a40008..eeb15648fb 100644 --- a/esphome/components/pn532/pn532.h +++ b/esphome/components/pn532/pn532.h @@ -143,7 +143,7 @@ class PN532OnFinishedWriteTrigger : public Trigger<> { template class PN532IsWritingCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->is_writing(); } + bool check(const Ts &...x) override { return this->parent_->is_writing(); } }; } // namespace pn532 diff --git a/esphome/components/pn7150/automation.h b/esphome/components/pn7150/automation.h index aebb1b7573..21329a998a 100644 --- a/esphome/components/pn7150/automation.h +++ b/esphome/components/pn7150/automation.h @@ -23,42 +23,42 @@ class PN7150OnFinishedWriteTrigger : public Trigger<> { template class PN7150IsWritingCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->is_writing(); } + bool check(const Ts &...x) override { return this->parent_->is_writing(); } }; template class EmulationOffAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->set_tag_emulation_off(); } + void play(const Ts &...x) override { this->parent_->set_tag_emulation_off(); } }; template class EmulationOnAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->set_tag_emulation_on(); } + void play(const Ts &...x) override { this->parent_->set_tag_emulation_on(); } }; template class PollingOffAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->set_polling_off(); } + void play(const Ts &...x) override { this->parent_->set_polling_off(); } }; template class PollingOnAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->set_polling_on(); } + void play(const Ts &...x) override { this->parent_->set_polling_on(); } }; template class SetCleanModeAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->clean_mode(); } + void play(const Ts &...x) override { this->parent_->clean_mode(); } }; template class SetFormatModeAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->format_mode(); } + void play(const Ts &...x) override { this->parent_->format_mode(); } }; template class SetReadModeAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->read_mode(); } + void play(const Ts &...x) override { this->parent_->read_mode(); } }; template class SetEmulationMessageAction : public Action, public Parented { TEMPLATABLE_VALUE(std::string, message) TEMPLATABLE_VALUE(bool, include_android_app_record) - void play(Ts... x) override { + void play(const Ts &...x) override { this->parent_->set_tag_emulation_message(this->message_.optional_value(x...), this->include_android_app_record_.optional_value(x...)); } @@ -68,14 +68,14 @@ template class SetWriteMessageAction : public Action, pub TEMPLATABLE_VALUE(std::string, message) TEMPLATABLE_VALUE(bool, include_android_app_record) - void play(Ts... x) override { + void play(const Ts &...x) override { this->parent_->set_tag_write_message(this->message_.optional_value(x...), this->include_android_app_record_.optional_value(x...)); } }; template class SetWriteModeAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->write_mode(); } + void play(const Ts &...x) override { this->parent_->write_mode(); } }; } // namespace pn7150 diff --git a/esphome/components/pn7160/automation.h b/esphome/components/pn7160/automation.h index 854fb11684..08148c2311 100644 --- a/esphome/components/pn7160/automation.h +++ b/esphome/components/pn7160/automation.h @@ -23,42 +23,42 @@ class PN7160OnFinishedWriteTrigger : public Trigger<> { template class PN7160IsWritingCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->is_writing(); } + bool check(const Ts &...x) override { return this->parent_->is_writing(); } }; template class EmulationOffAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->set_tag_emulation_off(); } + void play(const Ts &...x) override { this->parent_->set_tag_emulation_off(); } }; template class EmulationOnAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->set_tag_emulation_on(); } + void play(const Ts &...x) override { this->parent_->set_tag_emulation_on(); } }; template class PollingOffAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->set_polling_off(); } + void play(const Ts &...x) override { this->parent_->set_polling_off(); } }; template class PollingOnAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->set_polling_on(); } + void play(const Ts &...x) override { this->parent_->set_polling_on(); } }; template class SetCleanModeAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->clean_mode(); } + void play(const Ts &...x) override { this->parent_->clean_mode(); } }; template class SetFormatModeAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->format_mode(); } + void play(const Ts &...x) override { this->parent_->format_mode(); } }; template class SetReadModeAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->read_mode(); } + void play(const Ts &...x) override { this->parent_->read_mode(); } }; template class SetEmulationMessageAction : public Action, public Parented { TEMPLATABLE_VALUE(std::string, message) TEMPLATABLE_VALUE(bool, include_android_app_record) - void play(Ts... x) override { + void play(const Ts &...x) override { this->parent_->set_tag_emulation_message(this->message_.optional_value(x...), this->include_android_app_record_.optional_value(x...)); } @@ -68,14 +68,14 @@ template class SetWriteMessageAction : public Action, pub TEMPLATABLE_VALUE(std::string, message) TEMPLATABLE_VALUE(bool, include_android_app_record) - void play(Ts... x) override { + void play(const Ts &...x) override { this->parent_->set_tag_write_message(this->message_.optional_value(x...), this->include_android_app_record_.optional_value(x...)); } }; template class SetWriteModeAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->write_mode(); } + void play(const Ts &...x) override { this->parent_->write_mode(); } }; } // namespace pn7160 diff --git a/esphome/components/prometheus/prometheus_handler.cpp b/esphome/components/prometheus/prometheus_handler.cpp index 2677860c7c..252b477400 100644 --- a/esphome/components/prometheus/prometheus_handler.cpp +++ b/esphome/components/prometheus/prometheus_handler.cpp @@ -53,6 +53,18 @@ void PrometheusHandler::handleRequest(AsyncWebServerRequest *req) { this->lock_row_(stream, obj, area, node, friendly_name); #endif +#ifdef USE_EVENT + this->event_type_(stream); + for (auto *obj : App.get_events()) + this->event_row_(stream, obj, area, node, friendly_name); +#endif + +#ifdef USE_TEXT + this->text_type_(stream); + for (auto *obj : App.get_texts()) + this->text_row_(stream, obj, area, node, friendly_name); +#endif + #ifdef USE_TEXT_SENSOR this->text_sensor_type_(stream); for (auto *obj : App.get_text_sensors()) @@ -110,30 +122,48 @@ std::string PrometheusHandler::relabel_name_(EntityBase *obj) { void PrometheusHandler::add_area_label_(AsyncResponseStream *stream, std::string &area) { if (!area.empty()) { - stream->print(F("\",area=\"")); + stream->print(ESPHOME_F("\",area=\"")); stream->print(area.c_str()); } } void PrometheusHandler::add_node_label_(AsyncResponseStream *stream, std::string &node) { if (!node.empty()) { - stream->print(F("\",node=\"")); + stream->print(ESPHOME_F("\",node=\"")); stream->print(node.c_str()); } } void PrometheusHandler::add_friendly_name_label_(AsyncResponseStream *stream, std::string &friendly_name) { if (!friendly_name.empty()) { - stream->print(F("\",friendly_name=\"")); + stream->print(ESPHOME_F("\",friendly_name=\"")); stream->print(friendly_name.c_str()); } } +#ifdef USE_ESP8266 +void PrometheusHandler::print_metric_labels_(AsyncResponseStream *stream, const __FlashStringHelper *metric_name, + EntityBase *obj, std::string &area, std::string &node, + std::string &friendly_name) { +#else +void PrometheusHandler::print_metric_labels_(AsyncResponseStream *stream, const char *metric_name, EntityBase *obj, + std::string &area, std::string &node, std::string &friendly_name) { +#endif + stream->print(metric_name); + stream->print(ESPHOME_F("{id=\"")); + stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); + stream->print(ESPHOME_F("\",name=\"")); + stream->print(relabel_name_(obj).c_str()); +} + // Type-specific implementation #ifdef USE_SENSOR void PrometheusHandler::sensor_type_(AsyncResponseStream *stream) { - stream->print(F("#TYPE esphome_sensor_value gauge\n")); - stream->print(F("#TYPE esphome_sensor_failed gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_sensor_value gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_sensor_failed gauge\n")); } void PrometheusHandler::sensor_row_(AsyncResponseStream *stream, sensor::Sensor *obj, std::string &area, std::string &node, std::string &friendly_name) { @@ -141,37 +171,37 @@ void PrometheusHandler::sensor_row_(AsyncResponseStream *stream, sensor::Sensor return; if (!std::isnan(obj->state)) { // We have a valid value, output this value - stream->print(F("esphome_sensor_failed{id=\"")); + stream->print(ESPHOME_F("esphome_sensor_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 0\n")); + stream->print(ESPHOME_F("\"} 0\n")); // Data itself - stream->print(F("esphome_sensor_value{id=\"")); + stream->print(ESPHOME_F("esphome_sensor_value{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\",unit=\"")); - stream->print(obj->get_unit_of_measurement().c_str()); - stream->print(F("\"} ")); + stream->print(ESPHOME_F("\",unit=\"")); + stream->print(obj->get_unit_of_measurement_ref().c_str()); + stream->print(ESPHOME_F("\"} ")); stream->print(value_accuracy_to_string(obj->state, obj->get_accuracy_decimals()).c_str()); - stream->print(F("\n")); + stream->print(ESPHOME_F("\n")); } else { // Invalid state - stream->print(F("esphome_sensor_failed{id=\"")); + stream->print(ESPHOME_F("esphome_sensor_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 1\n")); + stream->print(ESPHOME_F("\"} 1\n")); } } #endif @@ -179,8 +209,8 @@ void PrometheusHandler::sensor_row_(AsyncResponseStream *stream, sensor::Sensor // Type-specific implementation #ifdef USE_BINARY_SENSOR void PrometheusHandler::binary_sensor_type_(AsyncResponseStream *stream) { - stream->print(F("#TYPE esphome_binary_sensor_value gauge\n")); - stream->print(F("#TYPE esphome_binary_sensor_failed gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_binary_sensor_value gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_binary_sensor_failed gauge\n")); } void PrometheusHandler::binary_sensor_row_(AsyncResponseStream *stream, binary_sensor::BinarySensor *obj, std::string &area, std::string &node, std::string &friendly_name) { @@ -188,204 +218,165 @@ void PrometheusHandler::binary_sensor_row_(AsyncResponseStream *stream, binary_s return; if (obj->has_state()) { // We have a valid value, output this value - stream->print(F("esphome_binary_sensor_failed{id=\"")); + stream->print(ESPHOME_F("esphome_binary_sensor_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 0\n")); + stream->print(ESPHOME_F("\"} 0\n")); // Data itself - stream->print(F("esphome_binary_sensor_value{id=\"")); + stream->print(ESPHOME_F("esphome_binary_sensor_value{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} ")); + stream->print(ESPHOME_F("\"} ")); stream->print(obj->state); - stream->print(F("\n")); + stream->print(ESPHOME_F("\n")); } else { // Invalid state - stream->print(F("esphome_binary_sensor_failed{id=\"")); + stream->print(ESPHOME_F("esphome_binary_sensor_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 1\n")); + stream->print(ESPHOME_F("\"} 1\n")); } } #endif #ifdef USE_FAN void PrometheusHandler::fan_type_(AsyncResponseStream *stream) { - stream->print(F("#TYPE esphome_fan_value gauge\n")); - stream->print(F("#TYPE esphome_fan_failed gauge\n")); - stream->print(F("#TYPE esphome_fan_speed gauge\n")); - stream->print(F("#TYPE esphome_fan_oscillation gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_fan_value gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_fan_failed gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_fan_speed gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_fan_oscillation gauge\n")); } void PrometheusHandler::fan_row_(AsyncResponseStream *stream, fan::Fan *obj, std::string &area, std::string &node, std::string &friendly_name) { if (obj->is_internal() && !this->include_internal_) return; - stream->print(F("esphome_fan_failed{id=\"")); + stream->print(ESPHOME_F("esphome_fan_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 0\n")); + stream->print(ESPHOME_F("\"} 0\n")); // Data itself - stream->print(F("esphome_fan_value{id=\"")); + stream->print(ESPHOME_F("esphome_fan_value{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} ")); + stream->print(ESPHOME_F("\"} ")); stream->print(obj->state); - stream->print(F("\n")); + stream->print(ESPHOME_F("\n")); // Speed if available if (obj->get_traits().supports_speed()) { - stream->print(F("esphome_fan_speed{id=\"")); + stream->print(ESPHOME_F("esphome_fan_speed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} ")); + stream->print(ESPHOME_F("\"} ")); stream->print(obj->speed); - stream->print(F("\n")); + stream->print(ESPHOME_F("\n")); } // Oscillation if available if (obj->get_traits().supports_oscillation()) { - stream->print(F("esphome_fan_oscillation{id=\"")); + stream->print(ESPHOME_F("esphome_fan_oscillation{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} ")); + stream->print(ESPHOME_F("\"} ")); stream->print(obj->oscillating); - stream->print(F("\n")); + stream->print(ESPHOME_F("\n")); } } #endif #ifdef USE_LIGHT void PrometheusHandler::light_type_(AsyncResponseStream *stream) { - stream->print(F("#TYPE esphome_light_state gauge\n")); - stream->print(F("#TYPE esphome_light_color gauge\n")); - stream->print(F("#TYPE esphome_light_effect_active gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_light_state gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_light_color gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_light_effect_active gauge\n")); } void PrometheusHandler::light_row_(AsyncResponseStream *stream, light::LightState *obj, std::string &area, std::string &node, std::string &friendly_name) { if (obj->is_internal() && !this->include_internal_) return; // State - stream->print(F("esphome_light_state{id=\"")); - stream->print(relabel_id_(obj).c_str()); - add_area_label_(stream, area); - add_node_label_(stream, node); - add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); - stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} ")); + print_metric_labels_(stream, ESPHOME_F("esphome_light_state"), obj, area, node, friendly_name); + stream->print(ESPHOME_F("\"} ")); stream->print(obj->remote_values.is_on()); - stream->print(F("\n")); + stream->print(ESPHOME_F("\n")); // Brightness and RGBW light::LightColorValues color = obj->current_values; float brightness, r, g, b, w; color.as_brightness(&brightness); color.as_rgbw(&r, &g, &b, &w); - stream->print(F("esphome_light_color{id=\"")); - stream->print(relabel_id_(obj).c_str()); - add_area_label_(stream, area); - add_node_label_(stream, node); - add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); - stream->print(relabel_name_(obj).c_str()); - stream->print(F("\",channel=\"brightness\"} ")); - stream->print(brightness); - stream->print(F("\n")); - stream->print(F("esphome_light_color{id=\"")); - stream->print(relabel_id_(obj).c_str()); - add_area_label_(stream, area); - add_node_label_(stream, node); - add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); - stream->print(relabel_name_(obj).c_str()); - stream->print(F("\",channel=\"r\"} ")); - stream->print(r); - stream->print(F("\n")); - stream->print(F("esphome_light_color{id=\"")); - stream->print(relabel_id_(obj).c_str()); - add_area_label_(stream, area); - add_node_label_(stream, node); - add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); - stream->print(relabel_name_(obj).c_str()); - stream->print(F("\",channel=\"g\"} ")); - stream->print(g); - stream->print(F("\n")); - stream->print(F("esphome_light_color{id=\"")); - stream->print(relabel_id_(obj).c_str()); - add_area_label_(stream, area); - add_node_label_(stream, node); - add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); - stream->print(relabel_name_(obj).c_str()); - stream->print(F("\",channel=\"b\"} ")); - stream->print(b); - stream->print(F("\n")); - stream->print(F("esphome_light_color{id=\"")); - stream->print(relabel_id_(obj).c_str()); - add_area_label_(stream, area); - add_node_label_(stream, node); - add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); - stream->print(relabel_name_(obj).c_str()); - stream->print(F("\",channel=\"w\"} ")); - stream->print(w); - stream->print(F("\n")); - // Effect - std::string effect = obj->get_effect_name(); - if (effect == "None") { - stream->print(F("esphome_light_effect_active{id=\"")); - stream->print(relabel_id_(obj).c_str()); - add_area_label_(stream, area); - add_node_label_(stream, node); - add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); - stream->print(relabel_name_(obj).c_str()); - stream->print(F("\",effect=\"None\"} 0\n")); - } else { - stream->print(F("esphome_light_effect_active{id=\"")); - stream->print(relabel_id_(obj).c_str()); - add_area_label_(stream, area); - add_node_label_(stream, node); - add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); - stream->print(relabel_name_(obj).c_str()); - stream->print(F("\",effect=\"")); - stream->print(effect.c_str()); - stream->print(F("\"} 1\n")); + if (obj->get_traits().supports_color_capability(light::ColorCapability::BRIGHTNESS)) { + print_metric_labels_(stream, ESPHOME_F("esphome_light_color"), obj, area, node, friendly_name); + stream->print(ESPHOME_F("\",channel=\"brightness\"} ")); + stream->print(brightness); + stream->print(ESPHOME_F("\n")); + } + if (obj->get_traits().supports_color_capability(light::ColorCapability::RGB)) { + print_metric_labels_(stream, ESPHOME_F("esphome_light_color"), obj, area, node, friendly_name); + stream->print(ESPHOME_F("\",channel=\"r\"} ")); + stream->print(r); + stream->print(ESPHOME_F("\n")); + print_metric_labels_(stream, ESPHOME_F("esphome_light_color"), obj, area, node, friendly_name); + stream->print(ESPHOME_F("\",channel=\"g\"} ")); + stream->print(g); + stream->print(ESPHOME_F("\n")); + print_metric_labels_(stream, ESPHOME_F("esphome_light_color"), obj, area, node, friendly_name); + stream->print(ESPHOME_F("\",channel=\"b\"} ")); + stream->print(b); + stream->print(ESPHOME_F("\n")); + } + if (obj->get_traits().supports_color_capability(light::ColorCapability::WHITE)) { + print_metric_labels_(stream, ESPHOME_F("esphome_light_color"), obj, area, node, friendly_name); + stream->print(ESPHOME_F("\",channel=\"w\"} ")); + stream->print(w); + stream->print(ESPHOME_F("\n")); + } + // Skip effect metrics if light has no effects + if (!obj->get_effects().empty()) { + // Effect + std::string effect = obj->get_effect_name(); + print_metric_labels_(stream, ESPHOME_F("esphome_light_effect_active"), obj, area, node, friendly_name); + stream->print(ESPHOME_F("\",effect=\"")); + // Only vary based on effect + if (effect == "None") { + stream->print(ESPHOME_F("None\"} 0\n")); + } else { + stream->print(effect.c_str()); + stream->print(ESPHOME_F("\"} 1\n")); + } } } #endif #ifdef USE_COVER void PrometheusHandler::cover_type_(AsyncResponseStream *stream) { - stream->print(F("#TYPE esphome_cover_value gauge\n")); - stream->print(F("#TYPE esphome_cover_failed gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_cover_value gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_cover_failed gauge\n")); } void PrometheusHandler::cover_row_(AsyncResponseStream *stream, cover::Cover *obj, std::string &area, std::string &node, std::string &friendly_name) { @@ -393,118 +384,118 @@ void PrometheusHandler::cover_row_(AsyncResponseStream *stream, cover::Cover *ob return; if (!std::isnan(obj->position)) { // We have a valid value, output this value - stream->print(F("esphome_cover_failed{id=\"")); + stream->print(ESPHOME_F("esphome_cover_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 0\n")); + stream->print(ESPHOME_F("\"} 0\n")); // Data itself - stream->print(F("esphome_cover_value{id=\"")); + stream->print(ESPHOME_F("esphome_cover_value{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} ")); + stream->print(ESPHOME_F("\"} ")); stream->print(obj->position); - stream->print(F("\n")); + stream->print(ESPHOME_F("\n")); if (obj->get_traits().get_supports_tilt()) { - stream->print(F("esphome_cover_tilt{id=\"")); + stream->print(ESPHOME_F("esphome_cover_tilt{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} ")); + stream->print(ESPHOME_F("\"} ")); stream->print(obj->tilt); - stream->print(F("\n")); + stream->print(ESPHOME_F("\n")); } } else { // Invalid state - stream->print(F("esphome_cover_failed{id=\"")); + stream->print(ESPHOME_F("esphome_cover_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 1\n")); + stream->print(ESPHOME_F("\"} 1\n")); } } #endif #ifdef USE_SWITCH void PrometheusHandler::switch_type_(AsyncResponseStream *stream) { - stream->print(F("#TYPE esphome_switch_value gauge\n")); - stream->print(F("#TYPE esphome_switch_failed gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_switch_value gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_switch_failed gauge\n")); } void PrometheusHandler::switch_row_(AsyncResponseStream *stream, switch_::Switch *obj, std::string &area, std::string &node, std::string &friendly_name) { if (obj->is_internal() && !this->include_internal_) return; - stream->print(F("esphome_switch_failed{id=\"")); + stream->print(ESPHOME_F("esphome_switch_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 0\n")); + stream->print(ESPHOME_F("\"} 0\n")); // Data itself - stream->print(F("esphome_switch_value{id=\"")); + stream->print(ESPHOME_F("esphome_switch_value{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} ")); + stream->print(ESPHOME_F("\"} ")); stream->print(obj->state); - stream->print(F("\n")); + stream->print(ESPHOME_F("\n")); } #endif #ifdef USE_LOCK void PrometheusHandler::lock_type_(AsyncResponseStream *stream) { - stream->print(F("#TYPE esphome_lock_value gauge\n")); - stream->print(F("#TYPE esphome_lock_failed gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_lock_value gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_lock_failed gauge\n")); } void PrometheusHandler::lock_row_(AsyncResponseStream *stream, lock::Lock *obj, std::string &area, std::string &node, std::string &friendly_name) { if (obj->is_internal() && !this->include_internal_) return; - stream->print(F("esphome_lock_failed{id=\"")); + stream->print(ESPHOME_F("esphome_lock_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 0\n")); + stream->print(ESPHOME_F("\"} 0\n")); // Data itself - stream->print(F("esphome_lock_value{id=\"")); + stream->print(ESPHOME_F("esphome_lock_value{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} ")); + stream->print(ESPHOME_F("\"} ")); stream->print(obj->state); - stream->print(F("\n")); + stream->print(ESPHOME_F("\n")); } #endif // Type-specific implementation #ifdef USE_TEXT_SENSOR void PrometheusHandler::text_sensor_type_(AsyncResponseStream *stream) { - stream->print(F("#TYPE esphome_text_sensor_value gauge\n")); - stream->print(F("#TYPE esphome_text_sensor_failed gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_text_sensor_value gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_text_sensor_failed gauge\n")); } void PrometheusHandler::text_sensor_row_(AsyncResponseStream *stream, text_sensor::TextSensor *obj, std::string &area, std::string &node, std::string &friendly_name) { @@ -512,37 +503,131 @@ void PrometheusHandler::text_sensor_row_(AsyncResponseStream *stream, text_senso return; if (obj->has_state()) { // We have a valid value, output this value - stream->print(F("esphome_text_sensor_failed{id=\"")); + stream->print(ESPHOME_F("esphome_text_sensor_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 0\n")); + stream->print(ESPHOME_F("\"} 0\n")); // Data itself - stream->print(F("esphome_text_sensor_value{id=\"")); + stream->print(ESPHOME_F("esphome_text_sensor_value{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\",value=\"")); + stream->print(ESPHOME_F("\",value=\"")); stream->print(obj->state.c_str()); - stream->print(F("\"} ")); - stream->print(F("1.0")); - stream->print(F("\n")); + stream->print(ESPHOME_F("\"} ")); + stream->print(ESPHOME_F("1.0")); + stream->print(ESPHOME_F("\n")); } else { // Invalid state - stream->print(F("esphome_text_sensor_failed{id=\"")); + stream->print(ESPHOME_F("esphome_text_sensor_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 1\n")); + stream->print(ESPHOME_F("\"} 1\n")); + } +} +#endif + +// Type-specific implementation +#ifdef USE_TEXT +void PrometheusHandler::text_type_(AsyncResponseStream *stream) { + stream->print(ESPHOME_F("#TYPE esphome_text_value gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_text_failed gauge\n")); +} +void PrometheusHandler::text_row_(AsyncResponseStream *stream, text::Text *obj, std::string &area, std::string &node, + std::string &friendly_name) { + if (obj->is_internal() && !this->include_internal_) + return; + if (obj->has_state()) { + // We have a valid value, output this value + stream->print(ESPHOME_F("esphome_text_failed{id=\"")); + stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); + stream->print(ESPHOME_F("\",name=\"")); + stream->print(relabel_name_(obj).c_str()); + stream->print(ESPHOME_F("\"} 0\n")); + // Data itself + stream->print(ESPHOME_F("esphome_text_value{id=\"")); + stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); + stream->print(ESPHOME_F("\",name=\"")); + stream->print(relabel_name_(obj).c_str()); + stream->print(ESPHOME_F("\",value=\"")); + stream->print(obj->state.c_str()); + stream->print(ESPHOME_F("\"} ")); + stream->print(ESPHOME_F("1.0")); + stream->print(ESPHOME_F("\n")); + } else { + // Invalid state + stream->print(ESPHOME_F("esphome_text_failed{id=\"")); + stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); + stream->print(ESPHOME_F("\",name=\"")); + stream->print(relabel_name_(obj).c_str()); + stream->print(ESPHOME_F("\"} 1\n")); + } +} +#endif + +// Type-specific implementation +#ifdef USE_EVENT +void PrometheusHandler::event_type_(AsyncResponseStream *stream) { + stream->print(ESPHOME_F("#TYPE esphome_event_value gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_event_failed gauge\n")); +} +void PrometheusHandler::event_row_(AsyncResponseStream *stream, event::Event *obj, std::string &area, std::string &node, + std::string &friendly_name) { + if (obj->is_internal() && !this->include_internal_) + return; + if (obj->get_last_event_type() != nullptr) { + // We have a valid event type, output this value + stream->print(ESPHOME_F("esphome_event_failed{id=\"")); + stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); + stream->print(ESPHOME_F("\",name=\"")); + stream->print(relabel_name_(obj).c_str()); + stream->print(ESPHOME_F("\"} 0\n")); + // Data itself + stream->print(ESPHOME_F("esphome_event_value{id=\"")); + stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); + stream->print(ESPHOME_F("\",name=\"")); + stream->print(relabel_name_(obj).c_str()); + stream->print(ESPHOME_F("\",last_event_type=\"")); + stream->print(obj->get_last_event_type()); + stream->print(ESPHOME_F("\"} ")); + stream->print(ESPHOME_F("1.0")); + stream->print(ESPHOME_F("\n")); + } else { + // No event triggered yet + stream->print(ESPHOME_F("esphome_event_failed{id=\"")); + stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); + stream->print(ESPHOME_F("\",name=\"")); + stream->print(relabel_name_(obj).c_str()); + stream->print(ESPHOME_F("\"} 1\n")); } } #endif @@ -550,8 +635,8 @@ void PrometheusHandler::text_sensor_row_(AsyncResponseStream *stream, text_senso // Type-specific implementation #ifdef USE_NUMBER void PrometheusHandler::number_type_(AsyncResponseStream *stream) { - stream->print(F("#TYPE esphome_number_value gauge\n")); - stream->print(F("#TYPE esphome_number_failed gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_number_value gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_number_failed gauge\n")); } void PrometheusHandler::number_row_(AsyncResponseStream *stream, number::Number *obj, std::string &area, std::string &node, std::string &friendly_name) { @@ -559,43 +644,43 @@ void PrometheusHandler::number_row_(AsyncResponseStream *stream, number::Number return; if (!std::isnan(obj->state)) { // We have a valid value, output this value - stream->print(F("esphome_number_failed{id=\"")); + stream->print(ESPHOME_F("esphome_number_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 0\n")); + stream->print(ESPHOME_F("\"} 0\n")); // Data itself - stream->print(F("esphome_number_value{id=\"")); + stream->print(ESPHOME_F("esphome_number_value{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} ")); + stream->print(ESPHOME_F("\"} ")); stream->print(obj->state); - stream->print(F("\n")); + stream->print(ESPHOME_F("\n")); } else { // Invalid state - stream->print(F("esphome_number_failed{id=\"")); + stream->print(ESPHOME_F("esphome_number_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 1\n")); + stream->print(ESPHOME_F("\"} 1\n")); } } #endif #ifdef USE_SELECT void PrometheusHandler::select_type_(AsyncResponseStream *stream) { - stream->print(F("#TYPE esphome_select_value gauge\n")); - stream->print(F("#TYPE esphome_select_failed gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_select_value gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_select_failed gauge\n")); } void PrometheusHandler::select_row_(AsyncResponseStream *stream, select::Select *obj, std::string &area, std::string &node, std::string &friendly_name) { @@ -603,105 +688,105 @@ void PrometheusHandler::select_row_(AsyncResponseStream *stream, select::Select return; if (obj->has_state()) { // We have a valid value, output this value - stream->print(F("esphome_select_failed{id=\"")); + stream->print(ESPHOME_F("esphome_select_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 0\n")); + stream->print(ESPHOME_F("\"} 0\n")); // Data itself - stream->print(F("esphome_select_value{id=\"")); + stream->print(ESPHOME_F("esphome_select_value{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\",value=\"")); - stream->print(obj->state.c_str()); - stream->print(F("\"} ")); - stream->print(F("1.0")); - stream->print(F("\n")); + stream->print(ESPHOME_F("\",value=\"")); + stream->print(obj->current_option()); + stream->print(ESPHOME_F("\"} ")); + stream->print(ESPHOME_F("1.0")); + stream->print(ESPHOME_F("\n")); } else { // Invalid state - stream->print(F("esphome_select_failed{id=\"")); + stream->print(ESPHOME_F("esphome_select_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 1\n")); + stream->print(ESPHOME_F("\"} 1\n")); } } #endif #ifdef USE_MEDIA_PLAYER void PrometheusHandler::media_player_type_(AsyncResponseStream *stream) { - stream->print(F("#TYPE esphome_media_player_state_value gauge\n")); - stream->print(F("#TYPE esphome_media_player_volume gauge\n")); - stream->print(F("#TYPE esphome_media_player_is_muted gauge\n")); - stream->print(F("#TYPE esphome_media_player_failed gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_media_player_state_value gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_media_player_volume gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_media_player_is_muted gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_media_player_failed gauge\n")); } void PrometheusHandler::media_player_row_(AsyncResponseStream *stream, media_player::MediaPlayer *obj, std::string &area, std::string &node, std::string &friendly_name) { if (obj->is_internal() && !this->include_internal_) return; - stream->print(F("esphome_media_player_failed{id=\"")); + stream->print(ESPHOME_F("esphome_media_player_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 0\n")); + stream->print(ESPHOME_F("\"} 0\n")); // Data itself - stream->print(F("esphome_media_player_state_value{id=\"")); + stream->print(ESPHOME_F("esphome_media_player_state_value{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\",value=\"")); + stream->print(ESPHOME_F("\",value=\"")); stream->print(media_player::media_player_state_to_string(obj->state)); - stream->print(F("\"} ")); - stream->print(F("1.0")); - stream->print(F("\n")); - stream->print(F("esphome_media_player_volume{id=\"")); + stream->print(ESPHOME_F("\"} ")); + stream->print(ESPHOME_F("1.0")); + stream->print(ESPHOME_F("\n")); + stream->print(ESPHOME_F("esphome_media_player_volume{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} ")); + stream->print(ESPHOME_F("\"} ")); stream->print(obj->volume); - stream->print(F("\n")); - stream->print(F("esphome_media_player_is_muted{id=\"")); + stream->print(ESPHOME_F("\n")); + stream->print(ESPHOME_F("esphome_media_player_is_muted{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} ")); + stream->print(ESPHOME_F("\"} ")); if (obj->is_muted()) { - stream->print(F("1.0")); + stream->print(ESPHOME_F("1.0")); } else { - stream->print(F("0.0")); + stream->print(ESPHOME_F("0.0")); } - stream->print(F("\n")); + stream->print(ESPHOME_F("\n")); } #endif #ifdef USE_UPDATE void PrometheusHandler::update_entity_type_(AsyncResponseStream *stream) { - stream->print(F("#TYPE esphome_update_entity_state gauge\n")); - stream->print(F("#TYPE esphome_update_entity_info gauge\n")); - stream->print(F("#TYPE esphome_update_entity_failed gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_update_entity_state gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_update_entity_info gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_update_entity_failed gauge\n")); } void PrometheusHandler::handle_update_state_(AsyncResponseStream *stream, update::UpdateState state) { @@ -730,168 +815,168 @@ void PrometheusHandler::update_entity_row_(AsyncResponseStream *stream, update:: return; if (obj->has_state()) { // We have a valid value, output this value - stream->print(F("esphome_update_entity_failed{id=\"")); + stream->print(ESPHOME_F("esphome_update_entity_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 0\n")); + stream->print(ESPHOME_F("\"} 0\n")); // First update state - stream->print(F("esphome_update_entity_state{id=\"")); + stream->print(ESPHOME_F("esphome_update_entity_state{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\",value=\"")); + stream->print(ESPHOME_F("\",value=\"")); handle_update_state_(stream, obj->state); - stream->print(F("\"} ")); - stream->print(F("1.0")); - stream->print(F("\n")); + stream->print(ESPHOME_F("\"} ")); + stream->print(ESPHOME_F("1.0")); + stream->print(ESPHOME_F("\n")); // Next update info - stream->print(F("esphome_update_entity_info{id=\"")); + stream->print(ESPHOME_F("esphome_update_entity_info{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\",current_version=\"")); + stream->print(ESPHOME_F("\",current_version=\"")); stream->print(obj->update_info.current_version.c_str()); - stream->print(F("\",latest_version=\"")); + stream->print(ESPHOME_F("\",latest_version=\"")); stream->print(obj->update_info.latest_version.c_str()); - stream->print(F("\",title=\"")); + stream->print(ESPHOME_F("\",title=\"")); stream->print(obj->update_info.title.c_str()); - stream->print(F("\"} ")); - stream->print(F("1.0")); - stream->print(F("\n")); + stream->print(ESPHOME_F("\"} ")); + stream->print(ESPHOME_F("1.0")); + stream->print(ESPHOME_F("\n")); } else { // Invalid state - stream->print(F("esphome_update_entity_failed{id=\"")); + stream->print(ESPHOME_F("esphome_update_entity_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 1\n")); + stream->print(ESPHOME_F("\"} 1\n")); } } #endif #ifdef USE_VALVE void PrometheusHandler::valve_type_(AsyncResponseStream *stream) { - stream->print(F("#TYPE esphome_valve_operation gauge\n")); - stream->print(F("#TYPE esphome_valve_failed gauge\n")); - stream->print(F("#TYPE esphome_valve_position gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_valve_operation gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_valve_failed gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_valve_position gauge\n")); } void PrometheusHandler::valve_row_(AsyncResponseStream *stream, valve::Valve *obj, std::string &area, std::string &node, std::string &friendly_name) { if (obj->is_internal() && !this->include_internal_) return; - stream->print(F("esphome_valve_failed{id=\"")); + stream->print(ESPHOME_F("esphome_valve_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} 0\n")); + stream->print(ESPHOME_F("\"} 0\n")); // Data itself - stream->print(F("esphome_valve_operation{id=\"")); + stream->print(ESPHOME_F("esphome_valve_operation{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\",operation=\"")); + stream->print(ESPHOME_F("\",operation=\"")); stream->print(valve::valve_operation_to_str(obj->current_operation)); - stream->print(F("\"} ")); - stream->print(F("1.0")); - stream->print(F("\n")); + stream->print(ESPHOME_F("\"} ")); + stream->print(ESPHOME_F("1.0")); + stream->print(ESPHOME_F("\n")); // Now see if position is supported if (obj->get_traits().get_supports_position()) { - stream->print(F("esphome_valve_position{id=\"")); + stream->print(ESPHOME_F("esphome_valve_position{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\"} ")); + stream->print(ESPHOME_F("\"} ")); stream->print(obj->position); - stream->print(F("\n")); + stream->print(ESPHOME_F("\n")); } } #endif #ifdef USE_CLIMATE void PrometheusHandler::climate_type_(AsyncResponseStream *stream) { - stream->print(F("#TYPE esphome_climate_setting gauge\n")); - stream->print(F("#TYPE esphome_climate_value gauge\n")); - stream->print(F("#TYPE esphome_climate_failed gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_climate_setting gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_climate_value gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_climate_failed gauge\n")); } void PrometheusHandler::climate_setting_row_(AsyncResponseStream *stream, climate::Climate *obj, std::string &area, std::string &node, std::string &friendly_name, std::string &setting, const LogString *setting_value) { - stream->print(F("esphome_climate_setting{id=\"")); + stream->print(ESPHOME_F("esphome_climate_setting{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\",category=\"")); + stream->print(ESPHOME_F("\",category=\"")); stream->print(setting.c_str()); - stream->print(F("\",setting_value=\"")); + stream->print(ESPHOME_F("\",setting_value=\"")); stream->print(LOG_STR_ARG(setting_value)); - stream->print(F("\"} ")); - stream->print(F("1.0")); - stream->print(F("\n")); + stream->print(ESPHOME_F("\"} ")); + stream->print(ESPHOME_F("1.0")); + stream->print(ESPHOME_F("\n")); } void PrometheusHandler::climate_value_row_(AsyncResponseStream *stream, climate::Climate *obj, std::string &area, std::string &node, std::string &friendly_name, std::string &category, std::string &climate_value) { - stream->print(F("esphome_climate_value{id=\"")); + stream->print(ESPHOME_F("esphome_climate_value{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\",category=\"")); + stream->print(ESPHOME_F("\",category=\"")); stream->print(category.c_str()); - stream->print(F("\"} ")); + stream->print(ESPHOME_F("\"} ")); stream->print(climate_value.c_str()); - stream->print(F("\n")); + stream->print(ESPHOME_F("\n")); } void PrometheusHandler::climate_failed_row_(AsyncResponseStream *stream, climate::Climate *obj, std::string &area, std::string &node, std::string &friendly_name, std::string &category, bool is_failed_value) { - stream->print(F("esphome_climate_failed{id=\"")); + stream->print(ESPHOME_F("esphome_climate_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); add_node_label_(stream, node); add_friendly_name_label_(stream, friendly_name); - stream->print(F("\",name=\"")); + stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); - stream->print(F("\",category=\"")); + stream->print(ESPHOME_F("\",category=\"")); stream->print(category.c_str()); - stream->print(F("\"} ")); + stream->print(ESPHOME_F("\"} ")); if (is_failed_value) { - stream->print(F("1.0")); + stream->print(ESPHOME_F("1.0")); } else { - stream->print(F("0.0")); + stream->print(ESPHOME_F("0.0")); } - stream->print(F("\n")); + stream->print(ESPHOME_F("\n")); } void PrometheusHandler::climate_row_(AsyncResponseStream *stream, climate::Climate *obj, std::string &area, @@ -916,7 +1001,7 @@ void PrometheusHandler::climate_row_(AsyncResponseStream *stream, climate::Clima auto min_temp_value = value_accuracy_to_string(traits.get_visual_min_temperature(), target_accuracy); climate_value_row_(stream, obj, area, node, friendly_name, min_temp, min_temp_value); // now check optional traits - if (traits.get_supports_current_temperature()) { + if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) { std::string current_temp = "current_temperature"; if (std::isnan(obj->current_temperature)) { climate_failed_row_(stream, obj, area, node, friendly_name, current_temp, true); @@ -927,7 +1012,7 @@ void PrometheusHandler::climate_row_(AsyncResponseStream *stream, climate::Clima climate_failed_row_(stream, obj, area, node, friendly_name, current_temp, false); } } - if (traits.get_supports_current_humidity()) { + if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) { std::string current_humidity = "current_humidity"; if (std::isnan(obj->current_humidity)) { climate_failed_row_(stream, obj, area, node, friendly_name, current_humidity, true); @@ -938,7 +1023,7 @@ void PrometheusHandler::climate_row_(AsyncResponseStream *stream, climate::Clima climate_failed_row_(stream, obj, area, node, friendly_name, current_humidity, false); } } - if (traits.get_supports_target_humidity()) { + if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) { std::string target_humidity = "target_humidity"; if (std::isnan(obj->target_humidity)) { climate_failed_row_(stream, obj, area, node, friendly_name, target_humidity, true); @@ -949,7 +1034,8 @@ void PrometheusHandler::climate_row_(AsyncResponseStream *stream, climate::Clima climate_failed_row_(stream, obj, area, node, friendly_name, target_humidity, false); } } - if (traits.get_supports_two_point_target_temperature()) { + if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | + climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) { std::string target_temp_low = "target_temperature_low"; auto target_temp_low_value = value_accuracy_to_string(obj->target_temperature_low, target_accuracy); climate_value_row_(stream, obj, area, node, friendly_name, target_temp_low, target_temp_low_value); @@ -961,7 +1047,7 @@ void PrometheusHandler::climate_row_(AsyncResponseStream *stream, climate::Clima auto target_temp_value = value_accuracy_to_string(obj->target_temperature, target_accuracy); climate_value_row_(stream, obj, area, node, friendly_name, target_temp, target_temp_value); } - if (traits.get_supports_action()) { + if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) { std::string climate_trait_category = "action"; const auto *climate_trait_value = climate::climate_action_to_string(obj->action); climate_setting_row_(stream, obj, area, node, friendly_name, climate_trait_category, climate_trait_value); diff --git a/esphome/components/prometheus/prometheus_handler.h b/esphome/components/prometheus/prometheus_handler.h index c4598f44b0..24243c8c98 100644 --- a/esphome/components/prometheus/prometheus_handler.h +++ b/esphome/components/prometheus/prometheus_handler.h @@ -66,6 +66,14 @@ class PrometheusHandler : public AsyncWebHandler, public Component { void add_area_label_(AsyncResponseStream *stream, std::string &area); void add_node_label_(AsyncResponseStream *stream, std::string &node); void add_friendly_name_label_(AsyncResponseStream *stream, std::string &friendly_name); + /// Print metric name and common labels (id, area, node, friendly_name, name) +#ifdef USE_ESP8266 + void print_metric_labels_(AsyncResponseStream *stream, const __FlashStringHelper *metric_name, EntityBase *obj, + std::string &area, std::string &node, std::string &friendly_name); +#else + void print_metric_labels_(AsyncResponseStream *stream, const char *metric_name, EntityBase *obj, std::string &area, + std::string &node, std::string &friendly_name); +#endif #ifdef USE_SENSOR /// Return the type for prometheus @@ -123,6 +131,22 @@ class PrometheusHandler : public AsyncWebHandler, public Component { std::string &friendly_name); #endif +#ifdef USE_EVENT + /// Return the type for prometheus + void event_type_(AsyncResponseStream *stream); + /// Return the event values state as prometheus data point + void event_row_(AsyncResponseStream *stream, event::Event *obj, std::string &area, std::string &node, + std::string &friendly_name); +#endif + +#ifdef USE_TEXT + /// Return the type for prometheus + void text_type_(AsyncResponseStream *stream); + /// Return the text values state as prometheus data point + void text_row_(AsyncResponseStream *stream, text::Text *obj, std::string &area, std::string &node, + std::string &friendly_name); +#endif + #ifdef USE_TEXT_SENSOR /// Return the type for prometheus void text_sensor_type_(AsyncResponseStream *stream); diff --git a/esphome/components/psram/__init__.py b/esphome/components/psram/__init__.py index fd7e70a055..c50c599855 100644 --- a/esphome/components/psram/__init__.py +++ b/esphome/components/psram/__init__.py @@ -1,6 +1,8 @@ import logging +import textwrap import esphome.codegen as cg +from esphome.components.const import CONF_IGNORE_NOT_FOUND from esphome.components.esp32 import ( CONF_CPU_FREQUENCY, CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES, @@ -16,6 +18,7 @@ from esphome.components.esp32.const import ( import esphome.config_validation as cv from esphome.const import ( CONF_ADVANCED, + CONF_DISABLED, CONF_FRAMEWORK, CONF_ID, CONF_MODE, @@ -32,6 +35,9 @@ DOMAIN = "psram" DEPENDENCIES = [PLATFORM_ESP32] +# PSRAM availability tracking for cross-component coordination +KEY_PSRAM_GUARANTEED = "psram_guaranteed" + _LOGGER = logging.getLogger(__name__) psram_ns = cg.esphome_ns.namespace(DOMAIN) @@ -61,6 +67,30 @@ SPIRAM_SPEEDS = { } +def supported() -> bool: + if not CORE.is_esp32: + return False + variant = get_esp32_variant() + return variant in SPIRAM_MODES + + +def is_guaranteed() -> bool: + """Check if PSRAM is guaranteed to be available. + + Returns True when PSRAM is configured with both 'disabled: false' and + 'ignore_not_found: false', meaning the device will fail to boot if PSRAM + is not found. This ensures safe use of high buffer configurations that + depend on PSRAM. + + This function should be called during code generation (to_code phase) by + components that need to know PSRAM availability for configuration decisions. + + Returns: + bool: True if PSRAM is guaranteed, False otherwise + """ + return CORE.data.get(KEY_PSRAM_GUARANTEED, False) + + def validate_psram_mode(config): esp32_config = fv.full_config.get()[PLATFORM_ESP32] if config[CONF_SPEED] == "120MHZ": @@ -94,56 +124,85 @@ def get_config_schema(config): variant = get_esp32_variant() speeds = [f"{s}MHZ" for s in SPIRAM_SPEEDS.get(variant, [])] if not speeds: - return cv.Invalid("PSRAM is not supported on this chip") + raise cv.Invalid("PSRAM is not supported on this chip") modes = SPIRAM_MODES[variant] + if CONF_MODE not in config and len(modes) != 1: + raise ( + cv.Invalid( + textwrap.dedent( + f""" + {variant} requires PSRAM mode selection; one of {", ".join(modes)} + Selection of the wrong mode for the board will cause a runtime failure to initialise PSRAM + """ + ) + ) + ) return cv.Schema( { cv.GenerateID(): cv.declare_id(PsramComponent), cv.Optional(CONF_MODE, default=modes[0]): cv.one_of(*modes, lower=True), cv.Optional(CONF_ENABLE_ECC, default=False): cv.boolean, cv.Optional(CONF_SPEED, default=speeds[0]): cv.one_of(*speeds, upper=True), + cv.Optional(CONF_DISABLED, default=False): cv.boolean, + cv.Optional(CONF_IGNORE_NOT_FOUND, default=True): cv.boolean, } )(config) CONFIG_SCHEMA = get_config_schema -FINAL_VALIDATE_SCHEMA = validate_psram_mode + +def _store_psram_guaranteed(config): + """Store PSRAM guaranteed status in CORE.data for other components. + + PSRAM is "guaranteed" when it will fail if not found, ensuring safe use + of high buffer configurations in network/wifi components. + + Called during final validation to ensure the flag is available + before any to_code() functions run. + """ + psram_guaranteed = not config[CONF_DISABLED] and not config[CONF_IGNORE_NOT_FOUND] + CORE.data[KEY_PSRAM_GUARANTEED] = psram_guaranteed + return config + + +FINAL_VALIDATE_SCHEMA = cv.All(validate_psram_mode, _store_psram_guaranteed) async def to_code(config): + if config[CONF_DISABLED]: + return if CORE.using_arduino: cg.add_build_flag("-DBOARD_HAS_PSRAM") if config[CONF_MODE] == TYPE_OCTAL: cg.add_platformio_option("board_build.arduino.memory_type", "qio_opi") - if CORE.using_esp_idf: - add_idf_sdkconfig_option( - f"CONFIG_{get_esp32_variant().upper()}_SPIRAM_SUPPORT", True - ) - add_idf_sdkconfig_option("CONFIG_SOC_SPIRAM_SUPPORTED", True) - add_idf_sdkconfig_option("CONFIG_SPIRAM", True) - add_idf_sdkconfig_option("CONFIG_SPIRAM_USE", True) - add_idf_sdkconfig_option("CONFIG_SPIRAM_USE_CAPS_ALLOC", True) - add_idf_sdkconfig_option("CONFIG_SPIRAM_IGNORE_NOTFOUND", True) + add_idf_sdkconfig_option( + f"CONFIG_{get_esp32_variant().upper()}_SPIRAM_SUPPORT", True + ) + add_idf_sdkconfig_option("CONFIG_SOC_SPIRAM_SUPPORTED", True) + add_idf_sdkconfig_option("CONFIG_SPIRAM", True) + add_idf_sdkconfig_option("CONFIG_SPIRAM_USE", True) + add_idf_sdkconfig_option("CONFIG_SPIRAM_USE_CAPS_ALLOC", True) + add_idf_sdkconfig_option( + "CONFIG_SPIRAM_IGNORE_NOTFOUND", config[CONF_IGNORE_NOT_FOUND] + ) - add_idf_sdkconfig_option( - f"CONFIG_SPIRAM_MODE_{SDK_MODES[config[CONF_MODE]]}", True - ) + add_idf_sdkconfig_option(f"CONFIG_SPIRAM_MODE_{SDK_MODES[config[CONF_MODE]]}", True) - # Remove MHz suffix, convert to int - speed = int(config[CONF_SPEED][:-3]) - add_idf_sdkconfig_option(f"CONFIG_SPIRAM_SPEED_{speed}M", True) - add_idf_sdkconfig_option("CONFIG_SPIRAM_SPEED", speed) - if config[CONF_MODE] == TYPE_OCTAL and speed == 120: - add_idf_sdkconfig_option("CONFIG_ESPTOOLPY_FLASHFREQ_120M", True) - add_idf_sdkconfig_option("CONFIG_BOOTLOADER_FLASH_DC_AWARE", True) - if CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] >= cv.Version(5, 4, 0): - add_idf_sdkconfig_option( - "CONFIG_SPIRAM_TIMING_TUNING_POINT_VIA_TEMPERATURE_SENSOR", True - ) - if config[CONF_ENABLE_ECC]: - add_idf_sdkconfig_option("CONFIG_SPIRAM_ECC_ENABLE", True) + # Remove MHz suffix, convert to int + speed = int(config[CONF_SPEED][:-3]) + add_idf_sdkconfig_option(f"CONFIG_SPIRAM_SPEED_{speed}M", True) + add_idf_sdkconfig_option("CONFIG_SPIRAM_SPEED", speed) + if config[CONF_MODE] == TYPE_OCTAL and speed == 120: + add_idf_sdkconfig_option("CONFIG_ESPTOOLPY_FLASHFREQ_120M", True) + add_idf_sdkconfig_option("CONFIG_BOOTLOADER_FLASH_DC_AWARE", True) + if CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] >= cv.Version(5, 4, 0): + add_idf_sdkconfig_option( + "CONFIG_SPIRAM_TIMING_TUNING_POINT_VIA_TEMPERATURE_SENSOR", True + ) + if config[CONF_ENABLE_ECC]: + add_idf_sdkconfig_option("CONFIG_SPIRAM_ECC_ENABLE", True) cg.add_define("USE_PSRAM") diff --git a/esphome/components/pulse_counter/automation.h b/esphome/components/pulse_counter/automation.h index d749540a95..0c0dc2552d 100644 --- a/esphome/components/pulse_counter/automation.h +++ b/esphome/components/pulse_counter/automation.h @@ -14,7 +14,7 @@ template class SetTotalPulsesAction : public Action { TEMPLATABLE_VALUE(uint32_t, total_pulses) - void play(Ts... x) override { this->pulse_counter_->set_total_pulses(this->total_pulses_.value(x...)); } + void play(const Ts &...x) override { this->pulse_counter_->set_total_pulses(this->total_pulses_.value(x...)); } protected: PulseCounterSensor *pulse_counter_; diff --git a/esphome/components/pulse_meter/automation.h b/esphome/components/pulse_meter/automation.h index 3112ded680..bf0768b7af 100644 --- a/esphome/components/pulse_meter/automation.h +++ b/esphome/components/pulse_meter/automation.h @@ -14,7 +14,7 @@ template class SetTotalPulsesAction : public Action { TEMPLATABLE_VALUE(uint32_t, total_pulses) - void play(Ts... x) override { this->pulse_meter_->set_total_pulses(this->total_pulses_.value(x...)); } + void play(const Ts &...x) override { this->pulse_meter_->set_total_pulses(this->total_pulses_.value(x...)); } protected: PulseMeterSensor *pulse_meter_; diff --git a/esphome/components/pulse_width/pulse_width.cpp b/esphome/components/pulse_width/pulse_width.cpp index 8d66861049..d083d48b32 100644 --- a/esphome/components/pulse_width/pulse_width.cpp +++ b/esphome/components/pulse_width/pulse_width.cpp @@ -17,8 +17,8 @@ void IRAM_ATTR PulseWidthSensorStore::gpio_intr(PulseWidthSensorStore *arg) { } void PulseWidthSensor::dump_config() { - LOG_SENSOR("", "Pulse Width", this) - LOG_UPDATE_INTERVAL(this) + LOG_SENSOR("", "Pulse Width", this); + LOG_UPDATE_INTERVAL(this); LOG_PIN(" Pin: ", this->pin_); } void PulseWidthSensor::update() { diff --git a/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp b/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp index 4b6c11b332..8436633619 100644 --- a/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp +++ b/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp @@ -14,7 +14,7 @@ void PVVXDisplay::dump_config() { " Service UUID : %s\n" " Characteristic UUID : %s\n" " Auto clear : %s", - this->parent_->address_str().c_str(), this->service_uuid_.to_string().c_str(), + this->parent_->address_str(), this->service_uuid_.to_string().c_str(), this->char_uuid_.to_string().c_str(), YESNO(this->auto_clear_enabled_)); #ifdef USE_TIME ESP_LOGCONFIG(TAG, " Set time on connection: %s", YESNO(this->time_ != nullptr)); @@ -28,12 +28,12 @@ void PVVXDisplay::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t switch (event) { case ESP_GATTC_OPEN_EVT: if (param->open.status == ESP_GATT_OK) { - ESP_LOGV(TAG, "[%s] Connected successfully!", this->parent_->address_str().c_str()); + ESP_LOGV(TAG, "[%s] Connected successfully!", this->parent_->address_str()); this->delayed_disconnect_(); } break; case ESP_GATTC_DISCONNECT_EVT: - ESP_LOGV(TAG, "[%s] Disconnected", this->parent_->address_str().c_str()); + ESP_LOGV(TAG, "[%s] Disconnected", this->parent_->address_str()); this->connection_established_ = false; this->cancel_timeout("disconnect"); this->char_handle_ = 0; @@ -41,15 +41,37 @@ void PVVXDisplay::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t case ESP_GATTC_SEARCH_CMPL_EVT: { auto *chr = this->parent_->get_characteristic(this->service_uuid_, this->char_uuid_); if (chr == nullptr) { - ESP_LOGW(TAG, "[%s] Characteristic not found.", this->parent_->address_str().c_str()); + ESP_LOGW(TAG, "[%s] Characteristic not found.", this->parent_->address_str()); break; } this->connection_established_ = true; this->char_handle_ = chr->handle; -#ifdef USE_TIME - this->sync_time_(); -#endif - this->display(); + + // Attempt to write immediately + // For devices without security, this will work + // For devices with security that are already paired, this will work + // For devices that need pairing, the write will be retried after auth completes + this->sync_time_and_display_(); + break; + } + default: + break; + } +} + +void PVVXDisplay::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { + switch (event) { + case ESP_GAP_BLE_AUTH_CMPL_EVT: { + if (!this->parent_->check_addr(param->ble_security.auth_cmpl.bd_addr)) + return; + + if (param->ble_security.auth_cmpl.success) { + ESP_LOGD(TAG, "[%s] Authentication successful, performing writes.", this->parent_->address_str()); + // Now that pairing is complete, perform the pending writes + this->sync_time_and_display_(); + } else { + ESP_LOGW(TAG, "[%s] Authentication failed.", this->parent_->address_str()); + } break; } default: @@ -67,22 +89,20 @@ void PVVXDisplay::update() { void PVVXDisplay::display() { if (!this->parent_->enabled) { - ESP_LOGD(TAG, "[%s] BLE client not enabled. Init connection.", this->parent_->address_str().c_str()); + ESP_LOGD(TAG, "[%s] BLE client not enabled. Init connection.", this->parent_->address_str()); this->parent_->set_enabled(true); return; } if (!this->connection_established_) { - ESP_LOGW(TAG, "[%s] Not connected to BLE client. State update can not be written.", - this->parent_->address_str().c_str()); + ESP_LOGW(TAG, "[%s] Not connected to BLE client. State update can not be written.", this->parent_->address_str()); return; } if (!this->char_handle_) { - ESP_LOGW(TAG, "[%s] No ble handle to BLE client. State update can not be written.", - this->parent_->address_str().c_str()); + ESP_LOGW(TAG, "[%s] No ble handle to BLE client. State update can not be written.", this->parent_->address_str()); return; } ESP_LOGD(TAG, "[%s] Send to display: bignum %d, smallnum: %d, cfg: 0x%02x, validity period: %u.", - this->parent_->address_str().c_str(), this->bignum_, this->smallnum_, this->cfg_, this->validity_period_); + this->parent_->address_str(), this->bignum_, this->smallnum_, this->cfg_, this->validity_period_); uint8_t blk[8] = {}; blk[0] = 0x22; blk[1] = this->bignum_ & 0xff; @@ -106,16 +126,16 @@ void PVVXDisplay::setcfgbit_(uint8_t bit, bool value) { void PVVXDisplay::send_to_setup_char_(uint8_t *blk, size_t size) { if (!this->connection_established_) { - ESP_LOGW(TAG, "[%s] Not connected to BLE client.", this->parent_->address_str().c_str()); + ESP_LOGW(TAG, "[%s] Not connected to BLE client.", this->parent_->address_str()); return; } auto status = esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_, size, blk, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); if (status) { - ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); } else { - ESP_LOGV(TAG, "[%s] send %u bytes", this->parent_->address_str().c_str(), size); + ESP_LOGV(TAG, "[%s] send %u bytes", this->parent_->address_str(), size); this->delayed_disconnect_(); } } @@ -127,26 +147,33 @@ void PVVXDisplay::delayed_disconnect_() { this->set_timeout("disconnect", this->disconnect_delay_ms_, [this]() { this->parent_->set_enabled(false); }); } +void PVVXDisplay::sync_time_and_display_() { +#ifdef USE_TIME + this->sync_time_(); +#endif + this->display(); +} + #ifdef USE_TIME void PVVXDisplay::sync_time_() { if (this->time_ == nullptr) return; if (!this->connection_established_) { - ESP_LOGW(TAG, "[%s] Not connected to BLE client. Time can not be synced.", this->parent_->address_str().c_str()); + ESP_LOGW(TAG, "[%s] Not connected to BLE client. Time can not be synced.", this->parent_->address_str()); return; } if (!this->char_handle_) { - ESP_LOGW(TAG, "[%s] No ble handle to BLE client. Time can not be synced.", this->parent_->address_str().c_str()); + ESP_LOGW(TAG, "[%s] No ble handle to BLE client. Time can not be synced.", this->parent_->address_str()); return; } auto time = this->time_->now(); if (!time.is_valid()) { - ESP_LOGW(TAG, "[%s] Time is not yet valid. Time can not be synced.", this->parent_->address_str().c_str()); + ESP_LOGW(TAG, "[%s] Time is not yet valid. Time can not be synced.", this->parent_->address_str()); return; } time.recalc_timestamp_utc(true); // calculate timestamp of local time uint8_t blk[5] = {}; - ESP_LOGD(TAG, "[%s] Sync time with timestamp %" PRIu64 ".", this->parent_->address_str().c_str(), time.timestamp); + ESP_LOGD(TAG, "[%s] Sync time with timestamp %" PRIu64 ".", this->parent_->address_str(), time.timestamp); blk[0] = 0x23; blk[1] = time.timestamp & 0xff; blk[2] = (time.timestamp >> 8) & 0xff; diff --git a/esphome/components/pvvx_mithermometer/display/pvvx_display.h b/esphome/components/pvvx_mithermometer/display/pvvx_display.h index 9739362024..8637506bae 100644 --- a/esphome/components/pvvx_mithermometer/display/pvvx_display.h +++ b/esphome/components/pvvx_mithermometer/display/pvvx_display.h @@ -3,6 +3,7 @@ #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/components/ble_client/ble_client.h" +#include "esphome/components/display/display.h" #include @@ -29,7 +30,7 @@ enum UNIT { UNIT_DEG_E, ///< show "°E" }; -using pvvx_writer_t = std::function; +using pvvx_writer_t = display::DisplayWriter; class PVVXDisplay : public ble_client::BLEClientNode, public PollingComponent { public: @@ -43,6 +44,7 @@ class PVVXDisplay : public ble_client::BLEClientNode, public PollingComponent { void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override; + void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override; /// Set validity period of the display information in seconds (1..65535) void set_validity_period(uint16_t validity_period) { this->validity_period_ = validity_period; } @@ -112,6 +114,7 @@ class PVVXDisplay : public ble_client::BLEClientNode, public PollingComponent { void setcfgbit_(uint8_t bit, bool value); void send_to_setup_char_(uint8_t *blk, size_t size); void delayed_disconnect_(); + void sync_time_and_display_(); #ifdef USE_TIME void sync_time_(); time::RealTimeClock *time_{nullptr}; @@ -124,7 +127,7 @@ class PVVXDisplay : public ble_client::BLEClientNode, public PollingComponent { esp32_ble_tracker::ESPBTUUID char_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw("00001f1f-0000-1000-8000-00805f9b34fb"); - optional writer_{}; + pvvx_writer_t writer_{}; }; } // namespace pvvx_mithermometer diff --git a/esphome/components/pzemac/pzemac.h b/esphome/components/pzemac/pzemac.h index 7a229b49ce..e5b96115f9 100644 --- a/esphome/components/pzemac/pzemac.h +++ b/esphome/components/pzemac/pzemac.h @@ -43,7 +43,7 @@ template class ResetEnergyAction : public Action { public: ResetEnergyAction(PZEMAC *pzemac) : pzemac_(pzemac) {} - void play(Ts... x) override { this->pzemac_->reset_energy_(); } + void play(const Ts &...x) override { this->pzemac_->reset_energy_(); } protected: PZEMAC *pzemac_; diff --git a/esphome/components/pzemdc/pzemdc.h b/esphome/components/pzemdc/pzemdc.h index b91ab4c0a5..2e6c26a10c 100644 --- a/esphome/components/pzemdc/pzemdc.h +++ b/esphome/components/pzemdc/pzemdc.h @@ -36,7 +36,7 @@ template class ResetEnergyAction : public Action { public: ResetEnergyAction(PZEMDC *pzemdc) : pzemdc_(pzemdc) {} - void play(Ts... x) override { this->pzemdc_->reset_energy(); } + void play(const Ts &...x) override { this->pzemdc_->reset_energy(); } protected: PZEMDC *pzemdc_; diff --git a/esphome/components/qmc5883l/qmc5883l.cpp b/esphome/components/qmc5883l/qmc5883l.cpp index c9196f2469..d2041a2d52 100644 --- a/esphome/components/qmc5883l/qmc5883l.cpp +++ b/esphome/components/qmc5883l/qmc5883l.cpp @@ -8,6 +8,7 @@ namespace esphome { namespace qmc5883l { static const char *const TAG = "qmc5883l"; + static const uint8_t QMC5883L_ADDRESS = 0x0D; static const uint8_t QMC5883L_REGISTER_DATA_X_LSB = 0x00; @@ -32,6 +33,10 @@ void QMC5883LComponent::setup() { } delay(10); + if (this->drdy_pin_) { + this->drdy_pin_->setup(); + } + uint8_t control_1 = 0; control_1 |= 0b01 << 0; // MODE (Mode) -> 0b00=standby, 0b01=continuous control_1 |= this->datarate_ << 2; @@ -64,6 +69,7 @@ void QMC5883LComponent::setup() { high_freq_.start(); } } + void QMC5883LComponent::dump_config() { ESP_LOGCONFIG(TAG, "QMC5883L:"); LOG_I2C_DEVICE(this); @@ -77,11 +83,20 @@ void QMC5883LComponent::dump_config() { LOG_SENSOR(" ", "Z Axis", this->z_sensor_); LOG_SENSOR(" ", "Heading", this->heading_sensor_); LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_PIN(" DRDY Pin: ", this->drdy_pin_); } + float QMC5883LComponent::get_setup_priority() const { return setup_priority::DATA; } + void QMC5883LComponent::update() { i2c::ErrorCode err; uint8_t status = false; + + // If DRDY pin is configured and the data is not ready return. + if (this->drdy_pin_ && !this->drdy_pin_->digital_read()) { + return; + } + // Status byte gets cleared when data is read, so we have to read this first. // If status and two axes are desired, it's possible to save one byte of traffic by enabling // ROL_PNT in setup and reading 7 bytes starting at the status register. diff --git a/esphome/components/qmc5883l/qmc5883l.h b/esphome/components/qmc5883l/qmc5883l.h index 3202e37780..5ba7180e23 100644 --- a/esphome/components/qmc5883l/qmc5883l.h +++ b/esphome/components/qmc5883l/qmc5883l.h @@ -3,6 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" +#include "esphome/core/hal.h" namespace esphome { namespace qmc5883l { @@ -33,6 +34,7 @@ class QMC5883LComponent : public PollingComponent, public i2c::I2CDevice { float get_setup_priority() const override; void update() override; + void set_drdy_pin(GPIOPin *pin) { drdy_pin_ = pin; } void set_datarate(QMC5883LDatarate datarate) { datarate_ = datarate; } void set_range(QMC5883LRange range) { range_ = range; } void set_oversampling(QMC5883LOversampling oversampling) { oversampling_ = oversampling; } @@ -51,6 +53,7 @@ class QMC5883LComponent : public PollingComponent, public i2c::I2CDevice { sensor::Sensor *z_sensor_{nullptr}; sensor::Sensor *heading_sensor_{nullptr}; sensor::Sensor *temperature_sensor_{nullptr}; + GPIOPin *drdy_pin_{nullptr}; enum ErrorCode { NONE = 0, COMMUNICATION_FAILED, diff --git a/esphome/components/qmc5883l/sensor.py b/esphome/components/qmc5883l/sensor.py index ade286cb9e..b79e370a05 100644 --- a/esphome/components/qmc5883l/sensor.py +++ b/esphome/components/qmc5883l/sensor.py @@ -1,8 +1,12 @@ +import logging + +from esphome import pins import esphome.codegen as cg from esphome.components import i2c, sensor import esphome.config_validation as cv from esphome.const import ( CONF_ADDRESS, + CONF_DATA_RATE, CONF_FIELD_STRENGTH_X, CONF_FIELD_STRENGTH_Y, CONF_FIELD_STRENGTH_Z, @@ -21,6 +25,10 @@ from esphome.const import ( UNIT_MICROTESLA, ) +_LOGGER = logging.getLogger(__name__) + +CONF_DRDY_PIN = "drdy_pin" + DEPENDENCIES = ["i2c"] qmc5883l_ns = cg.esphome_ns.namespace("qmc5883l") @@ -52,6 +60,18 @@ QMC5883LOversamplings = { } +def validate_config(config): + if ( + config[CONF_UPDATE_INTERVAL].total_milliseconds < 15 + and CONF_DRDY_PIN not in config + ): + _LOGGER.warning( + "[qmc5883l] 'update_interval' is less than 15ms and 'drdy_pin' is " + "not configured, this may result in I2C errors" + ) + return config + + def validate_enum(enum_values, units=None, int=True): _units = [] if units is not None: @@ -88,7 +108,7 @@ temperature_schema = sensor.sensor_schema( state_class=STATE_CLASS_MEASUREMENT, ) -CONFIG_SCHEMA = ( +CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(QMC5883LComponent), @@ -104,29 +124,25 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_FIELD_STRENGTH_Z): field_strength_schema, cv.Optional(CONF_HEADING): heading_schema, cv.Optional(CONF_TEMPERATURE): temperature_schema, + cv.Optional(CONF_DRDY_PIN): pins.gpio_input_pin_schema, + cv.Optional(CONF_DATA_RATE, default="200hz"): validate_enum( + QMC5883LDatarates, units=["hz", "Hz"] + ), } ) .extend(cv.polling_component_schema("60s")) - .extend(i2c.i2c_device_schema(0x0D)) + .extend(i2c.i2c_device_schema(0x0D)), + validate_config, ) -def auto_data_rate(config): - interval_sec = config[CONF_UPDATE_INTERVAL].total_milliseconds / 1000 - interval_hz = 1.0 / interval_sec - for datarate in sorted(QMC5883LDatarates.keys()): - if float(datarate) >= interval_hz: - return QMC5883LDatarates[datarate] - return QMC5883LDatarates[200] - - async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) cg.add(var.set_oversampling(config[CONF_OVERSAMPLING])) - cg.add(var.set_datarate(auto_data_rate(config))) + cg.add(var.set_datarate(config[CONF_DATA_RATE])) cg.add(var.set_range(config[CONF_RANGE])) if CONF_FIELD_STRENGTH_X in config: sens = await sensor.new_sensor(config[CONF_FIELD_STRENGTH_X]) @@ -143,3 +159,6 @@ async def to_code(config): if CONF_TEMPERATURE in config: sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) cg.add(var.set_temperature_sensor(sens)) + if CONF_DRDY_PIN in config: + pin = await cg.gpio_pin_expression(config[CONF_DRDY_PIN]) + cg.add(var.set_drdy_pin(pin)) diff --git a/esphome/components/qmp6988/qmp6988.cpp b/esphome/components/qmp6988/qmp6988.cpp index 6c22150f4f..57f54b6432 100644 --- a/esphome/components/qmp6988/qmp6988.cpp +++ b/esphome/components/qmp6988/qmp6988.cpp @@ -18,14 +18,6 @@ static const uint8_t QMP6988_TEMPERATURE_MSB_REG = 0xFA; /* Temperature MSB Reg static const uint8_t QMP6988_CALIBRATION_DATA_START = 0xA0; /* QMP6988 compensation coefficients */ static const uint8_t QMP6988_CALIBRATION_DATA_LENGTH = 25; -static const uint8_t SHIFT_RIGHT_4_POSITION = 4; -static const uint8_t SHIFT_LEFT_2_POSITION = 2; -static const uint8_t SHIFT_LEFT_4_POSITION = 4; -static const uint8_t SHIFT_LEFT_5_POSITION = 5; -static const uint8_t SHIFT_LEFT_8_POSITION = 8; -static const uint8_t SHIFT_LEFT_12_POSITION = 12; -static const uint8_t SHIFT_LEFT_16_POSITION = 16; - /* power mode */ static const uint8_t QMP6988_SLEEP_MODE = 0x00; static const uint8_t QMP6988_FORCED_MODE = 0x01; @@ -95,64 +87,45 @@ static const char *iir_filter_to_str(QMP6988IIRFilter filter) { } bool QMP6988Component::device_check_() { - uint8_t ret = 0; - - ret = this->read_register(QMP6988_CHIP_ID_REG, &(qmp6988_data_.chip_id), 1); - if (ret != i2c::ERROR_OK) { - ESP_LOGE(TAG, "%s: read chip ID (0xD1) failed", __func__); + if (this->read_register(QMP6988_CHIP_ID_REG, &(qmp6988_data_.chip_id), 1) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Read chip ID (0xD1) failed"); + return false; } - ESP_LOGD(TAG, "qmp6988 read chip id = 0x%x", qmp6988_data_.chip_id); + ESP_LOGV(TAG, "Read chip ID = 0x%x", qmp6988_data_.chip_id); return qmp6988_data_.chip_id == QMP6988_CHIP_ID; } bool QMP6988Component::get_calibration_data_() { - uint8_t status = 0; // BITFIELDS temp_COE; uint8_t a_data_uint8_tr[QMP6988_CALIBRATION_DATA_LENGTH] = {0}; - int len; - for (len = 0; len < QMP6988_CALIBRATION_DATA_LENGTH; len += 1) { - status = this->read_register(QMP6988_CALIBRATION_DATA_START + len, &a_data_uint8_tr[len], 1); - if (status != i2c::ERROR_OK) { - ESP_LOGE(TAG, "qmp6988 read calibration data (0xA0) error!"); + for (uint8_t len = 0; len < QMP6988_CALIBRATION_DATA_LENGTH; len += 1) { + if (this->read_register(QMP6988_CALIBRATION_DATA_START + len, &a_data_uint8_tr[len], 1) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Read calibration data (0xA0) error"); return false; } } qmp6988_data_.qmp6988_cali.COE_a0 = - (QMP6988_S32_t) (((a_data_uint8_tr[18] << SHIFT_LEFT_12_POSITION) | - (a_data_uint8_tr[19] << SHIFT_LEFT_4_POSITION) | (a_data_uint8_tr[24] & 0x0f)) - << 12); + (int32_t) encode_uint32(a_data_uint8_tr[18], a_data_uint8_tr[19], (a_data_uint8_tr[24] & 0x0f) << 4, 0); qmp6988_data_.qmp6988_cali.COE_a0 = qmp6988_data_.qmp6988_cali.COE_a0 >> 12; - qmp6988_data_.qmp6988_cali.COE_a1 = - (QMP6988_S16_t) (((a_data_uint8_tr[20]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[21]); - qmp6988_data_.qmp6988_cali.COE_a2 = - (QMP6988_S16_t) (((a_data_uint8_tr[22]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[23]); + qmp6988_data_.qmp6988_cali.COE_a1 = (int16_t) encode_uint16(a_data_uint8_tr[20], a_data_uint8_tr[21]); + qmp6988_data_.qmp6988_cali.COE_a2 = (int16_t) encode_uint16(a_data_uint8_tr[22], a_data_uint8_tr[23]); qmp6988_data_.qmp6988_cali.COE_b00 = - (QMP6988_S32_t) (((a_data_uint8_tr[0] << SHIFT_LEFT_12_POSITION) | (a_data_uint8_tr[1] << SHIFT_LEFT_4_POSITION) | - ((a_data_uint8_tr[24] & 0xf0) >> SHIFT_RIGHT_4_POSITION)) - << 12); + (int32_t) encode_uint32(a_data_uint8_tr[0], a_data_uint8_tr[1], a_data_uint8_tr[24] & 0xf0, 0); qmp6988_data_.qmp6988_cali.COE_b00 = qmp6988_data_.qmp6988_cali.COE_b00 >> 12; - qmp6988_data_.qmp6988_cali.COE_bt1 = - (QMP6988_S16_t) (((a_data_uint8_tr[2]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[3]); - qmp6988_data_.qmp6988_cali.COE_bt2 = - (QMP6988_S16_t) (((a_data_uint8_tr[4]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[5]); - qmp6988_data_.qmp6988_cali.COE_bp1 = - (QMP6988_S16_t) (((a_data_uint8_tr[6]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[7]); - qmp6988_data_.qmp6988_cali.COE_b11 = - (QMP6988_S16_t) (((a_data_uint8_tr[8]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[9]); - qmp6988_data_.qmp6988_cali.COE_bp2 = - (QMP6988_S16_t) (((a_data_uint8_tr[10]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[11]); - qmp6988_data_.qmp6988_cali.COE_b12 = - (QMP6988_S16_t) (((a_data_uint8_tr[12]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[13]); - qmp6988_data_.qmp6988_cali.COE_b21 = - (QMP6988_S16_t) (((a_data_uint8_tr[14]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[15]); - qmp6988_data_.qmp6988_cali.COE_bp3 = - (QMP6988_S16_t) (((a_data_uint8_tr[16]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[17]); + qmp6988_data_.qmp6988_cali.COE_bt1 = (int16_t) encode_uint16(a_data_uint8_tr[2], a_data_uint8_tr[3]); + qmp6988_data_.qmp6988_cali.COE_bt2 = (int16_t) encode_uint16(a_data_uint8_tr[4], a_data_uint8_tr[5]); + qmp6988_data_.qmp6988_cali.COE_bp1 = (int16_t) encode_uint16(a_data_uint8_tr[6], a_data_uint8_tr[7]); + qmp6988_data_.qmp6988_cali.COE_b11 = (int16_t) encode_uint16(a_data_uint8_tr[8], a_data_uint8_tr[9]); + qmp6988_data_.qmp6988_cali.COE_bp2 = (int16_t) encode_uint16(a_data_uint8_tr[10], a_data_uint8_tr[11]); + qmp6988_data_.qmp6988_cali.COE_b12 = (int16_t) encode_uint16(a_data_uint8_tr[12], a_data_uint8_tr[13]); + qmp6988_data_.qmp6988_cali.COE_b21 = (int16_t) encode_uint16(a_data_uint8_tr[14], a_data_uint8_tr[15]); + qmp6988_data_.qmp6988_cali.COE_bp3 = (int16_t) encode_uint16(a_data_uint8_tr[16], a_data_uint8_tr[17]); ESP_LOGV(TAG, "<-----------calibration data-------------->\r\n"); ESP_LOGV(TAG, "COE_a0[%d] COE_a1[%d] COE_a2[%d] COE_b00[%d]\r\n", qmp6988_data_.qmp6988_cali.COE_a0, @@ -166,17 +139,17 @@ bool QMP6988Component::get_calibration_data_() { qmp6988_data_.ik.a0 = qmp6988_data_.qmp6988_cali.COE_a0; // 20Q4 qmp6988_data_.ik.b00 = qmp6988_data_.qmp6988_cali.COE_b00; // 20Q4 - qmp6988_data_.ik.a1 = 3608L * (QMP6988_S32_t) qmp6988_data_.qmp6988_cali.COE_a1 - 1731677965L; // 31Q23 - qmp6988_data_.ik.a2 = 16889L * (QMP6988_S32_t) qmp6988_data_.qmp6988_cali.COE_a2 - 87619360L; // 30Q47 + qmp6988_data_.ik.a1 = 3608L * (int32_t) qmp6988_data_.qmp6988_cali.COE_a1 - 1731677965L; // 31Q23 + qmp6988_data_.ik.a2 = 16889L * (int32_t) qmp6988_data_.qmp6988_cali.COE_a2 - 87619360L; // 30Q47 - qmp6988_data_.ik.bt1 = 2982L * (QMP6988_S64_t) qmp6988_data_.qmp6988_cali.COE_bt1 + 107370906L; // 28Q15 - qmp6988_data_.ik.bt2 = 329854L * (QMP6988_S64_t) qmp6988_data_.qmp6988_cali.COE_bt2 + 108083093L; // 34Q38 - qmp6988_data_.ik.bp1 = 19923L * (QMP6988_S64_t) qmp6988_data_.qmp6988_cali.COE_bp1 + 1133836764L; // 31Q20 - qmp6988_data_.ik.b11 = 2406L * (QMP6988_S64_t) qmp6988_data_.qmp6988_cali.COE_b11 + 118215883L; // 28Q34 - qmp6988_data_.ik.bp2 = 3079L * (QMP6988_S64_t) qmp6988_data_.qmp6988_cali.COE_bp2 - 181579595L; // 29Q43 - qmp6988_data_.ik.b12 = 6846L * (QMP6988_S64_t) qmp6988_data_.qmp6988_cali.COE_b12 + 85590281L; // 29Q53 - qmp6988_data_.ik.b21 = 13836L * (QMP6988_S64_t) qmp6988_data_.qmp6988_cali.COE_b21 + 79333336L; // 29Q60 - qmp6988_data_.ik.bp3 = 2915L * (QMP6988_S64_t) qmp6988_data_.qmp6988_cali.COE_bp3 + 157155561L; // 28Q65 + qmp6988_data_.ik.bt1 = 2982L * (int64_t) qmp6988_data_.qmp6988_cali.COE_bt1 + 107370906L; // 28Q15 + qmp6988_data_.ik.bt2 = 329854L * (int64_t) qmp6988_data_.qmp6988_cali.COE_bt2 + 108083093L; // 34Q38 + qmp6988_data_.ik.bp1 = 19923L * (int64_t) qmp6988_data_.qmp6988_cali.COE_bp1 + 1133836764L; // 31Q20 + qmp6988_data_.ik.b11 = 2406L * (int64_t) qmp6988_data_.qmp6988_cali.COE_b11 + 118215883L; // 28Q34 + qmp6988_data_.ik.bp2 = 3079L * (int64_t) qmp6988_data_.qmp6988_cali.COE_bp2 - 181579595L; // 29Q43 + qmp6988_data_.ik.b12 = 6846L * (int64_t) qmp6988_data_.qmp6988_cali.COE_b12 + 85590281L; // 29Q53 + qmp6988_data_.ik.b21 = 13836L * (int64_t) qmp6988_data_.qmp6988_cali.COE_b21 + 79333336L; // 29Q60 + qmp6988_data_.ik.bp3 = 2915L * (int64_t) qmp6988_data_.qmp6988_cali.COE_bp3 + 157155561L; // 28Q65 ESP_LOGV(TAG, "<----------- int calibration data -------------->\r\n"); ESP_LOGV(TAG, "a0[%d] a1[%d] a2[%d] b00[%d]\r\n", qmp6988_data_.ik.a0, qmp6988_data_.ik.a1, qmp6988_data_.ik.a2, qmp6988_data_.ik.b00); @@ -188,55 +161,55 @@ bool QMP6988Component::get_calibration_data_() { return true; } -QMP6988_S16_t QMP6988Component::get_compensated_temperature_(qmp6988_ik_data_t *ik, QMP6988_S32_t dt) { - QMP6988_S16_t ret; - QMP6988_S64_t wk1, wk2; +int16_t QMP6988Component::get_compensated_temperature_(qmp6988_ik_data_t *ik, int32_t dt) { + int16_t ret; + int64_t wk1, wk2; // wk1: 60Q4 // bit size - wk1 = ((QMP6988_S64_t) ik->a1 * (QMP6988_S64_t) dt); // 31Q23+24-1=54 (54Q23) - wk2 = ((QMP6988_S64_t) ik->a2 * (QMP6988_S64_t) dt) >> 14; // 30Q47+24-1=53 (39Q33) - wk2 = (wk2 * (QMP6988_S64_t) dt) >> 10; // 39Q33+24-1=62 (52Q23) - wk2 = ((wk1 + wk2) / 32767) >> 19; // 54,52->55Q23 (20Q04) - ret = (QMP6988_S16_t) ((ik->a0 + wk2) >> 4); // 21Q4 -> 17Q0 + wk1 = ((int64_t) ik->a1 * (int64_t) dt); // 31Q23+24-1=54 (54Q23) + wk2 = ((int64_t) ik->a2 * (int64_t) dt) >> 14; // 30Q47+24-1=53 (39Q33) + wk2 = (wk2 * (int64_t) dt) >> 10; // 39Q33+24-1=62 (52Q23) + wk2 = ((wk1 + wk2) / 32767) >> 19; // 54,52->55Q23 (20Q04) + ret = (int16_t) ((ik->a0 + wk2) >> 4); // 21Q4 -> 17Q0 return ret; } -QMP6988_S32_t QMP6988Component::get_compensated_pressure_(qmp6988_ik_data_t *ik, QMP6988_S32_t dp, QMP6988_S16_t tx) { - QMP6988_S32_t ret; - QMP6988_S64_t wk1, wk2, wk3; +int32_t QMP6988Component::get_compensated_pressure_(qmp6988_ik_data_t *ik, int32_t dp, int16_t tx) { + int32_t ret; + int64_t wk1, wk2, wk3; // wk1 = 48Q16 // bit size - wk1 = ((QMP6988_S64_t) ik->bt1 * (QMP6988_S64_t) tx); // 28Q15+16-1=43 (43Q15) - wk2 = ((QMP6988_S64_t) ik->bp1 * (QMP6988_S64_t) dp) >> 5; // 31Q20+24-1=54 (49Q15) - wk1 += wk2; // 43,49->50Q15 - wk2 = ((QMP6988_S64_t) ik->bt2 * (QMP6988_S64_t) tx) >> 1; // 34Q38+16-1=49 (48Q37) - wk2 = (wk2 * (QMP6988_S64_t) tx) >> 8; // 48Q37+16-1=63 (55Q29) - wk3 = wk2; // 55Q29 - wk2 = ((QMP6988_S64_t) ik->b11 * (QMP6988_S64_t) tx) >> 4; // 28Q34+16-1=43 (39Q30) - wk2 = (wk2 * (QMP6988_S64_t) dp) >> 1; // 39Q30+24-1=62 (61Q29) - wk3 += wk2; // 55,61->62Q29 - wk2 = ((QMP6988_S64_t) ik->bp2 * (QMP6988_S64_t) dp) >> 13; // 29Q43+24-1=52 (39Q30) - wk2 = (wk2 * (QMP6988_S64_t) dp) >> 1; // 39Q30+24-1=62 (61Q29) - wk3 += wk2; // 62,61->63Q29 - wk1 += wk3 >> 14; // Q29 >> 14 -> Q15 - wk2 = ((QMP6988_S64_t) ik->b12 * (QMP6988_S64_t) tx); // 29Q53+16-1=45 (45Q53) - wk2 = (wk2 * (QMP6988_S64_t) tx) >> 22; // 45Q53+16-1=61 (39Q31) - wk2 = (wk2 * (QMP6988_S64_t) dp) >> 1; // 39Q31+24-1=62 (61Q30) - wk3 = wk2; // 61Q30 - wk2 = ((QMP6988_S64_t) ik->b21 * (QMP6988_S64_t) tx) >> 6; // 29Q60+16-1=45 (39Q54) - wk2 = (wk2 * (QMP6988_S64_t) dp) >> 23; // 39Q54+24-1=62 (39Q31) - wk2 = (wk2 * (QMP6988_S64_t) dp) >> 1; // 39Q31+24-1=62 (61Q20) - wk3 += wk2; // 61,61->62Q30 - wk2 = ((QMP6988_S64_t) ik->bp3 * (QMP6988_S64_t) dp) >> 12; // 28Q65+24-1=51 (39Q53) - wk2 = (wk2 * (QMP6988_S64_t) dp) >> 23; // 39Q53+24-1=62 (39Q30) - wk2 = (wk2 * (QMP6988_S64_t) dp); // 39Q30+24-1=62 (62Q30) - wk3 += wk2; // 62,62->63Q30 - wk1 += wk3 >> 15; // Q30 >> 15 = Q15 + wk1 = ((int64_t) ik->bt1 * (int64_t) tx); // 28Q15+16-1=43 (43Q15) + wk2 = ((int64_t) ik->bp1 * (int64_t) dp) >> 5; // 31Q20+24-1=54 (49Q15) + wk1 += wk2; // 43,49->50Q15 + wk2 = ((int64_t) ik->bt2 * (int64_t) tx) >> 1; // 34Q38+16-1=49 (48Q37) + wk2 = (wk2 * (int64_t) tx) >> 8; // 48Q37+16-1=63 (55Q29) + wk3 = wk2; // 55Q29 + wk2 = ((int64_t) ik->b11 * (int64_t) tx) >> 4; // 28Q34+16-1=43 (39Q30) + wk2 = (wk2 * (int64_t) dp) >> 1; // 39Q30+24-1=62 (61Q29) + wk3 += wk2; // 55,61->62Q29 + wk2 = ((int64_t) ik->bp2 * (int64_t) dp) >> 13; // 29Q43+24-1=52 (39Q30) + wk2 = (wk2 * (int64_t) dp) >> 1; // 39Q30+24-1=62 (61Q29) + wk3 += wk2; // 62,61->63Q29 + wk1 += wk3 >> 14; // Q29 >> 14 -> Q15 + wk2 = ((int64_t) ik->b12 * (int64_t) tx); // 29Q53+16-1=45 (45Q53) + wk2 = (wk2 * (int64_t) tx) >> 22; // 45Q53+16-1=61 (39Q31) + wk2 = (wk2 * (int64_t) dp) >> 1; // 39Q31+24-1=62 (61Q30) + wk3 = wk2; // 61Q30 + wk2 = ((int64_t) ik->b21 * (int64_t) tx) >> 6; // 29Q60+16-1=45 (39Q54) + wk2 = (wk2 * (int64_t) dp) >> 23; // 39Q54+24-1=62 (39Q31) + wk2 = (wk2 * (int64_t) dp) >> 1; // 39Q31+24-1=62 (61Q20) + wk3 += wk2; // 61,61->62Q30 + wk2 = ((int64_t) ik->bp3 * (int64_t) dp) >> 12; // 28Q65+24-1=51 (39Q53) + wk2 = (wk2 * (int64_t) dp) >> 23; // 39Q53+24-1=62 (39Q30) + wk2 = (wk2 * (int64_t) dp); // 39Q30+24-1=62 (62Q30) + wk3 += wk2; // 62,62->63Q30 + wk1 += wk3 >> 15; // Q30 >> 15 = Q15 wk1 /= 32767L; wk1 >>= 11; // Q15 >> 7 = Q4 wk1 += ik->b00; // Q4 + 20Q4 // wk1 >>= 4; // 28Q4 -> 24Q0 - ret = (QMP6988_S32_t) wk1; + ret = (int32_t) wk1; return ret; } @@ -274,7 +247,7 @@ void QMP6988Component::set_power_mode_(uint8_t power_mode) { delay(10); } -void QMP6988Component::write_filter_(unsigned char filter) { +void QMP6988Component::write_filter_(QMP6988IIRFilter filter) { uint8_t data; data = (filter & 0x03); @@ -282,7 +255,7 @@ void QMP6988Component::write_filter_(unsigned char filter) { delay(10); } -void QMP6988Component::write_oversampling_pressure_(unsigned char oversampling_p) { +void QMP6988Component::write_oversampling_pressure_(QMP6988Oversampling oversampling_p) { uint8_t data; this->read_register(QMP6988_CTRLMEAS_REG, &data, 1); @@ -292,7 +265,7 @@ void QMP6988Component::write_oversampling_pressure_(unsigned char oversampling_p delay(10); } -void QMP6988Component::write_oversampling_temperature_(unsigned char oversampling_t) { +void QMP6988Component::write_oversampling_temperature_(QMP6988Oversampling oversampling_t) { uint8_t data; this->read_register(QMP6988_CTRLMEAS_REG, &data, 1); @@ -302,16 +275,6 @@ void QMP6988Component::write_oversampling_temperature_(unsigned char oversamplin delay(10); } -void QMP6988Component::set_temperature_oversampling(QMP6988Oversampling oversampling_t) { - this->temperature_oversampling_ = oversampling_t; -} - -void QMP6988Component::set_pressure_oversampling(QMP6988Oversampling oversampling_p) { - this->pressure_oversampling_ = oversampling_p; -} - -void QMP6988Component::set_iir_filter(QMP6988IIRFilter iirfilter) { this->iir_filter_ = iirfilter; } - void QMP6988Component::calculate_altitude_(float pressure, float temp) { float altitude; altitude = (pow((101325 / pressure), 1 / 5.257) - 1) * (temp + 273.15) / 0.0065; @@ -320,10 +283,10 @@ void QMP6988Component::calculate_altitude_(float pressure, float temp) { void QMP6988Component::calculate_pressure_() { uint8_t err = 0; - QMP6988_U32_t p_read, t_read; - QMP6988_S32_t p_raw, t_raw; + uint32_t p_read, t_read; + int32_t p_raw, t_raw; uint8_t a_data_uint8_tr[6] = {0}; - QMP6988_S32_t t_int, p_int; + int32_t t_int, p_int; this->qmp6988_data_.temperature = 0; this->qmp6988_data_.pressure = 0; @@ -332,13 +295,11 @@ void QMP6988Component::calculate_pressure_() { ESP_LOGE(TAG, "Error reading raw pressure/temp values"); return; } - p_read = (QMP6988_U32_t) ((((QMP6988_U32_t) (a_data_uint8_tr[0])) << SHIFT_LEFT_16_POSITION) | - (((QMP6988_U16_t) (a_data_uint8_tr[1])) << SHIFT_LEFT_8_POSITION) | (a_data_uint8_tr[2])); - p_raw = (QMP6988_S32_t) (p_read - SUBTRACTOR); + p_read = encode_uint24(a_data_uint8_tr[0], a_data_uint8_tr[1], a_data_uint8_tr[2]); + p_raw = (int32_t) (p_read - SUBTRACTOR); - t_read = (QMP6988_U32_t) ((((QMP6988_U32_t) (a_data_uint8_tr[3])) << SHIFT_LEFT_16_POSITION) | - (((QMP6988_U16_t) (a_data_uint8_tr[4])) << SHIFT_LEFT_8_POSITION) | (a_data_uint8_tr[5])); - t_raw = (QMP6988_S32_t) (t_read - SUBTRACTOR); + t_read = encode_uint24(a_data_uint8_tr[3], a_data_uint8_tr[4], a_data_uint8_tr[5]); + t_raw = (int32_t) (t_read - SUBTRACTOR); t_int = this->get_compensated_temperature_(&(qmp6988_data_.ik), t_raw); p_int = this->get_compensated_pressure_(&(qmp6988_data_.ik), p_raw, t_int); @@ -348,10 +309,9 @@ void QMP6988Component::calculate_pressure_() { } void QMP6988Component::setup() { - bool ret; - ret = this->device_check_(); - if (!ret) { - ESP_LOGCONFIG(TAG, "Setup failed - device not found"); + if (!this->device_check_()) { + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); + return; } this->software_reset_(); @@ -365,9 +325,6 @@ void QMP6988Component::setup() { void QMP6988Component::dump_config() { ESP_LOGCONFIG(TAG, "QMP6988:"); LOG_I2C_DEVICE(this); - if (this->is_failed()) { - ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); - } LOG_UPDATE_INTERVAL(this); LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); @@ -377,8 +334,6 @@ void QMP6988Component::dump_config() { ESP_LOGCONFIG(TAG, " IIR Filter: %s", iir_filter_to_str(this->iir_filter_)); } -float QMP6988Component::get_setup_priority() const { return setup_priority::DATA; } - void QMP6988Component::update() { this->calculate_pressure_(); float pressurehectopascals = this->qmp6988_data_.pressure / 100; diff --git a/esphome/components/qmp6988/qmp6988.h b/esphome/components/qmp6988/qmp6988.h index 61b46a4189..5b0f80c77e 100644 --- a/esphome/components/qmp6988/qmp6988.h +++ b/esphome/components/qmp6988/qmp6988.h @@ -1,24 +1,17 @@ #pragma once +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensor/sensor.h" #include "esphome/core/component.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include "esphome/components/sensor/sensor.h" -#include "esphome/components/i2c/i2c.h" namespace esphome { namespace qmp6988 { -#define QMP6988_U16_t unsigned short -#define QMP6988_S16_t short -#define QMP6988_U32_t unsigned int -#define QMP6988_S32_t int -#define QMP6988_U64_t unsigned long long -#define QMP6988_S64_t long long - /* oversampling */ -enum QMP6988Oversampling { +enum QMP6988Oversampling : uint8_t { QMP6988_OVERSAMPLING_SKIPPED = 0x00, QMP6988_OVERSAMPLING_1X = 0x01, QMP6988_OVERSAMPLING_2X = 0x02, @@ -30,7 +23,7 @@ enum QMP6988Oversampling { }; /* filter */ -enum QMP6988IIRFilter { +enum QMP6988IIRFilter : uint8_t { QMP6988_IIR_FILTER_OFF = 0x00, QMP6988_IIR_FILTER_2X = 0x01, QMP6988_IIR_FILTER_4X = 0x02, @@ -40,18 +33,18 @@ enum QMP6988IIRFilter { }; using qmp6988_cali_data_t = struct Qmp6988CaliData { - QMP6988_S32_t COE_a0; - QMP6988_S16_t COE_a1; - QMP6988_S16_t COE_a2; - QMP6988_S32_t COE_b00; - QMP6988_S16_t COE_bt1; - QMP6988_S16_t COE_bt2; - QMP6988_S16_t COE_bp1; - QMP6988_S16_t COE_b11; - QMP6988_S16_t COE_bp2; - QMP6988_S16_t COE_b12; - QMP6988_S16_t COE_b21; - QMP6988_S16_t COE_bp3; + int32_t COE_a0; + int16_t COE_a1; + int16_t COE_a2; + int32_t COE_b00; + int16_t COE_bt1; + int16_t COE_bt2; + int16_t COE_bp1; + int16_t COE_b11; + int16_t COE_bp2; + int16_t COE_b12; + int16_t COE_b21; + int16_t COE_bp3; }; using qmp6988_fk_data_t = struct Qmp6988FkData { @@ -60,9 +53,9 @@ using qmp6988_fk_data_t = struct Qmp6988FkData { }; using qmp6988_ik_data_t = struct Qmp6988IkData { - QMP6988_S32_t a0, b00; - QMP6988_S32_t a1, a2; - QMP6988_S64_t bt1, bt2, bp1, b11, bp2, b12, b21, bp3; + int32_t a0, b00; + int32_t a1, a2; + int64_t bt1, bt2, bp1, b11, bp2, b12, b21, bp3; }; using qmp6988_data_t = struct Qmp6988Data { @@ -77,17 +70,18 @@ using qmp6988_data_t = struct Qmp6988Data { class QMP6988Component : public PollingComponent, public i2c::I2CDevice { public: - void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } - void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; } + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; } + void set_pressure_sensor(sensor::Sensor *pressure_sensor) { this->pressure_sensor_ = pressure_sensor; } void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; - void set_iir_filter(QMP6988IIRFilter iirfilter); - void set_temperature_oversampling(QMP6988Oversampling oversampling_t); - void set_pressure_oversampling(QMP6988Oversampling oversampling_p); + void set_iir_filter(QMP6988IIRFilter iirfilter) { this->iir_filter_ = iirfilter; } + void set_temperature_oversampling(QMP6988Oversampling oversampling_t) { + this->temperature_oversampling_ = oversampling_t; + } + void set_pressure_oversampling(QMP6988Oversampling oversampling_p) { this->pressure_oversampling_ = oversampling_p; } protected: qmp6988_data_t qmp6988_data_; @@ -102,14 +96,14 @@ class QMP6988Component : public PollingComponent, public i2c::I2CDevice { bool get_calibration_data_(); bool device_check_(); void set_power_mode_(uint8_t power_mode); - void write_oversampling_temperature_(unsigned char oversampling_t); - void write_oversampling_pressure_(unsigned char oversampling_p); - void write_filter_(unsigned char filter); + void write_oversampling_temperature_(QMP6988Oversampling oversampling_t); + void write_oversampling_pressure_(QMP6988Oversampling oversampling_p); + void write_filter_(QMP6988IIRFilter filter); void calculate_pressure_(); void calculate_altitude_(float pressure, float temp); - QMP6988_S32_t get_compensated_pressure_(qmp6988_ik_data_t *ik, QMP6988_S32_t dp, QMP6988_S16_t tx); - QMP6988_S16_t get_compensated_temperature_(qmp6988_ik_data_t *ik, QMP6988_S32_t dt); + int32_t get_compensated_pressure_(qmp6988_ik_data_t *ik, int32_t dp, int16_t tx); + int16_t get_compensated_temperature_(qmp6988_ik_data_t *ik, int32_t dt); }; } // namespace qmp6988 diff --git a/esphome/components/radon_eye_ble/__init__.py b/esphome/components/radon_eye_ble/__init__.py index 01910c81a8..99daef30e5 100644 --- a/esphome/components/radon_eye_ble/__init__.py +++ b/esphome/components/radon_eye_ble/__init__.py @@ -18,6 +18,6 @@ CONFIG_SCHEMA = cv.Schema( ).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield esp32_ble_tracker.register_ble_device(var, config) + await esp32_ble_tracker.register_ble_device(var, config) diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index 8163661c65..d24d24b000 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -17,6 +17,7 @@ from esphome.const import ( CONF_FAMILY, CONF_GROUP, CONF_ID, + CONF_INDEX, CONF_INVERTED, CONF_LEVEL, CONF_MAGNITUDE, @@ -38,7 +39,7 @@ from esphome.const import ( CONF_WAND_ID, CONF_ZERO, ) -from esphome.core import coroutine +from esphome.core import ID, coroutine from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor from esphome.util import Registry, SimpleRegistry @@ -616,6 +617,49 @@ async def dooya_action(var, config, args): cg.add(var.set_check(template_)) +# Dyson +DysonData, DysonBinarySensor, DysonTrigger, DysonAction, DysonDumper = declare_protocol( + "Dyson" +) +DYSON_SCHEMA = cv.Schema( + { + cv.Required(CONF_CODE): cv.hex_uint16_t, + cv.Optional(CONF_INDEX, default=0xFF): cv.hex_uint8_t, + } +) + + +@register_binary_sensor("dyson", DysonBinarySensor, DYSON_SCHEMA) +def dyson_binary_sensor(var, config): + cg.add( + var.set_data( + cg.StructInitializer( + DysonData, + ("code", config[CONF_CODE]), + ("index", config[CONF_INDEX]), + ) + ) + ) + + +@register_trigger("dyson", DysonTrigger, DysonData) +def dyson_trigger(var, config): + pass + + +@register_dumper("dyson", DysonDumper) +def dyson_dumper(var, config): + pass + + +@register_action("dyson", DysonAction, DYSON_SCHEMA) +async def dyson_action(var, config, args): + template_ = await cg.templatable(config[CONF_CODE], args, cg.uint16) + cg.add(var.set_code(template_)) + template_ = await cg.templatable(config[CONF_INDEX], args, cg.uint8) + cg.add(var.set_index(template_)) + + # JVC JVCData, JVCBinarySensor, JVCTrigger, JVCAction, JVCDumper = declare_protocol("JVC") JVC_SCHEMA = cv.Schema({cv.Required(CONF_DATA): cv.hex_uint32_t}) @@ -1056,6 +1100,52 @@ async def sony_action(var, config, args): cg.add(var.set_nbits(template_)) +# Symphony +SymphonyData, SymphonyBinarySensor, SymphonyTrigger, SymphonyAction, SymphonyDumper = ( + declare_protocol("Symphony") +) +SYMPHONY_SCHEMA = cv.Schema( + { + cv.Required(CONF_DATA): cv.hex_uint32_t, + cv.Required(CONF_NBITS): cv.int_range(min=1, max=32), + cv.Optional(CONF_COMMAND_REPEATS, default=2): cv.uint8_t, + } +) + + +@register_binary_sensor("symphony", SymphonyBinarySensor, SYMPHONY_SCHEMA) +def symphony_binary_sensor(var, config): + cg.add( + var.set_data( + cg.StructInitializer( + SymphonyData, + ("data", config[CONF_DATA]), + ("nbits", config[CONF_NBITS]), + ) + ) + ) + + +@register_trigger("symphony", SymphonyTrigger, SymphonyData) +def symphony_trigger(var, config): + pass + + +@register_dumper("symphony", SymphonyDumper) +def symphony_dumper(var, config): + pass + + +@register_action("symphony", SymphonyAction, SYMPHONY_SCHEMA) +async def symphony_action(var, config, args): + template_ = await cg.templatable(config[CONF_DATA], args, cg.uint32) + cg.add(var.set_data(template_)) + template_ = await cg.templatable(config[CONF_NBITS], args, cg.uint32) + cg.add(var.set_nbits(template_)) + template_ = await cg.templatable(config[CONF_COMMAND_REPEATS], args, cg.uint8) + cg.add(var.set_repeats(template_)) + + # Raw def validate_raw_alternating(value): assert isinstance(value, list) @@ -1782,14 +1872,12 @@ def nexa_dumper(var, config): @register_action("nexa", NexaAction, NEXA_SCHEMA) -def nexa_action(var, config, args): - cg.add(var.set_device((yield cg.templatable(config[CONF_DEVICE], args, cg.uint32)))) - cg.add(var.set_group((yield cg.templatable(config[CONF_GROUP], args, cg.uint8)))) - cg.add(var.set_state((yield cg.templatable(config[CONF_STATE], args, cg.uint8)))) - cg.add( - var.set_channel((yield cg.templatable(config[CONF_CHANNEL], args, cg.uint8))) - ) - cg.add(var.set_level((yield cg.templatable(config[CONF_LEVEL], args, cg.uint8)))) +async def nexa_action(var, config, args): + cg.add(var.set_device(await cg.templatable(config[CONF_DEVICE], args, cg.uint32))) + cg.add(var.set_group(await cg.templatable(config[CONF_GROUP], args, cg.uint8))) + cg.add(var.set_state(await cg.templatable(config[CONF_STATE], args, cg.uint8))) + cg.add(var.set_channel(await cg.templatable(config[CONF_CHANNEL], args, cg.uint8))) + cg.add(var.set_level(await cg.templatable(config[CONF_LEVEL], args, cg.uint8))) # Midea @@ -2016,7 +2104,9 @@ async def abbwelcome_action(var, config, args): ) cg.add(var.set_data_template(template_)) else: - cg.add(var.set_data_static(data_)) + arr_id = ID(f"{var.base}_data", is_declaration=True, type=cg.uint8) + arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data_)) + cg.add(var.set_data_static(arr, len(data_))) # Mirage diff --git a/esphome/components/remote_base/abbwelcome_protocol.h b/esphome/components/remote_base/abbwelcome_protocol.h index f2d0f5b547..4b922eb2f1 100644 --- a/esphome/components/remote_base/abbwelcome_protocol.h +++ b/esphome/components/remote_base/abbwelcome_protocol.h @@ -33,19 +33,13 @@ Message Format: class ABBWelcomeData { public: // Make default - ABBWelcomeData() { - std::fill(std::begin(this->data_), std::end(this->data_), 0); - this->data_[0] = 0x55; - this->data_[1] = 0xff; - } + ABBWelcomeData() : data_{0x55, 0xff} {} // Make from initializer_list - ABBWelcomeData(std::initializer_list data) { - std::fill(std::begin(this->data_), std::end(this->data_), 0); + ABBWelcomeData(std::initializer_list data) : data_{} { std::copy_n(data.begin(), std::min(data.size(), this->data_.size()), this->data_.begin()); } // Make from vector - ABBWelcomeData(const std::vector &data) { - std::fill(std::begin(this->data_), std::end(this->data_), 0); + ABBWelcomeData(const std::vector &data) : data_{} { std::copy_n(data.begin(), std::min(data.size(), this->data_.size()), this->data_.begin()); } // Default copy constructor @@ -220,10 +214,13 @@ template class ABBWelcomeAction : public RemoteTransmitterAction TEMPLATABLE_VALUE(uint8_t, message_type) TEMPLATABLE_VALUE(uint8_t, message_id) TEMPLATABLE_VALUE(bool, auto_message_id) - void set_data_static(std::vector data) { data_static_ = std::move(data); } - void set_data_template(std::function(Ts...)> func) { - this->data_func_ = func; - has_data_func_ = true; + void set_data_template(std::vector (*func)(Ts...)) { + this->data_.func = func; + this->len_ = -1; // Sentinel value indicates template mode + } + void set_data_static(const uint8_t *data, size_t len) { + this->data_.data = data; + this->len_ = len; // Length >= 0 indicates static mode } void encode(RemoteTransmitData *dst, Ts... x) override { ABBWelcomeData data; @@ -234,19 +231,25 @@ template class ABBWelcomeAction : public RemoteTransmitterAction data.set_message_type(this->message_type_.value(x...)); data.set_message_id(this->message_id_.value(x...)); data.auto_message_id = this->auto_message_id_.value(x...); - if (has_data_func_) { - data.set_data(this->data_func_(x...)); + std::vector data_vec; + if (this->len_ >= 0) { + // Static mode: copy from flash to vector + data_vec.assign(this->data_.data, this->data_.data + this->len_); } else { - data.set_data(this->data_static_); + // Template mode: call function + data_vec = this->data_.func(x...); } + data.set_data(data_vec); data.finalize(); ABBWelcomeProtocol().encode(dst, data); } protected: - std::function(Ts...)> data_func_{}; - std::vector data_static_{}; - bool has_data_func_{false}; + ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length + union Data { + std::vector (*func)(Ts...); // Function pointer (stateless lambdas) + const uint8_t *data; // Pointer to static data in flash + } data_; }; } // namespace remote_base diff --git a/esphome/components/remote_base/dyson_protocol.cpp b/esphome/components/remote_base/dyson_protocol.cpp new file mode 100644 index 0000000000..db4e1135f4 --- /dev/null +++ b/esphome/components/remote_base/dyson_protocol.cpp @@ -0,0 +1,71 @@ +#include "dyson_protocol.h" +#include "esphome/core/log.h" + +#include + +namespace esphome { +namespace remote_base { + +static const char *const TAG = "remote.dyson"; + +// pulsewidth [µs] +constexpr uint32_t PW_MARK_US = 780; +constexpr uint32_t PW_SHORT_US = 720; +constexpr uint32_t PW_LONG_US = 1500; +constexpr uint32_t PW_START_US = 2280; + +// MSB of 15 bit dyson code +constexpr uint16_t MSB_DYSON = (1 << 14); + +// required symbols in transmit buffer = (start_symbol + 15 data_symbols) +constexpr uint32_t N_SYMBOLS_REQ = 2u * (1 + 15); + +void DysonProtocol::encode(RemoteTransmitData *dst, const DysonData &data) { + uint32_t raw_code = (data.code << 2) + (data.index & 3); + dst->set_carrier_frequency(36000); + dst->reserve(N_SYMBOLS_REQ + 1); + dst->item(PW_START_US, PW_SHORT_US); + for (uint16_t mask = MSB_DYSON; mask != 0; mask >>= 1) { + if (mask == (mask & raw_code)) { + dst->item(PW_MARK_US, PW_LONG_US); + } else { + dst->item(PW_MARK_US, PW_SHORT_US); + } + } + dst->mark(PW_MARK_US); // final carrier pulse +} + +optional DysonProtocol::decode(RemoteReceiveData src) { + uint32_t n_received = static_cast(src.size()); + uint16_t raw_code = 0; + DysonData data{ + .code = 0, + .index = 0, + }; + if (n_received < N_SYMBOLS_REQ) + return {}; // invalid frame length + if (!src.expect_item(PW_START_US, PW_SHORT_US)) + return {}; // start not found + for (uint16_t mask = MSB_DYSON; mask != 0; mask >>= 1) { + if (src.expect_item(PW_MARK_US, PW_SHORT_US)) { + raw_code &= ~mask; // zero detected + } else if (src.expect_item(PW_MARK_US, PW_LONG_US)) { + raw_code |= mask; // one detected + } else { + return {}; // invalid data item + } + } + data.code = raw_code >> 2; // extract button code + data.index = raw_code & 3; // extract rolling index + if (src.expect_mark(PW_MARK_US)) { // check total length + return data; + } + return {}; // frame not complete +} + +void DysonProtocol::dump(const DysonData &data) { + ESP_LOGI(TAG, "Dyson: code=0x%x rolling index=%d", data.code, data.index); +} + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/dyson_protocol.h b/esphome/components/remote_base/dyson_protocol.h new file mode 100644 index 0000000000..d1c08fefba --- /dev/null +++ b/esphome/components/remote_base/dyson_protocol.h @@ -0,0 +1,46 @@ +#pragma once + +#include "remote_base.h" + +#include + +namespace esphome { +namespace remote_base { + +static constexpr uint8_t IGNORE_INDEX = 0xFF; + +struct DysonData { + uint16_t code; // the button, e.g. power, swing, fan++, ... + uint8_t index; // the rolling index counter + bool operator==(const DysonData &rhs) const { + if (IGNORE_INDEX == index || IGNORE_INDEX == rhs.index) { + return code == rhs.code; + } + return code == rhs.code && index == rhs.index; + } +}; + +class DysonProtocol : public RemoteProtocol { + public: + void encode(RemoteTransmitData *dst, const DysonData &data) override; + optional decode(RemoteReceiveData src) override; + void dump(const DysonData &data) override; +}; + +DECLARE_REMOTE_PROTOCOL(Dyson) + +template class DysonAction : public RemoteTransmitterActionBase { + public: + TEMPLATABLE_VALUE(uint16_t, code) + TEMPLATABLE_VALUE(uint8_t, index) + + void encode(RemoteTransmitData *dst, Ts... x) override { + DysonData data{}; + data.code = this->code_.value(x...); + data.index = this->index_.value(x...); + DysonProtocol().encode(dst, data); + } +}; + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/gobox_protocol.cpp b/esphome/components/remote_base/gobox_protocol.cpp index 54e0dff663..4f6de5e59e 100644 --- a/esphome/components/remote_base/gobox_protocol.cpp +++ b/esphome/components/remote_base/gobox_protocol.cpp @@ -10,8 +10,8 @@ constexpr uint32_t BIT_MARK_US = 580; // 70us seems like a safe time delta for constexpr uint32_t BIT_ONE_SPACE_US = 1640; constexpr uint32_t BIT_ZERO_SPACE_US = 545; constexpr uint64_t HEADER = 0b011001001100010uL; // 15 bits -constexpr uint64_t HEADER_SIZE = 15; -constexpr uint64_t CODE_SIZE = 17; +constexpr size_t HEADER_SIZE = 15; +constexpr size_t CODE_SIZE = 17; void GoboxProtocol::dump_timings_(const RawTimings &timings) const { ESP_LOGD(TAG, "Gobox: size=%u", timings.size()); @@ -39,7 +39,7 @@ void GoboxProtocol::encode(RemoteTransmitData *dst, const GoboxData &data) { } optional GoboxProtocol::decode(RemoteReceiveData src) { - if (src.size() < ((HEADER_SIZE + CODE_SIZE) * 2 + 1)) { + if (static_cast(src.size()) < ((HEADER_SIZE + CODE_SIZE) * 2 + 1)) { return {}; } diff --git a/esphome/components/remote_base/pronto_protocol.cpp b/esphome/components/remote_base/pronto_protocol.cpp index 35fd782248..9fbc9e85ba 100644 --- a/esphome/components/remote_base/pronto_protocol.cpp +++ b/esphome/components/remote_base/pronto_protocol.cpp @@ -71,6 +71,7 @@ static const uint16_t FALLBACK_FREQUENCY = 64767U; // To use with frequency = 0 static const uint32_t MICROSECONDS_IN_SECONDS = 1000000UL; static const uint16_t PRONTO_DEFAULT_GAP = 45000; static const uint16_t MARK_EXCESS_MICROS = 20; +static constexpr size_t PRONTO_LOG_CHUNK_SIZE = 230; static uint16_t to_frequency_k_hz(uint16_t code) { if (code == 0) @@ -225,18 +226,18 @@ optional ProntoProtocol::decode(RemoteReceiveData src) { } void ProntoProtocol::dump(const ProntoData &data) { - std::string rest; - - rest = data.data; ESP_LOGI(TAG, "Received Pronto: data="); - while (true) { - ESP_LOGI(TAG, "%s", rest.substr(0, 230).c_str()); - if (rest.size() > 230) { - rest = rest.substr(230); - } else { - break; - } - } + + const char *ptr = data.data.c_str(); + size_t remaining = data.data.size(); + + // Log in chunks, always logging at least once (even for empty string) + do { + size_t chunk_size = remaining < PRONTO_LOG_CHUNK_SIZE ? remaining : PRONTO_LOG_CHUNK_SIZE; + ESP_LOGI(TAG, "%.*s", (int) chunk_size, ptr); + ptr += chunk_size; + remaining -= chunk_size; + } while (remaining > 0); } } // namespace remote_base diff --git a/esphome/components/remote_base/raw_protocol.h b/esphome/components/remote_base/raw_protocol.h index 9b671e611f..941b6aab42 100644 --- a/esphome/components/remote_base/raw_protocol.h +++ b/esphome/components/remote_base/raw_protocol.h @@ -42,17 +42,20 @@ class RawTrigger : public Trigger, public Component, public RemoteRe template class RawAction : public RemoteTransmitterActionBase { public: - void set_code_template(std::function func) { this->code_func_ = func; } + void set_code_template(RawTimings (*func)(Ts...)) { + this->code_.func = func; + this->len_ = -1; // Sentinel value indicates template mode + } void set_code_static(const int32_t *code, size_t len) { - this->code_static_ = code; - this->code_static_len_ = len; + this->code_.data = code; + this->len_ = len; // Length >= 0 indicates static mode } TEMPLATABLE_VALUE(uint32_t, carrier_frequency); void encode(RemoteTransmitData *dst, Ts... x) override { - if (this->code_static_ != nullptr) { - for (size_t i = 0; i < this->code_static_len_; i++) { - auto val = this->code_static_[i]; + if (this->len_ >= 0) { + for (size_t i = 0; i < static_cast(this->len_); i++) { + auto val = this->code_.data[i]; if (val < 0) { dst->space(static_cast(-val)); } else { @@ -60,15 +63,17 @@ template class RawAction : public RemoteTransmitterActionBaseset_data(this->code_func_(x...)); + dst->set_data(this->code_.func(x...)); } dst->set_carrier_frequency(this->carrier_frequency_.value(x...)); } protected: - std::function code_func_{nullptr}; - const int32_t *code_static_{nullptr}; - int32_t code_static_len_{0}; + ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length + union Code { + RawTimings (*func)(Ts...); + const int32_t *data; + } code_; }; class RawDumper : public RemoteReceiverDumperBase { diff --git a/esphome/components/remote_base/remote_base.h b/esphome/components/remote_base/remote_base.h index b740ba8085..2cb79bf571 100644 --- a/esphome/components/remote_base/remote_base.h +++ b/esphome/components/remote_base/remote_base.h @@ -276,7 +276,7 @@ template class RemoteTransmitterActionBase : public RemoteTransm TEMPLATABLE_VALUE(uint32_t, send_wait) protected: - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->transmitter_->transmit(); this->encode(call.get_data(), x...); call.set_send_times(this->send_times_.value_or(x..., 1)); diff --git a/esphome/components/remote_base/symphony_protocol.cpp b/esphome/components/remote_base/symphony_protocol.cpp new file mode 100644 index 0000000000..34b5dba07f --- /dev/null +++ b/esphome/components/remote_base/symphony_protocol.cpp @@ -0,0 +1,120 @@ +#include "symphony_protocol.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace remote_base { + +static const char *const TAG = "remote.symphony"; + +// Reference implementation and timing details: +// IRremoteESP8266 ir_Symphony.cpp +// https://github.com/crankyoldgit/IRremoteESP8266/blob/master/src/ir_Symphony.cpp +// The implementation below mirrors the constant bit-time mapping and +// footer-gap handling used there. + +// Symphony protocol timing specifications (tuned to handset captures) +static const uint32_t BIT_ZERO_HIGH_US = 460; // short +static const uint32_t BIT_ZERO_LOW_US = 1260; // long +static const uint32_t BIT_ONE_HIGH_US = 1260; // long +static const uint32_t BIT_ONE_LOW_US = 460; // short +static const uint32_t CARRIER_FREQUENCY = 38000; + +// IRremoteESP8266 reference: kSymphonyFooterGap = 4 * (mark + space) +static const uint32_t FOOTER_GAP_US = 4 * (BIT_ZERO_HIGH_US + BIT_ZERO_LOW_US); +// Typical inter-frame gap (~34.8 ms observed) +static const uint32_t INTER_FRAME_GAP_US = 34760; + +void SymphonyProtocol::encode(RemoteTransmitData *dst, const SymphonyData &data) { + dst->set_carrier_frequency(CARRIER_FREQUENCY); + ESP_LOGD(TAG, "Sending Symphony: data=0x%0*X nbits=%u repeats=%u", (data.nbits + 3) / 4, (uint32_t) data.data, + data.nbits, data.repeats); + // Each bit produces a mark+space (2 entries). We fold the inter-frame/footer gap + // into the last bit's space of each frame to avoid over-length gaps. + dst->reserve(data.nbits * 2u * data.repeats); + + for (uint8_t repeats = 0; repeats < data.repeats; repeats++) { + // Data bits (MSB first) + for (uint32_t mask = 1UL << (data.nbits - 1); mask != 0; mask >>= 1) { + const bool is_last_bit = (mask == 1); + const bool is_last_frame = (repeats == (data.repeats - 1)); + if (is_last_bit) { + // Emit last bit's mark; replace its space with the proper gap + if (data.data & mask) { + dst->mark(BIT_ONE_HIGH_US); + } else { + dst->mark(BIT_ZERO_HIGH_US); + } + dst->space(is_last_frame ? FOOTER_GAP_US : INTER_FRAME_GAP_US); + } else { + if (data.data & mask) { + dst->item(BIT_ONE_HIGH_US, BIT_ONE_LOW_US); + } else { + dst->item(BIT_ZERO_HIGH_US, BIT_ZERO_LOW_US); + } + } + } + } +} + +optional SymphonyProtocol::decode(RemoteReceiveData src) { + auto is_valid_len = [](uint8_t nbits) -> bool { return nbits == 8 || nbits == 12 || nbits == 16; }; + + RemoteReceiveData s = src; // copy + SymphonyData out{0, 0, 1}; + + for (; out.nbits < 32; out.nbits++) { + if (s.expect_mark(BIT_ONE_HIGH_US)) { + if (!s.expect_space(BIT_ONE_LOW_US)) { + // Allow footer gap immediately after the last mark + if (s.peek_space_at_least(FOOTER_GAP_US)) { + uint8_t bits_with_this = out.nbits + 1; + if (is_valid_len(bits_with_this)) { + out.data = (out.data << 1UL) | 1UL; + out.nbits = bits_with_this; + return out; + } + } + return {}; + } + // Successfully consumed a '1' bit (mark + space) + out.data = (out.data << 1UL) | 1UL; + continue; + } else if (s.expect_mark(BIT_ZERO_HIGH_US)) { + if (!s.expect_space(BIT_ZERO_LOW_US)) { + // Allow footer gap immediately after the last mark + if (s.peek_space_at_least(FOOTER_GAP_US)) { + uint8_t bits_with_this = out.nbits + 1; + if (is_valid_len(bits_with_this)) { + out.data = (out.data << 1UL) | 0UL; + out.nbits = bits_with_this; + return out; + } + } + return {}; + } + // Successfully consumed a '0' bit (mark + space) + out.data = (out.data << 1UL) | 0UL; + continue; + } else { + // Completed a valid-length frame followed by a footer gap + if (is_valid_len(out.nbits) && s.peek_space_at_least(FOOTER_GAP_US)) { + return out; + } + return {}; + } + } + + if (is_valid_len(out.nbits) && s.peek_space_at_least(FOOTER_GAP_US)) { + return out; + } + + return {}; +} + +void SymphonyProtocol::dump(const SymphonyData &data) { + const int32_t hex_width = (data.nbits + 3) / 4; // pad to nibble width + ESP_LOGI(TAG, "Received Symphony: data=0x%0*X, nbits=%d", hex_width, (uint32_t) data.data, data.nbits); +} + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/symphony_protocol.h b/esphome/components/remote_base/symphony_protocol.h new file mode 100644 index 0000000000..7e77a268ba --- /dev/null +++ b/esphome/components/remote_base/symphony_protocol.h @@ -0,0 +1,44 @@ +#pragma once + +#include "esphome/core/component.h" +#include "remote_base.h" + +#include + +namespace esphome { +namespace remote_base { + +struct SymphonyData { + uint32_t data; + uint8_t nbits; + uint8_t repeats{1}; + + bool operator==(const SymphonyData &rhs) const { return data == rhs.data && nbits == rhs.nbits; } +}; + +class SymphonyProtocol : public RemoteProtocol { + public: + void encode(RemoteTransmitData *dst, const SymphonyData &data) override; + optional decode(RemoteReceiveData src) override; + void dump(const SymphonyData &data) override; +}; + +DECLARE_REMOTE_PROTOCOL(Symphony) + +template class SymphonyAction : public RemoteTransmitterActionBase { + public: + TEMPLATABLE_VALUE(uint32_t, data) + TEMPLATABLE_VALUE(uint8_t, nbits) + TEMPLATABLE_VALUE(uint8_t, repeats) + + void encode(RemoteTransmitData *dst, Ts... x) override { + SymphonyData data{}; + data.data = this->data_.value(x...); + data.nbits = this->nbits_.value(x...); + data.repeats = this->repeats_.value(x...); + SymphonyProtocol().encode(dst, data); + } +}; + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_receiver/__init__.py b/esphome/components/remote_receiver/__init__.py index 9095016b55..cd2b440645 100644 --- a/esphome/components/remote_receiver/__init__.py +++ b/esphome/components/remote_receiver/__init__.py @@ -5,6 +5,8 @@ from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( CONF_BUFFER_SIZE, + CONF_CARRIER_DUTY_PERCENT, + CONF_CARRIER_FREQUENCY, CONF_CLOCK_RESOLUTION, CONF_DUMP, CONF_FILTER, @@ -149,6 +151,14 @@ CONFIG_SCHEMA = remote_base.validate_triggers( ), cv.boolean, ), + cv.SplitDefault(CONF_CARRIER_DUTY_PERCENT, esp32=100): cv.All( + cv.only_on_esp32, + cv.percentage_int, + cv.Range(min=1, max=100), + ), + cv.SplitDefault(CONF_CARRIER_FREQUENCY, esp32="0Hz"): cv.All( + cv.only_on_esp32, cv.frequency, cv.int_ + ), } ) .extend(cv.COMPONENT_SCHEMA) @@ -168,6 +178,8 @@ async def to_code(config): cg.add(var.set_clock_resolution(config[CONF_CLOCK_RESOLUTION])) if CONF_FILTER_SYMBOLS in config: cg.add(var.set_filter_symbols(config[CONF_FILTER_SYMBOLS])) + cg.add(var.set_carrier_duty_percent(config[CONF_CARRIER_DUTY_PERCENT])) + cg.add(var.set_carrier_frequency(config[CONF_CARRIER_FREQUENCY])) else: var = cg.new_Pvariable(config[CONF_ID], pin) @@ -196,8 +208,8 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( PlatformFramework.ESP32_ARDUINO, PlatformFramework.ESP32_IDF, }, - "remote_receiver_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, - "remote_receiver_libretiny.cpp": { + "remote_receiver.cpp": { + PlatformFramework.ESP8266_ARDUINO, PlatformFramework.BK72XX_ARDUINO, PlatformFramework.RTL87XX_ARDUINO, PlatformFramework.LN882X_ARDUINO, diff --git a/esphome/components/remote_receiver/remote_receiver_esp8266.cpp b/esphome/components/remote_receiver/remote_receiver.cpp similarity index 97% rename from esphome/components/remote_receiver/remote_receiver_esp8266.cpp rename to esphome/components/remote_receiver/remote_receiver.cpp index b8ac29a543..a8438e20d7 100644 --- a/esphome/components/remote_receiver/remote_receiver_esp8266.cpp +++ b/esphome/components/remote_receiver/remote_receiver.cpp @@ -3,12 +3,12 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#ifdef USE_ESP8266 +#if defined(USE_LIBRETINY) || defined(USE_ESP8266) namespace esphome { namespace remote_receiver { -static const char *const TAG = "remote_receiver.esp8266"; +static const char *const TAG = "remote_receiver"; void IRAM_ATTR HOT RemoteReceiverComponentStore::gpio_intr(RemoteReceiverComponentStore *arg) { const uint32_t now = micros(); diff --git a/esphome/components/remote_receiver/remote_receiver.h b/esphome/components/remote_receiver/remote_receiver.h index 45e06e664a..3ddcf353c7 100644 --- a/esphome/components/remote_receiver/remote_receiver.h +++ b/esphome/components/remote_receiver/remote_receiver.h @@ -64,6 +64,8 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, void set_filter_symbols(uint32_t filter_symbols) { this->filter_symbols_ = filter_symbols; } void set_receive_symbols(uint32_t receive_symbols) { this->receive_symbols_ = receive_symbols; } void set_with_dma(bool with_dma) { this->with_dma_ = with_dma; } + void set_carrier_duty_percent(uint8_t carrier_duty_percent) { this->carrier_duty_percent_ = carrier_duty_percent; } + void set_carrier_frequency(uint32_t carrier_frequency) { this->carrier_frequency_ = carrier_frequency; } #endif void set_buffer_size(uint32_t buffer_size) { this->buffer_size_ = buffer_size; } void set_filter_us(uint32_t filter_us) { this->filter_us_ = filter_us; } @@ -76,6 +78,8 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, uint32_t filter_symbols_{0}; uint32_t receive_symbols_{0}; bool with_dma_{false}; + uint32_t carrier_frequency_{0}; + uint8_t carrier_duty_percent_{100}; esp_err_t error_code_{ESP_OK}; std::string error_string_{""}; #endif diff --git a/esphome/components/remote_receiver/remote_receiver_esp32.cpp b/esphome/components/remote_receiver/remote_receiver_esp32.cpp index 7e1bd3c457..49358eef3f 100644 --- a/esphome/components/remote_receiver/remote_receiver_esp32.cpp +++ b/esphome/components/remote_receiver/remote_receiver_esp32.cpp @@ -72,6 +72,21 @@ void RemoteReceiverComponent::setup() { return; } + if (this->carrier_frequency_ > 0 && 0 < this->carrier_duty_percent_ && this->carrier_duty_percent_ < 100) { + rmt_carrier_config_t carrier; + memset(&carrier, 0, sizeof(carrier)); + carrier.frequency_hz = this->carrier_frequency_; + carrier.duty_cycle = (float) this->carrier_duty_percent_ / 100.0f; + carrier.flags.polarity_active_low = this->pin_->is_inverted(); + error = rmt_apply_carrier(this->channel_, &carrier); + if (error != ESP_OK) { + this->error_code_ = error; + this->error_string_ = "in rmt_apply_carrier"; + this->mark_failed(); + return; + } + } + rmt_rx_event_callbacks_t callbacks; memset(&callbacks, 0, sizeof(callbacks)); callbacks.on_recv_done = rmt_callback; @@ -111,11 +126,13 @@ void RemoteReceiverComponent::dump_config() { " Filter symbols: %" PRIu32 "\n" " Receive symbols: %" PRIu32 "\n" " Tolerance: %" PRIu32 "%s\n" + " Carrier frequency: %" PRIu32 " hz\n" + " Carrier duty: %u%%\n" " Filter out pulses shorter than: %" PRIu32 " us\n" " Signal is done after %" PRIu32 " us of no changes", this->clock_resolution_, this->rmt_symbols_, this->filter_symbols_, this->receive_symbols_, this->tolerance_, (this->tolerance_mode_ == remote_base::TOLERANCE_MODE_TIME) ? " us" : "%", - this->filter_us_, this->idle_us_); + this->carrier_frequency_, this->carrier_duty_percent_, this->filter_us_, this->idle_us_); if (this->is_failed()) { ESP_LOGE(TAG, "Configuring RMT driver failed: %s (%s)", esp_err_to_name(this->error_code_), this->error_string_.c_str()); diff --git a/esphome/components/remote_receiver/remote_receiver_libretiny.cpp b/esphome/components/remote_receiver/remote_receiver_libretiny.cpp deleted file mode 100644 index 8d801b37d2..0000000000 --- a/esphome/components/remote_receiver/remote_receiver_libretiny.cpp +++ /dev/null @@ -1,125 +0,0 @@ -#include "remote_receiver.h" -#include "esphome/core/hal.h" -#include "esphome/core/helpers.h" -#include "esphome/core/log.h" - -#ifdef USE_LIBRETINY - -namespace esphome { -namespace remote_receiver { - -static const char *const TAG = "remote_receiver.libretiny"; - -void IRAM_ATTR HOT RemoteReceiverComponentStore::gpio_intr(RemoteReceiverComponentStore *arg) { - const uint32_t now = micros(); - // If the lhs is 1 (rising edge) we should write to an uneven index and vice versa - const uint32_t next = (arg->buffer_write_at + 1) % arg->buffer_size; - const bool level = arg->pin.digital_read(); - if (level != next % 2) - return; - - // If next is buffer_read, we have hit an overflow - if (next == arg->buffer_read_at) - return; - - const uint32_t last_change = arg->buffer[arg->buffer_write_at]; - const uint32_t time_since_change = now - last_change; - if (time_since_change <= arg->filter_us) - return; - - arg->buffer[arg->buffer_write_at = next] = now; -} - -void RemoteReceiverComponent::setup() { - this->pin_->setup(); - auto &s = this->store_; - s.filter_us = this->filter_us_; - s.pin = this->pin_->to_isr(); - s.buffer_size = this->buffer_size_; - - this->high_freq_.start(); - if (s.buffer_size % 2 != 0) { - // Make sure divisible by two. This way, we know that every 0bxxx0 index is a space and every 0bxxx1 index is a mark - s.buffer_size++; - } - - s.buffer = new uint32_t[s.buffer_size]; - void *buf = (void *) s.buffer; - memset(buf, 0, s.buffer_size * sizeof(uint32_t)); - - // First index is a space. - if (this->pin_->digital_read()) { - s.buffer_write_at = s.buffer_read_at = 1; - } else { - s.buffer_write_at = s.buffer_read_at = 0; - } - this->pin_->attach_interrupt(RemoteReceiverComponentStore::gpio_intr, &this->store_, gpio::INTERRUPT_ANY_EDGE); -} -void RemoteReceiverComponent::dump_config() { - ESP_LOGCONFIG(TAG, "Remote Receiver:"); - LOG_PIN(" Pin: ", this->pin_); - if (this->pin_->digital_read()) { - ESP_LOGW(TAG, "Remote Receiver Signal starts with a HIGH value. Usually this means you have to " - "invert the signal using 'inverted: True' in the pin schema!"); - } - ESP_LOGCONFIG(TAG, - " Buffer Size: %u\n" - " Tolerance: %u%s\n" - " Filter out pulses shorter than: %u us\n" - " Signal is done after %u us of no changes", - this->buffer_size_, this->tolerance_, - (this->tolerance_mode_ == remote_base::TOLERANCE_MODE_TIME) ? " us" : "%", this->filter_us_, - this->idle_us_); -} - -void RemoteReceiverComponent::loop() { - auto &s = this->store_; - - // copy write at to local variables, as it's volatile - const uint32_t write_at = s.buffer_write_at; - const uint32_t dist = (s.buffer_size + write_at - s.buffer_read_at) % s.buffer_size; - // signals must at least one rising and one leading edge - if (dist <= 1) - return; - const uint32_t now = micros(); - if (now - s.buffer[write_at] < this->idle_us_) { - // The last change was fewer than the configured idle time ago. - return; - } - - ESP_LOGVV(TAG, "read_at=%u write_at=%u dist=%u now=%u end=%u", s.buffer_read_at, write_at, dist, now, - s.buffer[write_at]); - - // Skip first value, it's from the previous idle level - s.buffer_read_at = (s.buffer_read_at + 1) % s.buffer_size; - uint32_t prev = s.buffer_read_at; - s.buffer_read_at = (s.buffer_read_at + 1) % s.buffer_size; - const uint32_t reserve_size = 1 + (s.buffer_size + write_at - s.buffer_read_at) % s.buffer_size; - this->temp_.clear(); - this->temp_.reserve(reserve_size); - int32_t multiplier = s.buffer_read_at % 2 == 0 ? 1 : -1; - - for (uint32_t i = 0; prev != write_at; i++) { - int32_t delta = s.buffer[s.buffer_read_at] - s.buffer[prev]; - if (uint32_t(delta) >= this->idle_us_) { - // already found a space longer than idle. There must have been two pulses - break; - } - - ESP_LOGVV(TAG, " i=%u buffer[%u]=%u - buffer[%u]=%u -> %d", i, s.buffer_read_at, s.buffer[s.buffer_read_at], prev, - s.buffer[prev], multiplier * delta); - this->temp_.push_back(multiplier * delta); - prev = s.buffer_read_at; - s.buffer_read_at = (s.buffer_read_at + 1) % s.buffer_size; - multiplier *= -1; - } - s.buffer_read_at = (s.buffer_size + s.buffer_read_at - 1) % s.buffer_size; - this->temp_.push_back(this->idle_us_ * multiplier); - - this->call_listeners_dumpers_(); -} - -} // namespace remote_receiver -} // namespace esphome - -#endif diff --git a/esphome/components/remote_transmitter/__init__.py b/esphome/components/remote_transmitter/__init__.py index 47a46ff56b..faa6c827f7 100644 --- a/esphome/components/remote_transmitter/__init__.py +++ b/esphome/components/remote_transmitter/__init__.py @@ -1,3 +1,5 @@ +import logging + from esphome import automation, pins import esphome.codegen as cg from esphome.components import esp32, esp32_rmt, remote_base @@ -13,20 +15,30 @@ from esphome.const import ( CONF_PIN, CONF_RMT_SYMBOLS, CONF_USE_DMA, + CONF_VALUE, PlatformFramework, ) from esphome.core import CORE +_LOGGER = logging.getLogger(__name__) + AUTO_LOAD = ["remote_base"] CONF_EOT_LEVEL = "eot_level" +CONF_NON_BLOCKING = "non_blocking" CONF_ON_TRANSMIT = "on_transmit" CONF_ON_COMPLETE = "on_complete" +CONF_TRANSMITTER_ID = remote_base.CONF_TRANSMITTER_ID remote_transmitter_ns = cg.esphome_ns.namespace("remote_transmitter") RemoteTransmitterComponent = remote_transmitter_ns.class_( "RemoteTransmitterComponent", remote_base.RemoteTransmitterBase, cg.Component ) +DigitalWriteAction = remote_transmitter_ns.class_( + "DigitalWriteAction", + automation.Action, + cg.Parented.template(RemoteTransmitterComponent), +) MULTI_CONF = True CONFIG_SCHEMA = cv.Schema( @@ -58,17 +70,51 @@ CONFIG_SCHEMA = cv.Schema( esp32_c6=48, esp32_h2=48, ): cv.All(cv.only_on_esp32, cv.int_range(min=2)), + cv.Optional(CONF_NON_BLOCKING): cv.All(cv.only_on_esp32, cv.boolean), cv.Optional(CONF_ON_TRANSMIT): automation.validate_automation(single=True), cv.Optional(CONF_ON_COMPLETE): automation.validate_automation(single=True), } ).extend(cv.COMPONENT_SCHEMA) +def _validate_non_blocking(config): + if CORE.is_esp32 and CONF_NON_BLOCKING not in config: + _LOGGER.warning( + "'non_blocking' is not set for 'remote_transmitter' and will default to 'true'.\n" + "The default behavior changed in 2025.11.0; previously blocking mode was used.\n" + "To silence this warning, explicitly set 'non_blocking: true' (or 'false')." + ) + config[CONF_NON_BLOCKING] = True + + +FINAL_VALIDATE_SCHEMA = _validate_non_blocking + +DIGITAL_WRITE_ACTION_SCHEMA = cv.maybe_simple_value( + { + cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(RemoteTransmitterComponent), + cv.Required(CONF_VALUE): cv.templatable(cv.boolean), + }, + key=CONF_VALUE, +) + + +@automation.register_action( + "remote_transmitter.digital_write", DigitalWriteAction, DIGITAL_WRITE_ACTION_SCHEMA +) +async def digital_write_action_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_TRANSMITTER_ID]) + template_ = await cg.templatable(config[CONF_VALUE], args, bool) + cg.add(var.set_value(template_)) + return var + + async def to_code(config): pin = await cg.gpio_pin_expression(config[CONF_PIN]) if CORE.is_esp32: var = cg.new_Pvariable(config[CONF_ID], pin) cg.add(var.set_rmt_symbols(config[CONF_RMT_SYMBOLS])) + cg.add(var.set_non_blocking(config[CONF_NON_BLOCKING])) if CONF_CLOCK_RESOLUTION in config: cg.add(var.set_clock_resolution(config[CONF_CLOCK_RESOLUTION])) if CONF_USE_DMA in config: @@ -105,8 +151,8 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( PlatformFramework.ESP32_ARDUINO, PlatformFramework.ESP32_IDF, }, - "remote_transmitter_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, - "remote_transmitter_libretiny.cpp": { + "remote_transmitter.cpp": { + PlatformFramework.ESP8266_ARDUINO, PlatformFramework.BK72XX_ARDUINO, PlatformFramework.RTL87XX_ARDUINO, PlatformFramework.LN882X_ARDUINO, diff --git a/esphome/components/remote_transmitter/automation.h b/esphome/components/remote_transmitter/automation.h new file mode 100644 index 0000000000..bee1d0be8a --- /dev/null +++ b/esphome/components/remote_transmitter/automation.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/remote_transmitter/remote_transmitter.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace remote_transmitter { + +template class DigitalWriteAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(bool, value) + void play(const Ts &...x) override { this->parent_->digital_write(this->value_.value(x...)); } +}; + +} // namespace remote_transmitter +} // namespace esphome diff --git a/esphome/components/remote_transmitter/remote_transmitter.cpp b/esphome/components/remote_transmitter/remote_transmitter.cpp index 425418ff39..347e9d9d33 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter.cpp @@ -2,10 +2,113 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" +#if defined(USE_LIBRETINY) || defined(USE_ESP8266) + namespace esphome { namespace remote_transmitter { static const char *const TAG = "remote_transmitter"; +void RemoteTransmitterComponent::setup() { + this->pin_->setup(); + this->pin_->digital_write(false); +} + +void RemoteTransmitterComponent::dump_config() { + ESP_LOGCONFIG(TAG, + "Remote Transmitter:\n" + " Carrier Duty: %u%%", + this->carrier_duty_percent_); + LOG_PIN(" Pin: ", this->pin_); +} + +void RemoteTransmitterComponent::calculate_on_off_time_(uint32_t carrier_frequency, uint32_t *on_time_period, + uint32_t *off_time_period) { + if (carrier_frequency == 0) { + *on_time_period = 0; + *off_time_period = 0; + return; + } + uint32_t period = (1000000UL + carrier_frequency / 2) / carrier_frequency; // round(1000000/freq) + period = std::max(uint32_t(1), period); + *on_time_period = (period * this->carrier_duty_percent_) / 100; + *off_time_period = period - *on_time_period; +} + +void RemoteTransmitterComponent::await_target_time_() { + const uint32_t current_time = micros(); + if (this->target_time_ == 0) { + this->target_time_ = current_time; + } else if ((int32_t) (this->target_time_ - current_time) > 0) { +#if defined(USE_LIBRETINY) + // busy loop for libretiny is required (see the comment inside micros() in wiring.c) + while ((int32_t) (this->target_time_ - micros()) > 0) + ; +#else + delayMicroseconds(this->target_time_ - current_time); +#endif + } +} + +void RemoteTransmitterComponent::mark_(uint32_t on_time, uint32_t off_time, uint32_t usec) { + this->await_target_time_(); + this->pin_->digital_write(true); + + const uint32_t target = this->target_time_ + usec; + if (this->carrier_duty_percent_ < 100 && (on_time > 0 || off_time > 0)) { + while (true) { // Modulate with carrier frequency + this->target_time_ += on_time; + if ((int32_t) (this->target_time_ - target) >= 0) + break; + this->await_target_time_(); + this->pin_->digital_write(false); + + this->target_time_ += off_time; + if ((int32_t) (this->target_time_ - target) >= 0) + break; + this->await_target_time_(); + this->pin_->digital_write(true); + } + } + this->target_time_ = target; +} + +void RemoteTransmitterComponent::space_(uint32_t usec) { + this->await_target_time_(); + this->pin_->digital_write(false); + this->target_time_ += usec; +} + +void RemoteTransmitterComponent::digital_write(bool value) { this->pin_->digital_write(value); } + +void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) { + ESP_LOGD(TAG, "Sending remote code"); + uint32_t on_time, off_time; + this->calculate_on_off_time_(this->temp_.get_carrier_frequency(), &on_time, &off_time); + this->target_time_ = 0; + this->transmit_trigger_->trigger(); + for (uint32_t i = 0; i < send_times; i++) { + InterruptLock lock; + for (int32_t item : this->temp_.get_data()) { + if (item > 0) { + const auto length = uint32_t(item); + this->mark_(on_time, off_time, length); + } else { + const auto length = uint32_t(-item); + this->space_(length); + } + App.feed_wdt(); + } + this->await_target_time_(); // wait for duration of last pulse + this->pin_->digital_write(false); + + if (i + 1 < send_times) + this->target_time_ += send_wait; + } + this->complete_trigger_->trigger(); +} + } // namespace remote_transmitter } // namespace esphome + +#endif diff --git a/esphome/components/remote_transmitter/remote_transmitter.h b/esphome/components/remote_transmitter/remote_transmitter.h index f0dab2aaf8..cc3b82ad61 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.h +++ b/esphome/components/remote_transmitter/remote_transmitter.h @@ -12,6 +12,25 @@ namespace esphome { namespace remote_transmitter { +#ifdef USE_ESP32 +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) +// IDF version 5.5.1 and above is required because of a bug in +// the RMT encoder: https://github.com/espressif/esp-idf/issues/17244 +typedef union { // NOLINT(modernize-use-using) + struct { + uint16_t duration : 15; + uint16_t level : 1; + }; + uint16_t val; +} rmt_symbol_half_t; + +struct RemoteTransmitterComponentStore { + uint32_t times{0}; + uint32_t index{0}; +}; +#endif +#endif + class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, public Component #ifdef USE_ESP32 @@ -30,10 +49,12 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, void set_carrier_duty_percent(uint8_t carrier_duty_percent) { this->carrier_duty_percent_ = carrier_duty_percent; } + void digital_write(bool value); + #if defined(USE_ESP32) void set_with_dma(bool with_dma) { this->with_dma_ = with_dma; } void set_eot_level(bool eot_level) { this->eot_level_ = eot_level; } - void digital_write(bool value); + void set_non_blocking(bool non_blocking) { this->non_blocking_ = non_blocking; } #endif Trigger<> *get_transmit_trigger() const { return this->transmit_trigger_; }; @@ -54,10 +75,16 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, #ifdef USE_ESP32 void configure_rmt_(); + void wait_for_rmt_(); +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) + RemoteTransmitterComponentStore store_{}; + std::vector rmt_temp_; +#else + std::vector rmt_temp_; +#endif uint32_t current_carrier_frequency_{38000}; bool initialized_{false}; - std::vector rmt_temp_; bool with_dma_{false}; bool eot_level_{false}; rmt_channel_handle_t channel_{NULL}; @@ -65,6 +92,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, esp_err_t error_code_{ESP_OK}; std::string error_string_{""}; bool inverted_{false}; + bool non_blocking_{false}; #endif uint8_t carrier_duty_percent_; diff --git a/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp b/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp index 119aa81e7e..59c85c99a8 100644 --- a/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp @@ -10,6 +10,46 @@ namespace remote_transmitter { static const char *const TAG = "remote_transmitter"; +// Maximum RMT symbol duration (15-bit field) +static constexpr uint32_t RMT_SYMBOL_DURATION_MAX = 0x7FFF; + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) +static size_t IRAM_ATTR HOT encoder_callback(const void *data, size_t size, size_t written, size_t free, + rmt_symbol_word_t *symbols, bool *done, void *arg) { + auto *store = static_cast(arg); + const auto *encoded = static_cast(data); + size_t length = size / sizeof(rmt_symbol_half_t); + size_t count = 0; + + // copy symbols + for (size_t i = 0; i < free; i++) { + uint16_t sym_0 = encoded[store->index++].val; + if (store->index >= length) { + store->index = 0; + store->times--; + if (store->times == 0) { + *done = true; + symbols[count++].val = sym_0; + return count; + } + } + uint16_t sym_1 = encoded[store->index++].val; + if (store->index >= length) { + store->index = 0; + store->times--; + if (store->times == 0) { + *done = true; + symbols[count++].val = sym_0 | (sym_1 << 16); + return count; + } + } + symbols[count++].val = sym_0 | (sym_1 << 16); + } + *done = false; + return count; +} +#endif + void RemoteTransmitterComponent::setup() { this->inverted_ = this->pin_->is_inverted(); this->configure_rmt_(); @@ -34,6 +74,17 @@ void RemoteTransmitterComponent::dump_config() { } void RemoteTransmitterComponent::digital_write(bool value) { +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) + rmt_symbol_half_t symbol = { + .duration = 1, + .level = value, + }; + rmt_transmit_config_t config; + memset(&config, 0, sizeof(config)); + config.flags.eot_level = value; + this->store_.times = 1; + this->store_.index = 0; +#else rmt_symbol_word_t symbol = { .duration0 = 1, .level0 = value, @@ -42,8 +93,8 @@ void RemoteTransmitterComponent::digital_write(bool value) { }; rmt_transmit_config_t config; memset(&config, 0, sizeof(config)); - config.loop_count = 0; config.flags.eot_level = value; +#endif esp_err_t error = rmt_transmit(this->channel_, this->encoder_, &symbol, sizeof(symbol), &config); if (error != ESP_OK) { ESP_LOGW(TAG, "rmt_transmit failed: %s", esp_err_to_name(error)); @@ -90,6 +141,20 @@ void RemoteTransmitterComponent::configure_rmt_() { gpio_pullup_dis(gpio_num_t(this->pin_->get_pin())); } +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) + rmt_simple_encoder_config_t encoder; + memset(&encoder, 0, sizeof(encoder)); + encoder.callback = encoder_callback; + encoder.arg = &this->store_; + encoder.min_chunk_size = 1; + error = rmt_new_simple_encoder(&encoder, &this->encoder_); + if (error != ESP_OK) { + this->error_code_ = error; + this->error_string_ = "in rmt_new_simple_encoder"; + this->mark_failed(); + return; + } +#else rmt_copy_encoder_config_t encoder; memset(&encoder, 0, sizeof(encoder)); error = rmt_new_copy_encoder(&encoder, &this->encoder_); @@ -99,6 +164,7 @@ void RemoteTransmitterComponent::configure_rmt_() { this->mark_failed(); return; } +#endif error = rmt_enable(this->channel_); if (error != ESP_OK) { @@ -130,6 +196,97 @@ void RemoteTransmitterComponent::configure_rmt_() { } } +void RemoteTransmitterComponent::wait_for_rmt_() { + esp_err_t error = rmt_tx_wait_all_done(this->channel_, -1); + if (error != ESP_OK) { + ESP_LOGW(TAG, "rmt_tx_wait_all_done failed: %s", esp_err_to_name(error)); + this->status_set_warning(); + } + + this->complete_trigger_->trigger(); +} + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) +void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) { + uint64_t total_duration = 0; + + if (this->is_failed()) { + return; + } + + // if the timeout was cancelled, block until the tx is complete + if (this->non_blocking_ && this->cancel_timeout("complete")) { + this->wait_for_rmt_(); + } + + if (this->current_carrier_frequency_ != this->temp_.get_carrier_frequency()) { + this->current_carrier_frequency_ = this->temp_.get_carrier_frequency(); + this->configure_rmt_(); + } + + this->rmt_temp_.clear(); + this->rmt_temp_.reserve(this->temp_.get_data().size() + 1); + + // encode any delay at the start of the buffer to simplify the encoder callback + // this will be skipped the first time around + total_duration += send_wait * (send_times - 1); + send_wait = this->from_microseconds_(static_cast(send_wait)); + while (send_wait > 0) { + int32_t duration = std::min(send_wait, uint32_t(RMT_SYMBOL_DURATION_MAX)); + this->rmt_temp_.push_back({ + .duration = static_cast(duration), + .level = static_cast(this->eot_level_), + }); + send_wait -= duration; + } + + // encode data + size_t offset = this->rmt_temp_.size(); + for (int32_t value : this->temp_.get_data()) { + bool level = value >= 0; + if (!level) { + value = -value; + } + total_duration += value * send_times; + value = this->from_microseconds_(static_cast(value)); + while (value > 0) { + int32_t duration = std::min(value, int32_t(RMT_SYMBOL_DURATION_MAX)); + this->rmt_temp_.push_back({ + .duration = static_cast(duration), + .level = static_cast(level ^ this->inverted_), + }); + value -= duration; + } + } + + if ((this->rmt_temp_.data() == nullptr) || this->rmt_temp_.size() <= offset) { + ESP_LOGE(TAG, "Empty data"); + return; + } + + this->transmit_trigger_->trigger(); + + rmt_transmit_config_t config; + memset(&config, 0, sizeof(config)); + config.flags.eot_level = this->eot_level_; + this->store_.times = send_times; + this->store_.index = offset; + esp_err_t error = rmt_transmit(this->channel_, this->encoder_, this->rmt_temp_.data(), + this->rmt_temp_.size() * sizeof(rmt_symbol_half_t), &config); + if (error != ESP_OK) { + ESP_LOGW(TAG, "rmt_transmit failed: %s", esp_err_to_name(error)); + this->status_set_warning(); + } else { + this->status_clear_warning(); + } + + if (this->non_blocking_) { + this->set_timeout("complete", total_duration / 1000, [this]() { this->wait_for_rmt_(); }); + } else { + this->wait_for_rmt_(); + } +} +#else void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) { if (this->is_failed()) return; @@ -151,7 +308,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen val = this->from_microseconds_(static_cast(val)); do { - int32_t item = std::min(val, int32_t(32767)); + int32_t item = std::min(val, int32_t(RMT_SYMBOL_DURATION_MAX)); val -= item; if (rmt_i % 2 == 0) { @@ -180,7 +337,6 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen for (uint32_t i = 0; i < send_times; i++) { rmt_transmit_config_t config; memset(&config, 0, sizeof(config)); - config.loop_count = 0; config.flags.eot_level = this->eot_level_; esp_err_t error = rmt_transmit(this->channel_, this->encoder_, this->rmt_temp_.data(), this->rmt_temp_.size() * sizeof(rmt_symbol_word_t), &config); @@ -200,6 +356,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen } this->complete_trigger_->trigger(); } +#endif } // namespace remote_transmitter } // namespace esphome diff --git a/esphome/components/remote_transmitter/remote_transmitter_esp8266.cpp b/esphome/components/remote_transmitter/remote_transmitter_esp8266.cpp deleted file mode 100644 index 73a1a7754f..0000000000 --- a/esphome/components/remote_transmitter/remote_transmitter_esp8266.cpp +++ /dev/null @@ -1,105 +0,0 @@ -#include "remote_transmitter.h" -#include "esphome/core/log.h" -#include "esphome/core/application.h" - -#ifdef USE_ESP8266 - -namespace esphome { -namespace remote_transmitter { - -static const char *const TAG = "remote_transmitter"; - -void RemoteTransmitterComponent::setup() { - this->pin_->setup(); - this->pin_->digital_write(false); -} - -void RemoteTransmitterComponent::dump_config() { - ESP_LOGCONFIG(TAG, - "Remote Transmitter:\n" - " Carrier Duty: %u%%", - this->carrier_duty_percent_); - LOG_PIN(" Pin: ", this->pin_); -} - -void RemoteTransmitterComponent::calculate_on_off_time_(uint32_t carrier_frequency, uint32_t *on_time_period, - uint32_t *off_time_period) { - if (carrier_frequency == 0) { - *on_time_period = 0; - *off_time_period = 0; - return; - } - uint32_t period = (1000000UL + carrier_frequency / 2) / carrier_frequency; // round(1000000/freq) - period = std::max(uint32_t(1), period); - *on_time_period = (period * this->carrier_duty_percent_) / 100; - *off_time_period = period - *on_time_period; -} - -void RemoteTransmitterComponent::await_target_time_() { - const uint32_t current_time = micros(); - if (this->target_time_ == 0) { - this->target_time_ = current_time; - } else if ((int32_t) (this->target_time_ - current_time) > 0) { - delayMicroseconds(this->target_time_ - current_time); - } -} - -void RemoteTransmitterComponent::mark_(uint32_t on_time, uint32_t off_time, uint32_t usec) { - this->await_target_time_(); - this->pin_->digital_write(true); - - const uint32_t target = this->target_time_ + usec; - if (this->carrier_duty_percent_ < 100 && (on_time > 0 || off_time > 0)) { - while (true) { // Modulate with carrier frequency - this->target_time_ += on_time; - if ((int32_t) (this->target_time_ - target) >= 0) - break; - this->await_target_time_(); - this->pin_->digital_write(false); - - this->target_time_ += off_time; - if ((int32_t) (this->target_time_ - target) >= 0) - break; - this->await_target_time_(); - this->pin_->digital_write(true); - } - } - this->target_time_ = target; -} - -void RemoteTransmitterComponent::space_(uint32_t usec) { - this->await_target_time_(); - this->pin_->digital_write(false); - this->target_time_ += usec; -} - -void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) { - ESP_LOGD(TAG, "Sending remote code"); - uint32_t on_time, off_time; - this->calculate_on_off_time_(this->temp_.get_carrier_frequency(), &on_time, &off_time); - this->target_time_ = 0; - this->transmit_trigger_->trigger(); - for (uint32_t i = 0; i < send_times; i++) { - for (int32_t item : this->temp_.get_data()) { - if (item > 0) { - const auto length = uint32_t(item); - this->mark_(on_time, off_time, length); - } else { - const auto length = uint32_t(-item); - this->space_(length); - } - App.feed_wdt(); - } - this->await_target_time_(); // wait for duration of last pulse - this->pin_->digital_write(false); - - if (i + 1 < send_times) - this->target_time_ += send_wait; - } - this->complete_trigger_->trigger(); -} - -} // namespace remote_transmitter -} // namespace esphome - -#endif diff --git a/esphome/components/remote_transmitter/remote_transmitter_libretiny.cpp b/esphome/components/remote_transmitter/remote_transmitter_libretiny.cpp deleted file mode 100644 index 42bf5bd95b..0000000000 --- a/esphome/components/remote_transmitter/remote_transmitter_libretiny.cpp +++ /dev/null @@ -1,108 +0,0 @@ -#include "remote_transmitter.h" -#include "esphome/core/log.h" -#include "esphome/core/application.h" - -#ifdef USE_LIBRETINY - -namespace esphome { -namespace remote_transmitter { - -static const char *const TAG = "remote_transmitter"; - -void RemoteTransmitterComponent::setup() { - this->pin_->setup(); - this->pin_->digital_write(false); -} - -void RemoteTransmitterComponent::dump_config() { - ESP_LOGCONFIG(TAG, - "Remote Transmitter:\n" - " Carrier Duty: %u%%", - this->carrier_duty_percent_); - LOG_PIN(" Pin: ", this->pin_); -} - -void RemoteTransmitterComponent::calculate_on_off_time_(uint32_t carrier_frequency, uint32_t *on_time_period, - uint32_t *off_time_period) { - if (carrier_frequency == 0) { - *on_time_period = 0; - *off_time_period = 0; - return; - } - uint32_t period = (1000000UL + carrier_frequency / 2) / carrier_frequency; // round(1000000/freq) - period = std::max(uint32_t(1), period); - *on_time_period = (period * this->carrier_duty_percent_) / 100; - *off_time_period = period - *on_time_period; -} - -void RemoteTransmitterComponent::await_target_time_() { - const uint32_t current_time = micros(); - if (this->target_time_ == 0) { - this->target_time_ = current_time; - } else { - while ((int32_t) (this->target_time_ - micros()) > 0) { - // busy loop that ensures micros is constantly called - } - } -} - -void RemoteTransmitterComponent::mark_(uint32_t on_time, uint32_t off_time, uint32_t usec) { - this->await_target_time_(); - this->pin_->digital_write(true); - - const uint32_t target = this->target_time_ + usec; - if (this->carrier_duty_percent_ < 100 && (on_time > 0 || off_time > 0)) { - while (true) { // Modulate with carrier frequency - this->target_time_ += on_time; - if ((int32_t) (this->target_time_ - target) >= 0) - break; - this->await_target_time_(); - this->pin_->digital_write(false); - - this->target_time_ += off_time; - if ((int32_t) (this->target_time_ - target) >= 0) - break; - this->await_target_time_(); - this->pin_->digital_write(true); - } - } - this->target_time_ = target; -} - -void RemoteTransmitterComponent::space_(uint32_t usec) { - this->await_target_time_(); - this->pin_->digital_write(false); - this->target_time_ += usec; -} - -void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) { - ESP_LOGD(TAG, "Sending remote code"); - uint32_t on_time, off_time; - this->calculate_on_off_time_(this->temp_.get_carrier_frequency(), &on_time, &off_time); - this->target_time_ = 0; - this->transmit_trigger_->trigger(); - for (uint32_t i = 0; i < send_times; i++) { - InterruptLock lock; - for (int32_t item : this->temp_.get_data()) { - if (item > 0) { - const auto length = uint32_t(item); - this->mark_(on_time, off_time, length); - } else { - const auto length = uint32_t(-item); - this->space_(length); - } - App.feed_wdt(); - } - this->await_target_time_(); // wait for duration of last pulse - this->pin_->digital_write(false); - - if (i + 1 < send_times) - this->target_time_ += send_wait; - } - this->complete_trigger_->trigger(); -} - -} // namespace remote_transmitter -} // namespace esphome - -#endif diff --git a/esphome/components/resampler/speaker/resampler_speaker.cpp b/esphome/components/resampler/speaker/resampler_speaker.cpp index 5e5615cbb9..ad61aca084 100644 --- a/esphome/components/resampler/speaker/resampler_speaker.cpp +++ b/esphome/components/resampler/speaker/resampler_speaker.cpp @@ -66,17 +66,17 @@ void ResamplerSpeaker::loop() { } if (event_group_bits & ResamplingEventGroupBits::ERR_ESP_NO_MEM) { - this->status_set_error("Resampler task failed to allocate the internal buffers"); + this->status_set_error(LOG_STR("Resampler task failed to allocate the internal buffers")); xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ERR_ESP_NO_MEM); this->state_ = speaker::STATE_STOPPING; } if (event_group_bits & ResamplingEventGroupBits::ERR_ESP_NOT_SUPPORTED) { - this->status_set_error("Cannot resample due to an unsupported audio stream"); + this->status_set_error(LOG_STR("Cannot resample due to an unsupported audio stream")); xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ERR_ESP_NOT_SUPPORTED); this->state_ = speaker::STATE_STOPPING; } if (event_group_bits & ResamplingEventGroupBits::ERR_ESP_FAIL) { - this->status_set_error("Resampler task failed"); + this->status_set_error(LOG_STR("Resampler task failed")); xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ERR_ESP_FAIL); this->state_ = speaker::STATE_STOPPING; } @@ -106,12 +106,12 @@ void ResamplerSpeaker::loop() { } else { switch (err) { case ESP_ERR_INVALID_STATE: - this->status_set_error("Failed to start resampler: resampler task failed to start"); + this->status_set_error(LOG_STR("Failed to start resampler: resampler task failed to start")); break; case ESP_ERR_NO_MEM: - this->status_set_error("Failed to start resampler: not enough memory for task stack"); + this->status_set_error(LOG_STR("Failed to start resampler: not enough memory for task stack")); default: - this->status_set_error("Failed to start resampler"); + this->status_set_error(LOG_STR("Failed to start resampler")); break; } diff --git a/esphome/components/rf_bridge/rf_bridge.h b/esphome/components/rf_bridge/rf_bridge.h index fe6dd96b38..d2f75c819d 100644 --- a/esphome/components/rf_bridge/rf_bridge.h +++ b/esphome/components/rf_bridge/rf_bridge.h @@ -98,7 +98,7 @@ template class RFBridgeSendCodeAction : public Action { TEMPLATABLE_VALUE(uint16_t, high) TEMPLATABLE_VALUE(uint32_t, code) - void play(Ts... x) { + void play(const Ts &...x) { RFBridgeData data{}; data.sync = this->sync_.value(x...); data.low = this->low_.value(x...); @@ -118,7 +118,7 @@ template class RFBridgeSendAdvancedCodeAction : public Actionlength_.value(x...); data.protocol = this->protocol_.value(x...); @@ -134,7 +134,7 @@ template class RFBridgeLearnAction : public Action { public: RFBridgeLearnAction(RFBridgeComponent *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->learn(); } + void play(const Ts &...x) { this->parent_->learn(); } protected: RFBridgeComponent *parent_; @@ -144,7 +144,7 @@ template class RFBridgeStartAdvancedSniffingAction : public Acti public: RFBridgeStartAdvancedSniffingAction(RFBridgeComponent *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->start_advanced_sniffing(); } + void play(const Ts &...x) { this->parent_->start_advanced_sniffing(); } protected: RFBridgeComponent *parent_; @@ -154,7 +154,7 @@ template class RFBridgeStopAdvancedSniffingAction : public Actio public: RFBridgeStopAdvancedSniffingAction(RFBridgeComponent *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->stop_advanced_sniffing(); } + void play(const Ts &...x) { this->parent_->stop_advanced_sniffing(); } protected: RFBridgeComponent *parent_; @@ -164,7 +164,7 @@ template class RFBridgeStartBucketSniffingAction : public Action public: RFBridgeStartBucketSniffingAction(RFBridgeComponent *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->start_bucket_sniffing(); } + void play(const Ts &...x) { this->parent_->start_bucket_sniffing(); } protected: RFBridgeComponent *parent_; @@ -175,7 +175,7 @@ template class RFBridgeSendRawAction : public Action { RFBridgeSendRawAction(RFBridgeComponent *parent) : parent_(parent) {} TEMPLATABLE_VALUE(std::string, raw) - void play(Ts... x) { this->parent_->send_raw(this->raw_.value(x...)); } + void play(const Ts &...x) { this->parent_->send_raw(this->raw_.value(x...)); } protected: RFBridgeComponent *parent_; @@ -186,7 +186,7 @@ template class RFBridgeBeepAction : public Action { RFBridgeBeepAction(RFBridgeComponent *parent) : parent_(parent) {} TEMPLATABLE_VALUE(uint16_t, duration) - void play(Ts... x) { this->parent_->beep(this->duration_.value(x...)); } + void play(const Ts &...x) { this->parent_->beep(this->duration_.value(x...)); } protected: RFBridgeComponent *parent_; diff --git a/esphome/components/rotary_encoder/rotary_encoder.cpp b/esphome/components/rotary_encoder/rotary_encoder.cpp index 20ea8d0293..26e20664f2 100644 --- a/esphome/components/rotary_encoder/rotary_encoder.cpp +++ b/esphome/components/rotary_encoder/rotary_encoder.cpp @@ -132,7 +132,7 @@ void RotaryEncoderSensor::setup() { int32_t initial_value = 0; switch (this->restore_mode_) { case ROTARY_ENCODER_RESTORE_DEFAULT_ZERO: - this->rtc_ = global_preferences->make_preference(this->get_object_id_hash()); + this->rtc_ = global_preferences->make_preference(this->get_preference_hash()); if (!this->rtc_.load(&initial_value)) { initial_value = 0; } diff --git a/esphome/components/rotary_encoder/rotary_encoder.h b/esphome/components/rotary_encoder/rotary_encoder.h index e88ee9152a..14442f0565 100644 --- a/esphome/components/rotary_encoder/rotary_encoder.h +++ b/esphome/components/rotary_encoder/rotary_encoder.h @@ -114,7 +114,7 @@ template class RotaryEncoderSetValueAction : public Actionencoder_->set_value(this->value_.value(x...)); } + void play(const Ts &...x) override { this->encoder_->set_value(this->value_.value(x...)); } protected: RotaryEncoderSensor *encoder_; diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index 46eabb5325..3a1ea16fa3 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -1,5 +1,5 @@ import logging -import os +from pathlib import Path from string import ascii_letters, digits import esphome.codegen as cg @@ -18,8 +18,8 @@ from esphome.const import ( PLATFORM_RP2040, ThreadModel, ) -from esphome.core import CORE, EsphomeError, coroutine_with_priority -from esphome.helpers import copy_file_if_changed, mkdir_p, read_file, write_file +from esphome.core import CORE, CoroPriority, EsphomeError, coroutine_with_priority +from esphome.helpers import copy_file_if_changed, read_file, write_file_if_changed from .const import KEY_BOARD, KEY_PIO_FILES, KEY_RP2040, rp2040_ns @@ -159,7 +159,7 @@ CONFIG_SCHEMA = cv.All( ) -@coroutine_with_priority(1000) +@coroutine_with_priority(CoroPriority.PLATFORM) async def to_code(config): cg.add(rp2040_ns.setup_preferences()) @@ -221,18 +221,18 @@ def generate_pio_files() -> bool: if not files: return False for key, data in files.items(): - pio_path = CORE.relative_build_path(f"src/pio/{key}.pio") - mkdir_p(os.path.dirname(pio_path)) - write_file(pio_path, data) + pio_path = CORE.build_path / "src" / "pio" / f"{key}.pio" + pio_path.parent.mkdir(parents=True, exist_ok=True) + write_file_if_changed(pio_path, data) includes.append(f"pio/{key}.pio.h") - write_file( + write_file_if_changed( CORE.relative_build_path("src/pio_includes.h"), "#pragma once\n" + "\n".join([f'#include "{include}"' for include in includes]), ) - dir = os.path.dirname(__file__) - build_pio_file = os.path.join(dir, "build_pio.py.script") + dir = Path(__file__).parent + build_pio_file = dir / "build_pio.py.script" copy_file_if_changed( build_pio_file, CORE.relative_build_path("build_pio.py"), @@ -243,8 +243,8 @@ def generate_pio_files() -> bool: # Called by writer.py def copy_files(): - dir = os.path.dirname(__file__) - post_build_file = os.path.join(dir, "post_build.py.script") + dir = Path(__file__).parent + post_build_file = dir / "post_build.py.script" copy_file_if_changed( post_build_file, CORE.relative_build_path("post_build.py"), @@ -252,4 +252,4 @@ def copy_files(): if generate_pio_files(): path = CORE.relative_src_path("esphome.h") content = read_file(path).rstrip("\n") - write_file(path, content + '\n#include "pio_includes.h"\n') + write_file_if_changed(path, content + '\n#include "pio_includes.h"\n') diff --git a/esphome/components/rp2040/gpio.h b/esphome/components/rp2040/gpio.h index 9bc66d9e4b..47a6fe17f2 100644 --- a/esphome/components/rp2040/gpio.h +++ b/esphome/components/rp2040/gpio.h @@ -29,8 +29,8 @@ class RP2040GPIOPin : public InternalGPIOPin { void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; uint8_t pin_; - bool inverted_; - gpio::Flags flags_; + bool inverted_{}; + gpio::Flags flags_{}; }; } // namespace rp2040 diff --git a/esphome/components/rp2040/gpio.py b/esphome/components/rp2040/gpio.py index 58514f7db5..193e567d17 100644 --- a/esphome/components/rp2040/gpio.py +++ b/esphome/components/rp2040/gpio.py @@ -94,6 +94,9 @@ async def rp2040_pin_to_code(config): var = cg.new_Pvariable(config[CONF_ID]) num = config[CONF_NUMBER] cg.add(var.set_pin(num)) - cg.add(var.set_inverted(config[CONF_INVERTED])) + # Only set if true to avoid bloating setup() function + # (inverted bit in pin_flags_ bitfield is zero-initialized to false) + if config[CONF_INVERTED]: + cg.add(var.set_inverted(True)) cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) return var diff --git a/esphome/components/rp2040_pwm/rp2040_pwm.h b/esphome/components/rp2040_pwm/rp2040_pwm.h index e499e72b06..b82765b1c0 100644 --- a/esphome/components/rp2040_pwm/rp2040_pwm.h +++ b/esphome/components/rp2040_pwm/rp2040_pwm.h @@ -45,7 +45,7 @@ template class SetFrequencyAction : public Action { SetFrequencyAction(RP2040PWM *parent) : parent_(parent) {} TEMPLATABLE_VALUE(float, frequency); - void play(Ts... x) { + void play(const Ts &...x) { float freq = this->frequency_.value(x...); this->parent_->update_frequency(freq); } diff --git a/esphome/components/rtttl/rtttl.cpp b/esphome/components/rtttl/rtttl.cpp index 65a3af1bbc..65fcc207d4 100644 --- a/esphome/components/rtttl/rtttl.cpp +++ b/esphome/components/rtttl/rtttl.cpp @@ -35,9 +35,9 @@ void Rtttl::dump_config() { void Rtttl::play(std::string rtttl) { if (this->state_ != State::STATE_STOPPED && this->state_ != State::STATE_STOPPING) { - int pos = this->rtttl_.find(':'); - auto name = this->rtttl_.substr(0, pos); - ESP_LOGW(TAG, "Already playing: %s", name.c_str()); + size_t pos = this->rtttl_.find(':'); + size_t len = (pos != std::string::npos) ? pos : this->rtttl_.length(); + ESP_LOGW(TAG, "Already playing: %.*s", (int) len, this->rtttl_.c_str()); return; } @@ -59,8 +59,7 @@ void Rtttl::play(std::string rtttl) { return; } - auto name = this->rtttl_.substr(0, this->position_); - ESP_LOGD(TAG, "Playing song %s", name.c_str()); + ESP_LOGD(TAG, "Playing song %.*s", (int) this->position_, this->rtttl_.c_str()); // get default duration this->position_ = this->rtttl_.find("d=", this->position_); @@ -138,11 +137,37 @@ void Rtttl::stop() { this->set_state_(STATE_STOPPING); } #endif + this->position_ = this->rtttl_.length(); + this->note_duration_ = 0; +} + +void Rtttl::finish_() { + ESP_LOGV(TAG, "Rtttl::finish_()"); +#ifdef USE_OUTPUT + if (this->output_ != nullptr) { + this->output_->set_level(0.0); + this->set_state_(State::STATE_STOPPED); + } +#endif +#ifdef USE_SPEAKER + if (this->speaker_ != nullptr) { + SpeakerSample sample[2]; + sample[0].left = 0; + sample[0].right = 0; + sample[1].left = 0; + sample[1].right = 0; + this->speaker_->play((uint8_t *) (&sample), 8); + this->speaker_->finish(); + this->set_state_(State::STATE_STOPPING); + } +#endif + // Ensure no more notes are played in case finish_() is called for an error. + this->position_ = this->rtttl_.length(); this->note_duration_ = 0; } void Rtttl::loop() { - if (this->note_duration_ == 0 || this->state_ == State::STATE_STOPPED) { + if (this->state_ == State::STATE_STOPPED) { this->disable_loop(); return; } @@ -152,6 +177,8 @@ void Rtttl::loop() { if (this->state_ == State::STATE_STOPPING) { if (this->speaker_->is_stopped()) { this->set_state_(State::STATE_STOPPED); + } else { + return; } } else if (this->state_ == State::STATE_INIT) { if (this->speaker_->is_stopped()) { @@ -187,7 +214,7 @@ void Rtttl::loop() { sample[x].right = 0; } - if (x >= SAMPLE_BUFFER_SIZE || this->samples_sent_ >= this->samples_count_) { + if (static_cast(x) >= SAMPLE_BUFFER_SIZE || this->samples_sent_ >= this->samples_count_) { break; } this->samples_sent_++; @@ -207,7 +234,7 @@ void Rtttl::loop() { if (this->output_ != nullptr && millis() - this->last_note_ < this->note_duration_) return; #endif - if (!this->rtttl_[this->position_]) { + if (this->position_ >= this->rtttl_.length()) { this->finish_(); return; } @@ -346,32 +373,7 @@ void Rtttl::loop() { this->last_note_ = millis(); } -void Rtttl::finish_() { -#ifdef USE_OUTPUT - if (this->output_ != nullptr) { - this->output_->set_level(0.0); - this->set_state_(State::STATE_STOPPED); - } -#endif -#ifdef USE_SPEAKER - if (this->speaker_ != nullptr) { - SpeakerSample sample[2]; - sample[0].left = 0; - sample[0].right = 0; - sample[1].left = 0; - sample[1].right = 0; - this->speaker_->play((uint8_t *) (&sample), 8); - - this->speaker_->finish(); - this->set_state_(State::STATE_STOPPING); - } -#endif - this->note_duration_ = 0; - this->on_finished_playback_callback_.call(); - ESP_LOGD(TAG, "Playback finished"); -} - -#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE static const LogString *state_to_string(State state) { switch (state) { case STATE_STOPPED: @@ -397,7 +399,11 @@ void Rtttl::set_state_(State state) { LOG_STR_ARG(state_to_string(state))); // Clear loop_done when transitioning from STOPPED to any other state - if (old_state == State::STATE_STOPPED && state != State::STATE_STOPPED) { + if (state == State::STATE_STOPPED) { + this->disable_loop(); + this->on_finished_playback_callback_.call(); + ESP_LOGD(TAG, "Playback finished"); + } else if (old_state == State::STATE_STOPPED) { this->enable_loop(); } } diff --git a/esphome/components/rtttl/rtttl.h b/esphome/components/rtttl/rtttl.h index 420948bfbf..1e924a897c 100644 --- a/esphome/components/rtttl/rtttl.h +++ b/esphome/components/rtttl/rtttl.h @@ -60,35 +60,60 @@ class Rtttl : public Component { } return ret; } + /** + * @brief Finalizes the playback of the RTTTL string. + * + * This method is called internally when the end of the RTTTL string is reached + * or when a parsing error occurs. It stops the output, sets the component state, + * and triggers the on_finished_playback_callback_. + */ void finish_(); void set_state_(State state); + /// The RTTTL string to play. std::string rtttl_{""}; + /// The current position in the RTTTL string. size_t position_{0}; + /// The duration of a whole note in milliseconds. uint16_t wholenote_; + /// The default duration of a note (e.g. 4 for a quarter note). uint16_t default_duration_; + /// The default octave for a note. uint16_t default_octave_; + /// The time the last note was started. uint32_t last_note_; + /// The duration of the current note in milliseconds. uint16_t note_duration_; + /// The frequency of the current note in Hz. uint32_t output_freq_; + /// The gain of the output. float gain_{0.6f}; + /// The current state of the RTTTL player. State state_{State::STATE_STOPPED}; #ifdef USE_OUTPUT + /// The output to write the sound to. output::FloatOutput *output_; #endif #ifdef USE_SPEAKER + /// The speaker to write the sound to. speaker::Speaker *speaker_{nullptr}; + /// The sample rate of the speaker. int sample_rate_{16000}; + /// The number of samples for one full cycle of a note's waveform, in Q10 fixed-point format. int samples_per_wave_{0}; + /// The number of samples sent. int samples_sent_{0}; + /// The total number of samples to send. int samples_count_{0}; + /// The number of samples for the gap between notes. int samples_gap_{0}; #endif + /// The callback to call when playback is finished. CallbackManager on_finished_playback_callback_; }; @@ -97,7 +122,7 @@ template class PlayAction : public Action { PlayAction(Rtttl *rtttl) : rtttl_(rtttl) {} TEMPLATABLE_VALUE(std::string, value) - void play(Ts... x) override { this->rtttl_->play(this->value_.value(x...)); } + void play(const Ts &...x) override { this->rtttl_->play(this->value_.value(x...)); } protected: Rtttl *rtttl_; @@ -105,12 +130,12 @@ template class PlayAction : public Action { template class StopAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->stop(); } + void play(const Ts &...x) override { this->parent_->stop(); } }; template class IsPlayingCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->is_playing(); } + bool check(const Ts &...x) override { return this->parent_->is_playing(); } }; class FinishedPlaybackTrigger : public Trigger<> { diff --git a/esphome/components/runtime_stats/runtime_stats.cpp b/esphome/components/runtime_stats/runtime_stats.cpp index 8f5d5daf01..f95be5291f 100644 --- a/esphome/components/runtime_stats/runtime_stats.cpp +++ b/esphome/components/runtime_stats/runtime_stats.cpp @@ -17,16 +17,8 @@ void RuntimeStatsCollector::record_component_time(Component *component, uint32_t if (component == nullptr) return; - // Check if we have cached the name for this component - auto name_it = this->component_names_cache_.find(component); - if (name_it == this->component_names_cache_.end()) { - // First time seeing this component, cache its name - const char *source = component->get_component_source(); - this->component_names_cache_[component] = source; - this->component_stats_[source].record_time(duration_ms); - } else { - this->component_stats_[name_it->second].record_time(duration_ms); - } + // Record stats using component pointer as key + this->component_stats_[component].record_time(duration_ms); if (this->next_log_time_ == 0) { this->next_log_time_ = current_time + this->log_interval_; @@ -42,9 +34,10 @@ void RuntimeStatsCollector::log_stats_() { std::vector stats_to_display; for (const auto &it : this->component_stats_) { + Component *component = it.first; const ComponentRuntimeStats &stats = it.second; if (stats.get_period_count() > 0) { - ComponentStatPair pair = {it.first, &stats}; + ComponentStatPair pair = {component, &stats}; stats_to_display.push_back(pair); } } @@ -54,12 +47,9 @@ void RuntimeStatsCollector::log_stats_() { // Log top components by period runtime for (const auto &it : stats_to_display) { - const char *source = it.name; - const ComponentRuntimeStats *stats = it.stats; - - ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", source, - stats->get_period_count(), stats->get_period_avg_time_ms(), stats->get_period_max_time_ms(), - stats->get_period_time_ms()); + ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", + LOG_STR_ARG(it.component->get_component_log_str()), it.stats->get_period_count(), + it.stats->get_period_avg_time_ms(), it.stats->get_period_max_time_ms(), it.stats->get_period_time_ms()); } // Log total stats since boot @@ -72,12 +62,9 @@ void RuntimeStatsCollector::log_stats_() { }); for (const auto &it : stats_to_display) { - const char *source = it.name; - const ComponentRuntimeStats *stats = it.stats; - - ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", source, - stats->get_total_count(), stats->get_total_avg_time_ms(), stats->get_total_max_time_ms(), - stats->get_total_time_ms()); + ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", + LOG_STR_ARG(it.component->get_component_log_str()), it.stats->get_total_count(), + it.stats->get_total_avg_time_ms(), it.stats->get_total_max_time_ms(), it.stats->get_total_time_ms()); } } diff --git a/esphome/components/runtime_stats/runtime_stats.h b/esphome/components/runtime_stats/runtime_stats.h index e2f8bee563..56122364c2 100644 --- a/esphome/components/runtime_stats/runtime_stats.h +++ b/esphome/components/runtime_stats/runtime_stats.h @@ -79,7 +79,7 @@ class ComponentRuntimeStats { // For sorting components by run time struct ComponentStatPair { - const char *name; + Component *component; const ComponentRuntimeStats *stats; bool operator>(const ComponentStatPair &other) const { @@ -109,15 +109,9 @@ class RuntimeStatsCollector { } } - // Use const char* keys for efficiency - // Custom comparator for const char* keys in map - // Without this, std::map would compare pointer addresses instead of string contents, - // causing identical component names at different addresses to be treated as different keys - struct CStrCompare { - bool operator()(const char *a, const char *b) const { return std::strcmp(a, b) < 0; } - }; - std::map component_stats_; - std::map component_names_cache_; + // Map from component to its stats + // We use Component* as the key since each component is unique + std::map component_stats_; uint32_t log_interval_; uint32_t next_log_time_; }; diff --git a/esphome/components/rx8130/__init__.py b/esphome/components/rx8130/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/rx8130/rx8130.cpp b/esphome/components/rx8130/rx8130.cpp new file mode 100644 index 0000000000..ba092a4834 --- /dev/null +++ b/esphome/components/rx8130/rx8130.cpp @@ -0,0 +1,128 @@ +#include "rx8130.h" +#include "esphome/core/log.h" + +// https://download.epsondevice.com/td/pdf/app/RX8130CE_en.pdf + +namespace esphome { +namespace rx8130 { + +static const uint8_t RX8130_REG_SEC = 0x10; +static const uint8_t RX8130_REG_MIN = 0x11; +static const uint8_t RX8130_REG_HOUR = 0x12; +static const uint8_t RX8130_REG_WDAY = 0x13; +static const uint8_t RX8130_REG_MDAY = 0x14; +static const uint8_t RX8130_REG_MONTH = 0x15; +static const uint8_t RX8130_REG_YEAR = 0x16; +static const uint8_t RX8130_REG_EXTEN = 0x1C; +static const uint8_t RX8130_REG_FLAG = 0x1D; +static const uint8_t RX8130_REG_CTRL0 = 0x1E; +static const uint8_t RX8130_REG_CTRL1 = 0x1F; +static const uint8_t RX8130_REG_DIG_OFFSET = 0x30; +static const uint8_t RX8130_BIT_CTRL_STOP = 0x40; +static const uint8_t RX8130_BAT_FLAGS = 0x30; +static const uint8_t RX8130_CLEAR_FLAGS = 0x00; + +static const char *const TAG = "rx8130"; + +constexpr uint8_t bcd2dec(uint8_t val) { return (val >> 4) * 10 + (val & 0x0f); } +constexpr uint8_t dec2bcd(uint8_t val) { return ((val / 10) << 4) + (val % 10); } + +void RX8130Component::setup() { + // Set digital offset to disabled with no offset + if (this->write_register(RX8130_REG_DIG_OFFSET, &RX8130_CLEAR_FLAGS, 1) != i2c::ERROR_OK) { + this->mark_failed(); + return; + } + // Disable wakeup timers + if (this->write_register(RX8130_REG_EXTEN, &RX8130_CLEAR_FLAGS, 1) != i2c::ERROR_OK) { + this->mark_failed(); + return; + } + // Clear VLF flag in case there has been data loss + if (this->write_register(RX8130_REG_FLAG, &RX8130_CLEAR_FLAGS, 1) != i2c::ERROR_OK) { + this->mark_failed(); + return; + } + // Clear test flag and disable interrupts + if (this->write_register(RX8130_REG_CTRL0, &RX8130_CLEAR_FLAGS, 1) != i2c::ERROR_OK) { + this->mark_failed(); + return; + } + // Enable battery charging and switching + if (this->write_register(RX8130_REG_CTRL1, &RX8130_BAT_FLAGS, 1) != i2c::ERROR_OK) { + this->mark_failed(); + return; + } + // Clear STOP bit + this->stop_(false); +} + +void RX8130Component::update() { this->read_time(); } + +void RX8130Component::dump_config() { + ESP_LOGCONFIG(TAG, "RX8130:"); + LOG_I2C_DEVICE(this); + RealTimeClock::dump_config(); +} + +void RX8130Component::read_time() { + uint8_t date[7]; + if (this->read_register(RX8130_REG_SEC, date, 7) != i2c::ERROR_OK) { + this->status_set_warning(ESP_LOG_MSG_COMM_FAIL); + return; + } + ESPTime rtc_time{ + .second = bcd2dec(date[0] & 0x7f), + .minute = bcd2dec(date[1] & 0x7f), + .hour = bcd2dec(date[2] & 0x3f), + .day_of_week = bcd2dec(date[3] & 0x7f), + .day_of_month = bcd2dec(date[4] & 0x3f), + .day_of_year = 1, // ignored by recalc_timestamp_utc(false) + .month = bcd2dec(date[5] & 0x1f), + .year = static_cast(bcd2dec(date[6]) + 2000), + .is_dst = false, // not used + .timestamp = 0 // overwritten by recalc_timestamp_utc(false) + }; + rtc_time.recalc_timestamp_utc(false); + if (!rtc_time.is_valid()) { + ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock."); + return; + } + ESP_LOGD(TAG, "Read UTC time: %04d-%02d-%02d %02d:%02d:%02d", rtc_time.year, rtc_time.month, rtc_time.day_of_month, + rtc_time.hour, rtc_time.minute, rtc_time.second); + time::RealTimeClock::synchronize_epoch_(rtc_time.timestamp); +} + +void RX8130Component::write_time() { + auto now = time::RealTimeClock::utcnow(); + if (!now.is_valid()) { + ESP_LOGE(TAG, "Invalid system time, not syncing to RTC."); + return; + } + uint8_t buff[7]; + buff[0] = dec2bcd(now.second); + buff[1] = dec2bcd(now.minute); + buff[2] = dec2bcd(now.hour); + buff[3] = dec2bcd(now.day_of_week); + buff[4] = dec2bcd(now.day_of_month); + buff[5] = dec2bcd(now.month); + buff[6] = dec2bcd(now.year % 100); + this->stop_(true); + if (this->write_register(RX8130_REG_SEC, buff, 7) != i2c::ERROR_OK) { + this->status_set_warning(ESP_LOG_MSG_COMM_FAIL); + } else { + ESP_LOGD(TAG, "Wrote UTC time: %04d-%02d-%02d %02d:%02d:%02d", now.year, now.month, now.day_of_month, now.hour, + now.minute, now.second); + } + this->stop_(false); +} + +void RX8130Component::stop_(bool stop) { + const uint8_t data = stop ? RX8130_BIT_CTRL_STOP : RX8130_CLEAR_FLAGS; + if (this->write_register(RX8130_REG_CTRL0, &data, 1) != i2c::ERROR_OK) { + this->status_set_warning(ESP_LOG_MSG_COMM_FAIL); + } +} + +} // namespace rx8130 +} // namespace esphome diff --git a/esphome/components/rx8130/rx8130.h b/esphome/components/rx8130/rx8130.h new file mode 100644 index 0000000000..6694c763cd --- /dev/null +++ b/esphome/components/rx8130/rx8130.h @@ -0,0 +1,35 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/time/real_time_clock.h" + +namespace esphome { +namespace rx8130 { + +class RX8130Component : public time::RealTimeClock, public i2c::I2CDevice { + public: + void setup() override; + void update() override; + void dump_config() override; + void read_time(); + void write_time(); + /// Ensure RTC is initialized at the correct time in the setup sequence + float get_setup_priority() const override { return setup_priority::DATA; } + + protected: + void stop_(bool stop); +}; + +template class WriteAction : public Action, public Parented { + public: + void play(const Ts... x) override { this->parent_->write_time(); } +}; + +template class ReadAction : public Action, public Parented { + public: + void play(const Ts... x) override { this->parent_->read_time(); } +}; + +} // namespace rx8130 +} // namespace esphome diff --git a/esphome/components/rx8130/time.py b/esphome/components/rx8130/time.py new file mode 100644 index 0000000000..cb0402bd32 --- /dev/null +++ b/esphome/components/rx8130/time.py @@ -0,0 +1,56 @@ +from esphome import automation +import esphome.codegen as cg +from esphome.components import i2c, time +import esphome.config_validation as cv +from esphome.const import CONF_ID + +CODEOWNERS = ["@beormund"] +DEPENDENCIES = ["i2c"] +rx8130_ns = cg.esphome_ns.namespace("rx8130") +RX8130Component = rx8130_ns.class_("RX8130Component", time.RealTimeClock, i2c.I2CDevice) +WriteAction = rx8130_ns.class_("WriteAction", automation.Action) +ReadAction = rx8130_ns.class_("ReadAction", automation.Action) + + +CONFIG_SCHEMA = time.TIME_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(RX8130Component), + } +).extend(i2c.i2c_device_schema(0x32)) + + +@automation.register_action( + "rx8130.write_time", + WriteAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(RX8130Component), + } + ), +) +async def rx8130_write_time_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +@automation.register_action( + "rx8130.read_time", + ReadAction, + automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(RX8130Component), + } + ), +) +async def rx8130_read_time_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + await time.register_time(var, config) diff --git a/esphome/components/safe_mode/__init__.py b/esphome/components/safe_mode/__init__.py index 991747b089..9944d71722 100644 --- a/esphome/components/safe_mode/__init__.py +++ b/esphome/components/safe_mode/__init__.py @@ -10,7 +10,7 @@ from esphome.const import ( CONF_TRIGGER_ID, KEY_PAST_SAFE_MODE, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.cpp_generator import RawExpression CODEOWNERS = ["@paulmonigatti", "@jsuanet", "@kbx81"] @@ -53,7 +53,7 @@ CONFIG_SCHEMA = cv.All( ) -@coroutine_with_priority(50.0) +@coroutine_with_priority(CoroPriority.APPLICATION) async def to_code(config): if not config[CONF_DISABLED]: var = cg.new_Pvariable(config[CONF_ID]) diff --git a/esphome/components/safe_mode/safe_mode.cpp b/esphome/components/safe_mode/safe_mode.cpp index 5a62604269..62bbca4fb1 100644 --- a/esphome/components/safe_mode/safe_mode.cpp +++ b/esphome/components/safe_mode/safe_mode.cpp @@ -15,11 +15,11 @@ namespace safe_mode { static const char *const TAG = "safe_mode"; void SafeModeComponent::dump_config() { - ESP_LOGCONFIG(TAG, "Safe Mode:"); ESP_LOGCONFIG(TAG, - " Boot considered successful after %" PRIu32 " seconds\n" - " Invoke after %u boot attempts\n" - " Remain for %" PRIu32 " seconds", + "Safe Mode:\n" + " Successful after: %" PRIu32 "s\n" + " Invoke after: %u attempts\n" + " Duration: %" PRIu32 "s", this->safe_mode_boot_is_good_after_ / 1000, // because milliseconds this->safe_mode_num_attempts_, this->safe_mode_enable_time_ / 1000); // because milliseconds @@ -27,7 +27,7 @@ void SafeModeComponent::dump_config() { 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_; if (remaining_restarts) { - ESP_LOGW(TAG, "Last reset occurred too quickly; will be invoked in %" PRIu32 " restarts", remaining_restarts); + ESP_LOGW(TAG, "Last reset too quick; invoke in %" PRIu32 " restarts", remaining_restarts); } else { ESP_LOGW(TAG, "SAFE MODE IS ACTIVE"); } @@ -72,43 +72,45 @@ bool SafeModeComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t en this->safe_mode_boot_is_good_after_ = boot_is_good_after; this->safe_mode_num_attempts_ = num_attempts; this->rtc_ = global_preferences->make_preference(233825507UL, false); - this->safe_mode_rtc_value_ = this->read_rtc_(); - bool is_manual_safe_mode = this->safe_mode_rtc_value_ == SafeModeComponent::ENTER_SAFE_MODE_MAGIC; + uint32_t rtc_val = this->read_rtc_(); + this->safe_mode_rtc_value_ = rtc_val; - if (is_manual_safe_mode) { - ESP_LOGI(TAG, "Safe mode invoked manually"); + bool is_manual = rtc_val == SafeModeComponent::ENTER_SAFE_MODE_MAGIC; + + if (is_manual) { + ESP_LOGI(TAG, "Manual mode"); } else { - ESP_LOGCONFIG(TAG, "There have been %" PRIu32 " suspected unsuccessful boot attempts", this->safe_mode_rtc_value_); + ESP_LOGCONFIG(TAG, "Unsuccessful boot attempts: %" PRIu32, rtc_val); } - if (this->safe_mode_rtc_value_ >= num_attempts || is_manual_safe_mode) { - this->clean_rtc(); - - if (!is_manual_safe_mode) { - ESP_LOGE(TAG, "Boot loop detected. Proceeding"); - } - - this->status_set_error(); - this->set_timeout(enable_time, []() { - ESP_LOGW(TAG, "Safe mode enable time has elapsed -- restarting"); - App.reboot(); - }); - - // Delay here to allow power to stabilize before Wi-Fi/Ethernet is initialised - delay(300); // NOLINT - App.setup(); - - ESP_LOGW(TAG, "SAFE MODE IS ACTIVE"); - - this->safe_mode_callback_.call(); - - return true; - } else { + if (rtc_val < num_attempts && !is_manual) { // increment counter - this->write_rtc_(this->safe_mode_rtc_value_ + 1); + this->write_rtc_(rtc_val + 1); return false; } + + this->clean_rtc(); + + if (!is_manual) { + ESP_LOGE(TAG, "Boot loop detected"); + } + + this->status_set_error(); + this->set_timeout(enable_time, []() { + ESP_LOGW(TAG, "Timeout, restarting"); + App.reboot(); + }); + + // Delay here to allow power to stabilize before Wi-Fi/Ethernet is initialised + delay(300); // NOLINT + App.setup(); + + ESP_LOGW(TAG, "SAFE MODE IS ACTIVE"); + + this->safe_mode_callback_.call(); + + return true; } void SafeModeComponent::write_rtc_(uint32_t val) { diff --git a/esphome/components/scd30/automation.h b/esphome/components/scd30/automation.h index 37b3bc1674..1f89e7c815 100644 --- a/esphome/components/scd30/automation.h +++ b/esphome/components/scd30/automation.h @@ -9,7 +9,7 @@ namespace scd30 { template class ForceRecalibrationWithReference : public Action, public Parented { public: - void play(Ts... x) override { + void play(const Ts &...x) override { if (this->value_.has_value()) { this->parent_->force_recalibration_with_reference(this->value_.value(x...)); } diff --git a/esphome/components/scd30/sensor.py b/esphome/components/scd30/sensor.py index 6981af4de9..194df8ec4f 100644 --- a/esphome/components/scd30/sensor.py +++ b/esphome/components/scd30/sensor.py @@ -66,7 +66,7 @@ CONFIG_SCHEMA = ( ), cv.Optional(CONF_AMBIENT_PRESSURE_COMPENSATION, default=0): cv.pressure, cv.Optional(CONF_TEMPERATURE_OFFSET): cv.All( - cv.temperature, + cv.temperature_delta, cv.float_range(min=0, max=655.35), ), cv.Optional(CONF_UPDATE_INTERVAL, default="60s"): cv.All( diff --git a/esphome/components/scd4x/automation.h b/esphome/components/scd4x/automation.h index dc43e9eb56..6ce1468577 100644 --- a/esphome/components/scd4x/automation.h +++ b/esphome/components/scd4x/automation.h @@ -9,7 +9,7 @@ namespace scd4x { template class PerformForcedCalibrationAction : public Action, public Parented { public: - void play(Ts... x) override { + void play(const Ts &...x) override { if (this->value_.has_value()) { this->parent_->perform_forced_calibration(this->value_.value(x...)); } @@ -21,7 +21,7 @@ template class PerformForcedCalibrationAction : public Action class FactoryResetAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->factory_reset(); } + void play(const Ts &...x) override { this->parent_->factory_reset(); } }; } // namespace scd4x diff --git a/esphome/components/scd4x/sensor.py b/esphome/components/scd4x/sensor.py index 6b2188cd5a..ec90234ac3 100644 --- a/esphome/components/scd4x/sensor.py +++ b/esphome/components/scd4x/sensor.py @@ -81,7 +81,7 @@ CONFIG_SCHEMA = ( cv.int_range(min=0, max=0xFFFF, max_included=False), ), cv.Optional(CONF_AMBIENT_PRESSURE_COMPENSATION): cv.pressure, - cv.Optional(CONF_TEMPERATURE_OFFSET, default="4°C"): cv.temperature, + cv.Optional(CONF_TEMPERATURE_OFFSET, default="4°C"): cv.temperature_delta, cv.Optional(CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE): cv.use_id( sensor.Sensor ), diff --git a/esphome/components/script/__init__.py b/esphome/components/script/__init__.py index ee1f6a4ad0..8d69981db0 100644 --- a/esphome/components/script/__init__.py +++ b/esphome/components/script/__init__.py @@ -45,13 +45,26 @@ def get_script(script_id): def check_max_runs(value): + # Set default for queued mode to prevent unbounded queue growth + if CONF_MAX_RUNS not in value and value[CONF_MODE] == CONF_QUEUED: + value[CONF_MAX_RUNS] = 5 + if CONF_MAX_RUNS not in value: return value + if value[CONF_MODE] not in [CONF_QUEUED, CONF_PARALLEL]: raise cv.Invalid( - "The option 'max_runs' is only valid in 'queue' and 'parallel' mode.", + "The option 'max_runs' is only valid in 'queued' and 'parallel' mode.", path=[CONF_MAX_RUNS], ) + + # Queued mode must have bounded queue (min 1), parallel mode can be unlimited (0) + if value[CONF_MODE] == CONF_QUEUED and value[CONF_MAX_RUNS] < 1: + raise cv.Invalid( + "The option 'max_runs' must be at least 1 for queued mode.", + path=[CONF_MAX_RUNS], + ) + return value @@ -106,7 +119,7 @@ CONFIG_SCHEMA = automation.validate_automation( cv.Optional(CONF_MODE, default=CONF_SINGLE): cv.one_of( *SCRIPT_MODES, lower=True ), - cv.Optional(CONF_MAX_RUNS): cv.positive_int, + cv.Optional(CONF_MAX_RUNS): cv.int_range(min=0, max=100), cv.Optional(CONF_PARAMETERS, default={}): cv.Schema( { validate_parameter_name: validate_parameter_type, @@ -124,7 +137,7 @@ async def to_code(config): template, func_args = parameters_to_template(conf[CONF_PARAMETERS]) trigger = cg.new_Pvariable(conf[CONF_ID], template) # Add a human-readable name to the script - cg.add(trigger.set_name(conf[CONF_ID].id)) + cg.add(trigger.set_name(cg.LogStringLiteral(conf[CONF_ID].id))) if CONF_MAX_RUNS in conf: cg.add(trigger.set_max_runs(conf[CONF_MAX_RUNS])) diff --git a/esphome/components/script/script.cpp b/esphome/components/script/script.cpp index 331f7dcd65..81f652d26a 100644 --- a/esphome/components/script/script.cpp +++ b/esphome/components/script/script.cpp @@ -6,9 +6,15 @@ namespace script { static const char *const TAG = "script"; +#ifdef USE_STORE_LOG_STR_IN_FLASH +void ScriptLogger::esp_log_(int level, int line, const __FlashStringHelper *format, const char *param) { + esp_log_printf_(level, TAG, line, format, param); +} +#else void ScriptLogger::esp_log_(int level, int line, const char *format, const char *param) { esp_log_printf_(level, TAG, line, format, param); } +#endif } // namespace script } // namespace esphome diff --git a/esphome/components/script/script.h b/esphome/components/script/script.h index 60175ec933..cd1a084f16 100644 --- a/esphome/components/script/script.h +++ b/esphome/components/script/script.h @@ -1,15 +1,26 @@ #pragma once +#include +#include +#include #include "esphome/core/automation.h" #include "esphome/core/component.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" - -#include namespace esphome { namespace script { class ScriptLogger { protected: +#ifdef USE_STORE_LOG_STR_IN_FLASH + void esp_logw_(int line, const __FlashStringHelper *format, const char *param) { + esp_log_(ESPHOME_LOG_LEVEL_WARN, line, format, param); + } + void esp_logd_(int line, const __FlashStringHelper *format, const char *param) { + esp_log_(ESPHOME_LOG_LEVEL_DEBUG, line, format, param); + } + void esp_log_(int level, int line, const __FlashStringHelper *format, const char *param); +#else void esp_logw_(int line, const char *format, const char *param) { esp_log_(ESPHOME_LOG_LEVEL_WARN, line, format, param); } @@ -17,6 +28,7 @@ class ScriptLogger { esp_log_(ESPHOME_LOG_LEVEL_DEBUG, line, format, param); } void esp_log_(int level, int line, const char *format, const char *param); +#endif }; /// The abstract base class for all script types. @@ -34,18 +46,18 @@ template class Script : public ScriptLogger, public Trigger &tuple) { - this->execute_tuple_(tuple, typename gens::type()); + this->execute_tuple_(tuple, std::make_index_sequence{}); } // Internal function to give scripts readable names. - void set_name(const std::string &name) { name_ = name; } + void set_name(const LogString *name) { name_ = name; } protected: - template void execute_tuple_(const std::tuple &tuple, seq /*unused*/) { + template void execute_tuple_(const std::tuple &tuple, std::index_sequence /*unused*/) { this->execute(std::get(tuple)...); } - std::string name_; + const LogString *name_{nullptr}; }; /** A script type for which only a single instance at a time is allowed. @@ -57,7 +69,8 @@ template class SingleScript : public Script { public: void execute(Ts... x) override { if (this->is_action_running()) { - this->esp_logw_(__LINE__, "Script '%s' is already running! (mode: single)", this->name_.c_str()); + this->esp_logw_(__LINE__, ESPHOME_LOG_FORMAT("Script '%s' is already running! (mode: single)"), + LOG_STR_ARG(this->name_)); return; } @@ -74,7 +87,7 @@ template class RestartScript : public Script { public: void execute(Ts... x) override { if (this->is_action_running()) { - this->esp_logd_(__LINE__, "Script '%s' restarting (mode: restart)", this->name_.c_str()); + this->esp_logd_(__LINE__, ESPHOME_LOG_FORMAT("Script '%s' restarting (mode: restart)"), LOG_STR_ARG(this->name_)); this->stop_action(); } @@ -85,21 +98,41 @@ template class RestartScript : public Script { /** A script type that queues new instances that are created. * * Only one instance of the script can be active at a time. + * + * Ring buffer implementation: + * - num_queued_ tracks the number of queued (waiting) instances, NOT including the currently running one + * - queue_front_ points to the next item to execute (read position) + * - Buffer size is max_runs_ - 1 (max total instances minus the running one) + * - Write position is calculated as: (queue_front_ + num_queued_) % (max_runs_ - 1) + * - When an item finishes, queue_front_ advances: (queue_front_ + 1) % (max_runs_ - 1) + * - First execute() runs immediately without queuing (num_queued_ stays 0) + * - Subsequent executes while running are queued starting at position 0 + * - Maximum total instances = max_runs_ (includes 1 running + (max_runs_ - 1) queued) */ template class QueueingScript : public Script, public Component { public: void execute(Ts... x) override { - if (this->is_action_running() || this->num_runs_ > 0) { - // num_runs_ is the number of *queued* instances, so total number of instances is - // num_runs_ + 1 - if (this->max_runs_ != 0 && this->num_runs_ + 1 >= this->max_runs_) { - this->esp_logw_(__LINE__, "Script '%s' maximum number of queued runs exceeded!", this->name_.c_str()); + if (this->is_action_running() || this->num_queued_ > 0) { + // num_queued_ is the number of *queued* instances (waiting, not including currently running) + // max_runs_ is the maximum *total* instances (running + queued) + // So we reject when num_queued_ + 1 >= max_runs_ (queued + running >= max) + if (this->num_queued_ + 1 >= this->max_runs_) { + this->esp_logw_(__LINE__, ESPHOME_LOG_FORMAT("Script '%s' max instances (running + queued) reached!"), + LOG_STR_ARG(this->name_)); return; } - this->esp_logd_(__LINE__, "Script '%s' queueing new instance (mode: queued)", this->name_.c_str()); - this->num_runs_++; - this->var_queue_.push(std::make_tuple(x...)); + // Initialize queue on first queued item (after capacity check) + this->lazy_init_queue_(); + + this->esp_logd_(__LINE__, ESPHOME_LOG_FORMAT("Script '%s' queueing new instance (mode: queued)"), + LOG_STR_ARG(this->name_)); + // Ring buffer: write to (queue_front_ + num_queued_) % queue_capacity + const size_t queue_capacity = static_cast(this->max_runs_ - 1); + size_t write_pos = (this->queue_front_ + this->num_queued_) % queue_capacity; + // Use std::make_unique to replace the unique_ptr + this->var_queue_[write_pos] = std::make_unique>(x...); + this->num_queued_++; return; } @@ -109,29 +142,46 @@ template class QueueingScript : public Script, public Com } void stop() override { - this->num_runs_ = 0; + // Clear all queued items to free memory immediately + // Resetting the array automatically destroys all unique_ptrs and their contents + this->var_queue_.reset(); + this->num_queued_ = 0; + this->queue_front_ = 0; Script::stop(); } void loop() override { - if (this->num_runs_ != 0 && !this->is_action_running()) { - this->num_runs_--; - auto &vars = this->var_queue_.front(); - this->var_queue_.pop(); - this->trigger_tuple_(vars, typename gens::type()); + if (this->num_queued_ != 0 && !this->is_action_running()) { + // Dequeue: decrement count, move tuple out (frees slot), advance read position + this->num_queued_--; + const size_t queue_capacity = static_cast(this->max_runs_ - 1); + auto tuple_ptr = std::move(this->var_queue_[this->queue_front_]); + this->queue_front_ = (this->queue_front_ + 1) % queue_capacity; + this->trigger_tuple_(*tuple_ptr, std::make_index_sequence{}); } } void set_max_runs(int max_runs) { max_runs_ = max_runs; } protected: - template void trigger_tuple_(const std::tuple &tuple, seq /*unused*/) { + // Lazy init queue on first use - avoids setup() ordering issues and saves memory + // if script is never executed during this boot cycle + inline void lazy_init_queue_() { + if (!this->var_queue_) { + // Allocate array of max_runs_ - 1 slots for queued items (running item is separate) + // unique_ptr array is zero-initialized, so all slots start as nullptr + this->var_queue_ = std::make_unique>[]>(this->max_runs_ - 1); + } + } + + template void trigger_tuple_(const std::tuple &tuple, std::index_sequence /*unused*/) { this->trigger(std::get(tuple)...); } - int num_runs_ = 0; - int max_runs_ = 0; - std::queue> var_queue_; + int num_queued_ = 0; // Number of queued instances (not including currently running) + int max_runs_ = 0; // Maximum total instances (running + queued) + size_t queue_front_ = 0; // Ring buffer read position (next item to execute) + std::unique_ptr>[]> var_queue_; // Ring buffer of queued parameters }; /** A script type that executes new instances in parallel. @@ -143,7 +193,8 @@ template class ParallelScript : public Script { public: void execute(Ts... x) override { if (this->max_runs_ != 0 && this->automation_parent_->num_running() >= this->max_runs_) { - this->esp_logw_(__LINE__, "Script '%s' maximum number of parallel runs exceeded!", this->name_.c_str()); + this->esp_logw_(__LINE__, ESPHOME_LOG_FORMAT("Script '%s' maximum number of parallel runs exceeded!"), + LOG_STR_ARG(this->name_)); return; } this->trigger(x...); @@ -164,7 +215,7 @@ template class ScriptExecuteAction, T template void set_args(F... x) { args_ = Args{x...}; } - void play(Ts... x) override { this->script_->execute_tuple(this->eval_args_(x...)); } + void play(const Ts &...x) override { this->script_->execute_tuple(this->eval_args_(x...)); } protected: // NOTE: @@ -198,7 +249,7 @@ template class ScriptStopAction : public Action public: ScriptStopAction(C *script) : script_(script) {} - void play(Ts... x) override { this->script_->stop(); } + void play(const Ts &...x) override { this->script_->stop(); } protected: C *script_; @@ -208,25 +259,46 @@ template class IsRunningCondition : public Conditionparent_->is_running(); } + bool check(const Ts &...x) override { return this->parent_->is_running(); } protected: C *parent_; }; +/** Wait for a script to finish before continuing. + * + * Uses queue-based storage to safely handle concurrent executions. + * While concurrent execution from the same trigger is uncommon, it's possible + * (e.g., rapid button presses, high-frequency sensor updates), so we use + * queue-based storage for correctness. + */ template class ScriptWaitAction : public Action, public Component { public: ScriptWaitAction(C *script) : script_(script) {} - void play_complex(Ts... x) override { + void setup() override { + // Start with loop disabled - only enable when there's work to do + // IMPORTANT: Only disable if num_running_ is 0, otherwise play_complex() was already + // called before our setup() (e.g., from on_boot trigger at same priority level) + // and we must not undo its enable_loop() call + if (this->num_running_ == 0) { + this->disable_loop(); + } + } + + void play_complex(const Ts &...x) override { this->num_running_++; // Check if we can continue immediately. if (!this->script_->is_running()) { this->play_next_(x...); return; } - this->var_ = std::make_tuple(x...); - this->loop(); + + // Store parameters for later execution + this->param_queue_.emplace_back(x...); + // Enable loop now that we have work to do - don't call loop() synchronously! + // Let the event loop call it to avoid reentrancy issues + this->enable_loop(); } void loop() override { @@ -236,15 +308,34 @@ template class ScriptWaitAction : public Action, if (this->script_->is_running()) return; - this->play_next_tuple_(this->var_); + // Only process ONE queued item per loop iteration + // Processing all items in a while loop causes infinite loops because + // play_next_() can trigger more items to be queued + if (!this->param_queue_.empty()) { + auto ¶ms = this->param_queue_.front(); + this->play_next_tuple_(params, std::make_index_sequence{}); + this->param_queue_.pop_front(); + } else { + // Queue is now empty - disable loop until next play_complex + this->disable_loop(); + } } - void play(Ts... x) override { /* ignore - see play_complex */ + void play(const Ts &...x) override { /* ignore - see play_complex */ + } + + void stop() override { + this->param_queue_.clear(); + this->disable_loop(); } protected: + template void play_next_tuple_(const std::tuple &tuple, std::index_sequence /*unused*/) { + this->play_next_(std::get(tuple)...); + } + C *script_; - std::tuple var_{}; + std::list> param_queue_; }; } // namespace script diff --git a/esphome/components/sdl/binary_sensor.py b/esphome/components/sdl/binary_sensor.py index 3ea6c2d218..e19a488800 100644 --- a/esphome/components/sdl/binary_sensor.py +++ b/esphome/components/sdl/binary_sensor.py @@ -12,241 +12,256 @@ CODEOWNERS = ["@bdm310"] STATE_ARG = "state" -SDL_KEYMAP = { - "SDLK_UNKNOWN": 0, - "SDLK_FIRST": 0, - "SDLK_BACKSPACE": 8, - "SDLK_TAB": 9, - "SDLK_CLEAR": 12, - "SDLK_RETURN": 13, - "SDLK_PAUSE": 19, - "SDLK_ESCAPE": 27, - "SDLK_SPACE": 32, - "SDLK_EXCLAIM": 33, - "SDLK_QUOTEDBL": 34, - "SDLK_HASH": 35, - "SDLK_DOLLAR": 36, - "SDLK_AMPERSAND": 38, - "SDLK_QUOTE": 39, - "SDLK_LEFTPAREN": 40, - "SDLK_RIGHTPAREN": 41, - "SDLK_ASTERISK": 42, - "SDLK_PLUS": 43, - "SDLK_COMMA": 44, - "SDLK_MINUS": 45, - "SDLK_PERIOD": 46, - "SDLK_SLASH": 47, - "SDLK_0": 48, - "SDLK_1": 49, - "SDLK_2": 50, - "SDLK_3": 51, - "SDLK_4": 52, - "SDLK_5": 53, - "SDLK_6": 54, - "SDLK_7": 55, - "SDLK_8": 56, - "SDLK_9": 57, - "SDLK_COLON": 58, - "SDLK_SEMICOLON": 59, - "SDLK_LESS": 60, - "SDLK_EQUALS": 61, - "SDLK_GREATER": 62, - "SDLK_QUESTION": 63, - "SDLK_AT": 64, - "SDLK_LEFTBRACKET": 91, - "SDLK_BACKSLASH": 92, - "SDLK_RIGHTBRACKET": 93, - "SDLK_CARET": 94, - "SDLK_UNDERSCORE": 95, - "SDLK_BACKQUOTE": 96, - "SDLK_a": 97, - "SDLK_b": 98, - "SDLK_c": 99, - "SDLK_d": 100, - "SDLK_e": 101, - "SDLK_f": 102, - "SDLK_g": 103, - "SDLK_h": 104, - "SDLK_i": 105, - "SDLK_j": 106, - "SDLK_k": 107, - "SDLK_l": 108, - "SDLK_m": 109, - "SDLK_n": 110, - "SDLK_o": 111, - "SDLK_p": 112, - "SDLK_q": 113, - "SDLK_r": 114, - "SDLK_s": 115, - "SDLK_t": 116, - "SDLK_u": 117, - "SDLK_v": 118, - "SDLK_w": 119, - "SDLK_x": 120, - "SDLK_y": 121, - "SDLK_z": 122, - "SDLK_DELETE": 127, - "SDLK_WORLD_0": 160, - "SDLK_WORLD_1": 161, - "SDLK_WORLD_2": 162, - "SDLK_WORLD_3": 163, - "SDLK_WORLD_4": 164, - "SDLK_WORLD_5": 165, - "SDLK_WORLD_6": 166, - "SDLK_WORLD_7": 167, - "SDLK_WORLD_8": 168, - "SDLK_WORLD_9": 169, - "SDLK_WORLD_10": 170, - "SDLK_WORLD_11": 171, - "SDLK_WORLD_12": 172, - "SDLK_WORLD_13": 173, - "SDLK_WORLD_14": 174, - "SDLK_WORLD_15": 175, - "SDLK_WORLD_16": 176, - "SDLK_WORLD_17": 177, - "SDLK_WORLD_18": 178, - "SDLK_WORLD_19": 179, - "SDLK_WORLD_20": 180, - "SDLK_WORLD_21": 181, - "SDLK_WORLD_22": 182, - "SDLK_WORLD_23": 183, - "SDLK_WORLD_24": 184, - "SDLK_WORLD_25": 185, - "SDLK_WORLD_26": 186, - "SDLK_WORLD_27": 187, - "SDLK_WORLD_28": 188, - "SDLK_WORLD_29": 189, - "SDLK_WORLD_30": 190, - "SDLK_WORLD_31": 191, - "SDLK_WORLD_32": 192, - "SDLK_WORLD_33": 193, - "SDLK_WORLD_34": 194, - "SDLK_WORLD_35": 195, - "SDLK_WORLD_36": 196, - "SDLK_WORLD_37": 197, - "SDLK_WORLD_38": 198, - "SDLK_WORLD_39": 199, - "SDLK_WORLD_40": 200, - "SDLK_WORLD_41": 201, - "SDLK_WORLD_42": 202, - "SDLK_WORLD_43": 203, - "SDLK_WORLD_44": 204, - "SDLK_WORLD_45": 205, - "SDLK_WORLD_46": 206, - "SDLK_WORLD_47": 207, - "SDLK_WORLD_48": 208, - "SDLK_WORLD_49": 209, - "SDLK_WORLD_50": 210, - "SDLK_WORLD_51": 211, - "SDLK_WORLD_52": 212, - "SDLK_WORLD_53": 213, - "SDLK_WORLD_54": 214, - "SDLK_WORLD_55": 215, - "SDLK_WORLD_56": 216, - "SDLK_WORLD_57": 217, - "SDLK_WORLD_58": 218, - "SDLK_WORLD_59": 219, - "SDLK_WORLD_60": 220, - "SDLK_WORLD_61": 221, - "SDLK_WORLD_62": 222, - "SDLK_WORLD_63": 223, - "SDLK_WORLD_64": 224, - "SDLK_WORLD_65": 225, - "SDLK_WORLD_66": 226, - "SDLK_WORLD_67": 227, - "SDLK_WORLD_68": 228, - "SDLK_WORLD_69": 229, - "SDLK_WORLD_70": 230, - "SDLK_WORLD_71": 231, - "SDLK_WORLD_72": 232, - "SDLK_WORLD_73": 233, - "SDLK_WORLD_74": 234, - "SDLK_WORLD_75": 235, - "SDLK_WORLD_76": 236, - "SDLK_WORLD_77": 237, - "SDLK_WORLD_78": 238, - "SDLK_WORLD_79": 239, - "SDLK_WORLD_80": 240, - "SDLK_WORLD_81": 241, - "SDLK_WORLD_82": 242, - "SDLK_WORLD_83": 243, - "SDLK_WORLD_84": 244, - "SDLK_WORLD_85": 245, - "SDLK_WORLD_86": 246, - "SDLK_WORLD_87": 247, - "SDLK_WORLD_88": 248, - "SDLK_WORLD_89": 249, - "SDLK_WORLD_90": 250, - "SDLK_WORLD_91": 251, - "SDLK_WORLD_92": 252, - "SDLK_WORLD_93": 253, - "SDLK_WORLD_94": 254, - "SDLK_WORLD_95": 255, - "SDLK_KP0": 256, - "SDLK_KP1": 257, - "SDLK_KP2": 258, - "SDLK_KP3": 259, - "SDLK_KP4": 260, - "SDLK_KP5": 261, - "SDLK_KP6": 262, - "SDLK_KP7": 263, - "SDLK_KP8": 264, - "SDLK_KP9": 265, - "SDLK_KP_PERIOD": 266, - "SDLK_KP_DIVIDE": 267, - "SDLK_KP_MULTIPLY": 268, - "SDLK_KP_MINUS": 269, - "SDLK_KP_PLUS": 270, - "SDLK_KP_ENTER": 271, - "SDLK_KP_EQUALS": 272, - "SDLK_UP": 273, - "SDLK_DOWN": 274, - "SDLK_RIGHT": 275, - "SDLK_LEFT": 276, - "SDLK_INSERT": 277, - "SDLK_HOME": 278, - "SDLK_END": 279, - "SDLK_PAGEUP": 280, - "SDLK_PAGEDOWN": 281, - "SDLK_F1": 282, - "SDLK_F2": 283, - "SDLK_F3": 284, - "SDLK_F4": 285, - "SDLK_F5": 286, - "SDLK_F6": 287, - "SDLK_F7": 288, - "SDLK_F8": 289, - "SDLK_F9": 290, - "SDLK_F10": 291, - "SDLK_F11": 292, - "SDLK_F12": 293, - "SDLK_F13": 294, - "SDLK_F14": 295, - "SDLK_F15": 296, - "SDLK_NUMLOCK": 300, - "SDLK_CAPSLOCK": 301, - "SDLK_SCROLLOCK": 302, - "SDLK_RSHIFT": 303, - "SDLK_LSHIFT": 304, - "SDLK_RCTRL": 305, - "SDLK_LCTRL": 306, - "SDLK_RALT": 307, - "SDLK_LALT": 308, - "SDLK_RMETA": 309, - "SDLK_LMETA": 310, - "SDLK_LSUPER": 311, - "SDLK_RSUPER": 312, - "SDLK_MODE": 313, - "SDLK_COMPOSE": 314, - "SDLK_HELP": 315, - "SDLK_PRINT": 316, - "SDLK_SYSREQ": 317, - "SDLK_BREAK": 318, - "SDLK_MENU": 319, - "SDLK_POWER": 320, - "SDLK_EURO": 321, - "SDLK_UNDO": 322, -} +SDL_KeyCode = cg.global_ns.enum("SDL_KeyCode") + +SDL_KEYS = ( + "SDLK_UNKNOWN", + "SDLK_RETURN", + "SDLK_ESCAPE", + "SDLK_BACKSPACE", + "SDLK_TAB", + "SDLK_SPACE", + "SDLK_EXCLAIM", + "SDLK_QUOTEDBL", + "SDLK_HASH", + "SDLK_PERCENT", + "SDLK_DOLLAR", + "SDLK_AMPERSAND", + "SDLK_QUOTE", + "SDLK_LEFTPAREN", + "SDLK_RIGHTPAREN", + "SDLK_ASTERISK", + "SDLK_PLUS", + "SDLK_COMMA", + "SDLK_MINUS", + "SDLK_PERIOD", + "SDLK_SLASH", + "SDLK_0", + "SDLK_1", + "SDLK_2", + "SDLK_3", + "SDLK_4", + "SDLK_5", + "SDLK_6", + "SDLK_7", + "SDLK_8", + "SDLK_9", + "SDLK_COLON", + "SDLK_SEMICOLON", + "SDLK_LESS", + "SDLK_EQUALS", + "SDLK_GREATER", + "SDLK_QUESTION", + "SDLK_AT", + "SDLK_LEFTBRACKET", + "SDLK_BACKSLASH", + "SDLK_RIGHTBRACKET", + "SDLK_CARET", + "SDLK_UNDERSCORE", + "SDLK_BACKQUOTE", + "SDLK_a", + "SDLK_b", + "SDLK_c", + "SDLK_d", + "SDLK_e", + "SDLK_f", + "SDLK_g", + "SDLK_h", + "SDLK_i", + "SDLK_j", + "SDLK_k", + "SDLK_l", + "SDLK_m", + "SDLK_n", + "SDLK_o", + "SDLK_p", + "SDLK_q", + "SDLK_r", + "SDLK_s", + "SDLK_t", + "SDLK_u", + "SDLK_v", + "SDLK_w", + "SDLK_x", + "SDLK_y", + "SDLK_z", + "SDLK_CAPSLOCK", + "SDLK_F1", + "SDLK_F2", + "SDLK_F3", + "SDLK_F4", + "SDLK_F5", + "SDLK_F6", + "SDLK_F7", + "SDLK_F8", + "SDLK_F9", + "SDLK_F10", + "SDLK_F11", + "SDLK_F12", + "SDLK_PRINTSCREEN", + "SDLK_SCROLLLOCK", + "SDLK_PAUSE", + "SDLK_INSERT", + "SDLK_HOME", + "SDLK_PAGEUP", + "SDLK_DELETE", + "SDLK_END", + "SDLK_PAGEDOWN", + "SDLK_RIGHT", + "SDLK_LEFT", + "SDLK_DOWN", + "SDLK_UP", + "SDLK_NUMLOCKCLEAR", + "SDLK_KP_DIVIDE", + "SDLK_KP_MULTIPLY", + "SDLK_KP_MINUS", + "SDLK_KP_PLUS", + "SDLK_KP_ENTER", + "SDLK_KP_1", + "SDLK_KP_2", + "SDLK_KP_3", + "SDLK_KP_4", + "SDLK_KP_5", + "SDLK_KP_6", + "SDLK_KP_7", + "SDLK_KP_8", + "SDLK_KP_9", + "SDLK_KP_0", + "SDLK_KP_PERIOD", + "SDLK_APPLICATION", + "SDLK_POWER", + "SDLK_KP_EQUALS", + "SDLK_F13", + "SDLK_F14", + "SDLK_F15", + "SDLK_F16", + "SDLK_F17", + "SDLK_F18", + "SDLK_F19", + "SDLK_F20", + "SDLK_F21", + "SDLK_F22", + "SDLK_F23", + "SDLK_F24", + "SDLK_EXECUTE", + "SDLK_HELP", + "SDLK_MENU", + "SDLK_SELECT", + "SDLK_STOP", + "SDLK_AGAIN", + "SDLK_UNDO", + "SDLK_CUT", + "SDLK_COPY", + "SDLK_PASTE", + "SDLK_FIND", + "SDLK_MUTE", + "SDLK_VOLUMEUP", + "SDLK_VOLUMEDOWN", + "SDLK_KP_COMMA", + "SDLK_KP_EQUALSAS400", + "SDLK_ALTERASE", + "SDLK_SYSREQ", + "SDLK_CANCEL", + "SDLK_CLEAR", + "SDLK_PRIOR", + "SDLK_RETURN2", + "SDLK_SEPARATOR", + "SDLK_OUT", + "SDLK_OPER", + "SDLK_CLEARAGAIN", + "SDLK_CRSEL", + "SDLK_EXSEL", + "SDLK_KP_00", + "SDLK_KP_000", + "SDLK_THOUSANDSSEPARATOR", + "SDLK_DECIMALSEPARATOR", + "SDLK_CURRENCYUNIT", + "SDLK_CURRENCYSUBUNIT", + "SDLK_KP_LEFTPAREN", + "SDLK_KP_RIGHTPAREN", + "SDLK_KP_LEFTBRACE", + "SDLK_KP_RIGHTBRACE", + "SDLK_KP_TAB", + "SDLK_KP_BACKSPACE", + "SDLK_KP_A", + "SDLK_KP_B", + "SDLK_KP_C", + "SDLK_KP_D", + "SDLK_KP_E", + "SDLK_KP_F", + "SDLK_KP_XOR", + "SDLK_KP_POWER", + "SDLK_KP_PERCENT", + "SDLK_KP_LESS", + "SDLK_KP_GREATER", + "SDLK_KP_AMPERSAND", + "SDLK_KP_DBLAMPERSAND", + "SDLK_KP_VERTICALBAR", + "SDLK_KP_DBLVERTICALBAR", + "SDLK_KP_COLON", + "SDLK_KP_HASH", + "SDLK_KP_SPACE", + "SDLK_KP_AT", + "SDLK_KP_EXCLAM", + "SDLK_KP_MEMSTORE", + "SDLK_KP_MEMRECALL", + "SDLK_KP_MEMCLEAR", + "SDLK_KP_MEMADD", + "SDLK_KP_MEMSUBTRACT", + "SDLK_KP_MEMMULTIPLY", + "SDLK_KP_MEMDIVIDE", + "SDLK_KP_PLUSMINUS", + "SDLK_KP_CLEAR", + "SDLK_KP_CLEARENTRY", + "SDLK_KP_BINARY", + "SDLK_KP_OCTAL", + "SDLK_KP_DECIMAL", + "SDLK_KP_HEXADECIMAL", + "SDLK_LCTRL", + "SDLK_LSHIFT", + "SDLK_LALT", + "SDLK_LGUI", + "SDLK_RCTRL", + "SDLK_RSHIFT", + "SDLK_RALT", + "SDLK_RGUI", + "SDLK_MODE", + "SDLK_AUDIONEXT", + "SDLK_AUDIOPREV", + "SDLK_AUDIOSTOP", + "SDLK_AUDIOPLAY", + "SDLK_AUDIOMUTE", + "SDLK_MEDIASELECT", + "SDLK_WWW", + "SDLK_MAIL", + "SDLK_CALCULATOR", + "SDLK_COMPUTER", + "SDLK_AC_SEARCH", + "SDLK_AC_HOME", + "SDLK_AC_BACK", + "SDLK_AC_FORWARD", + "SDLK_AC_STOP", + "SDLK_AC_REFRESH", + "SDLK_AC_BOOKMARKS", + "SDLK_BRIGHTNESSDOWN", + "SDLK_BRIGHTNESSUP", + "SDLK_DISPLAYSWITCH", + "SDLK_KBDILLUMTOGGLE", + "SDLK_KBDILLUMDOWN", + "SDLK_KBDILLUMUP", + "SDLK_EJECT", + "SDLK_SLEEP", + "SDLK_APP1", + "SDLK_APP2", + "SDLK_AUDIOREWIND", + "SDLK_AUDIOFASTFORWARD", + "SDLK_SOFTLEFT", + "SDLK_SOFTRIGHT", + "SDLK_CALL", + "SDLK_ENDCALL", +) + +SDL_KEYMAP = {key: getattr(SDL_KeyCode, key) for key in SDL_KEYS} CONFIG_SCHEMA = ( binary_sensor.binary_sensor_schema(BinarySensor) diff --git a/esphome/components/sdl/display.py b/esphome/components/sdl/display.py index ae8b0fd43a..78c180aa65 100644 --- a/esphome/components/sdl/display.py +++ b/esphome/components/sdl/display.py @@ -36,7 +36,9 @@ def get_sdl_options(value): if value != "": return value try: - return subprocess.check_output(["sdl2-config", "--cflags", "--libs"]).decode() + return subprocess.check_output( + ["sdl2-config", "--cflags", "--libs"], close_fds=False + ).decode() except Exception as e: raise cv.Invalid("Unable to run sdl2-config - have you installed sdl2?") from e diff --git a/esphome/components/sdm_meter/sensor.py b/esphome/components/sdm_meter/sensor.py index 24ae32c7bc..affbc0409e 100644 --- a/esphome/components/sdm_meter/sensor.py +++ b/esphome/components/sdm_meter/sensor.py @@ -1,6 +1,5 @@ import esphome.codegen as cg from esphome.components import modbus, sensor -from esphome.components.atm90e32.sensor import CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C import esphome.config_validation as cv from esphome.const import ( CONF_ACTIVE_POWER, @@ -12,7 +11,10 @@ from esphome.const import ( CONF_ID, CONF_IMPORT_ACTIVE_ENERGY, CONF_IMPORT_REACTIVE_ENERGY, + CONF_PHASE_A, CONF_PHASE_ANGLE, + CONF_PHASE_B, + CONF_PHASE_C, CONF_POWER_FACTOR, CONF_REACTIVE_POWER, CONF_TOTAL_POWER, diff --git a/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp b/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp index 2153ef278a..7e6fac3ab0 100644 --- a/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp +++ b/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp @@ -462,12 +462,12 @@ void MR24HPC1Component::r24_frame_parse_open_underlying_information_(uint8_t *da } else if ((this->existence_boundary_select_ != nullptr) && ((data[FRAME_COMMAND_WORD_INDEX] == 0x0a) || (data[FRAME_COMMAND_WORD_INDEX] == 0x8a))) { if (this->existence_boundary_select_->has_index(data[FRAME_DATA_INDEX] - 1)) { - this->existence_boundary_select_->publish_state(S_BOUNDARY_STR[data[FRAME_DATA_INDEX] - 1]); + this->existence_boundary_select_->publish_state(data[FRAME_DATA_INDEX] - 1); } } else if ((this->motion_boundary_select_ != nullptr) && ((data[FRAME_COMMAND_WORD_INDEX] == 0x0b) || (data[FRAME_COMMAND_WORD_INDEX] == 0x8b))) { if (this->motion_boundary_select_->has_index(data[FRAME_DATA_INDEX] - 1)) { - this->motion_boundary_select_->publish_state(S_BOUNDARY_STR[data[FRAME_DATA_INDEX] - 1]); + this->motion_boundary_select_->publish_state(data[FRAME_DATA_INDEX] - 1); } #endif } else if ((this->motion_trigger_number_ != nullptr) && @@ -568,7 +568,7 @@ void MR24HPC1Component::r24_frame_parse_work_status_(uint8_t *data) { } else if (data[FRAME_COMMAND_WORD_INDEX] == 0x07) { #ifdef USE_SELECT if ((this->scene_mode_select_ != nullptr) && (this->scene_mode_select_->has_index(data[FRAME_DATA_INDEX]))) { - this->scene_mode_select_->publish_state(S_SCENE_STR[data[FRAME_DATA_INDEX]]); + this->scene_mode_select_->publish_state(data[FRAME_DATA_INDEX]); } else { ESP_LOGD(TAG, "Select has index offset %d Error", data[FRAME_DATA_INDEX]); } @@ -601,7 +601,7 @@ void MR24HPC1Component::r24_frame_parse_work_status_(uint8_t *data) { } else if (data[FRAME_COMMAND_WORD_INDEX] == 0x87) { #ifdef USE_SELECT if ((this->scene_mode_select_ != nullptr) && (this->scene_mode_select_->has_index(data[FRAME_DATA_INDEX]))) { - this->scene_mode_select_->publish_state(S_SCENE_STR[data[FRAME_DATA_INDEX]]); + this->scene_mode_select_->publish_state(data[FRAME_DATA_INDEX]); } else { ESP_LOGD(TAG, "Select has index offset %d Error", data[FRAME_DATA_INDEX]); } @@ -662,7 +662,7 @@ void MR24HPC1Component::r24_frame_parse_human_information_(uint8_t *data) { ((data[FRAME_COMMAND_WORD_INDEX] == 0x0A) || (data[FRAME_COMMAND_WORD_INDEX] == 0x8A))) { // none:0x00 1s:0x01 30s:0x02 1min:0x03 2min:0x04 5min:0x05 10min:0x06 30min:0x07 1hour:0x08 if (data[FRAME_DATA_INDEX] < 9) { - this->unman_time_select_->publish_state(S_UNMANNED_TIME_STR[data[FRAME_DATA_INDEX]]); + this->unman_time_select_->publish_state(data[FRAME_DATA_INDEX]); } #endif } else if ((this->keep_away_text_sensor_ != nullptr) && diff --git a/esphome/components/seeed_mr24hpc1/select/existence_boundary_select.cpp b/esphome/components/seeed_mr24hpc1/select/existence_boundary_select.cpp index 03c2ec4745..81543055a4 100644 --- a/esphome/components/seeed_mr24hpc1/select/existence_boundary_select.cpp +++ b/esphome/components/seeed_mr24hpc1/select/existence_boundary_select.cpp @@ -3,12 +3,9 @@ namespace esphome { namespace seeed_mr24hpc1 { -void ExistenceBoundarySelect::control(const std::string &value) { - this->publish_state(value); - auto index = this->index_of(value); - if (index.has_value()) { - this->parent_->set_existence_boundary(index.value()); - } +void ExistenceBoundarySelect::control(size_t index) { + this->publish_state(index); + this->parent_->set_existence_boundary(index); } } // namespace seeed_mr24hpc1 diff --git a/esphome/components/seeed_mr24hpc1/select/existence_boundary_select.h b/esphome/components/seeed_mr24hpc1/select/existence_boundary_select.h index ad770a7296..933279dd13 100644 --- a/esphome/components/seeed_mr24hpc1/select/existence_boundary_select.h +++ b/esphome/components/seeed_mr24hpc1/select/existence_boundary_select.h @@ -11,7 +11,7 @@ class ExistenceBoundarySelect : public select::Select, public Parentedpublish_state(value); - auto index = this->index_of(value); - if (index.has_value()) { - this->parent_->set_motion_boundary(index.value()); - } +void MotionBoundarySelect::control(size_t index) { + this->publish_state(index); + this->parent_->set_motion_boundary(index); } } // namespace seeed_mr24hpc1 diff --git a/esphome/components/seeed_mr24hpc1/select/motion_boundary_select.h b/esphome/components/seeed_mr24hpc1/select/motion_boundary_select.h index 9058e3130b..b0051ae6b1 100644 --- a/esphome/components/seeed_mr24hpc1/select/motion_boundary_select.h +++ b/esphome/components/seeed_mr24hpc1/select/motion_boundary_select.h @@ -11,7 +11,7 @@ class MotionBoundarySelect : public select::Select, public Parentedpublish_state(value); - auto index = this->index_of(value); - if (index.has_value()) { - this->parent_->set_scene_mode(index.value()); - } +void SceneModeSelect::control(size_t index) { + this->publish_state(index); + this->parent_->set_scene_mode(index); } } // namespace seeed_mr24hpc1 diff --git a/esphome/components/seeed_mr24hpc1/select/scene_mode_select.h b/esphome/components/seeed_mr24hpc1/select/scene_mode_select.h index 95508d49b0..f478ea5b66 100644 --- a/esphome/components/seeed_mr24hpc1/select/scene_mode_select.h +++ b/esphome/components/seeed_mr24hpc1/select/scene_mode_select.h @@ -11,7 +11,7 @@ class SceneModeSelect : public select::Select, public Parentedpublish_state(value); - auto index = this->index_of(value); - if (index.has_value()) { - this->parent_->set_unman_time(index.value()); - } +void UnmanTimeSelect::control(size_t index) { + this->publish_state(index); + this->parent_->set_unman_time(index); } } // namespace seeed_mr24hpc1 diff --git a/esphome/components/seeed_mr24hpc1/select/unman_time_select.h b/esphome/components/seeed_mr24hpc1/select/unman_time_select.h index 7131988cda..a64ff4b840 100644 --- a/esphome/components/seeed_mr24hpc1/select/unman_time_select.h +++ b/esphome/components/seeed_mr24hpc1/select/unman_time_select.h @@ -11,7 +11,7 @@ class UnmanTimeSelect : public select::Select, public Parented(current_install_height_int); uint32_t select_index = find_nearest_index(install_height_float, INSTALL_HEIGHT, 7); - this->install_height_select_->publish_state(this->install_height_select_->at(select_index).value()); + this->install_height_select_->publish_state(select_index); } if (this->height_threshold_select_ != nullptr) { @@ -301,7 +301,7 @@ void MR60FDA2Component::process_frame_() { height_threshold_float = bit_cast(current_height_threshold_int); size_t select_index = find_nearest_index(height_threshold_float, HEIGHT_THRESHOLD, 7); - this->height_threshold_select_->publish_state(this->height_threshold_select_->at(select_index).value()); + this->height_threshold_select_->publish_state(select_index); } if (this->sensitivity_select_ != nullptr) { @@ -309,7 +309,7 @@ void MR60FDA2Component::process_frame_() { encode_uint32(current_data_buf_[11], current_data_buf_[10], current_data_buf_[9], current_data_buf_[8]); uint32_t select_index = find_nearest_index(current_sensitivity, SENSITIVITY, 3); - this->sensitivity_select_->publish_state(this->sensitivity_select_->at(select_index).value()); + this->sensitivity_select_->publish_state(select_index); } ESP_LOGD(TAG, "Mounting height: %.2f, Height threshold: %.2f, Sensitivity: %" PRIu32, install_height_float, diff --git a/esphome/components/seeed_mr60fda2/select/height_threshold_select.cpp b/esphome/components/seeed_mr60fda2/select/height_threshold_select.cpp index 037f8c6036..c963ccdadd 100644 --- a/esphome/components/seeed_mr60fda2/select/height_threshold_select.cpp +++ b/esphome/components/seeed_mr60fda2/select/height_threshold_select.cpp @@ -3,12 +3,9 @@ namespace esphome { namespace seeed_mr60fda2 { -void HeightThresholdSelect::control(const std::string &value) { - this->publish_state(value); - auto index = this->index_of(value); - if (index.has_value()) { - this->parent_->set_height_threshold(index.value()); - } +void HeightThresholdSelect::control(size_t index) { + this->publish_state(index); + this->parent_->set_height_threshold(index); } } // namespace seeed_mr60fda2 diff --git a/esphome/components/seeed_mr60fda2/select/height_threshold_select.h b/esphome/components/seeed_mr60fda2/select/height_threshold_select.h index b856dbc89a..f5707c7a88 100644 --- a/esphome/components/seeed_mr60fda2/select/height_threshold_select.h +++ b/esphome/components/seeed_mr60fda2/select/height_threshold_select.h @@ -11,7 +11,7 @@ class HeightThresholdSelect : public select::Select, public Parentedpublish_state(value); - auto index = this->index_of(value); - if (index.has_value()) { - this->parent_->set_install_height(index.value()); - } +void InstallHeightSelect::control(size_t index) { + this->publish_state(index); + this->parent_->set_install_height(index); } } // namespace seeed_mr60fda2 diff --git a/esphome/components/seeed_mr60fda2/select/install_height_select.h b/esphome/components/seeed_mr60fda2/select/install_height_select.h index 7430da3493..470d96c50c 100644 --- a/esphome/components/seeed_mr60fda2/select/install_height_select.h +++ b/esphome/components/seeed_mr60fda2/select/install_height_select.h @@ -11,7 +11,7 @@ class InstallHeightSelect : public select::Select, public Parentedpublish_state(value); - auto index = this->index_of(value); - if (index.has_value()) { - this->parent_->set_sensitivity(index.value()); - } +void SensitivitySelect::control(size_t index) { + this->publish_state(index); + this->parent_->set_sensitivity(index); } } // namespace seeed_mr60fda2 diff --git a/esphome/components/seeed_mr60fda2/select/sensitivity_select.h b/esphome/components/seeed_mr60fda2/select/sensitivity_select.h index d1accc1b5b..82ed4c5d79 100644 --- a/esphome/components/seeed_mr60fda2/select/sensitivity_select.h +++ b/esphome/components/seeed_mr60fda2/select/sensitivity_select.h @@ -11,7 +11,7 @@ class SensitivitySelect : public select::Select, public Parented { public: @@ -19,7 +18,7 @@ template class SelectSetAction : public Action { explicit SelectSetAction(Select *select) : select_(select) {} TEMPLATABLE_VALUE(std::string, option) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->select_->make_call(); call.set_option(this->option_.value(x...)); call.perform(); @@ -34,7 +33,7 @@ template class SelectSetIndexAction : public Action { explicit SelectSetIndexAction(Select *select) : select_(select) {} TEMPLATABLE_VALUE(size_t, index) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->select_->make_call(); call.set_index(this->index_.value(x...)); call.perform(); @@ -50,7 +49,7 @@ template class SelectOperationAction : public Action { TEMPLATABLE_VALUE(bool, cycle) TEMPLATABLE_VALUE(SelectOperation, operation) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->select_->make_call(); call.with_operation(this->operation_.value(x...)); if (this->cycle_.has_value()) { @@ -63,5 +62,4 @@ template class SelectOperationAction : public Action { Select *select_; }; -} // namespace select -} // namespace esphome +} // namespace esphome::select diff --git a/esphome/components/select/select.cpp b/esphome/components/select/select.cpp index 37887da27c..3ec413f167 100644 --- a/esphome/components/select/select.cpp +++ b/esphome/components/select/select.cpp @@ -1,62 +1,88 @@ #include "select.h" +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" +#include -namespace esphome { -namespace select { +namespace esphome::select { static const char *const TAG = "select"; -void Select::publish_state(const std::string &state) { +void Select::publish_state(const std::string &state) { this->publish_state(state.c_str()); } + +void Select::publish_state(const char *state) { auto index = this->index_of(state); - const auto *name = this->get_name().c_str(); if (index.has_value()) { - this->set_has_state(true); - this->state = state; - ESP_LOGD(TAG, "'%s': Sending state %s (index %zu)", name, state.c_str(), index.value()); - this->state_callback_.call(state, index.value()); + this->publish_state(index.value()); } else { - ESP_LOGE(TAG, "'%s': invalid state for publish_state(): %s", name, state.c_str()); + ESP_LOGE(TAG, "'%s': Invalid option %s", this->get_name().c_str(), state); } } +void Select::publish_state(size_t index) { + if (!this->has_index(index)) { + ESP_LOGE(TAG, "'%s': Invalid index %zu", this->get_name().c_str(), index); + return; + } + const char *option = this->option_at(index); + this->set_has_state(true); + this->active_index_ = index; +#pragma GCC diagnostic push +#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); + // Callback signature requires std::string, create temporary for compatibility + this->state_callback_.call(std::string(option), index); +#if defined(USE_SELECT) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_select_update(this); +#endif +} + +const char *Select::current_option() const { return this->has_state() ? this->option_at(this->active_index_) : ""; } + void Select::add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } -bool Select::has_option(const std::string &option) const { return this->index_of(option).has_value(); } +bool Select::has_option(const std::string &option) const { return this->index_of(option.c_str()).has_value(); } + +bool Select::has_option(const char *option) const { return this->index_of(option).has_value(); } bool Select::has_index(size_t index) const { return index < this->size(); } size_t Select::size() const { - auto options = traits.get_options(); + const auto &options = traits.get_options(); return options.size(); } -optional Select::index_of(const std::string &option) const { - auto options = traits.get_options(); - auto it = std::find(options.begin(), options.end(), option); - if (it == options.end()) { - return {}; +optional Select::index_of(const std::string &option) const { return this->index_of(option.c_str()); } + +optional Select::index_of(const char *option) const { + const auto &options = traits.get_options(); + for (size_t i = 0; i < options.size(); i++) { + if (strcmp(options[i], option) == 0) { + return i; + } } - return std::distance(options.begin(), it); + return {}; } optional Select::active_index() const { if (this->has_state()) { - return this->index_of(this->state); - } else { - return {}; + return this->active_index_; } + return {}; } optional Select::at(size_t index) const { if (this->has_index(index)) { - auto options = traits.get_options(); - return options.at(index); - } else { - return {}; + const auto &options = traits.get_options(); + return std::string(options.at(index)); } + return {}; } -} // namespace select -} // namespace esphome +const char *Select::option_at(size_t index) const { return traits.get_options().at(index); } + +} // namespace esphome::select diff --git a/esphome/components/select/select.h b/esphome/components/select/select.h index 3ab651b241..c4d7412d50 100644 --- a/esphome/components/select/select.h +++ b/esphome/components/select/select.h @@ -6,14 +6,13 @@ #include "select_call.h" #include "select_traits.h" -namespace esphome { -namespace select { +namespace esphome::select { #define LOG_SELECT(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - if (!(obj)->get_icon().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \ + if (!(obj)->get_icon_ref().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon_ref().c_str()); \ } \ } @@ -30,16 +29,31 @@ namespace select { */ class Select : public EntityBase { public: - std::string state; SelectTraits traits; +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + /// @deprecated Use current_option() instead. This member will be removed in ESPHome 2026.5.0. + ESPDEPRECATED("Use current_option() instead of .state. Will be removed in 2026.5.0", "2025.11.0") + std::string state{}; + + Select() = default; + ~Select() = default; +#pragma GCC diagnostic pop + void publish_state(const std::string &state); + void publish_state(const char *state); + void publish_state(size_t index); + + /// Return the currently selected option (as const char* from flash). + const char *current_option() const; /// Instantiate a SelectCall object to modify this select component's state. SelectCall make_call() { return SelectCall(this); } /// Return whether this select component contains the provided option. bool has_option(const std::string &option) const; + bool has_option(const char *option) const; /// Return whether this select component contains the provided index offset. bool has_index(size_t index) const; @@ -49,6 +63,7 @@ class Select : public EntityBase { /// Find the (optional) index offset of the provided option value. optional index_of(const std::string &option) const; + optional index_of(const char *option) const; /// Return the (optional) index offset of the currently active option. optional active_index() const; @@ -56,21 +71,46 @@ class Select : public EntityBase { /// Return the (optional) option value at the provided index offset. optional at(size_t index) const; + /// Return the option value at the provided index offset (as const char* from flash). + const char *option_at(size_t index) const; + void add_on_state_callback(std::function &&callback); protected: friend class SelectCall; - /** Set the value of the select, this is a virtual method that each select integration must implement. + size_t active_index_{0}; + + /** Set the value of the select by index, this is an optional virtual method. * - * This method is called by the SelectCall. + * IMPORTANT: At least ONE of the two control() methods must be overridden by derived classes. + * Overriding this index-based version is PREFERRED as it avoids string conversions. * - * @param value The value as validated by the SelectCall. + * This method is called by the SelectCall when the index is already known. + * Default implementation converts to string and calls control(const std::string&). + * + * @param index The index as validated by the SelectCall. */ - virtual void control(const std::string &value) = 0; + virtual void control(size_t index) { this->control(this->option_at(index)); } + + /** Set the value of the select, this is a virtual method that each select integration can implement. + * + * IMPORTANT: At least ONE of the two control() methods must be overridden by derived classes. + * Overriding control(size_t) is PREFERRED as it avoids string conversions. + * + * This method is called by control(size_t) when not overridden, or directly by external code. + * Default implementation converts to index and calls control(size_t). + * + * @param value The value as validated by the caller. + */ + virtual void control(const std::string &value) { + auto index = this->index_of(value); + if (index.has_value()) { + this->control(index.value()); + } + } CallbackManager state_callback_; }; -} // namespace select -} // namespace esphome +} // namespace esphome::select diff --git a/esphome/components/select/select_call.cpp b/esphome/components/select/select_call.cpp index 85f755645c..aecfed0d64 100644 --- a/esphome/components/select/select_call.cpp +++ b/esphome/components/select/select_call.cpp @@ -2,24 +2,25 @@ #include "select.h" #include "esphome/core/log.h" -namespace esphome { -namespace select { +namespace esphome::select { static const char *const TAG = "select"; -SelectCall &SelectCall::set_option(const std::string &option) { - return with_operation(SELECT_OP_SET).with_option(option); +SelectCall &SelectCall::set_option(const std::string &option) { return this->with_option(option); } + +SelectCall &SelectCall::set_option(const char *option) { return this->with_option(option); } + +SelectCall &SelectCall::set_index(size_t index) { return this->with_index(index); } + +SelectCall &SelectCall::select_next(bool cycle) { return this->with_operation(SELECT_OP_NEXT).with_cycle(cycle); } + +SelectCall &SelectCall::select_previous(bool cycle) { + return this->with_operation(SELECT_OP_PREVIOUS).with_cycle(cycle); } -SelectCall &SelectCall::set_index(size_t index) { return with_operation(SELECT_OP_SET_INDEX).with_index(index); } +SelectCall &SelectCall::select_first() { return this->with_operation(SELECT_OP_FIRST); } -SelectCall &SelectCall::select_next(bool cycle) { return with_operation(SELECT_OP_NEXT).with_cycle(cycle); } - -SelectCall &SelectCall::select_previous(bool cycle) { return with_operation(SELECT_OP_PREVIOUS).with_cycle(cycle); } - -SelectCall &SelectCall::select_first() { return with_operation(SELECT_OP_FIRST); } - -SelectCall &SelectCall::select_last() { return with_operation(SELECT_OP_LAST); } +SelectCall &SelectCall::select_last() { return this->with_operation(SELECT_OP_LAST); } SelectCall &SelectCall::with_operation(SelectOperation operation) { this->operation_ = operation; @@ -31,90 +32,96 @@ SelectCall &SelectCall::with_cycle(bool cycle) { return *this; } -SelectCall &SelectCall::with_option(const std::string &option) { - this->option_ = option; +SelectCall &SelectCall::with_option(const std::string &option) { return this->with_option(option.c_str()); } + +SelectCall &SelectCall::with_option(const char *option) { + this->operation_ = SELECT_OP_SET; + // Find the option index - this validates the option exists + this->index_ = this->parent_->index_of(option); return *this; } SelectCall &SelectCall::with_index(size_t index) { - this->index_ = index; + this->operation_ = SELECT_OP_SET; + if (index >= this->parent_->size()) { + ESP_LOGW(TAG, "'%s' - Index value %zu out of bounds", this->parent_->get_name().c_str(), index); + this->index_ = {}; // Store nullopt for invalid index + } else { + this->index_ = index; + } return *this; } +optional SelectCall::calculate_target_index_(const char *name) { + const auto &options = this->parent_->traits.get_options(); + if (options.empty()) { + ESP_LOGW(TAG, "'%s' - Select has no options", name); + return {}; + } + + if (this->operation_ == SELECT_OP_FIRST) { + return 0; + } + + if (this->operation_ == SELECT_OP_LAST) { + return options.size() - 1; + } + + if (this->operation_ == SELECT_OP_SET) { + ESP_LOGD(TAG, "'%s' - Setting", name); + if (!this->index_.has_value()) { + ESP_LOGW(TAG, "'%s' - No option set", name); + return {}; + } + return this->index_.value(); + } + + // SELECT_OP_NEXT or SELECT_OP_PREVIOUS + ESP_LOGD(TAG, "'%s' - Selecting %s, with%s cycling", name, + this->operation_ == SELECT_OP_NEXT ? LOG_STR_LITERAL("next") : LOG_STR_LITERAL("previous"), + this->cycle_ ? LOG_STR_LITERAL("") : LOG_STR_LITERAL("out")); + + const auto size = options.size(); + if (!this->parent_->has_state()) { + return this->operation_ == SELECT_OP_NEXT ? 0 : size - 1; + } + + // Use cached active_index_ instead of index_of() lookup + const auto active_index = this->parent_->active_index_; + if (this->cycle_) { + return (size + active_index + (this->operation_ == SELECT_OP_NEXT ? +1 : -1)) % size; + } + + if (this->operation_ == SELECT_OP_PREVIOUS && active_index > 0) { + return active_index - 1; + } + + if (this->operation_ == SELECT_OP_NEXT && active_index < size - 1) { + return active_index + 1; + } + + return {}; // Can't navigate further without cycling +} + void SelectCall::perform() { auto *parent = this->parent_; const auto *name = parent->get_name().c_str(); - const auto &traits = parent->traits; - auto options = traits.get_options(); if (this->operation_ == SELECT_OP_NONE) { ESP_LOGW(TAG, "'%s' - SelectCall performed without selecting an operation", name); return; } - if (options.empty()) { - ESP_LOGW(TAG, "'%s' - Cannot perform SelectCall, select has no options", name); + + // Calculate target index (with_index() and with_option() already validate bounds/existence) + auto target_index = this->calculate_target_index_(name); + if (!target_index.has_value()) { return; } - std::string target_value; - - if (this->operation_ == SELECT_OP_SET) { - ESP_LOGD(TAG, "'%s' - Setting", name); - if (!this->option_.has_value()) { - ESP_LOGW(TAG, "'%s' - No option value set for SelectCall", name); - return; - } - target_value = this->option_.value(); - } else if (this->operation_ == SELECT_OP_SET_INDEX) { - if (!this->index_.has_value()) { - ESP_LOGW(TAG, "'%s' - No index value set for SelectCall", name); - return; - } - if (this->index_.value() >= options.size()) { - ESP_LOGW(TAG, "'%s' - Index value %zu out of bounds", name, this->index_.value()); - return; - } - target_value = options[this->index_.value()]; - } else if (this->operation_ == SELECT_OP_FIRST) { - target_value = options.front(); - } else if (this->operation_ == SELECT_OP_LAST) { - target_value = options.back(); - } else if (this->operation_ == SELECT_OP_NEXT || this->operation_ == SELECT_OP_PREVIOUS) { - auto cycle = this->cycle_; - ESP_LOGD(TAG, "'%s' - Selecting %s, with%s cycling", name, this->operation_ == SELECT_OP_NEXT ? "next" : "previous", - cycle ? "" : "out"); - if (!parent->has_state()) { - target_value = this->operation_ == SELECT_OP_NEXT ? options.front() : options.back(); - } else { - auto index = parent->index_of(parent->state); - if (index.has_value()) { - auto size = options.size(); - if (cycle) { - auto use_index = (size + index.value() + (this->operation_ == SELECT_OP_NEXT ? +1 : -1)) % size; - target_value = options[use_index]; - } else { - if (this->operation_ == SELECT_OP_PREVIOUS && index.value() > 0) { - target_value = options[index.value() - 1]; - } else if (this->operation_ == SELECT_OP_NEXT && index.value() < options.size() - 1) { - target_value = options[index.value() + 1]; - } else { - return; - } - } - } else { - target_value = this->operation_ == SELECT_OP_NEXT ? options.front() : options.back(); - } - } - } - - if (std::find(options.begin(), options.end(), target_value) == options.end()) { - ESP_LOGW(TAG, "'%s' - Option %s is not a valid option", name, target_value.c_str()); - return; - } - - ESP_LOGD(TAG, "'%s' - Set selected option to: %s", name, target_value.c_str()); - parent->control(target_value); + auto idx = target_index.value(); + // All operations use indices, call control() by index to avoid string conversion + ESP_LOGD(TAG, "'%s' - Set selected option to: %s", name, parent->option_at(idx)); + parent->control(idx); } -} // namespace select -} // namespace esphome +} // namespace esphome::select diff --git a/esphome/components/select/select_call.h b/esphome/components/select/select_call.h index efc9a982ec..b31d890ef6 100644 --- a/esphome/components/select/select_call.h +++ b/esphome/components/select/select_call.h @@ -2,15 +2,13 @@ #include "esphome/core/helpers.h" -namespace esphome { -namespace select { +namespace esphome::select { class Select; enum SelectOperation { SELECT_OP_NONE, SELECT_OP_SET, - SELECT_OP_SET_INDEX, SELECT_OP_NEXT, SELECT_OP_PREVIOUS, SELECT_OP_FIRST, @@ -23,6 +21,7 @@ class SelectCall { void perform(); SelectCall &set_option(const std::string &option); + SelectCall &set_option(const char *option); SelectCall &set_index(size_t index); SelectCall &select_next(bool cycle); @@ -33,15 +32,16 @@ class SelectCall { SelectCall &with_operation(SelectOperation operation); SelectCall &with_cycle(bool cycle); SelectCall &with_option(const std::string &option); + SelectCall &with_option(const char *option); SelectCall &with_index(size_t index); protected: + __attribute__((always_inline)) inline optional calculate_target_index_(const char *name); + Select *const parent_; - optional option_; optional index_; SelectOperation operation_{SELECT_OP_NONE}; bool cycle_; }; -} // namespace select -} // namespace esphome +} // namespace esphome::select diff --git a/esphome/components/select/select_traits.cpp b/esphome/components/select/select_traits.cpp index a8cd4290c8..ff52c0d85b 100644 --- a/esphome/components/select/select_traits.cpp +++ b/esphome/components/select/select_traits.cpp @@ -1,11 +1,16 @@ #include "select_traits.h" -namespace esphome { -namespace select { +namespace esphome::select { -void SelectTraits::set_options(std::vector options) { this->options_ = std::move(options); } +void SelectTraits::set_options(const std::initializer_list &options) { this->options_ = options; } -const std::vector &SelectTraits::get_options() const { return this->options_; } +void SelectTraits::set_options(const FixedVector &options) { + this->options_.init(options.size()); + for (const auto &opt : options) { + this->options_.push_back(opt); + } +} -} // namespace select -} // namespace esphome +const FixedVector &SelectTraits::get_options() const { return this->options_; } + +} // namespace esphome::select diff --git a/esphome/components/select/select_traits.h b/esphome/components/select/select_traits.h index 128066dd6b..78a83e5944 100644 --- a/esphome/components/select/select_traits.h +++ b/esphome/components/select/select_traits.h @@ -1,19 +1,18 @@ #pragma once -#include -#include +#include "esphome/core/helpers.h" +#include -namespace esphome { -namespace select { +namespace esphome::select { class SelectTraits { public: - void set_options(std::vector options); - const std::vector &get_options() const; + void set_options(const std::initializer_list &options); + void set_options(const FixedVector &options); + const FixedVector &get_options() const; protected: - std::vector options_; + FixedVector options_; }; -} // namespace select -} // namespace esphome +} // namespace esphome::select diff --git a/esphome/components/sen5x/automation.h b/esphome/components/sen5x/automation.h index 423b942000..558ea46e47 100644 --- a/esphome/components/sen5x/automation.h +++ b/esphome/components/sen5x/automation.h @@ -11,7 +11,7 @@ template class StartFanAction : public Action { public: explicit StartFanAction(SEN5XComponent *sen5x) : sen5x_(sen5x) {} - void play(Ts... x) override { this->sen5x_->start_fan_cleaning(); } + void play(const Ts &...x) override { this->sen5x_->start_fan_cleaning(); } protected: SEN5XComponent *sen5x_; diff --git a/esphome/components/sen5x/sen5x.cpp b/esphome/components/sen5x/sen5x.cpp index 0f27ec1b10..3298a5b8db 100644 --- a/esphome/components/sen5x/sen5x.cpp +++ b/esphome/components/sen5x/sen5x.cpp @@ -29,6 +29,19 @@ static const int8_t SEN5X_INDEX_SCALE_FACTOR = 10; // static const int8_t SEN5X_MIN_INDEX_VALUE = 1 * SEN5X_INDEX_SCALE_FACTOR; // must be adjusted by the scale factor static const int16_t SEN5X_MAX_INDEX_VALUE = 500 * SEN5X_INDEX_SCALE_FACTOR; // must be adjusted by the scale factor +static const LogString *rht_accel_mode_to_string(RhtAccelerationMode mode) { + switch (mode) { + case LOW_ACCELERATION: + return LOG_STR("LOW"); + case MEDIUM_ACCELERATION: + return LOG_STR("MEDIUM"); + case HIGH_ACCELERATION: + return LOG_STR("HIGH"); + default: + return LOG_STR("UNKNOWN"); + } +} + void SEN5XComponent::setup() { // the sensor needs 1000 ms to enter the idle state this->set_timeout(1000, [this]() { @@ -38,6 +51,7 @@ void SEN5XComponent::setup() { this->mark_failed(); return; } + delay(20); // per datasheet uint16_t raw_read_status; if (!this->read_data(raw_read_status)) { @@ -49,7 +63,7 @@ void SEN5XComponent::setup() { uint32_t stop_measurement_delay = 0; // In order to query the device periodic measurement must be ceased if (raw_read_status) { - ESP_LOGD(TAG, "Sensor has data available, stopping periodic measurement"); + ESP_LOGD(TAG, "Data is available; stopping periodic measurement"); if (!this->write_command(SEN5X_CMD_STOP_MEASUREMENTS)) { ESP_LOGE(TAG, "Failed to stop measurements"); this->mark_failed(); @@ -70,7 +84,8 @@ void SEN5XComponent::setup() { this->serial_number_[0] = static_cast(uint16_t(raw_serial_number[0]) & 0xFF); this->serial_number_[1] = static_cast(raw_serial_number[0] & 0xFF); this->serial_number_[2] = static_cast(raw_serial_number[1] >> 8); - ESP_LOGD(TAG, "Serial number %02d.%02d.%02d", serial_number_[0], serial_number_[1], serial_number_[2]); + ESP_LOGV(TAG, "Serial number %02d.%02d.%02d", this->serial_number_[0], this->serial_number_[1], + this->serial_number_[2]); uint16_t raw_product_name[16]; if (!this->get_register(SEN5X_CMD_GET_PRODUCT_NAME, raw_product_name, 16, 20)) { @@ -87,45 +102,43 @@ void SEN5XComponent::setup() { // first char current_char = *current_int >> 8; if (current_char) { - product_name_.push_back(current_char); + this->product_name_.push_back(current_char); // second char current_char = *current_int & 0xFF; if (current_char) { - product_name_.push_back(current_char); + this->product_name_.push_back(current_char); } } current_int++; } while (current_char && --max); Sen5xType sen5x_type = UNKNOWN; - if (product_name_ == "SEN50") { + if (this->product_name_ == "SEN50") { sen5x_type = SEN50; } else { - if (product_name_ == "SEN54") { + if (this->product_name_ == "SEN54") { sen5x_type = SEN54; } else { - if (product_name_ == "SEN55") { + if (this->product_name_ == "SEN55") { sen5x_type = SEN55; } } - ESP_LOGD(TAG, "Productname %s", product_name_.c_str()); + ESP_LOGD(TAG, "Product name: %s", this->product_name_.c_str()); } if (this->humidity_sensor_ && sen5x_type == SEN50) { - ESP_LOGE(TAG, "For Relative humidity a SEN54 OR SEN55 is required. You are using a <%s> sensor", - this->product_name_.c_str()); + ESP_LOGE(TAG, "Relative humidity requires a SEN54 or SEN55"); this->humidity_sensor_ = nullptr; // mark as not used } if (this->temperature_sensor_ && sen5x_type == SEN50) { - ESP_LOGE(TAG, "For Temperature a SEN54 OR SEN55 is required. You are using a <%s> sensor", - this->product_name_.c_str()); + ESP_LOGE(TAG, "Temperature requires a SEN54 or SEN55"); this->temperature_sensor_ = nullptr; // mark as not used } if (this->voc_sensor_ && sen5x_type == SEN50) { - ESP_LOGE(TAG, "For VOC a SEN54 OR SEN55 is required. You are using a <%s> sensor", this->product_name_.c_str()); + ESP_LOGE(TAG, "VOC requires a SEN54 or SEN55"); this->voc_sensor_ = nullptr; // mark as not used } if (this->nox_sensor_ && sen5x_type != SEN55) { - ESP_LOGE(TAG, "For NOx a SEN55 is required. You are using a <%s> sensor", this->product_name_.c_str()); + ESP_LOGE(TAG, "NOx requires a SEN55"); this->nox_sensor_ = nullptr; // mark as not used } @@ -136,7 +149,7 @@ void SEN5XComponent::setup() { return; } this->firmware_version_ >>= 8; - ESP_LOGD(TAG, "Firmware version %d", this->firmware_version_); + ESP_LOGV(TAG, "Firmware version %d", this->firmware_version_); if (this->voc_sensor_ && this->store_baseline_) { uint32_t combined_serial = @@ -149,7 +162,7 @@ void SEN5XComponent::setup() { if (this->pref_.load(&this->voc_baselines_storage_)) { ESP_LOGI(TAG, "Loaded VOC baseline state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32, - this->voc_baselines_storage_.state0, voc_baselines_storage_.state1); + this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1); } // Initialize storage timestamp @@ -157,13 +170,13 @@ void SEN5XComponent::setup() { if (this->voc_baselines_storage_.state0 > 0 && this->voc_baselines_storage_.state1 > 0) { ESP_LOGI(TAG, "Setting VOC baseline from save state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32, - this->voc_baselines_storage_.state0, voc_baselines_storage_.state1); + this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1); uint16_t states[4]; - states[0] = voc_baselines_storage_.state0 >> 16; - states[1] = voc_baselines_storage_.state0 & 0xFFFF; - states[2] = voc_baselines_storage_.state1 >> 16; - states[3] = voc_baselines_storage_.state1 & 0xFFFF; + states[0] = this->voc_baselines_storage_.state0 >> 16; + states[1] = this->voc_baselines_storage_.state0 & 0xFFFF; + states[2] = this->voc_baselines_storage_.state1 >> 16; + states[3] = this->voc_baselines_storage_.state1 & 0xFFFF; if (!this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE, states, 4)) { ESP_LOGE(TAG, "Failed to set VOC baseline from saved state"); @@ -181,11 +194,11 @@ void SEN5XComponent::setup() { delay(20); uint16_t secs[2]; if (this->read_data(secs, 2)) { - auto_cleaning_interval_ = secs[0] << 16 | secs[1]; + this->auto_cleaning_interval_ = secs[0] << 16 | secs[1]; } } - if (acceleration_mode_.has_value()) { - result = this->write_command(SEN5X_CMD_RHT_ACCELERATION_MODE, acceleration_mode_.value()); + if (this->acceleration_mode_.has_value()) { + result = this->write_command(SEN5X_CMD_RHT_ACCELERATION_MODE, this->acceleration_mode_.value()); } else { result = this->write_command(SEN5X_CMD_RHT_ACCELERATION_MODE); } @@ -196,7 +209,7 @@ void SEN5XComponent::setup() { return; } delay(20); - if (!acceleration_mode_.has_value()) { + if (!this->acceleration_mode_.has_value()) { uint16_t mode; if (this->read_data(mode)) { this->acceleration_mode_ = RhtAccelerationMode(mode); @@ -226,19 +239,18 @@ void SEN5XComponent::setup() { } if (!this->write_command(cmd)) { - ESP_LOGE(TAG, "Error starting continuous measurements."); + ESP_LOGE(TAG, "Error starting continuous measurements"); this->error_code_ = MEASUREMENT_INIT_FAILED; this->mark_failed(); return; } - initialized_ = true; - ESP_LOGD(TAG, "Sensor initialized"); + this->initialized_ = true; }); }); } void SEN5XComponent::dump_config() { - ESP_LOGCONFIG(TAG, "sen5x:"); + ESP_LOGCONFIG(TAG, "SEN5X:"); LOG_I2C_DEVICE(this); if (this->is_failed()) { switch (this->error_code_) { @@ -246,16 +258,16 @@ void SEN5XComponent::dump_config() { ESP_LOGW(TAG, ESP_LOG_MSG_COMM_FAIL); break; case MEASUREMENT_INIT_FAILED: - ESP_LOGW(TAG, "Measurement Initialization failed"); + ESP_LOGW(TAG, "Measurement initialization failed"); break; case SERIAL_NUMBER_IDENTIFICATION_FAILED: - ESP_LOGW(TAG, "Unable to read sensor serial id"); + ESP_LOGW(TAG, "Unable to read serial ID"); break; case PRODUCT_NAME_FAILED: ESP_LOGW(TAG, "Unable to read product name"); break; case FIRMWARE_FAILED: - ESP_LOGW(TAG, "Unable to read sensor firmware version"); + ESP_LOGW(TAG, "Unable to read firmware version"); break; default: ESP_LOGW(TAG, "Unknown setup error"); @@ -263,26 +275,17 @@ void SEN5XComponent::dump_config() { } } ESP_LOGCONFIG(TAG, - " Productname: %s\n" + " Product name: %s\n" " Firmware version: %d\n" " Serial number %02d.%02d.%02d", - this->product_name_.c_str(), this->firmware_version_, serial_number_[0], serial_number_[1], - serial_number_[2]); + this->product_name_.c_str(), this->firmware_version_, this->serial_number_[0], this->serial_number_[1], + this->serial_number_[2]); if (this->auto_cleaning_interval_.has_value()) { - ESP_LOGCONFIG(TAG, " Auto cleaning interval %" PRId32 " seconds", auto_cleaning_interval_.value()); + ESP_LOGCONFIG(TAG, " Auto cleaning interval: %" PRId32 "s", this->auto_cleaning_interval_.value()); } if (this->acceleration_mode_.has_value()) { - switch (this->acceleration_mode_.value()) { - case LOW_ACCELERATION: - ESP_LOGCONFIG(TAG, " Low RH/T acceleration mode"); - break; - case MEDIUM_ACCELERATION: - ESP_LOGCONFIG(TAG, " Medium RH/T acceleration mode"); - break; - case HIGH_ACCELERATION: - ESP_LOGCONFIG(TAG, " High RH/T acceleration mode"); - break; - } + ESP_LOGCONFIG(TAG, " RH/T acceleration mode: %s", + LOG_STR_ARG(rht_accel_mode_to_string(this->acceleration_mode_.value()))); } LOG_UPDATE_INTERVAL(this); LOG_SENSOR(" ", "PM 1.0", this->pm_1_0_sensor_); @@ -296,7 +299,7 @@ void SEN5XComponent::dump_config() { } void SEN5XComponent::update() { - if (!initialized_) { + if (!this->initialized_) { return; } @@ -319,8 +322,8 @@ void SEN5XComponent::update() { this->voc_baselines_storage_.state1 = state1; if (this->pref_.save(&this->voc_baselines_storage_)) { - ESP_LOGI(TAG, "Stored VOC baseline state0: 0x%04" PRIX32 " ,state1: 0x%04" PRIX32, - this->voc_baselines_storage_.state0, voc_baselines_storage_.state1); + ESP_LOGI(TAG, "Stored VOC baseline state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32, + this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1); } else { ESP_LOGW(TAG, "Could not store VOC baselines"); } @@ -332,7 +335,7 @@ void SEN5XComponent::update() { if (!this->write_command(SEN5X_CMD_READ_MEASUREMENT)) { this->status_set_warning(); - ESP_LOGD(TAG, "write error read measurement (%d)", this->last_error_); + ESP_LOGD(TAG, "Write error: read measurement (%d)", this->last_error_); return; } this->set_timeout(20, [this]() { @@ -340,7 +343,7 @@ void SEN5XComponent::update() { if (!this->read_data(measurements, 8)) { this->status_set_warning(); - ESP_LOGD(TAG, "read data error (%d)", this->last_error_); + ESP_LOGD(TAG, "Read data error (%d)", this->last_error_); return; } @@ -412,7 +415,7 @@ bool SEN5XComponent::write_tuning_parameters_(uint16_t i2c_command, const GasTun params[5] = tuning.gain_factor; auto result = write_command(i2c_command, params, 6); if (!result) { - ESP_LOGE(TAG, "set tuning parameters failed. i2c command=%0xX, err=%d", i2c_command, this->last_error_); + ESP_LOGE(TAG, "Set tuning parameters failed (command=%0xX, err=%d)", i2c_command, this->last_error_); } return result; } @@ -423,7 +426,7 @@ bool SEN5XComponent::write_temperature_compensation_(const TemperatureCompensati params[1] = compensation.normalized_offset_slope; params[2] = compensation.time_constant; if (!write_command(SEN5X_CMD_TEMPERATURE_COMPENSATION, params, 3)) { - ESP_LOGE(TAG, "set temperature_compensation failed. Err=%d", this->last_error_); + ESP_LOGE(TAG, "Set temperature_compensation failed (%d)", this->last_error_); return false; } return true; @@ -432,7 +435,7 @@ bool SEN5XComponent::write_temperature_compensation_(const TemperatureCompensati bool SEN5XComponent::start_fan_cleaning() { if (!write_command(SEN5X_CMD_START_CLEANING_FAN)) { this->status_set_warning(); - ESP_LOGE(TAG, "write error start fan (%d)", this->last_error_); + ESP_LOGE(TAG, "Start fan cleaning failed (%d)", this->last_error_); return false; } else { ESP_LOGD(TAG, "Fan auto clean started"); diff --git a/esphome/components/sen5x/sen5x.h b/esphome/components/sen5x/sen5x.h index 0fa31605e6..9e5b6bf231 100644 --- a/esphome/components/sen5x/sen5x.h +++ b/esphome/components/sen5x/sen5x.h @@ -9,7 +9,7 @@ namespace esphome { namespace sen5x { -enum ERRORCODE { +enum ERRORCODE : uint8_t { COMMUNICATION_FAILED, SERIAL_NUMBER_IDENTIFICATION_FAILED, MEASUREMENT_INIT_FAILED, @@ -18,19 +18,17 @@ enum ERRORCODE { UNKNOWN }; -// Shortest time interval of 3H for storing baseline values. -// Prevents wear of the flash because of too many write operations -const uint32_t SHORTEST_BASELINE_STORE_INTERVAL = 10800; -// Store anyway if the baseline difference exceeds the max storage diff value -const uint32_t MAXIMUM_STORAGE_DIFF = 50; +enum RhtAccelerationMode : uint16_t { + LOW_ACCELERATION = 0, + MEDIUM_ACCELERATION = 1, + HIGH_ACCELERATION = 2, +}; struct Sen5xBaselines { int32_t state0; int32_t state1; } PACKED; // NOLINT -enum RhtAccelerationMode : uint16_t { LOW_ACCELERATION = 0, MEDIUM_ACCELERATION = 1, HIGH_ACCELERATION = 2 }; - struct GasTuning { uint16_t index_offset; uint16_t learning_time_offset_hours; @@ -46,6 +44,12 @@ struct TemperatureCompensation { uint16_t time_constant; }; +// Shortest time interval of 3H for storing baseline values. +// Prevents wear of the flash because of too many write operations +static const uint32_t SHORTEST_BASELINE_STORE_INTERVAL = 10800; +// Store anyway if the baseline difference exceeds the max storage diff value +static const uint32_t MAXIMUM_STORAGE_DIFF = 50; + class SEN5XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice { public: void setup() override; @@ -102,8 +106,14 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri protected: bool write_tuning_parameters_(uint16_t i2c_command, const GasTuning &tuning); bool write_temperature_compensation_(const TemperatureCompensation &compensation); + + uint32_t seconds_since_last_store_; + uint16_t firmware_version_; ERRORCODE error_code_; + uint8_t serial_number_[4]; bool initialized_{false}; + bool store_baseline_; + sensor::Sensor *pm_1_0_sensor_{nullptr}; sensor::Sensor *pm_2_5_sensor_{nullptr}; sensor::Sensor *pm_4_0_sensor_{nullptr}; @@ -115,18 +125,14 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri // SEN55 only sensor::Sensor *nox_sensor_{nullptr}; - std::string product_name_; - uint8_t serial_number_[4]; - uint16_t firmware_version_; - Sen5xBaselines voc_baselines_storage_; - bool store_baseline_; - uint32_t seconds_since_last_store_; - ESPPreferenceObject pref_; optional acceleration_mode_; optional auto_cleaning_interval_; optional voc_tuning_params_; optional nox_tuning_params_; optional temperature_compensation_; + ESPPreferenceObject pref_; + std::string product_name_; + Sen5xBaselines voc_baselines_storage_; }; } // namespace sen5x diff --git a/esphome/components/sen5x/sensor.py b/esphome/components/sen5x/sensor.py index f52de5fe85..9668a253c0 100644 --- a/esphome/components/sen5x/sensor.py +++ b/esphome/components/sen5x/sensor.py @@ -65,26 +65,47 @@ ACCELERATION_MODES = { "high": RhtAccelerationMode.HIGH_ACCELERATION, } -GAS_SENSOR = cv.Schema( - { - cv.Optional(CONF_ALGORITHM_TUNING): cv.Schema( - { - cv.Optional(CONF_INDEX_OFFSET, default=100): cv.int_range(1, 250), - cv.Optional(CONF_LEARNING_TIME_OFFSET_HOURS, default=12): cv.int_range( - 1, 1000 - ), - cv.Optional(CONF_LEARNING_TIME_GAIN_HOURS, default=12): cv.int_range( - 1, 1000 - ), - cv.Optional( - CONF_GATING_MAX_DURATION_MINUTES, default=720 - ): cv.int_range(0, 3000), - cv.Optional(CONF_STD_INITIAL, default=50): cv.int_, - cv.Optional(CONF_GAIN_FACTOR, default=230): cv.int_range(1, 1000), - } - ) - } -) + +def _gas_sensor( + *, + index_offset: int, + learning_time_offset: int, + learning_time_gain: int, + gating_max_duration: int, + std_initial: int, + gain_factor: int, +) -> cv.Schema: + return sensor.sensor_schema( + icon=ICON_RADIATOR, + accuracy_decimals=0, + device_class=DEVICE_CLASS_AQI, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + { + cv.Optional(CONF_ALGORITHM_TUNING): cv.Schema( + { + cv.Optional(CONF_INDEX_OFFSET, default=index_offset): cv.int_range( + 1, 250 + ), + cv.Optional( + CONF_LEARNING_TIME_OFFSET_HOURS, default=learning_time_offset + ): cv.int_range(1, 1000), + cv.Optional( + CONF_LEARNING_TIME_GAIN_HOURS, default=learning_time_gain + ): cv.int_range(1, 1000), + cv.Optional( + CONF_GATING_MAX_DURATION_MINUTES, default=gating_max_duration + ): cv.int_range(0, 3000), + cv.Optional(CONF_STD_INITIAL, default=std_initial): cv.int_range( + 10, 5000 + ), + cv.Optional(CONF_GAIN_FACTOR, default=gain_factor): cv.int_range( + 1, 1000 + ), + } + ) + } + ) def float_previously_pct(value): @@ -127,18 +148,22 @@ CONFIG_SCHEMA = ( state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_AUTO_CLEANING_INTERVAL): cv.update_interval, - cv.Optional(CONF_VOC): sensor.sensor_schema( - icon=ICON_RADIATOR, - accuracy_decimals=0, - device_class=DEVICE_CLASS_AQI, - state_class=STATE_CLASS_MEASUREMENT, - ).extend(GAS_SENSOR), - cv.Optional(CONF_NOX): sensor.sensor_schema( - icon=ICON_RADIATOR, - accuracy_decimals=0, - device_class=DEVICE_CLASS_AQI, - state_class=STATE_CLASS_MEASUREMENT, - ).extend(GAS_SENSOR), + cv.Optional(CONF_VOC): _gas_sensor( + index_offset=100, + learning_time_offset=12, + learning_time_gain=12, + gating_max_duration=180, + std_initial=50, + gain_factor=230, + ), + cv.Optional(CONF_NOX): _gas_sensor( + index_offset=1, + learning_time_offset=12, + learning_time_gain=12, + gating_max_duration=720, + std_initial=50, + gain_factor=230, + ), cv.Optional(CONF_STORE_BASELINE, default=True): cv.boolean, cv.Optional(CONF_VOC_BASELINE): cv.hex_uint16_t, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( @@ -194,16 +219,15 @@ async def to_code(config): await i2c.register_i2c_device(var, config) for key, funcName in SETTING_MAP.items(): - if key in config: - cg.add(getattr(var, funcName)(config[key])) + if cfg := config.get(key): + cg.add(getattr(var, funcName)(cfg)) for key, funcName in SENSOR_MAP.items(): - if key in config: - sens = await sensor.new_sensor(config[key]) + if cfg := config.get(key): + sens = await sensor.new_sensor(cfg) cg.add(getattr(var, funcName)(sens)) - if CONF_VOC in config and CONF_ALGORITHM_TUNING in config[CONF_VOC]: - cfg = config[CONF_VOC][CONF_ALGORITHM_TUNING] + if cfg := config.get(CONF_VOC, {}).get(CONF_ALGORITHM_TUNING): cg.add( var.set_voc_algorithm_tuning( cfg[CONF_INDEX_OFFSET], @@ -214,8 +238,7 @@ async def to_code(config): cfg[CONF_GAIN_FACTOR], ) ) - if CONF_NOX in config and CONF_ALGORITHM_TUNING in config[CONF_NOX]: - cfg = config[CONF_NOX][CONF_ALGORITHM_TUNING] + if cfg := config.get(CONF_NOX, {}).get(CONF_ALGORITHM_TUNING): cg.add( var.set_nox_algorithm_tuning( cfg[CONF_INDEX_OFFSET], @@ -225,12 +248,12 @@ async def to_code(config): cfg[CONF_GAIN_FACTOR], ) ) - if CONF_TEMPERATURE_COMPENSATION in config: + if cfg := config.get(CONF_TEMPERATURE_COMPENSATION): cg.add( var.set_temperature_compensation( - config[CONF_TEMPERATURE_COMPENSATION][CONF_OFFSET], - config[CONF_TEMPERATURE_COMPENSATION][CONF_NORMALIZED_OFFSET_SLOPE], - config[CONF_TEMPERATURE_COMPENSATION][CONF_TIME_CONSTANT], + cfg[CONF_OFFSET], + cfg[CONF_NORMALIZED_OFFSET_SLOPE], + cfg[CONF_TIME_CONSTANT], ) ) diff --git a/esphome/components/senseair/senseair.cpp b/esphome/components/senseair/senseair.cpp index e58ee157f7..84520d407d 100644 --- a/esphome/components/senseair/senseair.cpp +++ b/esphome/components/senseair/senseair.cpp @@ -53,10 +53,14 @@ void SenseAirComponent::update() { this->status_clear_warning(); const uint8_t length = response[2]; - const uint16_t status = (uint16_t(response[3]) << 8) | response[4]; - const int16_t ppm = int16_t((response[length + 1] << 8) | response[length + 2]); + const uint16_t status = encode_uint16(response[3], response[4]); + const uint16_t ppm = encode_uint16(response[length + 1], response[length + 2]); - ESP_LOGD(TAG, "SenseAir Received CO₂=%dppm Status=0x%02X", ppm, status); + ESP_LOGD(TAG, "SenseAir Received CO₂=%uppm Status=0x%02X", ppm, status); + if (ppm == 0 && (status & SenseAirStatus::OUT_OF_RANGE_ERROR) != 0) { + ESP_LOGD(TAG, "Discarding 0 ppm reading with out-of-range status."); + return; + } if (this->co2_sensor_ != nullptr) this->co2_sensor_->publish_state(ppm); } diff --git a/esphome/components/senseair/senseair.h b/esphome/components/senseair/senseair.h index 9f939d5b07..9db849075d 100644 --- a/esphome/components/senseair/senseair.h +++ b/esphome/components/senseair/senseair.h @@ -8,6 +8,17 @@ namespace esphome { namespace senseair { +enum SenseAirStatus : uint8_t { + FATAL_ERROR = 1 << 0, + OFFSET_ERROR = 1 << 1, + ALGORITHM_ERROR = 1 << 2, + OUTPUT_ERROR = 1 << 3, + SELF_DIAGNOSTIC_ERROR = 1 << 4, + OUT_OF_RANGE_ERROR = 1 << 5, + MEMORY_ERROR = 1 << 6, + RESERVED = 1 << 7 +}; + class SenseAirComponent : public PollingComponent, public uart::UARTDevice { public: void set_co2_sensor(sensor::Sensor *co2_sensor) { co2_sensor_ = co2_sensor; } @@ -31,7 +42,7 @@ template class SenseAirBackgroundCalibrationAction : public Acti public: SenseAirBackgroundCalibrationAction(SenseAirComponent *senseair) : senseair_(senseair) {} - void play(Ts... x) override { this->senseair_->background_calibration(); } + void play(const Ts &...x) override { this->senseair_->background_calibration(); } protected: SenseAirComponent *senseair_; @@ -41,7 +52,7 @@ template class SenseAirBackgroundCalibrationResultAction : publi public: SenseAirBackgroundCalibrationResultAction(SenseAirComponent *senseair) : senseair_(senseair) {} - void play(Ts... x) override { this->senseair_->background_calibration_result(); } + void play(const Ts &...x) override { this->senseair_->background_calibration_result(); } protected: SenseAirComponent *senseair_; @@ -51,7 +62,7 @@ template class SenseAirABCEnableAction : public Action { public: SenseAirABCEnableAction(SenseAirComponent *senseair) : senseair_(senseair) {} - void play(Ts... x) override { this->senseair_->abc_enable(); } + void play(const Ts &...x) override { this->senseair_->abc_enable(); } protected: SenseAirComponent *senseair_; @@ -61,7 +72,7 @@ template class SenseAirABCDisableAction : public Action { public: SenseAirABCDisableAction(SenseAirComponent *senseair) : senseair_(senseair) {} - void play(Ts... x) override { this->senseair_->abc_disable(); } + void play(const Ts &...x) override { this->senseair_->abc_disable(); } protected: SenseAirComponent *senseair_; @@ -71,7 +82,7 @@ template class SenseAirABCGetPeriodAction : public Action public: SenseAirABCGetPeriodAction(SenseAirComponent *senseair) : senseair_(senseair) {} - void play(Ts... x) override { this->senseair_->abc_get_period(); } + void play(const Ts &...x) override { this->senseair_->abc_get_period(); } protected: SenseAirComponent *senseair_; diff --git a/esphome/components/sensirion_common/i2c_sensirion.cpp b/esphome/components/sensirion_common/i2c_sensirion.cpp index f71b3c14cb..9eac6b4525 100644 --- a/esphome/components/sensirion_common/i2c_sensirion.cpp +++ b/esphome/components/sensirion_common/i2c_sensirion.cpp @@ -11,21 +11,22 @@ static const char *const TAG = "sensirion_i2c"; // To avoid memory allocations for small writes a stack buffer is used static const size_t BUFFER_STACK_SIZE = 16; -bool SensirionI2CDevice::read_data(uint16_t *data, uint8_t len) { +bool SensirionI2CDevice::read_data(uint16_t *data, const uint8_t len) { const uint8_t num_bytes = len * 3; - std::vector buf(num_bytes); + uint8_t buf[num_bytes]; - last_error_ = this->read(buf.data(), num_bytes); - if (last_error_ != i2c::ERROR_OK) { + this->last_error_ = this->read(buf, num_bytes); + if (this->last_error_ != i2c::ERROR_OK) { return false; } for (uint8_t i = 0; i < len; i++) { const uint8_t j = 3 * i; - uint8_t crc = sht_crc_(buf[j], buf[j + 1]); + // Use MSB first since Sensirion devices use CRC-8 with MSB first + uint8_t crc = crc8(&buf[j], 2, 0xFF, CRC_POLYNOMIAL, true); if (crc != buf[j + 2]) { - ESP_LOGE(TAG, "CRC8 Checksum invalid at pos %d! 0x%02X != 0x%02X", i, buf[j + 2], crc); - last_error_ = i2c::ERROR_CRC; + ESP_LOGE(TAG, "CRC invalid @ %d! 0x%02X != 0x%02X", i, buf[j + 2], crc); + this->last_error_ = i2c::ERROR_CRC; return false; } data[i] = encode_uint16(buf[j], buf[j + 1]); @@ -34,10 +35,10 @@ bool SensirionI2CDevice::read_data(uint16_t *data, uint8_t len) { } /*** * write command with parameters and insert crc - * use stack array for less than 4 parameters. Most sensirion i2c commands have less parameters + * use stack array for less than 4 parameters. Most Sensirion I2C commands have less parameters */ bool SensirionI2CDevice::write_command_(uint16_t command, CommandLen command_len, const uint16_t *data, - uint8_t data_len) { + const uint8_t data_len) { uint8_t temp_stack[BUFFER_STACK_SIZE]; std::unique_ptr temp_heap; uint8_t *temp; @@ -74,56 +75,27 @@ bool SensirionI2CDevice::write_command_(uint16_t command, CommandLen command_len temp[raw_idx++] = data[i] & 0xFF; temp[raw_idx++] = data[i] >> 8; #endif - temp[raw_idx++] = sht_crc_(data[i]); + // Use MSB first since Sensirion devices use CRC-8 with MSB first + uint8_t crc = crc8(&temp[raw_idx - 2], 2, 0xFF, CRC_POLYNOMIAL, true); + temp[raw_idx++] = crc; } - last_error_ = this->write(temp, raw_idx); - return last_error_ == i2c::ERROR_OK; + this->last_error_ = this->write(temp, raw_idx); + return this->last_error_ == i2c::ERROR_OK; } -bool SensirionI2CDevice::get_register_(uint16_t reg, CommandLen command_len, uint16_t *data, uint8_t len, - uint8_t delay_ms) { +bool SensirionI2CDevice::get_register_(uint16_t reg, CommandLen command_len, uint16_t *data, const uint8_t len, + const uint8_t delay_ms) { if (!this->write_command_(reg, command_len, nullptr, 0)) { - ESP_LOGE(TAG, "Failed to write i2c register=0x%X (%d) err=%d,", reg, command_len, this->last_error_); + ESP_LOGE(TAG, "Write failed: reg=0x%X (%d) err=%d,", reg, command_len, this->last_error_); return false; } delay(delay_ms); bool result = this->read_data(data, len); if (!result) { - ESP_LOGE(TAG, "Failed to read data from register=0x%X err=%d,", reg, this->last_error_); + ESP_LOGE(TAG, "Read failed: reg=0x%X err=%d,", reg, this->last_error_); } return result; } -// The 8-bit CRC checksum is transmitted after each data word -uint8_t SensirionI2CDevice::sht_crc_(uint16_t data) { - uint8_t bit; - uint8_t crc = 0xFF; -#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ - crc ^= data >> 8; -#else - crc ^= data & 0xFF; -#endif - for (bit = 8; bit > 0; --bit) { - if (crc & 0x80) { - crc = (crc << 1) ^ crc_polynomial_; - } else { - crc = (crc << 1); - } - } -#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ - crc ^= data & 0xFF; -#else - crc ^= data >> 8; -#endif - for (bit = 8; bit > 0; --bit) { - if (crc & 0x80) { - crc = (crc << 1) ^ crc_polynomial_; - } else { - crc = (crc << 1); - } - } - return crc; -} - } // namespace sensirion_common } // namespace esphome diff --git a/esphome/components/sensirion_common/i2c_sensirion.h b/esphome/components/sensirion_common/i2c_sensirion.h index aba93d6cc3..f3eb3761f6 100644 --- a/esphome/components/sensirion_common/i2c_sensirion.h +++ b/esphome/components/sensirion_common/i2c_sensirion.h @@ -8,90 +8,92 @@ namespace esphome { namespace sensirion_common { /** - * Implementation of a i2c functions for Sensirion sensors - * Sensirion data requires crc checking. + * Implementation of I2C functions for Sensirion sensors + * Sensirion data requires CRC checking. * Each 16 bit word is/must be followed 8 bit CRC code - * (Applies to read and write - note the i2c command code doesn't need a CRC) + * (Applies to read and write - note the I2C command code doesn't need a CRC) * Format: * | 16 Bit Command Code | 16 bit Data word 1 | CRC of DW 1 | 16 bit Data word 1 | CRC of DW 2 | .. */ +static const uint8_t CRC_POLYNOMIAL = 0x31; // default for Sensirion + class SensirionI2CDevice : public i2c::I2CDevice { public: enum CommandLen : uint8_t { ADDR_8_BIT = 1, ADDR_16_BIT = 2 }; - /** Read data words from i2c device. - * handles crc check used by Sensirion sensors + /** Read data words from I2C device. + * handles CRC check used by Sensirion sensors * @param data pointer to raw result * @param len number of words to read * @return true if reading succeeded */ bool read_data(uint16_t *data, uint8_t len); - /** Read 1 data word from i2c device. + /** Read 1 data word from I2C device. * @param data reference to raw result * @return true if reading succeeded */ bool read_data(uint16_t &data) { return this->read_data(&data, 1); } - /** get data words from i2c register. - * handles crc check used by Sensirion sensors - * @param i2c register + /** get data words from I2C register. + * handles CRC check used by Sensirion sensors + * @param I2C register * @param data pointer to raw result * @param len number of words to read - * @param delay milliseconds to to wait between sending the i2c command and reading the result + * @param delay milliseconds to to wait between sending the I2C command and reading the result * @return true if reading succeeded */ bool get_register(uint16_t command, uint16_t *data, uint8_t len, uint8_t delay = 0) { return get_register_(command, ADDR_16_BIT, data, len, delay); } - /** Read 1 data word from 16 bit i2c register. - * @param i2c register + /** Read 1 data word from 16 bit I2C register. + * @param I2C register * @param data reference to raw result - * @param delay milliseconds to to wait between sending the i2c command and reading the result + * @param delay milliseconds to to wait between sending the I2C command and reading the result * @return true if reading succeeded */ bool get_register(uint16_t i2c_register, uint16_t &data, uint8_t delay = 0) { return this->get_register_(i2c_register, ADDR_16_BIT, &data, 1, delay); } - /** get data words from i2c register. - * handles crc check used by Sensirion sensors - * @param i2c register + /** get data words from I2C register. + * handles CRC check used by Sensirion sensors + * @param I2C register * @param data pointer to raw result * @param len number of words to read - * @param delay milliseconds to to wait between sending the i2c command and reading the result + * @param delay milliseconds to to wait between sending the I2C command and reading the result * @return true if reading succeeded */ bool get_8bit_register(uint8_t i2c_register, uint16_t *data, uint8_t len, uint8_t delay = 0) { return get_register_(i2c_register, ADDR_8_BIT, data, len, delay); } - /** Read 1 data word from 8 bit i2c register. - * @param i2c register + /** Read 1 data word from 8 bit I2C register. + * @param I2C register * @param data reference to raw result - * @param delay milliseconds to to wait between sending the i2c command and reading the result + * @param delay milliseconds to to wait between sending the I2C command and reading the result * @return true if reading succeeded */ bool get_8bit_register(uint8_t i2c_register, uint16_t &data, uint8_t delay = 0) { return this->get_register_(i2c_register, ADDR_8_BIT, &data, 1, delay); } - /** Write a command to the i2c device. - * @param command i2c command to send + /** Write a command to the I2C device. + * @param command I2C command to send * @return true if reading succeeded */ template bool write_command(T i2c_register) { return write_command(i2c_register, nullptr, 0); } - /** Write a command and one data word to the i2c device . - * @param command i2c command to send - * @param data argument for the i2c command + /** Write a command and one data word to the I2C device . + * @param command I2C command to send + * @param data argument for the I2C command * @return true if reading succeeded */ template bool write_command(T i2c_register, uint16_t data) { return write_command(i2c_register, &data, 1); } /** Write a command with arguments as words - * @param i2c_register i2c command to send - an be uint8_t or uint16_t - * @param data vector arguments for the i2c command + * @param i2c_register I2C command to send - an be uint8_t or uint16_t + * @param data vector arguments for the I2C command * @return true if reading succeeded */ template bool write_command(T i2c_register, const std::vector &data) { @@ -99,57 +101,39 @@ class SensirionI2CDevice : public i2c::I2CDevice { } /** Write a command with arguments as words - * @param i2c_register i2c command to send - an be uint8_t or uint16_t - * @param data arguments for the i2c command + * @param i2c_register I2C command to send - an be uint8_t or uint16_t + * @param data arguments for the I2C command * @param len number of arguments (words) * @return true if reading succeeded */ template bool write_command(T i2c_register, const uint16_t *data, uint8_t len) { // limit to 8 or 16 bit only - static_assert(sizeof(i2c_register) == 1 || sizeof(i2c_register) == 2, - "only 8 or 16 bit command types are supported."); + static_assert(sizeof(i2c_register) == 1 || sizeof(i2c_register) == 2, "Only 8 or 16 bit command types supported"); return write_command_(i2c_register, CommandLen(sizeof(T)), data, len); } protected: - uint8_t crc_polynomial_{0x31u}; // default for sensirion /** Write a command with arguments as words - * @param command i2c command to send can be uint8_t or uint16_t + * @param command I2C command to send can be uint8_t or uint16_t * @param command_len either 1 for short 8 bit command or 2 for 16 bit command codes - * @param data arguments for the i2c command + * @param data arguments for the I2C command * @param data_len number of arguments (words) * @return true if reading succeeded */ bool write_command_(uint16_t command, CommandLen command_len, const uint16_t *data, uint8_t data_len); - /** get data words from i2c register. - * handles crc check used by Sensirion sensors - * @param i2c register + /** get data words from I2C register. + * handles CRC check used by Sensirion sensors + * @param I2C register * @param command_len either 1 for short 8 bit command or 2 for 16 bit command codes * @param data pointer to raw result * @param len number of words to read - * @param delay milliseconds to to wait between sending the i2c command and reading the result + * @param delay milliseconds to to wait between sending the I2C command and reading the result * @return true if reading succeeded */ bool get_register_(uint16_t reg, CommandLen command_len, uint16_t *data, uint8_t len, uint8_t delay); - /** 8-bit CRC checksum that is transmitted after each data word for read and write operation - * @param command i2c command to send - * @param data data word for which the crc8 checksum is calculated - * @param len number of arguments (words) - * @return 8 Bit CRC - */ - uint8_t sht_crc_(uint16_t data); - - /** 8-bit CRC checksum that is transmitted after each data word for read and write operation - * @param command i2c command to send - * @param data1 high byte of data word - * @param data2 low byte of data word - * @return 8 Bit CRC - */ - uint8_t sht_crc_(uint8_t data1, uint8_t data2) { return sht_crc_(encode_uint16(data1, data2)); } - - /** last error code from i2c operation + /** last error code from I2C operation */ i2c::ErrorCode last_error_; }; diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 2275027004..e8fec222a1 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -28,6 +28,8 @@ from esphome.const import ( CONF_ON_RAW_VALUE, CONF_ON_VALUE, CONF_ON_VALUE_RANGE, + CONF_OPTIMISTIC, + CONF_PERIOD, CONF_QUANTILE, CONF_SEND_EVERY, CONF_SEND_FIRST_AT, @@ -74,6 +76,7 @@ from esphome.const import ( DEVICE_CLASS_OZONE, DEVICE_CLASS_PH, DEVICE_CLASS_PM1, + DEVICE_CLASS_PM4, DEVICE_CLASS_PM10, DEVICE_CLASS_PM25, DEVICE_CLASS_POWER, @@ -88,6 +91,7 @@ from esphome.const import ( DEVICE_CLASS_SPEED, DEVICE_CLASS_SULPHUR_DIOXIDE, DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_TEMPERATURE_DELTA, DEVICE_CLASS_TIMESTAMP, DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, @@ -101,7 +105,7 @@ from esphome.const import ( DEVICE_CLASS_WIND_SPEED, ENTITY_CATEGORY_CONFIG, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass from esphome.util import Registry @@ -143,6 +147,7 @@ DEVICE_CLASSES = [ DEVICE_CLASS_PM1, DEVICE_CLASS_PM10, DEVICE_CLASS_PM25, + DEVICE_CLASS_PM4, DEVICE_CLASS_POWER, DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_PRECIPITATION, @@ -155,6 +160,7 @@ DEVICE_CLASSES = [ DEVICE_CLASS_SPEED, DEVICE_CLASS_SULPHUR_DIOXIDE, DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_TEMPERATURE_DELTA, DEVICE_CLASS_TIMESTAMP, DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, @@ -247,16 +253,23 @@ MaxFilter = sensor_ns.class_("MaxFilter", Filter) SlidingWindowMovingAverageFilter = sensor_ns.class_( "SlidingWindowMovingAverageFilter", Filter ) +StreamingMinFilter = sensor_ns.class_("StreamingMinFilter", Filter) +StreamingMaxFilter = sensor_ns.class_("StreamingMaxFilter", Filter) +StreamingMovingAverageFilter = sensor_ns.class_("StreamingMovingAverageFilter", Filter) ExponentialMovingAverageFilter = sensor_ns.class_( "ExponentialMovingAverageFilter", Filter ) ThrottleAverageFilter = sensor_ns.class_("ThrottleAverageFilter", Filter, cg.Component) LambdaFilter = sensor_ns.class_("LambdaFilter", Filter) +StatelessLambdaFilter = sensor_ns.class_("StatelessLambdaFilter", Filter) OffsetFilter = sensor_ns.class_("OffsetFilter", Filter) MultiplyFilter = sensor_ns.class_("MultiplyFilter", Filter) -FilterOutValueFilter = sensor_ns.class_("FilterOutValueFilter", Filter) +ValueListFilter = sensor_ns.class_("ValueListFilter", Filter) +FilterOutValueFilter = sensor_ns.class_("FilterOutValueFilter", ValueListFilter) ThrottleFilter = sensor_ns.class_("ThrottleFilter", Filter) -ThrottleWithPriorityFilter = sensor_ns.class_("ThrottleWithPriorityFilter", Filter) +ThrottleWithPriorityFilter = sensor_ns.class_( + "ThrottleWithPriorityFilter", ValueListFilter +) TimeoutFilter = sensor_ns.class_("TimeoutFilter", Filter, cg.Component) DebounceFilter = sensor_ns.class_("DebounceFilter", Filter, cg.Component) HeartbeatFilter = sensor_ns.class_("HeartbeatFilter", Filter, cg.Component) @@ -356,11 +369,6 @@ def sensor_schema( return _SENSOR_SCHEMA.extend(schema) -# Remove before 2025.11.0 -SENSOR_SCHEMA = sensor_schema() -SENSOR_SCHEMA.add_extra(cv.deprecated_schema_constant("sensor")) - - @FILTER_REGISTRY.register("offset", OffsetFilter, cv.templatable(cv.float_)) async def offset_filter_to_code(config, filter_id): template_ = await cg.templatable(config, [], float) @@ -448,14 +456,21 @@ async def skip_initial_filter_to_code(config, filter_id): return cg.new_Pvariable(filter_id, config) -@FILTER_REGISTRY.register("min", MinFilter, MIN_SCHEMA) +@FILTER_REGISTRY.register("min", Filter, MIN_SCHEMA) async def min_filter_to_code(config, filter_id): - return cg.new_Pvariable( - filter_id, - config[CONF_WINDOW_SIZE], - config[CONF_SEND_EVERY], - config[CONF_SEND_FIRST_AT], - ) + window_size: int = config[CONF_WINDOW_SIZE] + send_every: int = config[CONF_SEND_EVERY] + send_first_at: int = config[CONF_SEND_FIRST_AT] + + # Optimization: Use streaming filter for batch windows (window_size == send_every) + # Saves 99.98% memory for large windows (e.g., 20KB → 4 bytes for window_size=5000) + if window_size == send_every: + # Use streaming filter - O(1) memory instead of O(n) + rhs = StreamingMinFilter.new(window_size, send_first_at) + return cg.Pvariable(filter_id, rhs, StreamingMinFilter) + # Use sliding window filter - maintains ring buffer + rhs = MinFilter.new(window_size, send_every, send_first_at) + return cg.Pvariable(filter_id, rhs, MinFilter) MAX_SCHEMA = cv.All( @@ -470,14 +485,18 @@ MAX_SCHEMA = cv.All( ) -@FILTER_REGISTRY.register("max", MaxFilter, MAX_SCHEMA) +@FILTER_REGISTRY.register("max", Filter, MAX_SCHEMA) async def max_filter_to_code(config, filter_id): - return cg.new_Pvariable( - filter_id, - config[CONF_WINDOW_SIZE], - config[CONF_SEND_EVERY], - config[CONF_SEND_FIRST_AT], - ) + window_size: int = config[CONF_WINDOW_SIZE] + send_every: int = config[CONF_SEND_EVERY] + send_first_at: int = config[CONF_SEND_FIRST_AT] + + # Optimization: Use streaming filter for batch windows (window_size == send_every) + if window_size == send_every: + rhs = StreamingMaxFilter.new(window_size, send_first_at) + return cg.Pvariable(filter_id, rhs, StreamingMaxFilter) + rhs = MaxFilter.new(window_size, send_every, send_first_at) + return cg.Pvariable(filter_id, rhs, MaxFilter) SLIDING_AVERAGE_SCHEMA = cv.All( @@ -494,16 +513,20 @@ SLIDING_AVERAGE_SCHEMA = cv.All( @FILTER_REGISTRY.register( "sliding_window_moving_average", - SlidingWindowMovingAverageFilter, + Filter, SLIDING_AVERAGE_SCHEMA, ) async def sliding_window_moving_average_filter_to_code(config, filter_id): - return cg.new_Pvariable( - filter_id, - config[CONF_WINDOW_SIZE], - config[CONF_SEND_EVERY], - config[CONF_SEND_FIRST_AT], - ) + window_size: int = config[CONF_WINDOW_SIZE] + send_every: int = config[CONF_SEND_EVERY] + send_first_at: int = config[CONF_SEND_FIRST_AT] + + # Optimization: Use streaming filter for batch windows (window_size == send_every) + if window_size == send_every: + rhs = StreamingMovingAverageFilter.new(window_size, send_first_at) + return cg.Pvariable(filter_id, rhs, StreamingMovingAverageFilter) + rhs = SlidingWindowMovingAverageFilter.new(window_size, send_every, send_first_at) + return cg.Pvariable(filter_id, rhs, SlidingWindowMovingAverageFilter) EXPONENTIAL_AVERAGE_SCHEMA = cv.All( @@ -546,7 +569,7 @@ async def lambda_filter_to_code(config, filter_id): lambda_ = await cg.process_lambda( config, [(float, "x")], return_type=cg.optional.template(float) ) - return cg.new_Pvariable(filter_id, lambda_) + return automation.new_lambda_pvariable(filter_id, lambda_, StatelessLambdaFilter) DELTA_SCHEMA = cv.Schema( @@ -596,7 +619,7 @@ async def throttle_filter_to_code(config, filter_id): return cg.new_Pvariable(filter_id, config) -TIMEOUT_WITH_PRIORITY_SCHEMA = cv.maybe_simple_value( +THROTTLE_WITH_PRIORITY_SCHEMA = cv.maybe_simple_value( { cv.Required(CONF_TIMEOUT): cv.positive_time_period_milliseconds, cv.Optional(CONF_VALUE, default="nan"): cv.Any( @@ -610,7 +633,7 @@ TIMEOUT_WITH_PRIORITY_SCHEMA = cv.maybe_simple_value( @FILTER_REGISTRY.register( "throttle_with_priority", ThrottleWithPriorityFilter, - TIMEOUT_WITH_PRIORITY_SCHEMA, + THROTTLE_WITH_PRIORITY_SCHEMA, ) async def throttle_with_priority_filter_to_code(config, filter_id): if not isinstance(config[CONF_VALUE], list): @@ -619,10 +642,29 @@ async def throttle_with_priority_filter_to_code(config, filter_id): return cg.new_Pvariable(filter_id, config[CONF_TIMEOUT], template_) +HEARTBEAT_SCHEMA = cv.Schema( + { + cv.Required(CONF_PERIOD): cv.positive_time_period_milliseconds, + cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, + } +) + + @FILTER_REGISTRY.register( - "heartbeat", HeartbeatFilter, cv.positive_time_period_milliseconds + "heartbeat", + HeartbeatFilter, + cv.Any( + cv.positive_time_period_milliseconds, + HEARTBEAT_SCHEMA, + ), ) async def heartbeat_filter_to_code(config, filter_id): + if isinstance(config, dict): + var = cg.new_Pvariable(filter_id, config[CONF_PERIOD]) + await cg.register_component(var, {}) + cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) + return var + var = cg.new_Pvariable(filter_id, config) await cg.register_component(var, {}) return var @@ -631,7 +673,9 @@ async def heartbeat_filter_to_code(config, filter_id): TIMEOUT_SCHEMA = cv.maybe_simple_value( { cv.Required(CONF_TIMEOUT): cv.positive_time_period_milliseconds, - cv.Optional(CONF_VALUE, default="nan"): cv.templatable(cv.float_), + cv.Optional(CONF_VALUE, default="nan"): cv.Any( + "last", cv.templatable(cv.float_) + ), }, key=CONF_TIMEOUT, ) @@ -639,8 +683,11 @@ TIMEOUT_SCHEMA = cv.maybe_simple_value( @FILTER_REGISTRY.register("timeout", TimeoutFilter, TIMEOUT_SCHEMA) async def timeout_filter_to_code(config, filter_id): - template_ = await cg.templatable(config[CONF_VALUE], [], float) - var = cg.new_Pvariable(filter_id, config[CONF_TIMEOUT], template_) + if config[CONF_VALUE] == "last": + var = cg.new_Pvariable(filter_id, config[CONF_TIMEOUT]) + else: + template_ = await cg.templatable(config[CONF_VALUE], [], float) + var = cg.new_Pvariable(filter_id, config[CONF_TIMEOUT], template_) await cg.register_component(var, {}) return var @@ -827,7 +874,9 @@ async def setup_sensor_core_(var, config): cg.add(var.set_unit_of_measurement(unit_of_measurement)) if (accuracy_decimals := config.get(CONF_ACCURACY_DECIMALS)) is not None: cg.add(var.set_accuracy_decimals(accuracy_decimals)) - cg.add(var.set_force_update(config[CONF_FORCE_UPDATE])) + # Only set force_update if True (default is False) + if config[CONF_FORCE_UPDATE]: + cg.add(var.set_force_update(True)) if config.get(CONF_FILTERS): # must exist and not be empty filters = await build_filters(config[CONF_FILTERS]) cg.add(var.set_filters(filters)) @@ -1137,6 +1186,6 @@ def _lstsq(a, b): return _mat_dot(_mat_dot(x, a_t), b) -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(sensor_ns.using) diff --git a/esphome/components/sensor/automation.h b/esphome/components/sensor/automation.h index 8cd0adbeb2..df7d31a0c9 100644 --- a/esphome/components/sensor/automation.h +++ b/esphome/components/sensor/automation.h @@ -26,7 +26,7 @@ template class SensorPublishAction : public Action { SensorPublishAction(Sensor *sensor) : sensor_(sensor) {} TEMPLATABLE_VALUE(float, state) - void play(Ts... x) override { this->sensor_->publish_state(this->state_.value(x...)); } + void play(const Ts &...x) override { this->sensor_->publish_state(this->state_.value(x...)); } protected: Sensor *sensor_; @@ -40,7 +40,7 @@ class ValueRangeTrigger : public Trigger, public Component { template void set_max(V max) { this->max_ = max; } void setup() override { - this->rtc_ = global_preferences->make_preference(this->parent_->get_object_id_hash()); + this->rtc_ = global_preferences->make_preference(this->parent_->get_preference_hash()); bool initial_state; if (this->rtc_.load(&initial_state)) { this->previous_in_range_ = initial_state; @@ -90,7 +90,7 @@ template class SensorInRangeCondition : public Condition void set_min(float min) { this->min_ = min; } void set_max(float max) { this->max_ = max; } - bool check(Ts... x) override { + bool check(const Ts &...x) override { const float state = this->parent_->state; if (std::isnan(this->min_)) { return state <= this->max_; diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index f077ad2416..65d8dea31c 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -32,50 +32,76 @@ void Filter::initialize(Sensor *parent, Filter *next) { this->next_ = next; } -// MedianFilter -MedianFilter::MedianFilter(size_t window_size, size_t send_every, size_t send_first_at) - : send_every_(send_every), send_at_(send_every - send_first_at), window_size_(window_size) {} -void MedianFilter::set_send_every(size_t send_every) { this->send_every_ = send_every; } -void MedianFilter::set_window_size(size_t window_size) { this->window_size_ = window_size; } -optional MedianFilter::new_value(float value) { - while (this->queue_.size() >= this->window_size_) { - this->queue_.pop_front(); - } - this->queue_.push_back(value); - ESP_LOGVV(TAG, "MedianFilter(%p)::new_value(%f)", this, value); +// SlidingWindowFilter +SlidingWindowFilter::SlidingWindowFilter(size_t window_size, size_t send_every, size_t send_first_at) + : window_size_(window_size), send_every_(send_every), send_at_(send_every - send_first_at) { + // Allocate ring buffer once at initialization + this->window_.init(window_size); +} +optional SlidingWindowFilter::new_value(float value) { + // Add value to ring buffer + if (this->window_count_ < this->window_size_) { + // Buffer not yet full - just append + this->window_.push_back(value); + this->window_count_++; + } else { + // Buffer full - overwrite oldest value (ring buffer) + this->window_[this->window_head_] = value; + this->window_head_++; + if (this->window_head_ >= this->window_size_) { + this->window_head_ = 0; + } + } + + // Check if we should send a result if (++this->send_at_ >= this->send_every_) { this->send_at_ = 0; - - float median = NAN; - if (!this->queue_.empty()) { - // Copy queue without NaN values - std::vector median_queue; - median_queue.reserve(this->queue_.size()); - for (auto v : this->queue_) { - if (!std::isnan(v)) { - median_queue.push_back(v); - } - } - - sort(median_queue.begin(), median_queue.end()); - - size_t queue_size = median_queue.size(); - if (queue_size) { - if (queue_size % 2) { - median = median_queue[queue_size / 2]; - } else { - median = (median_queue[queue_size / 2] + median_queue[(queue_size / 2) - 1]) / 2.0f; - } - } - } - - ESP_LOGVV(TAG, "MedianFilter(%p)::new_value(%f) SENDING %f", this, value, median); - return median; + float result = this->compute_result(); + ESP_LOGVV(TAG, "SlidingWindowFilter(%p)::new_value(%f) SENDING %f", this, value, result); + return result; } return {}; } +// SortedWindowFilter +FixedVector SortedWindowFilter::get_window_values_() { + // Copy window without NaN values using FixedVector (no heap allocation) + // Returns unsorted values - caller will use std::nth_element for partial sorting as needed + FixedVector values; + values.init(this->window_count_); + for (size_t i = 0; i < this->window_count_; i++) { + float v = this->window_[i]; + if (!std::isnan(v)) { + values.push_back(v); + } + } + return values; +} + +// MedianFilter +float MedianFilter::compute_result() { + FixedVector values = this->get_window_values_(); + if (values.empty()) + return NAN; + + size_t size = values.size(); + size_t mid = size / 2; + + if (size % 2) { + // Odd number of elements - use nth_element to find middle element + std::nth_element(values.begin(), values.begin() + mid, values.end()); + return values[mid]; + } + // Even number of elements - need both middle elements + // Use nth_element to find upper middle element + std::nth_element(values.begin(), values.begin() + mid, values.end()); + float upper = values[mid]; + // Find the maximum of the lower half (which is now everything before mid) + float lower = *std::max_element(values.begin(), values.begin() + mid); + return (lower + upper) / 2.0f; +} + // SkipInitialFilter SkipInitialFilter::SkipInitialFilter(size_t num_to_ignore) : num_to_ignore_(num_to_ignore) {} optional SkipInitialFilter::new_value(float value) { @@ -91,136 +117,39 @@ optional SkipInitialFilter::new_value(float value) { // QuantileFilter QuantileFilter::QuantileFilter(size_t window_size, size_t send_every, size_t send_first_at, float quantile) - : send_every_(send_every), send_at_(send_every - send_first_at), window_size_(window_size), quantile_(quantile) {} -void QuantileFilter::set_send_every(size_t send_every) { this->send_every_ = send_every; } -void QuantileFilter::set_window_size(size_t window_size) { this->window_size_ = window_size; } -void QuantileFilter::set_quantile(float quantile) { this->quantile_ = quantile; } -optional QuantileFilter::new_value(float value) { - while (this->queue_.size() >= this->window_size_) { - this->queue_.pop_front(); - } - this->queue_.push_back(value); - ESP_LOGVV(TAG, "QuantileFilter(%p)::new_value(%f), quantile:%f", this, value, this->quantile_); + : SortedWindowFilter(window_size, send_every, send_first_at), quantile_(quantile) {} - if (++this->send_at_ >= this->send_every_) { - this->send_at_ = 0; +float QuantileFilter::compute_result() { + FixedVector values = this->get_window_values_(); + if (values.empty()) + return NAN; - float result = NAN; - if (!this->queue_.empty()) { - // Copy queue without NaN values - std::vector quantile_queue; - for (auto v : this->queue_) { - if (!std::isnan(v)) { - quantile_queue.push_back(v); - } - } + size_t position = ceilf(values.size() * this->quantile_) - 1; + ESP_LOGVV(TAG, "QuantileFilter(%p)::position: %zu/%zu", this, position + 1, values.size()); - sort(quantile_queue.begin(), quantile_queue.end()); - - size_t queue_size = quantile_queue.size(); - if (queue_size) { - size_t position = ceilf(queue_size * this->quantile_) - 1; - ESP_LOGVV(TAG, "QuantileFilter(%p)::position: %zu/%zu", this, position + 1, queue_size); - result = quantile_queue[position]; - } - } - - ESP_LOGVV(TAG, "QuantileFilter(%p)::new_value(%f) SENDING %f", this, value, result); - return result; - } - return {}; + // Use nth_element to find the quantile element (O(n) instead of O(n log n)) + std::nth_element(values.begin(), values.begin() + position, values.end()); + return values[position]; } // MinFilter -MinFilter::MinFilter(size_t window_size, size_t send_every, size_t send_first_at) - : send_every_(send_every), send_at_(send_every - send_first_at), window_size_(window_size) {} -void MinFilter::set_send_every(size_t send_every) { this->send_every_ = send_every; } -void MinFilter::set_window_size(size_t window_size) { this->window_size_ = window_size; } -optional MinFilter::new_value(float value) { - while (this->queue_.size() >= this->window_size_) { - this->queue_.pop_front(); - } - this->queue_.push_back(value); - ESP_LOGVV(TAG, "MinFilter(%p)::new_value(%f)", this, value); - - if (++this->send_at_ >= this->send_every_) { - this->send_at_ = 0; - - float min = NAN; - for (auto v : this->queue_) { - if (!std::isnan(v)) { - min = std::isnan(min) ? v : std::min(min, v); - } - } - - ESP_LOGVV(TAG, "MinFilter(%p)::new_value(%f) SENDING %f", this, value, min); - return min; - } - return {}; -} +float MinFilter::compute_result() { return this->find_extremum_>(); } // MaxFilter -MaxFilter::MaxFilter(size_t window_size, size_t send_every, size_t send_first_at) - : send_every_(send_every), send_at_(send_every - send_first_at), window_size_(window_size) {} -void MaxFilter::set_send_every(size_t send_every) { this->send_every_ = send_every; } -void MaxFilter::set_window_size(size_t window_size) { this->window_size_ = window_size; } -optional MaxFilter::new_value(float value) { - while (this->queue_.size() >= this->window_size_) { - this->queue_.pop_front(); - } - this->queue_.push_back(value); - ESP_LOGVV(TAG, "MaxFilter(%p)::new_value(%f)", this, value); - - if (++this->send_at_ >= this->send_every_) { - this->send_at_ = 0; - - float max = NAN; - for (auto v : this->queue_) { - if (!std::isnan(v)) { - max = std::isnan(max) ? v : std::max(max, v); - } - } - - ESP_LOGVV(TAG, "MaxFilter(%p)::new_value(%f) SENDING %f", this, value, max); - return max; - } - return {}; -} +float MaxFilter::compute_result() { return this->find_extremum_>(); } // SlidingWindowMovingAverageFilter -SlidingWindowMovingAverageFilter::SlidingWindowMovingAverageFilter(size_t window_size, size_t send_every, - size_t send_first_at) - : send_every_(send_every), send_at_(send_every - send_first_at), window_size_(window_size) {} -void SlidingWindowMovingAverageFilter::set_send_every(size_t send_every) { this->send_every_ = send_every; } -void SlidingWindowMovingAverageFilter::set_window_size(size_t window_size) { this->window_size_ = window_size; } -optional SlidingWindowMovingAverageFilter::new_value(float value) { - while (this->queue_.size() >= this->window_size_) { - this->queue_.pop_front(); - } - this->queue_.push_back(value); - ESP_LOGVV(TAG, "SlidingWindowMovingAverageFilter(%p)::new_value(%f)", this, value); - - if (++this->send_at_ >= this->send_every_) { - this->send_at_ = 0; - - float sum = 0; - size_t valid_count = 0; - for (auto v : this->queue_) { - if (!std::isnan(v)) { - sum += v; - valid_count++; - } +float SlidingWindowMovingAverageFilter::compute_result() { + float sum = 0; + size_t valid_count = 0; + for (size_t i = 0; i < this->window_count_; i++) { + float v = this->window_[i]; + if (!std::isnan(v)) { + sum += v; + valid_count++; } - - float average = NAN; - if (valid_count) { - average = sum / valid_count; - } - - ESP_LOGVV(TAG, "SlidingWindowMovingAverageFilter(%p)::new_value(%f) SENDING %f", this, value, average); - return average; } - return {}; + return valid_count ? sum / valid_count : NAN; } // ExponentialMovingAverageFilter @@ -299,27 +228,40 @@ MultiplyFilter::MultiplyFilter(TemplatableValue multiplier) : multiplier_ optional MultiplyFilter::new_value(float value) { return value * this->multiplier_.value(); } -// FilterOutValueFilter -FilterOutValueFilter::FilterOutValueFilter(std::vector> values_to_filter_out) - : values_to_filter_out_(std::move(values_to_filter_out)) {} +// ValueListFilter (base class) +ValueListFilter::ValueListFilter(std::initializer_list> values) : values_(values) {} -optional FilterOutValueFilter::new_value(float value) { +bool ValueListFilter::value_matches_any_(float sensor_value) { int8_t accuracy = this->parent_->get_accuracy_decimals(); float accuracy_mult = powf(10.0f, accuracy); - for (auto filter_value : this->values_to_filter_out_) { - if (std::isnan(filter_value.value())) { - if (std::isnan(value)) { - return {}; - } + float rounded_sensor = roundf(accuracy_mult * sensor_value); + + for (auto &filter_value : this->values_) { + float fv = filter_value.value(); + + // Handle NaN comparison + if (std::isnan(fv)) { + if (std::isnan(sensor_value)) + return true; continue; } - float rounded_filter_out = roundf(accuracy_mult * filter_value.value()); - float rounded_value = roundf(accuracy_mult * value); - if (rounded_filter_out == rounded_value) { - return {}; - } + + // Compare rounded values + if (roundf(accuracy_mult * fv) == rounded_sensor) + return true; } - return value; + + return false; +} + +// FilterOutValueFilter +FilterOutValueFilter::FilterOutValueFilter(std::initializer_list> values_to_filter_out) + : ValueListFilter(values_to_filter_out) {} + +optional FilterOutValueFilter::new_value(float value) { + if (this->value_matches_any_(value)) + return {}; // Filter out + return value; // Pass through } // ThrottleFilter @@ -334,33 +276,15 @@ optional ThrottleFilter::new_value(float value) { } // ThrottleWithPriorityFilter -ThrottleWithPriorityFilter::ThrottleWithPriorityFilter(uint32_t min_time_between_inputs, - std::vector> prioritized_values) - : min_time_between_inputs_(min_time_between_inputs), prioritized_values_(std::move(prioritized_values)) {} +ThrottleWithPriorityFilter::ThrottleWithPriorityFilter( + uint32_t min_time_between_inputs, std::initializer_list> prioritized_values) + : ValueListFilter(prioritized_values), min_time_between_inputs_(min_time_between_inputs) {} optional ThrottleWithPriorityFilter::new_value(float value) { - bool is_prioritized_value = false; - int8_t accuracy = this->parent_->get_accuracy_decimals(); - float accuracy_mult = powf(10.0f, accuracy); const uint32_t now = App.get_loop_component_start_time(); - // First, determine if the new value is one of the prioritized values - for (auto prioritized_value : this->prioritized_values_) { - if (std::isnan(prioritized_value.value())) { - if (std::isnan(value)) { - is_prioritized_value = true; - break; - } - continue; - } - float rounded_prioritized_value = roundf(accuracy_mult * prioritized_value.value()); - float rounded_value = roundf(accuracy_mult * value); - if (rounded_prioritized_value == rounded_value) { - is_prioritized_value = true; - break; - } - } - // Finally, determine if the new value should be throttled and pass it through if not - if (this->last_input_ == 0 || now - this->last_input_ >= min_time_between_inputs_ || is_prioritized_value) { + // Allow value through if: no previous input, time expired, or is prioritized + if (this->last_input_ == 0 || now - this->last_input_ >= min_time_between_inputs_ || + this->value_matches_any_(value)) { this->last_input_ = now; return value; } @@ -389,7 +313,7 @@ optional DeltaFilter::new_value(float value) { } // OrFilter -OrFilter::OrFilter(std::vector filters) : filters_(std::move(filters)), phi_(this) {} +OrFilter::OrFilter(std::initializer_list filters) : filters_(filters), phi_(this) {} OrFilter::PhiNode::PhiNode(OrFilter *or_parent) : or_parent_(or_parent) {} optional OrFilter::PhiNode::new_value(float value) { @@ -402,14 +326,14 @@ optional OrFilter::PhiNode::new_value(float value) { } optional OrFilter::new_value(float value) { this->has_value_ = false; - for (Filter *filter : this->filters_) + for (auto *filter : this->filters_) filter->input(value); return {}; } void OrFilter::initialize(Sensor *parent, Filter *next) { Filter::initialize(parent, next); - for (Filter *filter : this->filters_) { + for (auto *filter : this->filters_) { filter->initialize(parent, &this->phi_); } this->phi_.initialize(parent, nullptr); @@ -417,12 +341,17 @@ void OrFilter::initialize(Sensor *parent, Filter *next) { // TimeoutFilter optional TimeoutFilter::new_value(float value) { - this->set_timeout("timeout", this->time_period_, [this]() { this->output(this->value_.value()); }); + if (this->value_.has_value()) { + this->set_timeout("timeout", this->time_period_, [this]() { this->output(this->value_.value().value()); }); + } else { + this->set_timeout("timeout", this->time_period_, [this, value]() { this->output(value); }); + } return value; } -TimeoutFilter::TimeoutFilter(uint32_t time_period, TemplatableValue new_value) - : time_period_(time_period), value_(std::move(new_value)) {} +TimeoutFilter::TimeoutFilter(uint32_t time_period) : time_period_(time_period) {} +TimeoutFilter::TimeoutFilter(uint32_t time_period, const TemplatableValue &new_value) + : time_period_(time_period), value_(new_value) {} float TimeoutFilter::get_setup_priority() const { return setup_priority::HARDWARE; } // DebounceFilter @@ -443,8 +372,12 @@ optional HeartbeatFilter::new_value(float value) { this->last_input_ = value; this->has_value_ = true; + if (this->optimistic_) { + return value; + } return {}; } + void HeartbeatFilter::setup() { this->set_interval("heartbeat", this->time_period_, [this]() { ESP_LOGVV(TAG, "HeartbeatFilter(%p)::interval(has_value=%s, last_input=%f)", this, YESNO(this->has_value_), @@ -455,20 +388,27 @@ void HeartbeatFilter::setup() { this->output(this->last_input_); }); } + float HeartbeatFilter::get_setup_priority() const { return setup_priority::HARDWARE; } +CalibrateLinearFilter::CalibrateLinearFilter(std::initializer_list> linear_functions) + : linear_functions_(linear_functions) {} + optional CalibrateLinearFilter::new_value(float value) { - for (std::array f : this->linear_functions_) { + for (const auto &f : this->linear_functions_) { if (!std::isfinite(f[2]) || value < f[2]) return (value * f[0]) + f[1]; } return NAN; } +CalibratePolynomialFilter::CalibratePolynomialFilter(std::initializer_list coefficients) + : coefficients_(coefficients) {} + optional CalibratePolynomialFilter::new_value(float value) { float res = 0.0f; float x = 1.0f; - for (float coefficient : this->coefficients_) { + for (const auto &coefficient : this->coefficients_) { res += x * coefficient; x *= value; } @@ -538,5 +478,78 @@ optional ToNTCTemperatureFilter::new_value(float value) { return temp; } +// StreamingFilter (base class) +StreamingFilter::StreamingFilter(size_t window_size, size_t send_first_at) + : window_size_(window_size), send_first_at_(send_first_at) {} + +optional StreamingFilter::new_value(float value) { + // Process the value (child class tracks min/max/sum/etc) + this->process_value(value); + + this->count_++; + + // Check if we should send (handle send_first_at for first value) + bool should_send = false; + if (this->first_send_ && this->count_ >= this->send_first_at_) { + should_send = true; + this->first_send_ = false; + } else if (!this->first_send_ && this->count_ >= this->window_size_) { + should_send = true; + } + + if (should_send) { + float result = this->compute_batch_result(); + // Reset for next batch + this->count_ = 0; + this->reset_batch(); + ESP_LOGVV(TAG, "StreamingFilter(%p)::new_value(%f) SENDING %f", this, value, result); + return result; + } + + return {}; +} + +// StreamingMinFilter +void StreamingMinFilter::process_value(float value) { + // Update running minimum (ignore NaN values) + if (!std::isnan(value)) { + this->current_min_ = std::isnan(this->current_min_) ? value : std::min(this->current_min_, value); + } +} + +float StreamingMinFilter::compute_batch_result() { return this->current_min_; } + +void StreamingMinFilter::reset_batch() { this->current_min_ = NAN; } + +// StreamingMaxFilter +void StreamingMaxFilter::process_value(float value) { + // Update running maximum (ignore NaN values) + if (!std::isnan(value)) { + this->current_max_ = std::isnan(this->current_max_) ? value : std::max(this->current_max_, value); + } +} + +float StreamingMaxFilter::compute_batch_result() { return this->current_max_; } + +void StreamingMaxFilter::reset_batch() { this->current_max_ = NAN; } + +// StreamingMovingAverageFilter +void StreamingMovingAverageFilter::process_value(float value) { + // Accumulate sum (ignore NaN values) + if (!std::isnan(value)) { + this->sum_ += value; + this->valid_count_++; + } +} + +float StreamingMovingAverageFilter::compute_batch_result() { + return this->valid_count_ > 0 ? this->sum_ / this->valid_count_ : NAN; +} + +void StreamingMovingAverageFilter::reset_batch() { + this->sum_ = 0.0f; + this->valid_count_ = 0; +} + } // namespace sensor } // namespace esphome diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index 5765c9a081..75e28a1efe 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -44,11 +44,75 @@ class Filter { Sensor *parent_{nullptr}; }; +/** Base class for filters that use a sliding window of values. + * + * Uses a ring buffer to efficiently maintain a fixed-size sliding window without + * reallocations or pop_front() overhead. Eliminates deque fragmentation issues. + */ +class SlidingWindowFilter : public Filter { + public: + SlidingWindowFilter(size_t window_size, size_t send_every, size_t send_first_at); + + optional new_value(float value) final; + + protected: + /// Called by new_value() to compute the filtered result from the current window + virtual float compute_result() = 0; + + /// Access the sliding window values (ring buffer implementation) + /// Use: for (size_t i = 0; i < window_count_; i++) { float val = window_[i]; } + FixedVector window_; + size_t window_head_{0}; ///< Index where next value will be written + size_t window_count_{0}; ///< Number of valid values in window (0 to window_size_) + size_t window_size_; ///< Maximum window size + size_t send_every_; ///< Send result every N values + size_t send_at_; ///< Counter for send_every +}; + +/** Base class for Min/Max filters. + * + * Provides a templated helper to find extremum values efficiently. + */ +class MinMaxFilter : public SlidingWindowFilter { + public: + using SlidingWindowFilter::SlidingWindowFilter; + + protected: + /// Helper to find min or max value in window, skipping NaN values + /// Usage: find_extremum_>() for min, find_extremum_>() for max + template float find_extremum_() { + float result = NAN; + Compare comp; + for (size_t i = 0; i < this->window_count_; i++) { + float v = this->window_[i]; + if (!std::isnan(v)) { + result = std::isnan(result) ? v : (comp(v, result) ? v : result); + } + } + return result; + } +}; + +/** Base class for filters that need a sorted window (Median, Quantile). + * + * Extends SlidingWindowFilter to provide a helper that filters out NaN values. + * Derived classes use std::nth_element for efficient partial sorting. + */ +class SortedWindowFilter : public SlidingWindowFilter { + public: + using SlidingWindowFilter::SlidingWindowFilter; + + protected: + /// Helper to get non-NaN values from the window (not sorted - caller will use nth_element) + /// Returns empty FixedVector if all values are NaN + FixedVector get_window_values_(); +}; + /** Simple quantile filter. * - * Takes the quantile of the last values and pushes it out every . + * Takes the quantile of the last values and pushes it out every . */ -class QuantileFilter : public Filter { +class QuantileFilter : public SortedWindowFilter { public: /** Construct a QuantileFilter. * @@ -61,25 +125,18 @@ class QuantileFilter : public Filter { */ explicit QuantileFilter(size_t window_size, size_t send_every, size_t send_first_at, float quantile); - optional new_value(float value) override; - - void set_send_every(size_t send_every); - void set_window_size(size_t window_size); - void set_quantile(float quantile); + void set_quantile(float quantile) { this->quantile_ = quantile; } protected: - std::deque queue_; - size_t send_every_; - size_t send_at_; - size_t window_size_; + float compute_result() override; float quantile_; }; /** Simple median filter. * - * Takes the median of the last values and pushes it out every . + * Takes the median of the last values and pushes it out every . */ -class MedianFilter : public Filter { +class MedianFilter : public SortedWindowFilter { public: /** Construct a MedianFilter. * @@ -89,18 +146,10 @@ class MedianFilter : public Filter { * on startup being published on the first *raw* value, so with no filter applied. Must be less than or equal to * send_every. */ - explicit MedianFilter(size_t window_size, size_t send_every, size_t send_first_at); - - optional new_value(float value) override; - - void set_send_every(size_t send_every); - void set_window_size(size_t window_size); + using SortedWindowFilter::SortedWindowFilter; protected: - std::deque queue_; - size_t send_every_; - size_t send_at_; - size_t window_size_; + float compute_result() override; }; /** Simple skip filter. @@ -123,9 +172,9 @@ class SkipInitialFilter : public Filter { /** Simple min filter. * - * Takes the min of the last values and pushes it out every . + * Takes the min of the last values and pushes it out every . */ -class MinFilter : public Filter { +class MinFilter : public MinMaxFilter { public: /** Construct a MinFilter. * @@ -135,25 +184,17 @@ class MinFilter : public Filter { * on startup being published on the first *raw* value, so with no filter applied. Must be less than or equal to * send_every. */ - explicit MinFilter(size_t window_size, size_t send_every, size_t send_first_at); - - optional new_value(float value) override; - - void set_send_every(size_t send_every); - void set_window_size(size_t window_size); + using MinMaxFilter::MinMaxFilter; protected: - std::deque queue_; - size_t send_every_; - size_t send_at_; - size_t window_size_; + float compute_result() override; }; /** Simple max filter. * - * Takes the max of the last values and pushes it out every . + * Takes the max of the last values and pushes it out every . */ -class MaxFilter : public Filter { +class MaxFilter : public MinMaxFilter { public: /** Construct a MaxFilter. * @@ -163,18 +204,10 @@ class MaxFilter : public Filter { * on startup being published on the first *raw* value, so with no filter applied. Must be less than or equal to * send_every. */ - explicit MaxFilter(size_t window_size, size_t send_every, size_t send_first_at); - - optional new_value(float value) override; - - void set_send_every(size_t send_every); - void set_window_size(size_t window_size); + using MinMaxFilter::MinMaxFilter; protected: - std::deque queue_; - size_t send_every_; - size_t send_at_; - size_t window_size_; + float compute_result() override; }; /** Simple sliding window moving average filter. @@ -182,7 +215,7 @@ class MaxFilter : public Filter { * Essentially just takes takes the average of the last window_size values and pushes them out * every send_every. */ -class SlidingWindowMovingAverageFilter : public Filter { +class SlidingWindowMovingAverageFilter : public SlidingWindowFilter { public: /** Construct a SlidingWindowMovingAverageFilter. * @@ -192,18 +225,10 @@ class SlidingWindowMovingAverageFilter : public Filter { * on startup being published on the first *raw* value, so with no filter applied. Must be less than or equal to * send_every. */ - explicit SlidingWindowMovingAverageFilter(size_t window_size, size_t send_every, size_t send_first_at); - - optional new_value(float value) override; - - void set_send_every(size_t send_every); - void set_window_size(size_t window_size); + using SlidingWindowFilter::SlidingWindowFilter; protected: - std::deque queue_; - size_t send_every_; - size_t send_at_; - size_t window_size_; + float compute_result() override; }; /** Simple exponential moving average filter. @@ -271,6 +296,21 @@ class LambdaFilter : public Filter { lambda_filter_t lambda_filter_; }; +/** Optimized lambda filter for stateless lambdas (no capture). + * + * Uses function pointer instead of std::function to reduce memory overhead. + * Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function). + */ +class StatelessLambdaFilter : public Filter { + public: + explicit StatelessLambdaFilter(optional (*lambda_filter)(float)) : lambda_filter_(lambda_filter) {} + + optional new_value(float value) override { return this->lambda_filter_(value); } + + protected: + optional (*lambda_filter_)(float); +}; + /// A simple filter that adds `offset` to each value it receives. class OffsetFilter : public Filter { public: @@ -292,15 +332,28 @@ class MultiplyFilter : public Filter { TemplatableValue multiplier_; }; +/** Base class for filters that compare sensor values against a list of configured values. + * + * This base class provides common functionality for filters that need to check if a sensor + * value matches any value in a configured list, with proper handling of NaN values and + * accuracy-based rounding for comparisons. + */ +class ValueListFilter : public Filter { + protected: + explicit ValueListFilter(std::initializer_list> values); + + /// Check if sensor value matches any configured value (with accuracy rounding) + bool value_matches_any_(float sensor_value); + + FixedVector> values_; +}; + /// A simple filter that only forwards the filter chain if it doesn't receive `value_to_filter_out`. -class FilterOutValueFilter : public Filter { +class FilterOutValueFilter : public ValueListFilter { public: - explicit FilterOutValueFilter(std::vector> values_to_filter_out); + explicit FilterOutValueFilter(std::initializer_list> values_to_filter_out); optional new_value(float value) override; - - protected: - std::vector> values_to_filter_out_; }; class ThrottleFilter : public Filter { @@ -315,22 +368,22 @@ class ThrottleFilter : public Filter { }; /// Same as 'throttle' but will immediately publish values contained in `value_to_prioritize`. -class ThrottleWithPriorityFilter : public Filter { +class ThrottleWithPriorityFilter : public ValueListFilter { public: explicit ThrottleWithPriorityFilter(uint32_t min_time_between_inputs, - std::vector> prioritized_values); + std::initializer_list> prioritized_values); optional new_value(float value) override; protected: uint32_t last_input_{0}; uint32_t min_time_between_inputs_; - std::vector> prioritized_values_; }; class TimeoutFilter : public Filter, public Component { public: - explicit TimeoutFilter(uint32_t time_period, TemplatableValue new_value); + explicit TimeoutFilter(uint32_t time_period); + explicit TimeoutFilter(uint32_t time_period, const TemplatableValue &new_value); optional new_value(float value) override; @@ -338,7 +391,7 @@ class TimeoutFilter : public Filter, public Component { protected: uint32_t time_period_; - TemplatableValue value_; + optional> value_; }; class DebounceFilter : public Filter, public Component { @@ -358,15 +411,16 @@ class HeartbeatFilter : public Filter, public Component { explicit HeartbeatFilter(uint32_t time_period); void setup() override; - optional new_value(float value) override; - float get_setup_priority() const override; + void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } + protected: uint32_t time_period_; float last_input_; bool has_value_{false}; + bool optimistic_{false}; }; class DeltaFilter : public Filter { @@ -384,7 +438,7 @@ class DeltaFilter : public Filter { class OrFilter : public Filter { public: - explicit OrFilter(std::vector filters); + explicit OrFilter(std::initializer_list filters); void initialize(Sensor *parent, Filter *next) override; @@ -400,28 +454,27 @@ class OrFilter : public Filter { OrFilter *or_parent_; }; - std::vector filters_; + FixedVector filters_; PhiNode phi_; bool has_value_{false}; }; class CalibrateLinearFilter : public Filter { public: - CalibrateLinearFilter(std::vector> linear_functions) - : linear_functions_(std::move(linear_functions)) {} + explicit CalibrateLinearFilter(std::initializer_list> linear_functions); optional new_value(float value) override; protected: - std::vector> linear_functions_; + FixedVector> linear_functions_; }; class CalibratePolynomialFilter : public Filter { public: - CalibratePolynomialFilter(std::vector coefficients) : coefficients_(std::move(coefficients)) {} + explicit CalibratePolynomialFilter(std::initializer_list coefficients); optional new_value(float value) override; protected: - std::vector coefficients_; + FixedVector coefficients_; }; class ClampFilter : public Filter { @@ -475,5 +528,81 @@ class ToNTCTemperatureFilter : public Filter { double c_; }; +/** Base class for streaming filters (batch windows where window_size == send_every). + * + * When window_size equals send_every, we don't need a sliding window. + * This base class handles the common batching logic. + */ +class StreamingFilter : public Filter { + public: + StreamingFilter(size_t window_size, size_t send_first_at); + + optional new_value(float value) final; + + protected: + /// Called by new_value() to process each value in the batch + virtual void process_value(float value) = 0; + + /// Called by new_value() to compute the result after collecting window_size values + virtual float compute_batch_result() = 0; + + /// Called by new_value() to reset internal state after sending a result + virtual void reset_batch() = 0; + + size_t window_size_; + size_t count_{0}; + size_t send_first_at_; + bool first_send_{true}; +}; + +/** Streaming min filter for batch windows (window_size == send_every). + * + * Uses O(1) memory instead of O(n) by tracking only the minimum value. + */ +class StreamingMinFilter : public StreamingFilter { + public: + using StreamingFilter::StreamingFilter; + + protected: + void process_value(float value) override; + float compute_batch_result() override; + void reset_batch() override; + + float current_min_{NAN}; +}; + +/** Streaming max filter for batch windows (window_size == send_every). + * + * Uses O(1) memory instead of O(n) by tracking only the maximum value. + */ +class StreamingMaxFilter : public StreamingFilter { + public: + using StreamingFilter::StreamingFilter; + + protected: + void process_value(float value) override; + float compute_batch_result() override; + void reset_batch() override; + + float current_max_{NAN}; +}; + +/** Streaming moving average filter for batch windows (window_size == send_every). + * + * Uses O(1) memory instead of O(n) by tracking only sum and count. + */ +class StreamingMovingAverageFilter : public StreamingFilter { + public: + using StreamingFilter::StreamingFilter; + + protected: + void process_value(float value) override; + float compute_batch_result() override; + void reset_batch() override; + + float sum_{0.0f}; + size_t valid_count_{0}; +}; + } // namespace sensor } // namespace esphome diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index 0a82677bc9..df6bd644e8 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -1,4 +1,6 @@ #include "sensor.h" +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" namespace esphome { @@ -6,17 +8,45 @@ namespace sensor { static const char *const TAG = "sensor"; -std::string state_class_to_string(StateClass state_class) { +// Function implementation of LOG_SENSOR macro to reduce code size +void log_sensor(const char *tag, const char *prefix, const char *type, Sensor *obj) { + if (obj == nullptr) { + return; + } + + ESP_LOGCONFIG(tag, + "%s%s '%s'\n" + "%s State Class: '%s'\n" + "%s Unit of Measurement: '%s'\n" + "%s Accuracy Decimals: %d", + prefix, type, obj->get_name().c_str(), prefix, + LOG_STR_ARG(state_class_to_string(obj->get_state_class())), prefix, + obj->get_unit_of_measurement_ref().c_str(), prefix, obj->get_accuracy_decimals()); + + if (!obj->get_device_class_ref().empty()) { + ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj->get_device_class_ref().c_str()); + } + + if (!obj->get_icon_ref().empty()) { + ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj->get_icon_ref().c_str()); + } + + if (obj->get_force_update()) { + ESP_LOGV(tag, "%s Force Update: YES", prefix); + } +} + +const LogString *state_class_to_string(StateClass state_class) { switch (state_class) { case STATE_CLASS_MEASUREMENT: - return "measurement"; + return LOG_STR("measurement"); case STATE_CLASS_TOTAL_INCREASING: - return "total_increasing"; + return LOG_STR("total_increasing"); case STATE_CLASS_TOTAL: - return "total"; + return LOG_STR("total"); case STATE_CLASS_NONE: default: - return ""; + return LOG_STR(""); } } @@ -79,12 +109,12 @@ void Sensor::add_filter(Filter *filter) { } filter->initialize(this, nullptr); } -void Sensor::add_filters(const std::vector &filters) { +void Sensor::add_filters(std::initializer_list filters) { for (Filter *filter : filters) { this->add_filter(filter); } } -void Sensor::set_filters(const std::vector &filters) { +void Sensor::set_filters(std::initializer_list filters) { this->clear_filters(); this->add_filters(filters); } @@ -101,8 +131,11 @@ 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().c_str(), this->get_accuracy_decimals()); + this->get_unit_of_measurement_ref().c_str(), this->get_accuracy_decimals()); this->callback_.call(state); +#if defined(USE_SENSOR) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_sensor_update(this); +#endif } } // namespace sensor diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index c2ded0f2c3..a4210e5e6c 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -6,32 +6,15 @@ #include "esphome/core/log.h" #include "esphome/components/sensor/filter.h" -#include +#include #include namespace esphome { namespace sensor { -#define LOG_SENSOR(prefix, type, obj) \ - if ((obj) != nullptr) { \ - ESP_LOGCONFIG(TAG, \ - "%s%s '%s'\n" \ - "%s State Class: '%s'\n" \ - "%s Unit of Measurement: '%s'\n" \ - "%s Accuracy Decimals: %d", \ - prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str(), prefix, \ - state_class_to_string((obj)->get_state_class()).c_str(), prefix, \ - (obj)->get_unit_of_measurement().c_str(), prefix, (obj)->get_accuracy_decimals()); \ - if (!(obj)->get_device_class().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class().c_str()); \ - } \ - if (!(obj)->get_icon().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \ - } \ - if ((obj)->get_force_update()) { \ - ESP_LOGV(TAG, "%s Force Update: YES", prefix); \ - } \ - } +void log_sensor(const char *tag, const char *prefix, const char *type, Sensor *obj); + +#define LOG_SENSOR(prefix, type, obj) log_sensor(TAG, prefix, LOG_STR_LITERAL(type), obj) #define SUB_SENSOR(name) \ protected: \ @@ -50,7 +33,7 @@ enum StateClass : uint8_t { STATE_CLASS_TOTAL = 3, }; -std::string state_class_to_string(StateClass state_class); +const LogString *state_class_to_string(StateClass state_class); /** Base-class for all sensors. * @@ -94,10 +77,10 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa * SlidingWindowMovingAverageFilter(15, 15), // average over last 15 values * }); */ - void add_filters(const std::vector &filters); + void add_filters(std::initializer_list filters); /// Clear the filters and replace them by filters. - void set_filters(const std::vector &filters); + void set_filters(std::initializer_list filters); /// Clear the entire filter chain. void clear_filters(); diff --git a/esphome/components/servo/servo.h b/esphome/components/servo/servo.h index ff1708dc53..3d15aefefe 100644 --- a/esphome/components/servo/servo.h +++ b/esphome/components/servo/servo.h @@ -57,7 +57,7 @@ template class ServoWriteAction : public Action { ServoWriteAction(Servo *servo) : servo_(servo) {} TEMPLATABLE_VALUE(float, value) - void play(Ts... x) override { this->servo_->write(this->value_.value(x...)); } + void play(const Ts &...x) override { this->servo_->write(this->value_.value(x...)); } protected: Servo *servo_; @@ -67,7 +67,7 @@ template class ServoDetachAction : public Action { public: ServoDetachAction(Servo *servo) : servo_(servo) {} - void play(Ts... x) override { this->servo_->detach(); } + void play(const Ts &...x) override { this->servo_->detach(); } protected: Servo *servo_; diff --git a/esphome/components/sfa30/sfa30.cpp b/esphome/components/sfa30/sfa30.cpp index 99709d5fbb..bbe3bcd7d2 100644 --- a/esphome/components/sfa30/sfa30.cpp +++ b/esphome/components/sfa30/sfa30.cpp @@ -73,17 +73,17 @@ void SFA30Component::update() { } if (this->formaldehyde_sensor_ != nullptr) { - const float formaldehyde = raw_data[0] / 5.0f; + const float formaldehyde = static_cast(raw_data[0]) / 5.0f; this->formaldehyde_sensor_->publish_state(formaldehyde); } if (this->humidity_sensor_ != nullptr) { - const float humidity = raw_data[1] / 100.0f; + const float humidity = static_cast(raw_data[1]) / 100.0f; this->humidity_sensor_->publish_state(humidity); } if (this->temperature_sensor_ != nullptr) { - const float temperature = raw_data[2] / 200.0f; + const float temperature = static_cast(raw_data[2]) / 200.0f; this->temperature_sensor_->publish_state(temperature); } diff --git a/esphome/components/sgp30/sgp30.cpp b/esphome/components/sgp30/sgp30.cpp index 42baff6d23..9e8d6b332c 100644 --- a/esphome/components/sgp30/sgp30.cpp +++ b/esphome/components/sgp30/sgp30.cpp @@ -1,9 +1,11 @@ #include "sgp30.h" -#include #include "esphome/core/application.h" #include "esphome/core/hal.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include + namespace esphome { namespace sgp30 { @@ -39,9 +41,8 @@ void SGP30Component::setup() { this->mark_failed(); return; } - this->serial_number_ = (uint64_t(raw_serial_number[0]) << 24) | (uint64_t(raw_serial_number[1]) << 16) | - (uint64_t(raw_serial_number[2])); - ESP_LOGD(TAG, "Serial Number: %" PRIu64, this->serial_number_); + this->serial_number_ = encode_uint24(raw_serial_number[0], raw_serial_number[1], raw_serial_number[2]); + ESP_LOGD(TAG, "Serial number: %" PRIu64, this->serial_number_); // Featureset identification for future use uint16_t raw_featureset; @@ -61,11 +62,11 @@ void SGP30Component::setup() { this->mark_failed(); return; } - ESP_LOGD(TAG, "Product version: 0x%0X", uint16_t(this->featureset_ & 0x1FF)); + ESP_LOGV(TAG, "Product version: 0x%0X", uint16_t(this->featureset_ & 0x1FF)); // Sensor initialization if (!this->write_command(SGP30_CMD_IAQ_INIT)) { - ESP_LOGE(TAG, "Sensor sgp30_iaq_init failed."); + ESP_LOGE(TAG, "sgp30_iaq_init failed"); this->error_code_ = MEASUREMENT_INIT_FAILED; this->mark_failed(); return; @@ -77,7 +78,7 @@ void SGP30Component::setup() { uint32_t hash = fnv1_hash(App.get_compilation_time() + std::to_string(this->serial_number_)); this->pref_ = global_preferences->make_preference(hash, true); - if (this->pref_.load(&this->baselines_storage_)) { + if (this->store_baseline_ && this->pref_.load(&this->baselines_storage_)) { ESP_LOGI(TAG, "Loaded eCO2 baseline: 0x%04X, TVOC baseline: 0x%04X", this->baselines_storage_.eco2, baselines_storage_.tvoc); this->eco2_baseline_ = this->baselines_storage_.eco2; @@ -123,7 +124,7 @@ void SGP30Component::read_iaq_baseline_() { uint16_t eco2baseline = (raw_data[0]); uint16_t tvocbaseline = (raw_data[1]); - ESP_LOGI(TAG, "Current eCO2 baseline: 0x%04X, TVOC baseline: 0x%04X", eco2baseline, tvocbaseline); + ESP_LOGI(TAG, "Baselines: eCO2: 0x%04X, TVOC: 0x%04X", eco2baseline, tvocbaseline); if (eco2baseline != this->eco2_baseline_ || tvocbaseline != this->tvoc_baseline_) { this->eco2_baseline_ = eco2baseline; this->tvoc_baseline_ = tvocbaseline; @@ -142,7 +143,7 @@ void SGP30Component::read_iaq_baseline_() { this->baselines_storage_.eco2 = this->eco2_baseline_; this->baselines_storage_.tvoc = this->tvoc_baseline_; if (this->pref_.save(&this->baselines_storage_)) { - ESP_LOGI(TAG, "Store eCO2 baseline: 0x%04X, TVOC baseline: 0x%04X", this->baselines_storage_.eco2, + ESP_LOGI(TAG, "Store baselines: eCO2: 0x%04X, TVOC: 0x%04X", this->baselines_storage_.eco2, this->baselines_storage_.tvoc); } else { ESP_LOGW(TAG, "Could not store eCO2 and TVOC baselines"); @@ -164,7 +165,7 @@ void SGP30Component::send_env_data_() { if (this->humidity_sensor_ != nullptr) humidity = this->humidity_sensor_->state; if (std::isnan(humidity) || humidity < 0.0f || humidity > 100.0f) { - ESP_LOGW(TAG, "Compensation not possible yet: bad humidity data."); + ESP_LOGW(TAG, "Compensation not possible yet: bad humidity data"); return; } else { ESP_LOGD(TAG, "External compensation data received: Humidity %0.2f%%", humidity); @@ -174,7 +175,7 @@ void SGP30Component::send_env_data_() { temperature = float(this->temperature_sensor_->state); } if (std::isnan(temperature) || temperature < -40.0f || temperature > 85.0f) { - ESP_LOGW(TAG, "Compensation not possible yet: bad temperature value data."); + ESP_LOGW(TAG, "Compensation not possible yet: bad temperature value"); return; } else { ESP_LOGD(TAG, "External compensation data received: Temperature %0.2f°C", temperature); @@ -192,18 +193,17 @@ void SGP30Component::send_env_data_() { ((humidity * 0.061121f * std::exp((18.678f - temperature / 234.5f) * (temperature / (257.14f + temperature)))) / (273.15f + temperature)); } - uint8_t humidity_full = uint8_t(std::floor(absolute_humidity)); - uint8_t humidity_dec = uint8_t(std::floor((absolute_humidity - std::floor(absolute_humidity)) * 256)); - ESP_LOGD(TAG, "Calculated Absolute humidity: %0.3f g/m³ (0x%04X)", absolute_humidity, - uint16_t(uint16_t(humidity_full) << 8 | uint16_t(humidity_dec))); - uint8_t crc = sht_crc_(humidity_full, humidity_dec); - uint8_t data[4]; - data[0] = SGP30_CMD_SET_ABSOLUTE_HUMIDITY & 0xFF; - data[1] = humidity_full; - data[2] = humidity_dec; - data[3] = crc; + uint8_t data[4] = { + SGP30_CMD_SET_ABSOLUTE_HUMIDITY & 0xFF, + uint8_t(std::floor(absolute_humidity)), // humidity_full + uint8_t(std::floor((absolute_humidity - std::floor(absolute_humidity)) * 256)), // humidity_dec + 0, + }; + data[3] = crc8(&data[1], 2, 0xFF, sensirion_common::CRC_POLYNOMIAL, true); + ESP_LOGD(TAG, "Calculated absolute humidity: %0.3f g/m³ (0x%04X)", absolute_humidity, + encode_uint16(data[1], data[2])); if (!this->write_bytes(SGP30_CMD_SET_ABSOLUTE_HUMIDITY >> 8, data, 4)) { - ESP_LOGE(TAG, "Error sending compensation data."); + ESP_LOGE(TAG, "Error sending compensation data"); } } @@ -212,15 +212,14 @@ void SGP30Component::write_iaq_baseline_(uint16_t eco2_baseline, uint16_t tvoc_b data[0] = SGP30_CMD_SET_IAQ_BASELINE & 0xFF; data[1] = tvoc_baseline >> 8; data[2] = tvoc_baseline & 0xFF; - data[3] = sht_crc_(data[1], data[2]); + data[3] = crc8(&data[1], 2, 0xFF, sensirion_common::CRC_POLYNOMIAL, true); data[4] = eco2_baseline >> 8; data[5] = eco2_baseline & 0xFF; - data[6] = sht_crc_(data[4], data[5]); + data[6] = crc8(&data[4], 2, 0xFF, sensirion_common::CRC_POLYNOMIAL, true); if (!this->write_bytes(SGP30_CMD_SET_IAQ_BASELINE >> 8, data, 7)) { - ESP_LOGE(TAG, "Error applying eCO2 baseline: 0x%04X, TVOC baseline: 0x%04X", eco2_baseline, tvoc_baseline); + ESP_LOGE(TAG, "Error applying baselines: eCO2: 0x%04X, TVOC: 0x%04X", eco2_baseline, tvoc_baseline); } else { - ESP_LOGI(TAG, "Initial baselines applied successfully! eCO2 baseline: 0x%04X, TVOC baseline: 0x%04X", eco2_baseline, - tvoc_baseline); + ESP_LOGI(TAG, "Initial baselines applied: eCO2: 0x%04X, TVOC: 0x%04X", eco2_baseline, tvoc_baseline); } } @@ -236,10 +235,10 @@ void SGP30Component::dump_config() { ESP_LOGW(TAG, "Measurement Initialization failed"); break; case INVALID_ID: - ESP_LOGW(TAG, "Sensor reported an invalid ID. Is this an SGP30?"); + ESP_LOGW(TAG, "Invalid ID"); break; case UNSUPPORTED_ID: - ESP_LOGW(TAG, "Sensor reported an unsupported ID (SGPC3)"); + ESP_LOGW(TAG, "Unsupported ID"); break; default: ESP_LOGW(TAG, "Unknown setup error"); @@ -249,12 +248,12 @@ void SGP30Component::dump_config() { ESP_LOGCONFIG(TAG, " Serial number: %" PRIu64, this->serial_number_); if (this->eco2_baseline_ != 0x0000 && this->tvoc_baseline_ != 0x0000) { ESP_LOGCONFIG(TAG, - " Baseline:\n" - " eCO2 Baseline: 0x%04X\n" - " TVOC Baseline: 0x%04X", + " Baselines:\n" + " eCO2: 0x%04X\n" + " TVOC: 0x%04X", this->eco2_baseline_, this->tvoc_baseline_); } else { - ESP_LOGCONFIG(TAG, " Baseline: No baseline configured"); + ESP_LOGCONFIG(TAG, " Baselines not configured"); } ESP_LOGCONFIG(TAG, " Warm up time: %" PRIu32 "s", this->required_warm_up_time_); } @@ -266,8 +265,8 @@ void SGP30Component::dump_config() { ESP_LOGCONFIG(TAG, "Store baseline: %s", YESNO(this->store_baseline_)); if (this->humidity_sensor_ != nullptr && this->temperature_sensor_ != nullptr) { ESP_LOGCONFIG(TAG, " Compensation:"); - LOG_SENSOR(" ", "Temperature Source:", this->temperature_sensor_); - LOG_SENSOR(" ", "Humidity Source:", this->humidity_sensor_); + LOG_SENSOR(" ", "Temperature source:", this->temperature_sensor_); + LOG_SENSOR(" ", "Humidity source:", this->humidity_sensor_); } else { ESP_LOGCONFIG(TAG, " Compensation: No source configured"); } @@ -289,7 +288,7 @@ void SGP30Component::update() { float eco2 = (raw_data[0]); float tvoc = (raw_data[1]); - ESP_LOGD(TAG, "Got eCO2=%.1fppm TVOC=%.1fppb", eco2, tvoc); + ESP_LOGV(TAG, "eCO2=%.1fppm TVOC=%.1fppb", eco2, tvoc); if (this->eco2_sensor_ != nullptr) this->eco2_sensor_->publish_state(eco2); if (this->tvoc_sensor_ != nullptr) diff --git a/esphome/components/sgp30/sgp30.h b/esphome/components/sgp30/sgp30.h index e6429a7bfa..4648a33e15 100644 --- a/esphome/components/sgp30/sgp30.h +++ b/esphome/components/sgp30/sgp30.h @@ -1,8 +1,8 @@ #pragma once -#include "esphome/core/component.h" -#include "esphome/components/sensor/sensor.h" #include "esphome/components/sensirion_common/i2c_sensirion.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/core/component.h" #include "esphome/core/preferences.h" #include @@ -38,14 +38,16 @@ class SGP30Component : public PollingComponent, public sensirion_common::Sensiri void read_iaq_baseline_(); bool is_sensor_baseline_reliable_(); void write_iaq_baseline_(uint16_t eco2_baseline, uint16_t tvoc_baseline); + uint64_t serial_number_; - uint16_t featureset_; uint32_t required_warm_up_time_; uint32_t seconds_since_last_store_; - SGP30Baselines baselines_storage_; - ESPPreferenceObject pref_; + uint16_t featureset_; + uint16_t eco2_baseline_{0x0000}; + uint16_t tvoc_baseline_{0x0000}; + bool store_baseline_; - enum ErrorCode { + enum ErrorCode : uint8_t { COMMUNICATION_FAILED, MEASUREMENT_INIT_FAILED, INVALID_ID, @@ -53,14 +55,13 @@ class SGP30Component : public PollingComponent, public sensirion_common::Sensiri UNKNOWN } error_code_{UNKNOWN}; + ESPPreferenceObject pref_; + SGP30Baselines baselines_storage_; + sensor::Sensor *eco2_sensor_{nullptr}; sensor::Sensor *tvoc_sensor_{nullptr}; sensor::Sensor *eco2_sensor_baseline_{nullptr}; sensor::Sensor *tvoc_sensor_baseline_{nullptr}; - uint16_t eco2_baseline_{0x0000}; - uint16_t tvoc_baseline_{0x0000}; - bool store_baseline_; - /// Input sensor for humidity and temperature compensation. sensor::Sensor *humidity_sensor_{nullptr}; sensor::Sensor *temperature_sensor_{nullptr}; diff --git a/esphome/components/sgp4x/sgp4x.cpp b/esphome/components/sgp4x/sgp4x.cpp index da52993a87..99d88006f7 100644 --- a/esphome/components/sgp4x/sgp4x.cpp +++ b/esphome/components/sgp4x/sgp4x.cpp @@ -211,7 +211,7 @@ void SGP4xComponent::measure_raw_() { if (!this->write_command(command, data, 2)) { ESP_LOGD(TAG, "write error (%d)", this->last_error_); - this->status_set_warning("measurement request failed"); + this->status_set_warning(LOG_STR("measurement request failed")); return; } @@ -220,7 +220,7 @@ void SGP4xComponent::measure_raw_() { raw_data[1] = 0; if (!this->read_data(raw_data, response_words)) { ESP_LOGD(TAG, "read error (%d)", this->last_error_); - this->status_set_warning("measurement read failed"); + this->status_set_warning(LOG_STR("measurement read failed")); this->voc_index_ = this->nox_index_ = UINT16_MAX; return; } diff --git a/esphome/components/sha256/__init__.py b/esphome/components/sha256/__init__.py new file mode 100644 index 0000000000..f07157416d --- /dev/null +++ b/esphome/components/sha256/__init__.py @@ -0,0 +1,22 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.core import CORE +from esphome.helpers import IS_MACOS +from esphome.types import ConfigType + +CODEOWNERS = ["@esphome/core"] + +sha256_ns = cg.esphome_ns.namespace("sha256") + +CONFIG_SCHEMA = cv.Schema({}) + + +async def to_code(config: ConfigType) -> None: + # Add OpenSSL library for host platform + if not CORE.is_host: + return + if IS_MACOS: + # macOS needs special handling for Homebrew OpenSSL + cg.add_build_flag("-I/opt/homebrew/opt/openssl/include") + cg.add_build_flag("-L/opt/homebrew/opt/openssl/lib") + cg.add_build_flag("-lcrypto") diff --git a/esphome/components/sha256/sha256.cpp b/esphome/components/sha256/sha256.cpp new file mode 100644 index 0000000000..32abbd739d --- /dev/null +++ b/esphome/components/sha256/sha256.cpp @@ -0,0 +1,116 @@ +#include "sha256.h" + +// Only compile SHA256 implementation on platforms that support it +#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_HOST) + +#include "esphome/core/helpers.h" +#include + +namespace esphome::sha256 { + +#if defined(USE_ESP32) || defined(USE_LIBRETINY) + +// CRITICAL ESP32-S3 HARDWARE SHA ACCELERATION REQUIREMENTS: +// +// The ESP32-S3 uses hardware DMA for SHA acceleration. The mbedtls_sha256_context structure contains +// internal state that the DMA engine references. This imposes two critical constraints: +// +// 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]). +// +// 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() { +// sha256::SHA256 hasher; // Created locally +// hasher.init(); +// hasher.add(data, len); // Any size, no chunking needed +// hasher.calculate(); +// bool ok = hasher.equals_hex(expected); +// // hasher destroyed when function returns +// } +// +// INCORRECT USAGE (WILL FAIL ON ESP32-S3): +// void my_function() { +// sha256::SHA256 hasher; +// helper(&hasher); // WRONG: Passed to different stack frame +// } +// void helper(HashBase *h) { +// h->init(); // WRONG: Will produce truncated/corrupted output +// } + +SHA256::~SHA256() { mbedtls_sha256_free(&this->ctx_); } + +void SHA256::init() { + mbedtls_sha256_init(&this->ctx_); + mbedtls_sha256_starts(&this->ctx_, 0); // 0 = SHA256, not SHA224 +} + +void SHA256::add(const uint8_t *data, size_t len) { mbedtls_sha256_update(&this->ctx_, data, len); } + +void SHA256::calculate() { mbedtls_sha256_finish(&this->ctx_, this->digest_); } + +#elif defined(USE_ESP8266) || defined(USE_RP2040) + +SHA256::~SHA256() = default; + +void SHA256::init() { + br_sha256_init(&this->ctx_); + this->calculated_ = false; +} + +void SHA256::add(const uint8_t *data, size_t len) { br_sha256_update(&this->ctx_, data, len); } + +void SHA256::calculate() { + if (!this->calculated_) { + br_sha256_out(&this->ctx_, this->digest_); + this->calculated_ = true; + } +} + +#elif defined(USE_HOST) + +SHA256::~SHA256() { + if (this->ctx_) { + EVP_MD_CTX_free(this->ctx_); + } +} + +void SHA256::init() { + if (this->ctx_) { + EVP_MD_CTX_free(this->ctx_); + } + this->ctx_ = EVP_MD_CTX_new(); + EVP_DigestInit_ex(this->ctx_, EVP_sha256(), nullptr); + this->calculated_ = false; +} + +void SHA256::add(const uint8_t *data, size_t len) { + if (!this->ctx_) { + this->init(); + } + EVP_DigestUpdate(this->ctx_, data, len); +} + +void SHA256::calculate() { + if (!this->ctx_) { + this->init(); + } + if (!this->calculated_) { + unsigned int len = 32; + EVP_DigestFinal_ex(this->ctx_, this->digest_, &len); + this->calculated_ = true; + } +} + +#else +#error "SHA256 not supported on this platform" +#endif + +} // namespace esphome::sha256 + +#endif // Platform check diff --git a/esphome/components/sha256/sha256.h b/esphome/components/sha256/sha256.h new file mode 100644 index 0000000000..a2b62799e1 --- /dev/null +++ b/esphome/components/sha256/sha256.h @@ -0,0 +1,60 @@ +#pragma once + +#include "esphome/core/defines.h" + +// Only define SHA256 on platforms that support it +#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_HOST) + +#include +#include +#include +#include "esphome/core/hash_base.h" + +#if defined(USE_ESP32) || defined(USE_LIBRETINY) +#include "mbedtls/sha256.h" +#elif defined(USE_ESP8266) || defined(USE_RP2040) +#include +#elif defined(USE_HOST) +#include +#else +#error "SHA256 not supported on this platform" +#endif + +namespace esphome::sha256 { + +class SHA256 : public esphome::HashBase { + public: + SHA256() = default; + ~SHA256() override; + + void init() override; + void add(const uint8_t *data, size_t len) override; + using HashBase::add; // Bring base class overload into scope + void add(const std::string &data) { this->add((const uint8_t *) data.c_str(), data.length()); } + + void calculate() override; + + /// Get the size of the hash in bytes (32 for SHA256) + size_t get_size() const override { return 32; } + + protected: +#if defined(USE_ESP32) || defined(USE_LIBRETINY) + // CRITICAL: The mbedtls context MUST be stack-allocated (not a pointer) for ESP32-S3 hardware SHA acceleration. + // The ESP32-S3 DMA engine references this structure's memory addresses. If the context is passed to another + // function (crossing stack frames) or if VLAs are present, the DMA operations will corrupt memory and produce + // truncated/incorrect hash results. + mbedtls_sha256_context ctx_{}; +#elif defined(USE_ESP8266) || defined(USE_RP2040) + br_sha256_context ctx_{}; + bool calculated_{false}; +#elif defined(USE_HOST) + EVP_MD_CTX *ctx_{nullptr}; + bool calculated_{false}; +#else +#error "SHA256 not supported on this platform" +#endif +}; + +} // namespace esphome::sha256 + +#endif // Platform check diff --git a/esphome/components/shelly_dimmer/light.py b/esphome/components/shelly_dimmer/light.py index bb2c3ceee8..c96bc380d7 100644 --- a/esphome/components/shelly_dimmer/light.py +++ b/esphome/components/shelly_dimmer/light.py @@ -183,7 +183,7 @@ CONFIG_SCHEMA = ( ) -def to_code(config): +async def to_code(config): fw_hex = get_firmware(config[CONF_FIRMWARE]) fw_major, fw_minor = parse_firmware_version(config[CONF_FIRMWARE][CONF_VERSION]) @@ -193,17 +193,17 @@ def to_code(config): cg.add_define("USE_SHD_FIRMWARE_MINOR_VERSION", fw_minor) var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) - yield cg.register_component(var, config) + await cg.register_component(var, config) config.pop( CONF_UPDATE_INTERVAL ) # drop UPDATE_INTERVAL as it does not apply to the light component - yield light.register_light(var, config) - yield uart.register_uart_device(var, config) + await light.register_light(var, config) + await uart.register_uart_device(var, config) - nrst_pin = yield cg.gpio_pin_expression(config[CONF_NRST_PIN]) + nrst_pin = await cg.gpio_pin_expression(config[CONF_NRST_PIN]) cg.add(var.set_nrst_pin(nrst_pin)) - boot0_pin = yield cg.gpio_pin_expression(config[CONF_BOOT0_PIN]) + boot0_pin = await cg.gpio_pin_expression(config[CONF_BOOT0_PIN]) cg.add(var.set_boot0_pin(boot0_pin)) cg.add(var.set_leading_edge(config[CONF_LEADING_EDGE])) @@ -217,5 +217,5 @@ def to_code(config): continue conf = config[key] - sens = yield sensor.new_sensor(conf) + sens = await sensor.new_sensor(conf) cg.add(getattr(var, f"set_{key}_sensor")(sens)) diff --git a/esphome/components/sht4x/sht4x.cpp b/esphome/components/sht4x/sht4x.cpp index 637c8c1a9d..9d29746f0b 100644 --- a/esphome/components/sht4x/sht4x.cpp +++ b/esphome/components/sht4x/sht4x.cpp @@ -7,16 +7,28 @@ namespace sht4x { static const char *const TAG = "sht4x"; static const uint8_t MEASURECOMMANDS[] = {0xFD, 0xF6, 0xE0}; +static const uint8_t SERIAL_NUMBER_COMMAND = 0x89; void SHT4XComponent::start_heater_() { uint8_t cmd[] = {MEASURECOMMANDS[this->heater_command_]}; ESP_LOGD(TAG, "Heater turning on"); if (this->write(cmd, 1) != i2c::ERROR_OK) { - this->status_set_error("Failed to turn on heater"); + this->status_set_error(LOG_STR("Failed to turn on heater")); } } +void SHT4XComponent::read_serial_number_() { + uint16_t buffer[2]; + if (!this->get_8bit_register(SERIAL_NUMBER_COMMAND, buffer, 2, 1)) { + ESP_LOGE(TAG, "Get serial number failed"); + this->serial_number_ = 0; + return; + } + this->serial_number_ = (uint32_t(buffer[0]) << 16) | (uint32_t(buffer[1])); + ESP_LOGD(TAG, "Serial number: %08" PRIx32, this->serial_number_); +} + void SHT4XComponent::setup() { auto err = this->write(nullptr, 0); if (err != i2c::ERROR_OK) { @@ -24,6 +36,8 @@ void SHT4XComponent::setup() { return; } + this->read_serial_number_(); + if (std::isfinite(this->duty_cycle_) && this->duty_cycle_ > 0.0f) { uint32_t heater_interval = static_cast(static_cast(this->heater_time_) / this->duty_cycle_); ESP_LOGD(TAG, "Heater interval: %" PRIu32, heater_interval); @@ -54,18 +68,25 @@ void SHT4XComponent::setup() { } void SHT4XComponent::dump_config() { - ESP_LOGCONFIG(TAG, "SHT4x:"); + ESP_LOGCONFIG(TAG, + "SHT4x:\n" + " Serial number: %08" PRIx32, + this->serial_number_); + LOG_I2C_DEVICE(this); if (this->is_failed()) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); } + if (this->serial_number_ == 0) { + ESP_LOGW(TAG, "Get serial number failed"); + } } void SHT4XComponent::update() { // Send command if (!this->write_command(MEASURECOMMANDS[this->precision_])) { // Warning will be printed only if warning status is not set yet - this->status_set_warning("Failed to send measurement command"); + this->status_set_warning(LOG_STR("Failed to send measurement command")); return; } diff --git a/esphome/components/sht4x/sht4x.h b/esphome/components/sht4x/sht4x.h index accc7323be..aec0f3d7f8 100644 --- a/esphome/components/sht4x/sht4x.h +++ b/esphome/components/sht4x/sht4x.h @@ -36,7 +36,9 @@ class SHT4XComponent : public PollingComponent, public sensirion_common::Sensiri float duty_cycle_; void start_heater_(); + void read_serial_number_(); uint8_t heater_command_; + uint32_t serial_number_; sensor::Sensor *temp_sensor_{nullptr}; sensor::Sensor *humidity_sensor_{nullptr}; diff --git a/esphome/components/sim800l/sim800l.cpp b/esphome/components/sim800l/sim800l.cpp index d97b0ae364..55cadcf182 100644 --- a/esphome/components/sim800l/sim800l.cpp +++ b/esphome/components/sim800l/sim800l.cpp @@ -288,11 +288,15 @@ void Sim800LComponent::parse_cmd_(std::string message) { if (item == 3) { // stat uint8_t current_call_state = parse_number(message.substr(start, end - start)).value_or(6); if (current_call_state != this->call_state_) { - ESP_LOGD(TAG, "Call state is now: %d", current_call_state); - if (current_call_state == 0) - this->call_connected_callback_.call(); + if (current_call_state == 4) { + ESP_LOGV(TAG, "Premature call state '4'. Ignoring, waiting for RING"); + } else { + this->call_state_ = current_call_state; + ESP_LOGD(TAG, "Call state is now: %d", current_call_state); + if (current_call_state == 0) + this->call_connected_callback_.call(); + } } - this->call_state_ = current_call_state; break; } // item 4 = "" diff --git a/esphome/components/sim800l/sim800l.h b/esphome/components/sim800l/sim800l.h index bf7efd6915..a2da686ce1 100644 --- a/esphome/components/sim800l/sim800l.h +++ b/esphome/components/sim800l/sim800l.h @@ -162,7 +162,7 @@ template class Sim800LSendSmsAction : public Action { TEMPLATABLE_VALUE(std::string, recipient) TEMPLATABLE_VALUE(std::string, message) - void play(Ts... x) { + void play(const Ts &...x) { auto recipient = this->recipient_.value(x...); auto message = this->message_.value(x...); this->parent_->send_sms(recipient, message); @@ -177,7 +177,7 @@ template class Sim800LSendUssdAction : public Action { Sim800LSendUssdAction(Sim800LComponent *parent) : parent_(parent) {} TEMPLATABLE_VALUE(std::string, ussd) - void play(Ts... x) { + void play(const Ts &...x) { auto ussd_code = this->ussd_.value(x...); this->parent_->send_ussd(ussd_code); } @@ -191,7 +191,7 @@ template class Sim800LDialAction : public Action { Sim800LDialAction(Sim800LComponent *parent) : parent_(parent) {} TEMPLATABLE_VALUE(std::string, recipient) - void play(Ts... x) { + void play(const Ts &...x) { auto recipient = this->recipient_.value(x...); this->parent_->dial(recipient); } @@ -203,7 +203,7 @@ template class Sim800LConnectAction : public Action { public: Sim800LConnectAction(Sim800LComponent *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->connect(); } + void play(const Ts &...x) { this->parent_->connect(); } protected: Sim800LComponent *parent_; @@ -213,7 +213,7 @@ template class Sim800LDisconnectAction : public Action { public: Sim800LDisconnectAction(Sim800LComponent *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->disconnect(); } + void play(const Ts &...x) { this->parent_->disconnect(); } protected: Sim800LComponent *parent_; diff --git a/esphome/components/sntp/sntp_component.cpp b/esphome/components/sntp/sntp_component.cpp index ccd9af3153..c4d78b6e0b 100644 --- a/esphome/components/sntp/sntp_component.cpp +++ b/esphome/components/sntp/sntp_component.cpp @@ -14,17 +14,27 @@ namespace sntp { static const char *const TAG = "sntp"; +#if defined(USE_ESP32) +SNTPComponent *SNTPComponent::instance = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +#endif + void SNTPComponent::setup() { #if defined(USE_ESP32) + SNTPComponent::instance = this; if (esp_sntp_enabled()) { esp_sntp_stop(); } esp_sntp_setoperatingmode(ESP_SNTP_OPMODE_POLL); size_t i = 0; for (auto &server : this->servers_) { - esp_sntp_setservername(i++, server.c_str()); + esp_sntp_setservername(i++, server); } esp_sntp_set_sync_interval(this->get_update_interval()); + esp_sntp_set_time_sync_notification_cb([](struct timeval *tv) { + if (SNTPComponent::instance != nullptr) { + SNTPComponent::instance->defer([]() { SNTPComponent::instance->time_synced(); }); + } + }); esp_sntp_init(); #else sntp_stop(); @@ -32,8 +42,16 @@ void SNTPComponent::setup() { size_t i = 0; for (auto &server : this->servers_) { - sntp_setservername(i++, server.c_str()); + sntp_setservername(i++, server); } + +#if defined(USE_ESP8266) + settimeofday_cb([this](bool from_sntp) { + if (from_sntp) + this->time_synced(); + }); +#endif + sntp_init(); #endif } @@ -41,12 +59,14 @@ void SNTPComponent::dump_config() { ESP_LOGCONFIG(TAG, "SNTP Time:"); size_t i = 0; for (auto &server : this->servers_) { - ESP_LOGCONFIG(TAG, " Server %zu: '%s'", i++, server.c_str()); + ESP_LOGCONFIG(TAG, " Server %zu: '%s'", i++, server); } + RealTimeClock::dump_config(); } void SNTPComponent::update() { #if !defined(USE_ESP32) - // force resync + // Some platforms currently cannot set the sync interval at runtime so we need + // to do the re-sync by hand for now. if (sntp_enabled()) { sntp_stop(); this->has_time_ = false; @@ -55,23 +75,31 @@ void SNTPComponent::update() { #endif } void SNTPComponent::loop() { +// The loop is used to infer whether we have valid time on platforms where we +// cannot tell whether SNTP has succeeded. +// One limitation of this approach is that we cannot tell if it was the SNTP +// component that set the time. +// ESP-IDF and ESP8266 use callbacks from the SNTP task to trigger the +// `on_time_sync` trigger on successful sync events. +#if defined(USE_ESP32) || defined(USE_ESP8266) + this->disable_loop(); +#endif + if (this->has_time_) return; + this->time_synced(); +} + +void SNTPComponent::time_synced() { auto time = this->now(); - if (!time.is_valid()) + this->has_time_ = time.is_valid(); + if (!this->has_time_) return; ESP_LOGD(TAG, "Synchronized time: %04d-%02d-%02d %02d:%02d:%02d", time.year, time.month, time.day_of_month, time.hour, time.minute, time.second); this->time_sync_callback_.call(); - this->has_time_ = true; - -#ifdef USE_ESP_IDF - // On ESP-IDF, time sync is permanent and update() doesn't force resync - // Time is now synchronized, no need to check anymore - this->disable_loop(); -#endif } } // namespace sntp diff --git a/esphome/components/sntp/sntp_component.h b/esphome/components/sntp/sntp_component.h index a4e8267383..8f2e411c18 100644 --- a/esphome/components/sntp/sntp_component.h +++ b/esphome/components/sntp/sntp_component.h @@ -2,10 +2,14 @@ #include "esphome/core/component.h" #include "esphome/components/time/real_time_clock.h" +#include namespace esphome { namespace sntp { +// Server count is calculated at compile time by Python codegen +// SNTP_SERVER_COUNT will always be defined + /// The SNTP component allows you to configure local timekeeping via Simple Network Time Protocol. /// /// \note @@ -14,10 +18,7 @@ namespace sntp { /// \see https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html class SNTPComponent : public time::RealTimeClock { public: - SNTPComponent(const std::vector &servers) : servers_(servers) {} - - // Note: set_servers() has been removed and replaced by a constructor - calling set_servers after setup would - // have had no effect anyway, and making the strings immutable avoids the need to strdup their contents. + SNTPComponent(const std::array &servers) : servers_(servers) {} void setup() override; void dump_config() override; @@ -26,9 +27,19 @@ class SNTPComponent : public time::RealTimeClock { void update() override; void loop() override; + void time_synced(); + protected: - std::vector servers_; + // Store const char pointers to string literals + // ESP8266: strings in rodata (RAM), but avoids std::string overhead (~24 bytes each) + // Other platforms: strings in flash + std::array servers_; bool has_time_{false}; + +#if defined(USE_ESP32) + private: + static SNTPComponent *instance; +#endif }; } // namespace sntp diff --git a/esphome/components/sntp/time.py b/esphome/components/sntp/time.py index 1c8ee402ad..69a2436d3d 100644 --- a/esphome/components/sntp/time.py +++ b/esphome/components/sntp/time.py @@ -1,9 +1,14 @@ +import logging + import esphome.codegen as cg from esphome.components import time as time_ +from esphome.config_helpers import merge_config import esphome.config_validation as cv from esphome.const import ( CONF_ID, + CONF_PLATFORM, CONF_SERVERS, + CONF_TIME, PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, @@ -12,13 +17,74 @@ from esphome.const import ( PLATFORM_RTL87XX, ) from esphome.core import CORE +import esphome.final_validate as fv +from esphome.types import ConfigType + +_LOGGER = logging.getLogger(__name__) DEPENDENCIES = ["network"] + +CONF_SNTP = "sntp" + sntp_ns = cg.esphome_ns.namespace("sntp") SNTPComponent = sntp_ns.class_("SNTPComponent", time_.RealTimeClock) DEFAULT_SERVERS = ["0.pool.ntp.org", "1.pool.ntp.org", "2.pool.ntp.org"] + +def _sntp_final_validate(config: ConfigType) -> None: + """Merge multiple SNTP instances into one, similar to OTA merging behavior.""" + full_conf = fv.full_config.get() + time_confs = full_conf.get(CONF_TIME, []) + + sntp_configs: list[ConfigType] = [] + other_time_configs: list[ConfigType] = [] + + for time_conf in time_confs: + if time_conf.get(CONF_PLATFORM) == CONF_SNTP: + sntp_configs.append(time_conf) + else: + other_time_configs.append(time_conf) + + if len(sntp_configs) <= 1: + return + + # Merge all SNTP configs into the first one + merged = sntp_configs[0] + for sntp_conf in sntp_configs[1:]: + # Validate that IDs are consistent if manually specified + if merged[CONF_ID].is_manual and sntp_conf[CONF_ID].is_manual: + raise cv.Invalid( + f"Found multiple SNTP configurations but {CONF_ID} is inconsistent" + ) + merged = merge_config(merged, sntp_conf) + + # Deduplicate servers while preserving order + servers = merged[CONF_SERVERS] + unique_servers = list(dict.fromkeys(servers)) + + # Warn if we're dropping servers due to 3-server limit + if len(unique_servers) > 3: + dropped = unique_servers[3:] + unique_servers = unique_servers[:3] + _LOGGER.warning( + "SNTP supports maximum 3 servers. Dropped excess server(s): %s", + dropped, + ) + + merged[CONF_SERVERS] = unique_servers + + _LOGGER.warning( + "Found and merged %d SNTP time configurations into one instance", + len(sntp_configs), + ) + + # Replace time configs with merged SNTP + other time platforms + other_time_configs.append(merged) + full_conf[CONF_TIME] = other_time_configs + fv.full_config.set(full_conf) + + CONFIG_SCHEMA = cv.All( time_.TIME_SCHEMA.extend( { @@ -40,9 +106,16 @@ CONFIG_SCHEMA = cv.All( ), ) +FINAL_VALIDATE_SCHEMA = _sntp_final_validate + async def to_code(config): servers = config[CONF_SERVERS] + + # Define server count at compile time + cg.add_define("SNTP_SERVER_COUNT", len(servers)) + + # Pass string literals to constructor - stored in flash/rodata by compiler var = cg.new_Pvariable(config[CONF_ID], servers) await cg.register_component(var, config) diff --git a/esphome/components/socket/__init__.py b/esphome/components/socket/__init__.py index e085a09eac..49e074a6ee 100644 --- a/esphome/components/socket/__init__.py +++ b/esphome/components/socket/__init__.py @@ -1,3 +1,5 @@ +from collections.abc import Callable, MutableMapping + import esphome.codegen as cg import esphome.config_validation as cv from esphome.core import CORE @@ -9,6 +11,59 @@ IMPLEMENTATION_LWIP_TCP = "lwip_tcp" IMPLEMENTATION_LWIP_SOCKETS = "lwip_sockets" IMPLEMENTATION_BSD_SOCKETS = "bsd_sockets" +# Socket tracking infrastructure +# Components register their socket needs and platforms read this to configure appropriately +KEY_SOCKET_CONSUMERS = "socket_consumers" + +# Wake loop threadsafe support tracking +KEY_WAKE_LOOP_THREADSAFE_REQUIRED = "wake_loop_threadsafe_required" + + +def consume_sockets( + value: int, consumer: str +) -> Callable[[MutableMapping], MutableMapping]: + """Register socket usage for a component. + + Args: + value: Number of sockets needed by the component + consumer: Name of the component consuming the sockets + + Returns: + A validator function that records the socket usage + """ + + def _consume_sockets(config: MutableMapping) -> MutableMapping: + consumers: dict[str, int] = CORE.data.setdefault(KEY_SOCKET_CONSUMERS, {}) + consumers[consumer] = consumers.get(consumer, 0) + value + return config + + return _consume_sockets + + +def require_wake_loop_threadsafe() -> None: + """Mark that wake_loop_threadsafe support is required by a component. + + Call this from components that need to wake the main event loop from background threads. + This enables the shared UDP loopback socket mechanism (~208 bytes RAM). + The socket is shared across all components that use this feature. + + IMPORTANT: This is for background thread context only, NOT ISR context. + Socket operations are not safe to call from ISR handlers. + + Example: + from esphome.components import socket + + async def to_code(config): + socket.require_wake_loop_threadsafe() + """ + # Only set up once (idempotent - multiple components can call this) + if not CORE.data.get(KEY_WAKE_LOOP_THREADSAFE_REQUIRED, False): + CORE.data[KEY_WAKE_LOOP_THREADSAFE_REQUIRED] = True + cg.add_define("USE_WAKE_LOOP_THREADSAFE") + # Consume 1 socket for the shared wake notification socket + consume_sockets(1, "socket.wake_loop_threadsafe")({}) + + CONFIG_SCHEMA = cv.Schema( { cv.SplitDefault( diff --git a/esphome/components/socket/bsd_sockets_impl.cpp b/esphome/components/socket/bsd_sockets_impl.cpp index e056696bcf..c7cca62027 100644 --- a/esphome/components/socket/bsd_sockets_impl.cpp +++ b/esphome/components/socket/bsd_sockets_impl.cpp @@ -145,7 +145,7 @@ class BSDSocketImpl : public Socket { } ssize_t sendto(const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen) override { - return ::sendto(fd_, buf, len, flags, to, tolen); + return ::sendto(fd_, buf, len, flags, to, tolen); // NOLINT(readability-suspicious-call-argument) } int setblocking(bool blocking) override { diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index 2d64a275df..e0d93d8e2f 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -9,7 +9,7 @@ #include "lwip/tcp.h" #include #include -#include +#include #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -40,27 +40,14 @@ class LWIPRawImpl : public Socket { void init() { LWIP_LOG("init(%p)", pcb_); tcp_arg(pcb_, this); - tcp_accept(pcb_, LWIPRawImpl::s_accept_fn); tcp_recv(pcb_, LWIPRawImpl::s_recv_fn); tcp_err(pcb_, LWIPRawImpl::s_err_fn); } std::unique_ptr accept(struct sockaddr *addr, socklen_t *addrlen) override { - if (pcb_ == nullptr) { - errno = EBADF; - return nullptr; - } - if (accepted_sockets_.empty()) { - errno = EWOULDBLOCK; - return nullptr; - } - std::unique_ptr sock = std::move(accepted_sockets_.front()); - accepted_sockets_.pop(); - if (addr != nullptr) { - sock->getpeername(addr, addrlen); - } - LWIP_LOG("accept(%p)", sock.get()); - return std::unique_ptr(std::move(sock)); + // Non-listening sockets return error + errno = EINVAL; + return nullptr; } int bind(const struct sockaddr *name, socklen_t addrlen) override { if (pcb_ == nullptr) { @@ -185,16 +172,7 @@ class LWIPRawImpl : public Socket { errno = ECONNRESET; return ""; } - char buffer[50] = {}; - if (IP_IS_V4_VAL(pcb_->remote_ip)) { - inet_ntoa_r(pcb_->remote_ip, buffer, sizeof(buffer)); - } -#if LWIP_IPV6 - else if (IP_IS_V6_VAL(pcb_->remote_ip)) { - inet6_ntoa_r(pcb_->remote_ip, buffer, sizeof(buffer)); - } -#endif - return std::string(buffer); + return this->format_ip_address_(pcb_->remote_ip); } int getsockname(struct sockaddr *name, socklen_t *addrlen) override { if (pcb_ == nullptr) { @@ -212,16 +190,7 @@ class LWIPRawImpl : public Socket { errno = ECONNRESET; return ""; } - char buffer[50] = {}; - if (IP_IS_V4_VAL(pcb_->local_ip)) { - inet_ntoa_r(pcb_->local_ip, buffer, sizeof(buffer)); - } -#if LWIP_IPV6 - else if (IP_IS_V6_VAL(pcb_->local_ip)) { - inet6_ntoa_r(pcb_->local_ip, buffer, sizeof(buffer)); - } -#endif - return std::string(buffer); + return this->format_ip_address_(pcb_->local_ip); } int getsockopt(int level, int optname, void *optval, socklen_t *optlen) override { if (pcb_ == nullptr) { @@ -286,25 +255,10 @@ class LWIPRawImpl : public Socket { return -1; } int listen(int backlog) override { - if (pcb_ == nullptr) { - errno = EBADF; - return -1; - } - LWIP_LOG("tcp_listen_with_backlog(%p backlog=%d)", pcb_, backlog); - struct tcp_pcb *listen_pcb = tcp_listen_with_backlog(pcb_, backlog); - if (listen_pcb == nullptr) { - tcp_abort(pcb_); - pcb_ = nullptr; - errno = EOPNOTSUPP; - return -1; - } - // tcp_listen reallocates the pcb, replace ours - pcb_ = listen_pcb; - // set callbacks on new pcb - LWIP_LOG("tcp_arg(%p)", pcb_); - tcp_arg(pcb_, this); - tcp_accept(pcb_, LWIPRawImpl::s_accept_fn); - return 0; + // Regular sockets can't be converted to listening - this shouldn't happen + // as listen() should only be called on sockets created for listening + errno = EOPNOTSUPP; + return -1; } ssize_t read(void *buf, size_t len) override { if (pcb_ == nullptr) { @@ -485,20 +439,6 @@ class LWIPRawImpl : public Socket { return 0; } - err_t accept_fn(struct tcp_pcb *newpcb, err_t err) { - LWIP_LOG("accept(newpcb=%p err=%d)", newpcb, err); - if (err != ERR_OK || newpcb == nullptr) { - // "An error code if there has been an error accepting. Only return ERR_ABRT if you have - // called tcp_abort from within the callback function!" - // https://www.nongnu.org/lwip/2_1_x/tcp_8h.html#a00517abce6856d6c82f0efebdafb734d - // nothing to do here, we just don't push it to the queue - return ERR_OK; - } - auto sock = make_unique(family_, newpcb); - sock->init(); - accepted_sockets_.push(std::move(sock)); - return ERR_OK; - } void err_fn(err_t err) { LWIP_LOG("err(err=%d)", err); // "If a connection is aborted because of an error, the application is alerted of this event by @@ -530,11 +470,6 @@ class LWIPRawImpl : public Socket { return ERR_OK; } - static err_t s_accept_fn(void *arg, struct tcp_pcb *newpcb, err_t err) { - LWIPRawImpl *arg_this = reinterpret_cast(arg); - return arg_this->accept_fn(newpcb, err); - } - static void s_err_fn(void *arg, err_t err) { LWIPRawImpl *arg_this = reinterpret_cast(arg); arg_this->err_fn(err); @@ -546,6 +481,19 @@ class LWIPRawImpl : public Socket { } protected: + std::string format_ip_address_(const ip_addr_t &ip) { + char buffer[50] = {}; + if (IP_IS_V4_VAL(ip)) { + inet_ntoa_r(ip, buffer, sizeof(buffer)); + } +#if LWIP_IPV6 + else if (IP_IS_V6_VAL(ip)) { + inet6_ntoa_r(ip, buffer, sizeof(buffer)); + } +#endif + return std::string(buffer); + } + int ip2sockaddr_(ip_addr_t *ip, uint16_t port, struct sockaddr *name, socklen_t *addrlen) { if (family_ == AF_INET) { if (*addrlen < sizeof(struct sockaddr_in)) { @@ -586,22 +534,133 @@ class LWIPRawImpl : public Socket { return -1; } + // Member ordering optimized to minimize padding on 32-bit systems + // Largest members first (4 bytes), then smaller members (1 byte each) struct tcp_pcb *pcb_; - std::queue> accepted_sockets_; - bool rx_closed_ = false; pbuf *rx_buf_ = nullptr; size_t rx_buf_offset_ = 0; + bool rx_closed_ = false; // don't use lwip nodelay flag, it sometimes causes reconnect // instead use it for determining whether to call lwip_output bool nodelay_ = false; sa_family_t family_ = 0; }; +// Listening socket class - only allocates accept queue when needed (for bind+listen sockets) +// This saves 16 bytes (12 bytes array + 1 byte count + 3 bytes padding) for regular connected sockets on ESP8266/RP2040 +class LWIPRawListenImpl : public LWIPRawImpl { + public: + LWIPRawListenImpl(sa_family_t family, struct tcp_pcb *pcb) : LWIPRawImpl(family, pcb) {} + + void init() { + LWIP_LOG("init(%p)", pcb_); + tcp_arg(pcb_, this); + tcp_accept(pcb_, LWIPRawListenImpl::s_accept_fn); + tcp_err(pcb_, LWIPRawImpl::s_err_fn); // Use base class error handler + } + + std::unique_ptr accept(struct sockaddr *addr, socklen_t *addrlen) override { + if (pcb_ == nullptr) { + errno = EBADF; + return nullptr; + } + if (accepted_socket_count_ == 0) { + errno = EWOULDBLOCK; + return nullptr; + } + // Take from front for FIFO ordering + std::unique_ptr sock = std::move(accepted_sockets_[0]); + // Shift remaining sockets forward + for (uint8_t i = 1; i < accepted_socket_count_; i++) { + accepted_sockets_[i - 1] = std::move(accepted_sockets_[i]); + } + accepted_socket_count_--; + LWIP_LOG("Connection accepted by application, queue size: %d", accepted_socket_count_); + if (addr != nullptr) { + sock->getpeername(addr, addrlen); + } + LWIP_LOG("accept(%p)", sock.get()); + return std::unique_ptr(std::move(sock)); + } + + int listen(int backlog) override { + if (pcb_ == nullptr) { + errno = EBADF; + return -1; + } + LWIP_LOG("tcp_listen_with_backlog(%p backlog=%d)", pcb_, backlog); + struct tcp_pcb *listen_pcb = tcp_listen_with_backlog(pcb_, backlog); + if (listen_pcb == nullptr) { + tcp_abort(pcb_); + pcb_ = nullptr; + errno = EOPNOTSUPP; + return -1; + } + // tcp_listen reallocates the pcb, replace ours + pcb_ = listen_pcb; + // set callbacks on new pcb + LWIP_LOG("tcp_arg(%p)", pcb_); + tcp_arg(pcb_, this); + tcp_accept(pcb_, LWIPRawListenImpl::s_accept_fn); + return 0; + } + + private: + err_t accept_fn(struct tcp_pcb *newpcb, err_t err) { + LWIP_LOG("accept(newpcb=%p err=%d)", newpcb, err); + if (err != ERR_OK || newpcb == nullptr) { + // "An error code if there has been an error accepting. Only return ERR_ABRT if you have + // called tcp_abort from within the callback function!" + // https://www.nongnu.org/lwip/2_1_x/tcp_8h.html#a00517abce6856d6c82f0efebdafb734d + // nothing to do here, we just don't push it to the queue + return ERR_OK; + } + // Check if we've reached the maximum accept queue size + if (accepted_socket_count_ >= MAX_ACCEPTED_SOCKETS) { + LWIP_LOG("Rejecting connection, queue full (%d)", accepted_socket_count_); + // Abort the connection when queue is full + tcp_abort(newpcb); + // Must return ERR_ABRT since we called tcp_abort() + return ERR_ABRT; + } + auto sock = make_unique(family_, newpcb); + sock->init(); + accepted_sockets_[accepted_socket_count_++] = std::move(sock); + LWIP_LOG("Accepted connection, queue size: %d", accepted_socket_count_); + return ERR_OK; + } + + static err_t s_accept_fn(void *arg, struct tcp_pcb *newpcb, err_t err) { + LWIPRawListenImpl *arg_this = reinterpret_cast(arg); + return arg_this->accept_fn(newpcb, err); + } + + // Accept queue - holds incoming connections briefly until the event loop calls accept() + // This is NOT a connection pool - just a temporary queue between LWIP callbacks and the main loop + // 3 slots is plenty since connections are pulled out quickly by the event loop + // + // Memory analysis: std::array<3> vs original std::queue implementation: + // - std::queue uses std::deque internally which on 32-bit systems needs: + // 24 bytes (deque object) + 32+ bytes (map array) + heap allocations + // Total: ~56+ bytes minimum, plus heap fragmentation + // - std::array<3>: 12 bytes fixed (3 pointers × 4 bytes) + // Saves ~44+ bytes RAM per listening socket + avoids ALL heap allocations + // Used on ESP8266 and RP2040 (platforms using LWIP_TCP implementation) + // + // By using a separate listening socket class, regular connected sockets save + // 16 bytes (12 bytes array + 1 byte count + 3 bytes padding) of memory overhead on 32-bit systems + static constexpr size_t MAX_ACCEPTED_SOCKETS = 3; + std::array, MAX_ACCEPTED_SOCKETS> accepted_sockets_; + uint8_t accepted_socket_count_ = 0; // Number of sockets currently in queue +}; + std::unique_ptr socket(int domain, int type, int protocol) { auto *pcb = tcp_new(); if (pcb == nullptr) return nullptr; - auto *sock = new LWIPRawImpl((sa_family_t) domain, pcb); // NOLINT(cppcoreguidelines-owning-memory) + // Create listening socket implementation since user sockets typically bind+listen + // Accepted connections are created directly as LWIPRawImpl in the accept callback + auto *sock = new LWIPRawListenImpl((sa_family_t) domain, pcb); // NOLINT(cppcoreguidelines-owning-memory) sock->init(); return std::unique_ptr{sock}; } diff --git a/esphome/components/socket/socket.cpp b/esphome/components/socket/socket.cpp index 1c8e72b8fd..cc9232d21a 100644 --- a/esphome/components/socket/socket.cpp +++ b/esphome/components/socket/socket.cpp @@ -61,9 +61,18 @@ socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const std::stri server->sin6_family = AF_INET6; server->sin6_port = htons(port); +#ifdef USE_SOCKET_IMPL_BSD_SOCKETS + // Use standard inet_pton for BSD sockets + if (inet_pton(AF_INET6, ip_address.c_str(), &server->sin6_addr) != 1) { + errno = EINVAL; + return 0; + } +#else + // Use LWIP-specific functions ip6_addr_t ip6; inet6_aton(ip_address.c_str(), &ip6); memcpy(server->sin6_addr.un.u32_addr, ip6.addr, sizeof(ip6.addr)); +#endif return sizeof(sockaddr_in6); } #endif /* USE_NETWORK_IPV6 */ diff --git a/esphome/components/sonoff_d1/sonoff_d1.cpp b/esphome/components/sonoff_d1/sonoff_d1.cpp index e3d55681c5..cd09f31dd7 100644 --- a/esphome/components/sonoff_d1/sonoff_d1.cpp +++ b/esphome/components/sonoff_d1/sonoff_d1.cpp @@ -50,7 +50,7 @@ static const char *const TAG = "sonoff_d1"; uint8_t SonoffD1Output::calc_checksum_(const uint8_t *cmd, const size_t len) { uint8_t crc = 0; - for (int i = 2; i < len - 1; i++) { + for (size_t i = 2; i < len - 1; i++) { crc += cmd[i]; } return crc; diff --git a/esphome/components/sound_level/sound_level.cpp b/esphome/components/sound_level/sound_level.cpp index decf630aba..db6b168bbc 100644 --- a/esphome/components/sound_level/sound_level.cpp +++ b/esphome/components/sound_level/sound_level.cpp @@ -56,7 +56,7 @@ void SoundLevelComponent::loop() { } } else { if (!this->status_has_warning()) { - this->status_set_warning("Microphone isn't running, can't compute statistics"); + this->status_set_warning(LOG_STR("Microphone isn't running, can't compute statistics")); // Deallocate buffers, if necessary this->stop_(); diff --git a/esphome/components/sound_level/sound_level.h b/esphome/components/sound_level/sound_level.h index 6a80a60ac7..dc35f69fe2 100644 --- a/esphome/components/sound_level/sound_level.h +++ b/esphome/components/sound_level/sound_level.h @@ -60,12 +60,12 @@ class SoundLevelComponent : public Component { template class StartAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->start(); } + void play(const Ts &...x) override { this->parent_->start(); } }; template class StopAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->stop(); } + void play(const Ts &...x) override { this->parent_->stop(); } }; } // namespace sound_level diff --git a/esphome/components/speaker/__init__.py b/esphome/components/speaker/__init__.py index 2ac1ca0cb9..18e1d9782c 100644 --- a/esphome/components/speaker/__init__.py +++ b/esphome/components/speaker/__init__.py @@ -3,8 +3,8 @@ import esphome.codegen as cg from esphome.components import audio, audio_dac import esphome.config_validation as cv from esphome.const import CONF_DATA, CONF_ID, CONF_VOLUME -from esphome.core import CORE -from esphome.coroutine import coroutine_with_priority +from esphome.core import CORE, ID +from esphome.coroutine import CoroPriority, coroutine_with_priority AUTO_LOAD = ["audio"] CODEOWNERS = ["@jesserockz", "@kahrendt"] @@ -90,7 +90,10 @@ async def speaker_play_action(config, action_id, template_arg, args): templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8)) cg.add(var.set_data_template(templ)) else: - cg.add(var.set_data_static(data)) + # Generate static array in flash to avoid RAM copy + arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8) + arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data)) + cg.add(var.set_data_static(arr, len(data))) return var @@ -138,7 +141,7 @@ async def speaker_mute_action_to_code(config, action_id, template_arg, args): return cg.new_Pvariable(action_id, template_arg, paren) -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(speaker_ns.using) cg.add_define("USE_SPEAKER") diff --git a/esphome/components/speaker/automation.h b/esphome/components/speaker/automation.h index c083796eea..391c9e4c62 100644 --- a/esphome/components/speaker/automation.h +++ b/esphome/components/speaker/automation.h @@ -10,40 +10,45 @@ namespace speaker { template class PlayAction : public Action, public Parented { public: - void set_data_template(std::function(Ts...)> func) { - this->data_func_ = func; - this->static_ = false; - } - void set_data_static(const std::vector &data) { - this->data_static_ = data; - this->static_ = true; + void set_data_template(std::vector (*func)(Ts...)) { + this->data_.func = func; + this->len_ = -1; // Sentinel value indicates template mode } - void play(Ts... x) override { - if (this->static_) { - this->parent_->play(this->data_static_); + void set_data_static(const uint8_t *data, size_t len) { + this->data_.data = data; + this->len_ = len; // Length >= 0 indicates static mode + } + + void play(const Ts &...x) override { + if (this->len_ >= 0) { + // Static mode: pass pointer directly to play(const uint8_t *, size_t) + this->parent_->play(this->data_.data, static_cast(this->len_)); } else { - auto val = this->data_func_(x...); + // Template mode: call function and pass vector to play(const std::vector &) + auto val = this->data_.func(x...); this->parent_->play(val); } } protected: - bool static_{false}; - std::function(Ts...)> data_func_{}; - std::vector data_static_{}; + ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length + union Data { + std::vector (*func)(Ts...); // Function pointer (stateless lambdas) + const uint8_t *data; // Pointer to static data in flash + } data_; }; template class VolumeSetAction : public Action, public Parented { TEMPLATABLE_VALUE(float, volume) - void play(Ts... x) override { this->parent_->set_volume(this->volume_.value(x...)); } + void play(const Ts &...x) override { this->parent_->set_volume(this->volume_.value(x...)); } }; template class MuteOnAction : public Action { public: explicit MuteOnAction(Speaker *speaker) : speaker_(speaker) {} - void play(Ts... x) override { this->speaker_->set_mute_state(true); } + void play(const Ts &...x) override { this->speaker_->set_mute_state(true); } protected: Speaker *speaker_; @@ -53,7 +58,7 @@ template class MuteOffAction : public Action { public: explicit MuteOffAction(Speaker *speaker) : speaker_(speaker) {} - void play(Ts... x) override { this->speaker_->set_mute_state(false); } + void play(const Ts &...x) override { this->speaker_->set_mute_state(false); } protected: Speaker *speaker_; @@ -61,22 +66,22 @@ template class MuteOffAction : public Action { template class StopAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->stop(); } + void play(const Ts &...x) override { this->parent_->stop(); } }; template class FinishAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->finish(); } + void play(const Ts &...x) override { this->parent_->finish(); } }; template class IsPlayingCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->is_running(); } + bool check(const Ts &...x) override { return this->parent_->is_running(); } }; template class IsStoppedCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->is_stopped(); } + bool check(const Ts &...x) override { return this->parent_->is_stopped(); } }; } // namespace speaker diff --git a/esphome/components/speaker/media_player/__init__.py b/esphome/components/speaker/media_player/__init__.py index 1c2e7dc0e1..062bff92f8 100644 --- a/esphome/components/speaker/media_player/__init__.py +++ b/esphome/components/speaker/media_player/__init__.py @@ -6,7 +6,7 @@ from pathlib import Path from esphome import automation, external_files import esphome.codegen as cg -from esphome.components import audio, esp32, media_player, speaker +from esphome.components import audio, esp32, media_player, network, psram, speaker import esphome.config_validation as cv from esphome.const import ( CONF_BUFFER_SIZE, @@ -26,10 +26,13 @@ from esphome.const import ( from esphome.core import CORE, HexInt from esphome.core.entity_helpers import inherit_property_from from esphome.external_files import download_content +from esphome.final_validate import full_config _LOGGER = logging.getLogger(__name__) -AUTO_LOAD = ["audio", "psram"] + +AUTO_LOAD = ["audio"] +DEPENDENCIES = ["network"] CODEOWNERS = ["@kahrendt", "@synesthesiam"] DOMAIN = "media_player" @@ -147,7 +150,7 @@ def _read_audio_file_and_type(file_config): elif file_source == TYPE_WEB: path = _compute_local_file_path(conf_file) else: - raise cv.Invalid("Unsupported file source.") + raise cv.Invalid("Unsupported file source") with open(path, "rb") as f: data = f.read() @@ -215,12 +218,19 @@ def _validate_repeated_speaker(config): return config -def _validate_supported_local_file(config): +def _final_validate(config): + # Default to using codec if psram is enabled + if (use_codec := config.get(CONF_CODEC_SUPPORT_ENABLED)) is None: + use_codec = psram.DOMAIN in full_config.get() + conf_id = config[CONF_ID].id + core_data = CORE.data.setdefault(DOMAIN, {conf_id: {}}) + core_data[conf_id][CONF_CODEC_SUPPORT_ENABLED] = use_codec + for file_config in config.get(CONF_FILES, []): _, media_file_type = _read_audio_file_and_type(file_config) if str(media_file_type) == str(audio.AUDIO_FILE_TYPE_ENUM["NONE"]): - raise cv.Invalid("Unsupported local media file.") - if not config[CONF_CODEC_SUPPORT_ENABLED] and str(media_file_type) != str( + raise cv.Invalid("Unsupported local media file") + if not use_codec and str(media_file_type) != str( audio.AUDIO_FILE_TYPE_ENUM["WAV"] ): # Only wav files are supported @@ -271,6 +281,18 @@ PIPELINE_SCHEMA = cv.Schema( } ) + +def _request_high_performance_networking(config): + """Request high performance networking for streaming media. + + Speaker media player streams audio data, so it always benefits from + optimized WiFi and lwip settings regardless of codec support. + Called during config validation to ensure flags are set before to_code(). + """ + network.require_high_performance_networking() + return config + + CONFIG_SCHEMA = cv.All( media_player.media_player_schema(SpeakerMediaPlayer).extend( { @@ -279,9 +301,11 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_BUFFER_SIZE, default=1000000): cv.int_range( min=4000, max=4000000 ), - cv.Optional(CONF_CODEC_SUPPORT_ENABLED, default=True): cv.boolean, + cv.Optional(CONF_CODEC_SUPPORT_ENABLED): cv.boolean, cv.Optional(CONF_FILES): cv.ensure_list(MEDIA_FILE_TYPE_SCHEMA), - cv.Optional(CONF_TASK_STACK_IN_PSRAM, default=False): cv.boolean, + cv.Optional(CONF_TASK_STACK_IN_PSRAM): cv.All( + cv.boolean, cv.requires_component(psram.DOMAIN) + ), cv.Optional(CONF_VOLUME_INCREMENT, default=0.05): cv.percentage, cv.Optional(CONF_VOLUME_INITIAL, default=0.5): cv.percentage, cv.Optional(CONF_VOLUME_MAX, default=1.0): cv.percentage, @@ -293,6 +317,7 @@ CONFIG_SCHEMA = cv.All( ), cv.only_with_esp_idf, _validate_repeated_speaker, + _request_high_performance_networking, ) @@ -304,46 +329,16 @@ FINAL_VALIDATE_SCHEMA = cv.All( }, extra=cv.ALLOW_EXTRA, ), - _validate_supported_local_file, + _final_validate, ) async def to_code(config): - if config[CONF_CODEC_SUPPORT_ENABLED]: - # Compile all supported audio codecs and optimize the wifi settings - + if CORE.data[DOMAIN][config[CONF_ID].id][CONF_CODEC_SUPPORT_ENABLED]: + # Compile all supported audio codecs cg.add_define("USE_AUDIO_FLAC_SUPPORT", True) cg.add_define("USE_AUDIO_MP3_SUPPORT", True) - # Wifi settings based on https://github.com/espressif/esp-adf/issues/297#issuecomment-783811702 - esp32.add_idf_sdkconfig_option("CONFIG_ESP32_WIFI_STATIC_RX_BUFFER_NUM", 16) - esp32.add_idf_sdkconfig_option("CONFIG_ESP32_WIFI_DYNAMIC_RX_BUFFER_NUM", 512) - esp32.add_idf_sdkconfig_option("CONFIG_ESP32_WIFI_STATIC_TX_BUFFER", True) - esp32.add_idf_sdkconfig_option("CONFIG_ESP32_WIFI_TX_BUFFER_TYPE", 0) - esp32.add_idf_sdkconfig_option("CONFIG_ESP32_WIFI_STATIC_TX_BUFFER_NUM", 8) - esp32.add_idf_sdkconfig_option("CONFIG_ESP32_WIFI_CACHE_TX_BUFFER_NUM", 32) - esp32.add_idf_sdkconfig_option("CONFIG_ESP32_WIFI_AMPDU_TX_ENABLED", True) - esp32.add_idf_sdkconfig_option("CONFIG_ESP32_WIFI_TX_BA_WIN", 16) - esp32.add_idf_sdkconfig_option("CONFIG_ESP32_WIFI_AMPDU_RX_ENABLED", True) - esp32.add_idf_sdkconfig_option("CONFIG_ESP32_WIFI_RX_BA_WIN", 32) - esp32.add_idf_sdkconfig_option("CONFIG_LWIP_MAX_ACTIVE_TCP", 16) - esp32.add_idf_sdkconfig_option("CONFIG_LWIP_MAX_LISTENING_TCP", 16) - esp32.add_idf_sdkconfig_option("CONFIG_TCP_MAXRTX", 12) - esp32.add_idf_sdkconfig_option("CONFIG_TCP_SYNMAXRTX", 6) - esp32.add_idf_sdkconfig_option("CONFIG_TCP_MSS", 1436) - esp32.add_idf_sdkconfig_option("CONFIG_TCP_MSL", 60000) - esp32.add_idf_sdkconfig_option("CONFIG_TCP_SND_BUF_DEFAULT", 65535) - esp32.add_idf_sdkconfig_option("CONFIG_TCP_WND_DEFAULT", 512000) - esp32.add_idf_sdkconfig_option("CONFIG_TCP_RECVMBOX_SIZE", 512) - esp32.add_idf_sdkconfig_option("CONFIG_TCP_QUEUE_OOSEQ", True) - esp32.add_idf_sdkconfig_option("CONFIG_TCP_OVERSIZE_MSS", True) - esp32.add_idf_sdkconfig_option("CONFIG_LWIP_WND_SCALE", True) - esp32.add_idf_sdkconfig_option("CONFIG_LWIP_TCP_RCV_SCALE", 3) - esp32.add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_RECVMBOX_SIZE", 512) - - # Allocate wifi buffers in PSRAM - esp32.add_idf_sdkconfig_option("CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP", True) - var = await media_player.new_media_player(config) await cg.register_component(var, config) @@ -351,8 +346,8 @@ async def to_code(config): cg.add(var.set_buffer_size(config[CONF_BUFFER_SIZE])) - cg.add(var.set_task_stack_in_psram(config[CONF_TASK_STACK_IN_PSRAM])) - if config[CONF_TASK_STACK_IN_PSRAM]: + if config.get(CONF_TASK_STACK_IN_PSRAM): + cg.add(var.set_task_stack_in_psram(True)) esp32.add_idf_sdkconfig_option( "CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True ) diff --git a/esphome/components/speaker/media_player/audio_pipeline.cpp b/esphome/components/speaker/media_player/audio_pipeline.cpp index 8811ea1644..dc8572ae43 100644 --- a/esphome/components/speaker/media_player/audio_pipeline.cpp +++ b/esphome/components/speaker/media_player/audio_pipeline.cpp @@ -259,13 +259,10 @@ esp_err_t AudioPipeline::allocate_communications_() { esp_err_t AudioPipeline::start_tasks_() { if (this->read_task_handle_ == nullptr) { if (this->read_task_stack_buffer_ == nullptr) { - if (this->task_stack_in_psram_) { - RAMAllocator stack_allocator(RAMAllocator::ALLOC_EXTERNAL); - this->read_task_stack_buffer_ = stack_allocator.allocate(READ_TASK_STACK_SIZE); - } else { - RAMAllocator stack_allocator(RAMAllocator::ALLOC_INTERNAL); - this->read_task_stack_buffer_ = stack_allocator.allocate(READ_TASK_STACK_SIZE); - } + // Reader task uses the AudioReader class which uses esp_http_client. This crashes on IDF 5.4 if the task stack is + // in PSRAM. As a workaround, always allocate the read task in internal memory. + RAMAllocator stack_allocator(RAMAllocator::ALLOC_INTERNAL); + this->read_task_stack_buffer_ = stack_allocator.allocate(READ_TASK_STACK_SIZE); } if (this->read_task_stack_buffer_ == nullptr) { diff --git a/esphome/components/speaker/media_player/automation.h b/esphome/components/speaker/media_player/automation.h index d1a01aabc4..fdf3db07f9 100644 --- a/esphome/components/speaker/media_player/automation.h +++ b/esphome/components/speaker/media_player/automation.h @@ -14,7 +14,7 @@ template class PlayOnDeviceMediaAction : public Action, p TEMPLATABLE_VALUE(audio::AudioFile *, audio_file) TEMPLATABLE_VALUE(bool, announcement) TEMPLATABLE_VALUE(bool, enqueue) - void play(Ts... x) override { + void play(const Ts &...x) override { this->parent_->play_file(this->audio_file_.value(x...), this->announcement_.value(x...), this->enqueue_.value(x...)); } diff --git a/esphome/components/speaker/media_player/speaker_media_player.cpp b/esphome/components/speaker/media_player/speaker_media_player.cpp index 2c30f17c78..b45a78010a 100644 --- a/esphome/components/speaker/media_player/speaker_media_player.cpp +++ b/esphome/components/speaker/media_player/speaker_media_player.cpp @@ -55,7 +55,7 @@ void SpeakerMediaPlayer::setup() { this->media_control_command_queue_ = xQueueCreate(MEDIA_CONTROLS_QUEUE_LENGTH, sizeof(MediaCallCommand)); - this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); + this->pref_ = global_preferences->make_preference(this->get_preference_hash()); VolumeRestoreState volume_restore_state; if (this->pref_.load(&volume_restore_state)) { diff --git a/esphome/components/speed/fan/speed_fan.cpp b/esphome/components/speed/fan/speed_fan.cpp index 57bd795416..801593c2ac 100644 --- a/esphome/components/speed/fan/speed_fan.cpp +++ b/esphome/components/speed/fan/speed_fan.cpp @@ -29,7 +29,7 @@ void SpeedFan::control(const fan::FanCall &call) { this->oscillating = *call.get_oscillating(); if (call.get_direction().has_value()) this->direction = *call.get_direction(); - this->preset_mode = call.get_preset_mode(); + this->set_preset_mode_(call.get_preset_mode()); this->write_state_(); this->publish_state(); diff --git a/esphome/components/speed/fan/speed_fan.h b/esphome/components/speed/fan/speed_fan.h index 6537bce3f6..e9a389e0f3 100644 --- a/esphome/components/speed/fan/speed_fan.h +++ b/esphome/components/speed/fan/speed_fan.h @@ -1,7 +1,5 @@ #pragma once -#include - #include "esphome/core/component.h" #include "esphome/components/output/binary_output.h" #include "esphome/components/output/float_output.h" @@ -18,7 +16,7 @@ class SpeedFan : public Component, public fan::Fan { void set_output(output::FloatOutput *output) { this->output_ = output; } void set_oscillating(output::BinaryOutput *oscillating) { this->oscillating_ = oscillating; } void set_direction(output::BinaryOutput *direction) { this->direction_ = direction; } - void set_preset_modes(const std::set &presets) { this->preset_modes_ = presets; } + void set_preset_modes(std::initializer_list presets) { this->preset_modes_ = presets; } fan::FanTraits get_traits() override { return this->traits_; } protected: @@ -30,7 +28,7 @@ class SpeedFan : public Component, public fan::Fan { output::BinaryOutput *direction_{nullptr}; int speed_count_{}; fan::FanTraits traits_; - std::set preset_modes_{}; + std::vector preset_modes_{}; }; } // namespace speed diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py index a436bc6dab..d803ee66dc 100644 --- a/esphome/components/spi/__init__.py +++ b/esphome/components/spi/__init__.py @@ -35,7 +35,7 @@ from esphome.const import ( PLATFORM_RP2040, PlatformFramework, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority import esphome.final_validate as fv CODEOWNERS = ["@esphome/core", "@clydebarrow"] @@ -276,9 +276,6 @@ def get_spi_interface(index): return ["&SPI", "&SPI1"][index] if index == 0: return "&SPI" - # Following code can't apply to C2, H2 or 8266 since they have only one SPI - if get_target_variant() in (VARIANT_ESP32S3, VARIANT_ESP32S2): - return "new SPIClass(FSPI)" return "new SPIClass(HSPI)" @@ -351,7 +348,7 @@ CONFIG_SCHEMA = cv.All( ) -@coroutine_with_priority(1.0) +@coroutine_with_priority(CoroPriority.BUS) async def to_code(configs): cg.add_define("USE_SPI") cg.add_global(spi_ns.using) diff --git a/esphome/components/split_buffer/__init__.py b/esphome/components/split_buffer/__init__.py new file mode 100644 index 0000000000..be7472936f --- /dev/null +++ b/esphome/components/split_buffer/__init__.py @@ -0,0 +1,5 @@ +CODEOWNERS = ["@jesserockz"] + +# Allows split_buffer to be configured in yaml, to allow use of the C++ api. + +CONFIG_SCHEMA = {} diff --git a/esphome/components/split_buffer/split_buffer.cpp b/esphome/components/split_buffer/split_buffer.cpp new file mode 100644 index 0000000000..526a19c71c --- /dev/null +++ b/esphome/components/split_buffer/split_buffer.cpp @@ -0,0 +1,144 @@ +#include "split_buffer.h" + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome::split_buffer { +static constexpr const char *const TAG = "split_buffer"; + +SplitBuffer::~SplitBuffer() { this->free(); } + +bool SplitBuffer::init(size_t total_length) { + this->free(); // Clean up any existing allocation + + if (total_length == 0) { + return false; + } + + this->total_length_ = total_length; + size_t current_buffer_size = total_length; + + RAMAllocator ptr_allocator; + RAMAllocator allocator; + + // Try to allocate the entire buffer first + while (current_buffer_size > 0) { + // Calculate how many buffers we need of this size + size_t needed_buffers = (total_length + current_buffer_size - 1) / current_buffer_size; + + // Try to allocate array of buffer pointers + uint8_t **temp_buffers = ptr_allocator.allocate(needed_buffers); + if (temp_buffers == nullptr) { + // If we can't even allocate the pointer array, don't need to continue + ESP_LOGE(TAG, "Failed to allocate pointers"); + return false; + } + + // Initialize all pointers to null + for (size_t i = 0; i < needed_buffers; i++) { + temp_buffers[i] = nullptr; + } + + // Try to allocate all the buffers + bool allocation_success = true; + for (size_t i = 0; i < needed_buffers; i++) { + size_t this_buffer_size = current_buffer_size; + // Last buffer might be smaller if total_length is not divisible by current_buffer_size + if (i == needed_buffers - 1 && total_length % current_buffer_size != 0) { + this_buffer_size = total_length % current_buffer_size; + } + + temp_buffers[i] = allocator.allocate(this_buffer_size); + if (temp_buffers[i] == nullptr) { + allocation_success = false; + break; + } + + // Initialize buffer to zero + memset(temp_buffers[i], 0, this_buffer_size); + } + + if (allocation_success) { + // Success! Store the result + this->buffers_ = temp_buffers; + this->buffer_count_ = needed_buffers; + this->buffer_size_ = current_buffer_size; + ESP_LOGD(TAG, "Allocated %zu * %zu bytes - %zu bytes", this->buffer_count_, this->buffer_size_, + this->total_length_); + return true; + } + + // Allocation failed, clean up and try smaller buffers + for (size_t i = 0; i < needed_buffers; i++) { + if (temp_buffers[i] != nullptr) { + allocator.deallocate(temp_buffers[i], 0); + } + } + ptr_allocator.deallocate(temp_buffers, 0); + + // Halve the buffer size and try again + current_buffer_size = current_buffer_size / 2; + } + + ESP_LOGE(TAG, "Failed to allocate %zu bytes", total_length); + return false; +} + +void SplitBuffer::free() { + if (this->buffers_ != nullptr) { + RAMAllocator allocator; + for (size_t i = 0; i < this->buffer_count_; i++) { + if (this->buffers_[i] != nullptr) { + allocator.deallocate(this->buffers_[i], 0); + } + } + RAMAllocator ptr_allocator; + ptr_allocator.deallocate(this->buffers_, 0); + this->buffers_ = nullptr; + } + this->buffer_count_ = 0; + this->buffer_size_ = 0; + this->total_length_ = 0; +} + +const uint8_t &SplitBuffer::operator[](size_t index) const { + if (index >= this->total_length_) { + ESP_LOGE(TAG, "Out of bounds - %zu >= %zu", index, this->total_length_); + // Return reference to a static dummy byte since we can't throw exceptions. + // the byte is non-const since it will also be used by the non-const [] overload. + static uint8_t dummy = 0; + return dummy; + } + + const auto buffer_index = index / this->buffer_size_; + const auto offset_in_buffer = index % this->buffer_size_; + + return this->buffers_[buffer_index][offset_in_buffer]; +} + +// non-const version of operator[] for write access +uint8_t &SplitBuffer::operator[](size_t index) { + // avoid code duplication. These casts are safe since we know the object is not const. + return const_cast(static_cast(this)->operator[](index)); +} + +/** + * Fill the entire buffer with a single byte value + * @param value Fill value + */ +void SplitBuffer::fill(uint8_t value) const { + if (this->buffer_count_ == 0) + return; + // clear all the full sized buffers + size_t i = 0; + for (; i != this->buffer_count_ - 1; i++) { + memset(this->buffers_[i], value, this->buffer_size_); + } + // clear the last, potentially short, buffer. + // `i` is guaranteed to equal the last index since the loop terminates at that value. + // where all buffers are the same size, the modulus must return the size, not 0. + auto size_last = ((this->total_length_ - 1) % this->buffer_size_) + 1; + memset(this->buffers_[i], value, size_last); +} + +} // namespace esphome::split_buffer diff --git a/esphome/components/split_buffer/split_buffer.h b/esphome/components/split_buffer/split_buffer.h new file mode 100644 index 0000000000..b615ddce74 --- /dev/null +++ b/esphome/components/split_buffer/split_buffer.h @@ -0,0 +1,46 @@ +#pragma once + +#include +#include + +namespace esphome::split_buffer { +/** + * A SplitBuffer allocates a large memory buffer potentially as multiple smaller buffers + * to facilitate allocation of large buffers on devices with fragmented memory spaces. + * Each sub-buffer is the same size, except for the last one which may be smaller. + * Standard array indexing using `[]` is possible on the buffer, but, since the buffer may not be contiguous in memory, + * there is no easy way to access the buffer as a single array, i.e. no `.data()` access like a vector. + */ +class SplitBuffer { + public: + SplitBuffer() = default; + ~SplitBuffer(); + + // Initialize the buffer with the desired total length + bool init(size_t total_length); + + // Free all allocated buffers + void free(); + + // Access operators + uint8_t &operator[](size_t index); + const uint8_t &operator[](size_t index) const; + void fill(uint8_t value) const; + + // Get the total length + size_t size() const { return this->total_length_; } + + // Get buffer information + size_t get_buffer_count() const { return this->buffer_count_; } + + // Check if successfully initialized + bool is_valid() const { return this->buffers_ != nullptr && this->buffer_count_ > 0; } + + private: + uint8_t **buffers_{nullptr}; + size_t buffer_count_{0}; + size_t buffer_size_{0}; + size_t total_length_{0}; +}; + +} // namespace esphome::split_buffer diff --git a/esphome/components/sprinkler/automation.h b/esphome/components/sprinkler/automation.h index 59c6cd50e1..d6c877ae90 100644 --- a/esphome/components/sprinkler/automation.h +++ b/esphome/components/sprinkler/automation.h @@ -13,7 +13,7 @@ template class SetDividerAction : public Action { TEMPLATABLE_VALUE(uint32_t, divider) - void play(Ts... x) override { this->sprinkler_->set_divider(this->divider_.optional_value(x...)); } + void play(const Ts &...x) override { this->sprinkler_->set_divider(this->divider_.optional_value(x...)); } protected: Sprinkler *sprinkler_; @@ -25,7 +25,7 @@ template class SetMultiplierAction : public Action { TEMPLATABLE_VALUE(float, multiplier) - void play(Ts... x) override { this->sprinkler_->set_multiplier(this->multiplier_.optional_value(x...)); } + void play(const Ts &...x) override { this->sprinkler_->set_multiplier(this->multiplier_.optional_value(x...)); } protected: Sprinkler *sprinkler_; @@ -38,7 +38,7 @@ template class QueueValveAction : public Action { TEMPLATABLE_VALUE(size_t, valve_number) TEMPLATABLE_VALUE(uint32_t, valve_run_duration) - void play(Ts... x) override { + void play(const Ts &...x) override { this->sprinkler_->queue_valve(this->valve_number_.optional_value(x...), this->valve_run_duration_.optional_value(x...)); } @@ -51,7 +51,7 @@ template class ClearQueuedValvesAction : public Action { public: explicit ClearQueuedValvesAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} - void play(Ts... x) override { this->sprinkler_->clear_queued_valves(); } + void play(const Ts &...x) override { this->sprinkler_->clear_queued_valves(); } protected: Sprinkler *sprinkler_; @@ -63,7 +63,7 @@ template class SetRepeatAction : public Action { TEMPLATABLE_VALUE(uint32_t, repeat) - void play(Ts... x) override { this->sprinkler_->set_repeat(this->repeat_.optional_value(x...)); } + void play(const Ts &...x) override { this->sprinkler_->set_repeat(this->repeat_.optional_value(x...)); } protected: Sprinkler *sprinkler_; @@ -76,7 +76,7 @@ template class SetRunDurationAction : public Action { TEMPLATABLE_VALUE(size_t, valve_number) TEMPLATABLE_VALUE(uint32_t, valve_run_duration) - void play(Ts... x) override { + void play(const Ts &...x) override { this->sprinkler_->set_valve_run_duration(this->valve_number_.optional_value(x...), this->valve_run_duration_.optional_value(x...)); } @@ -89,7 +89,7 @@ template class StartFromQueueAction : public Action { public: explicit StartFromQueueAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} - void play(Ts... x) override { this->sprinkler_->start_from_queue(); } + void play(const Ts &...x) override { this->sprinkler_->start_from_queue(); } protected: Sprinkler *sprinkler_; @@ -99,7 +99,7 @@ template class StartFullCycleAction : public Action { public: explicit StartFullCycleAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} - void play(Ts... x) override { this->sprinkler_->start_full_cycle(); } + void play(const Ts &...x) override { this->sprinkler_->start_full_cycle(); } protected: Sprinkler *sprinkler_; @@ -112,7 +112,7 @@ template class StartSingleValveAction : public Action { TEMPLATABLE_VALUE(size_t, valve_to_start) TEMPLATABLE_VALUE(uint32_t, valve_run_duration) - void play(Ts... x) override { + void play(const Ts &...x) override { this->sprinkler_->start_single_valve(this->valve_to_start_.optional_value(x...), this->valve_run_duration_.optional_value(x...)); } @@ -125,7 +125,7 @@ template class ShutdownAction : public Action { public: explicit ShutdownAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} - void play(Ts... x) override { this->sprinkler_->shutdown(); } + void play(const Ts &...x) override { this->sprinkler_->shutdown(); } protected: Sprinkler *sprinkler_; @@ -135,7 +135,7 @@ template class NextValveAction : public Action { public: explicit NextValveAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} - void play(Ts... x) override { this->sprinkler_->next_valve(); } + void play(const Ts &...x) override { this->sprinkler_->next_valve(); } protected: Sprinkler *sprinkler_; @@ -145,7 +145,7 @@ template class PreviousValveAction : public Action { public: explicit PreviousValveAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} - void play(Ts... x) override { this->sprinkler_->previous_valve(); } + void play(const Ts &...x) override { this->sprinkler_->previous_valve(); } protected: Sprinkler *sprinkler_; @@ -155,7 +155,7 @@ template class PauseAction : public Action { public: explicit PauseAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} - void play(Ts... x) override { this->sprinkler_->pause(); } + void play(const Ts &...x) override { this->sprinkler_->pause(); } protected: Sprinkler *sprinkler_; @@ -165,7 +165,7 @@ template class ResumeAction : public Action { public: explicit ResumeAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} - void play(Ts... x) override { this->sprinkler_->resume(); } + void play(const Ts &...x) override { this->sprinkler_->resume(); } protected: Sprinkler *sprinkler_; @@ -175,7 +175,7 @@ template class ResumeOrStartAction : public Action { public: explicit ResumeOrStartAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} - void play(Ts... x) override { this->sprinkler_->resume_or_start_full_cycle(); } + void play(const Ts &...x) override { this->sprinkler_->resume_or_start_full_cycle(); } protected: Sprinkler *sprinkler_; diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp index e191498857..8edb240a41 100644 --- a/esphome/components/sprinkler/sprinkler.cpp +++ b/esphome/components/sprinkler/sprinkler.cpp @@ -81,7 +81,7 @@ void SprinklerControllerNumber::setup() { if (!this->restore_value_) { value = this->initial_value_; } else { - this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); + this->pref_ = global_preferences->make_preference(this->get_preference_hash()); if (!this->pref_.load(&value)) { if (!std::isnan(this->initial_value_)) { value = this->initial_value_; @@ -650,7 +650,7 @@ void Sprinkler::set_valve_run_duration(const optional valve_number, cons return; } auto call = this->valve_[valve_number.value()].run_duration_number->make_call(); - if (this->valve_[valve_number.value()].run_duration_number->traits.get_unit_of_measurement() == MIN_STR) { + if (this->valve_[valve_number.value()].run_duration_number->traits.get_unit_of_measurement_ref() == MIN_STR) { call.set_value(run_duration.value() / 60.0); } else { call.set_value(run_duration.value()); @@ -732,7 +732,7 @@ uint32_t Sprinkler::valve_run_duration(const size_t valve_number) { return 0; } if (this->valve_[valve_number].run_duration_number != nullptr) { - if (this->valve_[valve_number].run_duration_number->traits.get_unit_of_measurement() == MIN_STR) { + if (this->valve_[valve_number].run_duration_number->traits.get_unit_of_measurement_ref() == MIN_STR) { return static_cast(roundf(this->valve_[valve_number].run_duration_number->state * 60)); } else { return static_cast(roundf(this->valve_[valve_number].run_duration_number->state)); diff --git a/esphome/components/sps30/automation.h b/esphome/components/sps30/automation.h index 443aafb575..67af813687 100644 --- a/esphome/components/sps30/automation.h +++ b/esphome/components/sps30/automation.h @@ -11,7 +11,7 @@ template class StartFanAction : public Action { public: explicit StartFanAction(SPS30Component *sps30) : sps30_(sps30) {} - void play(Ts... x) override { this->sps30_->start_fan_cleaning(); } + void play(const Ts &...x) override { this->sps30_->start_fan_cleaning(); } protected: SPS30Component *sps30_; diff --git a/esphome/components/sps30/sps30.cpp b/esphome/components/sps30/sps30.cpp index 272acc78f2..21a782e49a 100644 --- a/esphome/components/sps30/sps30.cpp +++ b/esphome/components/sps30/sps30.cpp @@ -43,31 +43,33 @@ void SPS30Component::setup() { this->serial_number_[i * 2] = static_cast(raw_serial_number[i] >> 8); this->serial_number_[i * 2 + 1] = uint16_t(uint16_t(raw_serial_number[i] & 0xFF)); } - ESP_LOGD(TAG, " Serial Number: '%s'", this->serial_number_); + ESP_LOGV(TAG, " Serial number: %s", this->serial_number_); bool result; if (this->fan_interval_.has_value()) { // override default value - result = write_command(SPS30_CMD_SET_AUTOMATIC_CLEANING_INTERVAL_SECONDS, this->fan_interval_.value()); + result = this->write_command(SPS30_CMD_SET_AUTOMATIC_CLEANING_INTERVAL_SECONDS, this->fan_interval_.value()); } else { - result = write_command(SPS30_CMD_SET_AUTOMATIC_CLEANING_INTERVAL_SECONDS); - } - if (result) { - delay(20); - uint16_t secs[2]; - if (this->read_data(secs, 2)) { - fan_interval_ = secs[0] << 16 | secs[1]; - } + result = this->write_command(SPS30_CMD_SET_AUTOMATIC_CLEANING_INTERVAL_SECONDS); } - this->status_clear_warning(); - this->skipped_data_read_cycles_ = 0; - this->start_continuous_measurement_(); + this->set_timeout(20, [this, result]() { + if (result) { + uint16_t secs[2]; + if (this->read_data(secs, 2)) { + this->fan_interval_ = secs[0] << 16 | secs[1]; + } + } + this->status_clear_warning(); + this->skipped_data_read_cycles_ = 0; + this->start_continuous_measurement_(); + this->setup_complete_ = true; + }); }); } void SPS30Component::dump_config() { - ESP_LOGCONFIG(TAG, "sps30:"); + ESP_LOGCONFIG(TAG, "SPS30:"); LOG_I2C_DEVICE(this); if (this->is_failed()) { switch (this->error_code_) { @@ -78,16 +80,16 @@ void SPS30Component::dump_config() { ESP_LOGW(TAG, "Measurement Initialization failed"); break; case SERIAL_NUMBER_REQUEST_FAILED: - ESP_LOGW(TAG, "Unable to request sensor serial number"); + ESP_LOGW(TAG, "Unable to request serial number"); break; case SERIAL_NUMBER_READ_FAILED: - ESP_LOGW(TAG, "Unable to read sensor serial number"); + ESP_LOGW(TAG, "Unable to read serial number"); break; case FIRMWARE_VERSION_REQUEST_FAILED: - ESP_LOGW(TAG, "Unable to request sensor firmware version"); + ESP_LOGW(TAG, "Unable to request firmware version"); break; case FIRMWARE_VERSION_READ_FAILED: - ESP_LOGW(TAG, "Unable to read sensor firmware version"); + ESP_LOGW(TAG, "Unable to read firmware version"); break; default: ESP_LOGW(TAG, "Unknown setup error"); @@ -96,9 +98,9 @@ void SPS30Component::dump_config() { } LOG_UPDATE_INTERVAL(this); ESP_LOGCONFIG(TAG, - " Serial Number: '%s'\n" + " Serial number: %s\n" " Firmware version v%0d.%0d", - this->serial_number_, (raw_firmware_version_ >> 8), uint16_t(raw_firmware_version_ & 0xFF)); + this->serial_number_, this->raw_firmware_version_ >> 8, this->raw_firmware_version_ & 0xFF); LOG_SENSOR(" ", "PM1.0 Weight Concentration", this->pm_1_0_sensor_); LOG_SENSOR(" ", "PM2.5 Weight Concentration", this->pm_2_5_sensor_); LOG_SENSOR(" ", "PM4 Weight Concentration", this->pm_4_0_sensor_); @@ -111,17 +113,19 @@ void SPS30Component::dump_config() { } void SPS30Component::update() { + if (!this->setup_complete_) + return; /// Check if warning flag active (sensor reconnected?) if (this->status_has_warning()) { - ESP_LOGD(TAG, "Trying to reconnect"); + ESP_LOGD(TAG, "Reconnecting"); if (this->write_command(SPS30_CMD_SOFT_RESET)) { - ESP_LOGD(TAG, "Soft-reset successful. Waiting for reconnection in 500 ms"); + ESP_LOGD(TAG, "Soft-reset successful; waiting 500 ms"); this->set_timeout(500, [this]() { this->start_continuous_measurement_(); /// Sensor restarted and reading attempt made next cycle this->status_clear_warning(); this->skipped_data_read_cycles_ = 0; - ESP_LOGD(TAG, "Reconnect successful. Resuming continuous measurement"); + ESP_LOGD(TAG, "Reconnected; resuming continuous measurement"); }); } else { ESP_LOGD(TAG, "Soft-reset failed"); @@ -136,12 +140,12 @@ void SPS30Component::update() { uint16_t raw_read_status; if (!this->read_data(&raw_read_status, 1) || raw_read_status == 0x00) { - ESP_LOGD(TAG, "Not ready yet"); + ESP_LOGD(TAG, "Not ready"); this->skipped_data_read_cycles_++; /// The following logic is required to address the cases when a sensor is quickly replaced before it's marked /// as failed so that new sensor is eventually forced to be reinitialized for continuous measurement. if (this->skipped_data_read_cycles_ > MAX_SKIPPED_DATA_CYCLES_BEFORE_ERROR) { - ESP_LOGD(TAG, "Exceeded max allowed attempts; communication will be reinitialized"); + ESP_LOGD(TAG, "Exceeded max attempts; will reinitialize"); this->status_set_warning(); } return; @@ -211,11 +215,6 @@ void SPS30Component::update() { } bool SPS30Component::start_continuous_measurement_() { - uint8_t data[4]; - data[0] = SPS30_CMD_START_CONTINUOUS_MEASUREMENTS & 0xFF; - data[1] = 0x03; - data[2] = 0x00; - data[3] = sht_crc_(0x03, 0x00); if (!this->write_command(SPS30_CMD_START_CONTINUOUS_MEASUREMENTS, SPS30_CMD_START_CONTINUOUS_MEASUREMENTS_ARG)) { ESP_LOGE(TAG, "Error initiating measurements"); return false; @@ -224,9 +223,9 @@ bool SPS30Component::start_continuous_measurement_() { } bool SPS30Component::start_fan_cleaning() { - if (!write_command(SPS30_CMD_START_FAN_CLEANING)) { + if (!this->write_command(SPS30_CMD_START_FAN_CLEANING)) { this->status_set_warning(); - ESP_LOGE(TAG, "write error start fan (%d)", this->last_error_); + ESP_LOGE(TAG, "Start fan cleaning failed (%d)", this->last_error_); return false; } else { ESP_LOGD(TAG, "Fan auto clean started"); diff --git a/esphome/components/sps30/sps30.h b/esphome/components/sps30/sps30.h index 04189247e8..18847e16d9 100644 --- a/esphome/components/sps30/sps30.h +++ b/esphome/components/sps30/sps30.h @@ -30,12 +30,14 @@ class SPS30Component : public PollingComponent, public sensirion_common::Sensiri bool start_fan_cleaning(); protected: - char serial_number_[17] = {0}; /// Terminating NULL character + bool setup_complete_{false}; uint16_t raw_firmware_version_; - bool start_continuous_measurement_(); + char serial_number_[17] = {0}; /// Terminating NULL character uint8_t skipped_data_read_cycles_ = 0; - enum ErrorCode { + bool start_continuous_measurement_(); + + enum ErrorCode : uint8_t { COMMUNICATION_FAILED, FIRMWARE_VERSION_REQUEST_FAILED, FIRMWARE_VERSION_READ_FAILED, diff --git a/esphome/components/st7567_i2c/st7567_i2c.cpp b/esphome/components/st7567_i2c/st7567_i2c.cpp index 4970367343..14c21d5148 100644 --- a/esphome/components/st7567_i2c/st7567_i2c.cpp +++ b/esphome/components/st7567_i2c/st7567_i2c.cpp @@ -50,9 +50,10 @@ void HOT I2CST7567::write_display_data() { static const size_t BLOCK_SIZE = 64; for (uint8_t x = 0; x < (uint8_t) this->get_width_internal(); x += BLOCK_SIZE) { + size_t remaining = static_cast(this->get_width_internal()) - x; + size_t chunk = remaining > BLOCK_SIZE ? BLOCK_SIZE : remaining; this->write_register(esphome::st7567_base::ST7567_SET_START_LINE, &buffer_[y * this->get_width_internal() + x], - this->get_width_internal() - x > BLOCK_SIZE ? BLOCK_SIZE : this->get_width_internal() - x, - true); + chunk); } } } diff --git a/esphome/components/st7789v/st7789v.cpp b/esphome/components/st7789v/st7789v.cpp index 44f2293ac4..ade9c1126f 100644 --- a/esphome/components/st7789v/st7789v.cpp +++ b/esphome/components/st7789v/st7789v.cpp @@ -176,8 +176,9 @@ void ST7789V::write_display_data() { if (this->eightbitcolor_) { uint8_t temp_buffer[TEMP_BUFFER_SIZE]; size_t temp_index = 0; - for (int line = 0; line < this->get_buffer_length_(); line = line + this->get_width_internal()) { - for (int index = 0; index < this->get_width_internal(); ++index) { + size_t width = static_cast(this->get_width_internal()); + for (size_t line = 0; line < this->get_buffer_length_(); line += width) { + for (size_t index = 0; index < width; ++index) { auto color = display::ColorUtil::color_to_565( display::ColorUtil::to_color(this->buffer_[index + line], display::ColorOrder::COLOR_ORDER_RGB, display::ColorBitness::COLOR_BITNESS_332, true)); diff --git a/esphome/components/st7920/st7920.h b/esphome/components/st7920/st7920.h index c9fdad454d..c48fe8cc1c 100644 --- a/esphome/components/st7920/st7920.h +++ b/esphome/components/st7920/st7920.h @@ -9,7 +9,7 @@ namespace st7920 { class ST7920; -using st7920_writer_t = std::function; +using st7920_writer_t = display::DisplayWriter; class ST7920 : public display::DisplayBuffer, public spi::SPIDevice writer_local_{}; + st7920_writer_t writer_local_{}; }; } // namespace st7920 diff --git a/esphome/components/statsd/statsd.cpp b/esphome/components/statsd/statsd.cpp index 05f71c7b24..7729f36858 100644 --- a/esphome/components/statsd/statsd.cpp +++ b/esphome/components/statsd/statsd.cpp @@ -151,7 +151,7 @@ void StatsdComponent::send_(std::string *out) { int n_bytes = this->sock_->sendto(out->c_str(), out->length(), 0, reinterpret_cast(&this->destination_), sizeof(this->destination_)); - if (n_bytes != out->length()) { + if (n_bytes != static_cast(out->length())) { ESP_LOGE(TAG, "Failed to send UDP packed (%d of %d)", n_bytes, out->length()); } #endif diff --git a/esphome/components/statsd/statsd.h b/esphome/components/statsd/statsd.h index 34f84cbe00..eab77a7a6e 100644 --- a/esphome/components/statsd/statsd.h +++ b/esphome/components/statsd/statsd.h @@ -28,21 +28,6 @@ namespace esphome { namespace statsd { -using sensor_type_t = enum { TYPE_SENSOR, TYPE_BINARY_SENSOR }; - -using sensors_t = struct { - const char *name; - sensor_type_t type; - union { -#ifdef USE_SENSOR - esphome::sensor::Sensor *sensor; -#endif -#ifdef USE_BINARY_SENSOR - esphome::binary_sensor::BinarySensor *binary_sensor; -#endif - }; -}; - class StatsdComponent : public PollingComponent { public: ~StatsdComponent(); @@ -71,6 +56,20 @@ class StatsdComponent : public PollingComponent { const char *prefix_; uint16_t port_; + using sensor_type_t = enum { TYPE_SENSOR, TYPE_BINARY_SENSOR }; + using sensors_t = struct { + const char *name; + sensor_type_t type; + union { +#ifdef USE_SENSOR + esphome::sensor::Sensor *sensor; +#endif +#ifdef USE_BINARY_SENSOR + esphome::binary_sensor::BinarySensor *binary_sensor; +#endif + }; + }; + std::vector sensors_; #ifdef USE_ESP8266 diff --git a/esphome/components/status_led/__init__.py b/esphome/components/status_led/__init__.py index b299ae7ff7..b0fce37126 100644 --- a/esphome/components/status_led/__init__.py +++ b/esphome/components/status_led/__init__.py @@ -2,7 +2,7 @@ from esphome import pins import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_PIN -from esphome.core import coroutine_with_priority +from esphome.core import CoroPriority, coroutine_with_priority status_led_ns = cg.esphome_ns.namespace("status_led") StatusLED = status_led_ns.class_("StatusLED", cg.Component) @@ -15,7 +15,7 @@ CONFIG_SCHEMA = cv.Schema( ).extend(cv.COMPONENT_SCHEMA) -@coroutine_with_priority(80.0) +@coroutine_with_priority(CoroPriority.STATUS) async def to_code(config): pin = await cg.gpio_pin_expression(config[CONF_PIN]) rhs = StatusLED.new(pin) diff --git a/esphome/components/stepper/__init__.py b/esphome/components/stepper/__init__.py index c234388e7e..62bc71f2d1 100644 --- a/esphome/components/stepper/__init__.py +++ b/esphome/components/stepper/__init__.py @@ -10,7 +10,7 @@ from esphome.const import ( CONF_SPEED, CONF_TARGET, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority IS_PLATFORM_COMPONENT = True @@ -178,6 +178,6 @@ async def stepper_set_deceleration_to_code(config, action_id, template_arg, args return var -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(stepper_ns.using) diff --git a/esphome/components/stepper/stepper.h b/esphome/components/stepper/stepper.h index 1cf4830b1f..2bad672494 100644 --- a/esphome/components/stepper/stepper.h +++ b/esphome/components/stepper/stepper.h @@ -44,7 +44,7 @@ template class SetTargetAction : public Action { TEMPLATABLE_VALUE(int32_t, target) - void play(Ts... x) override { this->parent_->set_target(this->target_.value(x...)); } + void play(const Ts &...x) override { this->parent_->set_target(this->target_.value(x...)); } protected: Stepper *parent_; @@ -56,7 +56,7 @@ template class ReportPositionAction : public Action { TEMPLATABLE_VALUE(int32_t, position) - void play(Ts... x) override { this->parent_->report_position(this->position_.value(x...)); } + void play(const Ts &...x) override { this->parent_->report_position(this->position_.value(x...)); } protected: Stepper *parent_; @@ -68,7 +68,7 @@ template class SetSpeedAction : public Action { TEMPLATABLE_VALUE(float, speed); - void play(Ts... x) override { + void play(const Ts &...x) override { float speed = this->speed_.value(x...); this->parent_->set_max_speed(speed); this->parent_->on_update_speed(); @@ -84,7 +84,7 @@ template class SetAccelerationAction : public Action { TEMPLATABLE_VALUE(float, acceleration); - void play(Ts... x) override { + void play(const Ts &...x) override { float acceleration = this->acceleration_.value(x...); this->parent_->set_acceleration(acceleration); } @@ -99,7 +99,7 @@ template class SetDecelerationAction : public Action { TEMPLATABLE_VALUE(float, deceleration); - void play(Ts... x) override { + void play(const Ts &...x) override { float deceleration = this->deceleration_.value(x...); this->parent_->set_deceleration(deceleration); } diff --git a/esphome/components/stts22h/__init__.py b/esphome/components/stts22h/__init__.py new file mode 100644 index 0000000000..a33c0b554b --- /dev/null +++ b/esphome/components/stts22h/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@B48D81EFCC"] diff --git a/esphome/components/stts22h/sensor.py b/esphome/components/stts22h/sensor.py new file mode 100644 index 0000000000..094c233361 --- /dev/null +++ b/esphome/components/stts22h/sensor.py @@ -0,0 +1,33 @@ +import esphome.codegen as cg +from esphome.components import i2c, sensor +import esphome.config_validation as cv +from esphome.const import ( + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, +) + +DEPENDENCIES = ["i2c"] + +sensor_ns = cg.esphome_ns.namespace("stts22h") +stts22h = sensor_ns.class_( + "STTS22HComponent", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + stts22h, + accuracy_decimals=2, + unit_of_measurement=UNIT_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x3C)) +) + + +async def to_code(config): + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/stts22h/stts22h.cpp b/esphome/components/stts22h/stts22h.cpp new file mode 100644 index 0000000000..2b2559c843 --- /dev/null +++ b/esphome/components/stts22h/stts22h.cpp @@ -0,0 +1,101 @@ +#include "esphome/core/log.h" +#include "stts22h.h" + +namespace esphome::stts22h { + +static const char *const TAG = "stts22h"; + +static const uint8_t WHOAMI_REG = 0x01; +static const uint8_t CTRL_REG = 0x04; +static const uint8_t TEMPERATURE_REG = 0x06; + +// CTRL_REG flags +static const uint8_t LOW_ODR_CTRL_ENABLE_FLAG = 0x80; // Flag to enable low ODR mode in CTRL_REG +static const uint8_t FREERUN_CTRL_ENABLE_FLAG = 0x04; // Flag to enable FREERUN mode in CTRL_REG +static const uint8_t ADD_INC_ENABLE_FLAG = 0x08; // Flag to enable ADD_INC (IF_ADD_INC) mode in CTRL_REG + +static const uint8_t WHOAMI_STTS22H_IDENTIFICATION = 0xA0; // ID value of STTS22H in WHOAMI_REG + +static const float SENSOR_SCALE = 0.01f; // Sensor resolution in degrees Celsius + +void STTS22HComponent::setup() { + // Check if device is a STTS22H + if (!this->is_stts22h_sensor_()) { + this->mark_failed(LOG_STR("Device is not a STTS22H sensor")); + return; + } + + this->initialize_sensor_(); +} + +void STTS22HComponent::update() { + if (this->is_failed()) { + return; + } + + this->publish_state(this->read_temperature_()); +} + +void STTS22HComponent::dump_config() { + LOG_SENSOR("", "STTS22H", this); + LOG_I2C_DEVICE(this); + LOG_UPDATE_INTERVAL(this); + if (this->is_failed()) { + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); + } +} + +float STTS22HComponent::read_temperature_() { + uint8_t temp_reg_value[2]; + if (this->read_register(TEMPERATURE_REG, temp_reg_value, 2) != i2c::NO_ERROR) { + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); + return NAN; + } + + // Combine the two bytes into a single 16-bit signed integer + // The STTS22H temperature data is in two's complement format + int16_t temp_raw_value = static_cast(encode_uint16(temp_reg_value[1], temp_reg_value[0])); + return temp_raw_value * SENSOR_SCALE; // Apply sensor resolution +} + +bool STTS22HComponent::is_stts22h_sensor_() { + uint8_t whoami_value; + if (this->read_register(WHOAMI_REG, &whoami_value, 1) != i2c::NO_ERROR) { + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); + return false; + } + + if (whoami_value != WHOAMI_STTS22H_IDENTIFICATION) { + this->mark_failed(LOG_STR("Unexpected WHOAMI identifier. Sensor is not a STTS22H")); + return false; + } + + return true; +} + +void STTS22HComponent::initialize_sensor_() { + // Read current CTRL_REG configuration + uint8_t ctrl_value; + if (this->read_register(CTRL_REG, &ctrl_value, 1) != i2c::NO_ERROR) { + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); + return; + } + + // Enable low ODR mode and enable ADD_INC + // Before low ODR mode can be used, + // FREERUN bit must be cleared (see sensor documentation) + ctrl_value &= ~FREERUN_CTRL_ENABLE_FLAG; // Clear FREERUN bit + if (this->write_register(CTRL_REG, &ctrl_value, 1) != i2c::NO_ERROR) { + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); + return; + } + + // Enable LOW ODR mode and ADD_INC + ctrl_value |= LOW_ODR_CTRL_ENABLE_FLAG | ADD_INC_ENABLE_FLAG; // Set LOW ODR bit and ADD_INC bit + if (this->write_register(CTRL_REG, &ctrl_value, 1) != i2c::NO_ERROR) { + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); + return; + } +} + +} // namespace esphome::stts22h diff --git a/esphome/components/stts22h/stts22h.h b/esphome/components/stts22h/stts22h.h new file mode 100644 index 0000000000..442a263e49 --- /dev/null +++ b/esphome/components/stts22h/stts22h.h @@ -0,0 +1,21 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome::stts22h { + +class STTS22HComponent : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; + void update() override; + void dump_config() override; + + protected: + void initialize_sensor_(); + bool is_stts22h_sensor_(); + float read_temperature_(); +}; + +} // namespace esphome::stts22h diff --git a/esphome/components/substitutions/__init__.py b/esphome/components/substitutions/__init__.py index a96f56a045..7e15f714f7 100644 --- a/esphome/components/substitutions/__init__.py +++ b/esphome/components/substitutions/__init__.py @@ -1,12 +1,14 @@ import logging +from re import Match +from typing import Any from esphome import core -from esphome.config_helpers import Extend, Remove, merge_config +from esphome.config_helpers import Extend, Remove, merge_config, merge_dicts_ordered import esphome.config_validation as cv from esphome.const import CONF_SUBSTITUTIONS, VALID_SUBSTITUTIONS_CHARACTERS -from esphome.yaml_util import ESPHomeDataBase, make_data_base +from esphome.yaml_util import ESPHomeDataBase, ESPLiteralValue, make_data_base -from .jinja import Jinja, JinjaStr, TemplateError, TemplateRuntimeError, has_jinja +from .jinja import Jinja, JinjaError, JinjaStr, has_jinja CODEOWNERS = ["@esphome/core"] _LOGGER = logging.getLogger(__name__) @@ -39,7 +41,34 @@ async def to_code(config): pass -def _expand_jinja(value, orig_value, path, jinja, ignore_missing): +def _restore_data_base(value: Any, orig_value: ESPHomeDataBase) -> ESPHomeDataBase: + """This function restores ESPHomeDataBase metadata held by the original string. + This is needed because during jinja evaluation, strings can be replaced by other types, + but we want to keep the original metadata for error reporting and source mapping. + For example, if a substitution replaces a string with a dictionary, we want that items + in the dictionary to still point to the original document location + """ + if isinstance(value, ESPHomeDataBase): + return value + if isinstance(value, dict): + return { + _restore_data_base(k, orig_value): _restore_data_base(v, orig_value) + for k, v in value.items() + } + if isinstance(value, list): + return [_restore_data_base(v, orig_value) for v in value] + if isinstance(value, str): + return make_data_base(value, orig_value) + return value + + +def _expand_jinja( + value: str | JinjaStr, + orig_value: str | JinjaStr, + path, + jinja: Jinja, + ignore_missing: bool, +) -> Any: if has_jinja(value): # If the original value passed in to this function is a JinjaStr, it means it contains an unresolved # Jinja expression from a previous pass. @@ -57,23 +86,25 @@ def _expand_jinja(value, orig_value, path, jinja, ignore_missing): "->".join(str(x) for x in path), err.message, ) - except ( - TemplateError, - TemplateRuntimeError, - RuntimeError, - ArithmeticError, - AttributeError, - TypeError, - ) as err: + except JinjaError as err: raise cv.Invalid( - f"{type(err).__name__} Error evaluating jinja expression '{value}': {str(err)}." - f" See {'->'.join(str(x) for x in path)}", + f"{err.error_name()} Error evaluating jinja expression '{value}': {str(err.parent())}." + f"\nEvaluation stack: (most recent evaluation last)\n{err.stack_trace_str()}" + f"\nRelevant context:\n{err.context_trace_str()}" + f"\nSee {'->'.join(str(x) for x in path)}", path, ) + # If the original, unexpanded string, contained document metadata (ESPHomeDatabase), + # assign this same document metadata to the resulting value. + if isinstance(orig_value, ESPHomeDataBase): + value = _restore_data_base(value, orig_value) + return value -def _expand_substitutions(substitutions, value, path, jinja, ignore_missing): +def _expand_substitutions( + substitutions: dict, value: str, path, jinja: Jinja, ignore_missing: bool +) -> Any: if "$" not in value: return value @@ -81,14 +112,14 @@ def _expand_substitutions(substitutions, value, path, jinja, ignore_missing): i = 0 while True: - m = cv.VARIABLE_PROG.search(value, i) + m: Match[str] = cv.VARIABLE_PROG.search(value, i) if not m: # No more variable substitutions found. See if the remainder looks like a jinja template value = _expand_jinja(value, orig_value, path, jinja, ignore_missing) break i, j = m.span(0) - name = m.group(1) + name: str = m.group(1) if name.startswith("{") and name.endswith("}"): name = name[1:-1] if name not in substitutions: @@ -103,7 +134,7 @@ def _expand_substitutions(substitutions, value, path, jinja, ignore_missing): i = j continue - sub = substitutions[name] + sub: Any = substitutions[name] if i == 0 and j == len(value): # The variable spans the whole expression, e.g., "${varName}". Return its resolved value directly @@ -126,7 +157,15 @@ def _expand_substitutions(substitutions, value, path, jinja, ignore_missing): return value -def _substitute_item(substitutions, item, path, jinja, ignore_missing): +def _substitute_item( + substitutions: dict, + item: Any, + path: list[int | str], + jinja: Jinja, + ignore_missing: bool, +) -> Any | None: + if isinstance(item, ESPLiteralValue): + return None # do not substitute inside literal blocks if isinstance(item, list): for i, it in enumerate(item): sub = _substitute_item(substitutions, it, path + [i], jinja, ignore_missing) @@ -163,15 +202,17 @@ def _substitute_item(substitutions, item, path, jinja, ignore_missing): return None -def do_substitution_pass(config, command_line_substitutions, ignore_missing=False): +def do_substitution_pass( + config: dict, command_line_substitutions: dict, ignore_missing: bool = False +) -> None: if CONF_SUBSTITUTIONS not in config and not command_line_substitutions: return # Merge substitutions in config, overriding with substitutions coming from command line: - substitutions = { - **config.get(CONF_SUBSTITUTIONS, {}), - **(command_line_substitutions or {}), - } + # Use merge_dicts_ordered to preserve OrderedDict type for move_to_end() + substitutions = merge_dicts_ordered( + config.get(CONF_SUBSTITUTIONS, {}), command_line_substitutions or {} + ) with cv.prepend_path("substitutions"): if not isinstance(substitutions, dict): raise cv.Invalid( diff --git a/esphome/components/substitutions/jinja.py b/esphome/components/substitutions/jinja.py index cf393d2a5d..fb9f843da2 100644 --- a/esphome/components/substitutions/jinja.py +++ b/esphome/components/substitutions/jinja.py @@ -1,9 +1,16 @@ +from ast import literal_eval +from collections.abc import Iterator +from itertools import chain, islice import logging import math import re +from types import GeneratorType +from typing import Any import jinja2 as jinja -from jinja2.nativetypes import NativeEnvironment +from jinja2.nativetypes import NativeCodeGenerator, NativeTemplate + +from esphome.yaml_util import ESPLiteralValue TemplateError = jinja.TemplateError TemplateSyntaxError = jinja.TemplateSyntaxError @@ -21,10 +28,30 @@ detect_jinja_re = re.compile( ) -def has_jinja(st): +def has_jinja(st: str) -> bool: return detect_jinja_re.search(st) is not None +# SAFE_GLOBALS defines a allowlist of built-in functions or modules that are considered safe to expose +# in Jinja templates or other sandboxed evaluation contexts. Only functions that do not allow +# arbitrary code execution, file access, or other security risks are included. +# +# The following functions are considered safe: +# - math: The entire math module is injected, allowing access to mathematical functions like sin, cos, sqrt, etc. +# - ord: Converts a character to its Unicode code point integer. +# - chr: Converts an integer to its corresponding Unicode character. +# - len: Returns the length of a sequence or collection. +# +# These functions were chosen because they are pure, have no side effects, and do not provide access +# to the file system, environment, or other potentially sensitive resources. +SAFE_GLOBALS = { + "math": math, # Inject entire math module + "ord": ord, + "chr": chr, + "len": len, +} + + class JinjaStr(str): """ Wraps a string containing an unresolved Jinja expression, @@ -37,22 +64,112 @@ class JinjaStr(str): later in the main substitutions pass. """ + Undefined = object() + def __new__(cls, value: str, upvalues=None): - obj = super().__new__(cls, value) - obj.upvalues = upvalues or {} + if isinstance(value, JinjaStr): + base = str(value) + merged = {**value.upvalues, **(upvalues or {})} + else: + base = value + merged = dict(upvalues or {}) + obj = super().__new__(cls, base) + obj.upvalues = merged + obj.result = JinjaStr.Undefined return obj - def __init__(self, value: str, upvalues=None): - self.upvalues = upvalues or {} + +class JinjaError(Exception): + def __init__(self, context_trace: dict, expr: str): + self.context_trace = context_trace + self.eval_stack = [expr] + + def parent(self): + return self.__context__ + + def error_name(self): + return type(self.parent()).__name__ + + def context_trace_str(self): + return "\n".join( + f" {k} = {repr(v)} ({type(v).__name__})" + for k, v in self.context_trace.items() + ) + + def stack_trace_str(self): + return "\n".join( + f" {len(self.eval_stack) - i}: {expr}{i == 0 and ' <-- ' + self.error_name() or ''}" + for i, expr in enumerate(self.eval_stack) + ) -class Jinja: +class TrackerContext(jinja.runtime.Context): + def resolve_or_missing(self, key): + val = super().resolve_or_missing(key) + if isinstance(val, JinjaStr): + self.environment.context_trace[key] = val + val, _ = self.environment.expand(val) + self.environment.context_trace[key] = val + return val + + +def _concat_nodes_override(values: Iterator[Any]) -> Any: + """ + This function customizes how Jinja preserves native types when concatenating + multiple result nodes together. If the result is a single node, its value + is returned. Otherwise, the nodes are concatenated as strings. If + the result can be parsed with `ast.literal_eval`, the parsed + value is returned. Otherwise, the string is returned. + This helps preserve metadata such as ESPHomeDataBase from original values + and mimicks how HomeAssistant deals with template evaluation and preserving + the original datatype. + """ + head: list[Any] = list(islice(values, 2)) + + if not head: + return None + + if len(head) == 1: + raw = head[0] + if not isinstance(raw, str): + return raw + else: + if isinstance(values, GeneratorType): + values = chain(head, values) + raw = "".join([str(v) for v in values]) + + result = None + try: + # Attempt to parse the concatenated string into a Python literal. + # This allows expressions like "1 + 2" to be evaluated to the integer 3. + # If the result is also a string or there is a parsing error, + # fall back to returning the raw string. This is consistent with + # Home Assistant's behavior when evaluating templates + result = literal_eval(raw) + except (ValueError, SyntaxError, MemoryError, TypeError): + pass + else: + if isinstance(result, set): + # Sets are not supported, return raw string + return raw + + if not isinstance(result, str): + return result + + return raw + + +class Jinja(jinja.Environment): """ Wraps a Jinja environment """ - def __init__(self, context_vars): - self.env = NativeEnvironment( + # jinja environment customization overrides + code_generator_class = NativeCodeGenerator + concat = staticmethod(_concat_nodes_override) + + def __init__(self, context_vars: dict): + super().__init__( trim_blocks=True, lstrip_blocks=True, block_start_string="<%", @@ -63,38 +180,76 @@ class Jinja: variable_end_string="}", undefined=jinja.StrictUndefined, ) - self.env.add_extension("jinja2.ext.do") - self.env.globals["math"] = math # Inject entire math module + self.context_class = TrackerContext + self.add_extension("jinja2.ext.do") + self.context_trace = {} self.context_vars = {**context_vars} - self.env.globals = {**self.env.globals, **self.context_vars} + for k, v in self.context_vars.items(): + if isinstance(v, ESPLiteralValue): + continue + if isinstance(v, str) and not isinstance(v, JinjaStr) and has_jinja(v): + self.context_vars[k] = JinjaStr(v, self.context_vars) - def expand(self, content_str): + self.globals = { + **self.globals, + **self.context_vars, + **SAFE_GLOBALS, + } + + def expand(self, content_str: str | JinjaStr) -> Any: """ Renders a string that may contain Jinja expressions or statements - Returns the resulting processed string if all values could be resolved. + Returns the resulting value if all variables and expressions could be resolved. Otherwise, it returns a tagged (JinjaStr) string that captures variables in scope (upvalues), like a closure for later evaluation. """ result = None override_vars = {} if isinstance(content_str, JinjaStr): + if content_str.result is not JinjaStr.Undefined: + return content_str.result, None # If `value` is already a JinjaStr, it means we are trying to evaluate it again # in a parent pass. # Hopefully, all required variables are visible now. override_vars = content_str.upvalues + + old_trace = self.context_trace + self.context_trace = {} try: - template = self.env.from_string(content_str) + template = self.from_string(content_str) result = template.render(override_vars) if isinstance(result, Undefined): - # This happens when the expression is simply an undefined variable. Jinja does not - # raise an exception, instead we get "Undefined". - # Trigger an UndefinedError exception so we skip to below. - print("" + result) + print("" + result) # force a UndefinedError exception except (TemplateSyntaxError, UndefinedError) as err: # `content_str` contains a Jinja expression that refers to a variable that is undefined # in this scope. Perhaps it refers to a root substitution that is not visible yet. - # Therefore, return the original `content_str` as a JinjaStr, which contains the variables + # Therefore, return `content_str` as a JinjaStr, which contains the variables # that are actually visible to it at this point to postpone evaluation. return JinjaStr(content_str, {**self.context_vars, **override_vars}), err + except JinjaError as err: + err.context_trace = {**self.context_trace, **err.context_trace} + err.eval_stack.append(content_str) + raise err + except ( + TemplateError, + TemplateRuntimeError, + RuntimeError, + ArithmeticError, + AttributeError, + TypeError, + ) as err: + raise JinjaError(self.context_trace, content_str) from err + finally: + self.context_trace = old_trace + + if isinstance(content_str, JinjaStr): + content_str.result = result return result, None + + +class JinjaTemplate(NativeTemplate): + environment_class = Jinja + + +Jinja.template_class = JinjaTemplate diff --git a/esphome/components/sun/sun.h b/esphome/components/sun/sun.h index 77d62d34c3..67a0306a37 100644 --- a/esphome/components/sun/sun.h +++ b/esphome/components/sun/sun.h @@ -115,7 +115,7 @@ template class SunCondition : public Condition, public Pa TEMPLATABLE_VALUE(double, elevation); void set_above(bool above) { above_ = above; } - bool check(Ts... x) override { + bool check(const Ts &...x) override { double elevation = this->elevation_.value(x...); double current = this->parent_->elevation(); if (this->above_) { diff --git a/esphome/components/switch/__init__.py b/esphome/components/switch/__init__.py index a595d43445..e9473012cf 100644 --- a/esphome/components/switch/__init__.py +++ b/esphome/components/switch/__init__.py @@ -10,16 +10,18 @@ from esphome.const import ( CONF_ID, CONF_INVERTED, CONF_MQTT_ID, + CONF_ON_STATE, CONF_ON_TURN_OFF, CONF_ON_TURN_ON, CONF_RESTORE_MODE, + CONF_STATE, CONF_TRIGGER_ID, CONF_WEB_SERVER, DEVICE_CLASS_EMPTY, DEVICE_CLASS_OUTLET, DEVICE_CLASS_SWITCH, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass @@ -48,12 +50,16 @@ RESTORE_MODES = { } +ControlAction = switch_ns.class_("ControlAction", automation.Action) ToggleAction = switch_ns.class_("ToggleAction", automation.Action) TurnOffAction = switch_ns.class_("TurnOffAction", automation.Action) TurnOnAction = switch_ns.class_("TurnOnAction", automation.Action) SwitchPublishAction = switch_ns.class_("SwitchPublishAction", automation.Action) SwitchCondition = switch_ns.class_("SwitchCondition", Condition) +SwitchStateTrigger = switch_ns.class_( + "SwitchStateTrigger", automation.Trigger.template(bool) +) SwitchTurnOnTrigger = switch_ns.class_( "SwitchTurnOnTrigger", automation.Trigger.template() ) @@ -75,6 +81,11 @@ _SWITCH_SCHEMA = ( cv.Optional(CONF_RESTORE_MODE, default="ALWAYS_OFF"): cv.enum( RESTORE_MODES, upper=True, space="_" ), + cv.Optional(CONF_ON_STATE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SwitchStateTrigger), + } + ), cv.Optional(CONF_ON_TURN_ON): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SwitchTurnOnTrigger), @@ -128,16 +139,14 @@ def switch_schema( return _SWITCH_SCHEMA.extend(schema) -# Remove before 2025.11.0 -SWITCH_SCHEMA = switch_schema(Switch) -SWITCH_SCHEMA.add_extra(cv.deprecated_schema_constant("switch")) - - async def setup_switch_core_(var, config): await setup_entity(var, config, "switch") if (inverted := config.get(CONF_INVERTED)) is not None: cg.add(var.set_inverted(inverted)) + for conf in config.get(CONF_ON_STATE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(bool, "x")], conf) for conf in config.get(CONF_ON_TURN_ON, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) @@ -177,6 +186,23 @@ SWITCH_ACTION_SCHEMA = maybe_simple_id( cv.Required(CONF_ID): cv.use_id(Switch), } ) +SWITCH_CONTROL_ACTION_SCHEMA = automation.maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(Switch), + cv.Required(CONF_STATE): cv.templatable(cv.boolean), + } +) + + +@automation.register_action( + "switch.control", ControlAction, SWITCH_CONTROL_ACTION_SCHEMA +) +async def switch_control_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = await cg.templatable(config[CONF_STATE], args, bool) + cg.add(var.set_state(template_)) + return var @automation.register_action("switch.toggle", ToggleAction, SWITCH_ACTION_SCHEMA) @@ -199,6 +225,6 @@ async def switch_is_off_to_code(config, condition_id, template_arg, args): return cg.new_Pvariable(condition_id, template_arg, paren, False) -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(switch_ns.using) diff --git a/esphome/components/switch/automation.h b/esphome/components/switch/automation.h index 579daf4d24..27d3474c97 100644 --- a/esphome/components/switch/automation.h +++ b/esphome/components/switch/automation.h @@ -11,7 +11,7 @@ template class TurnOnAction : public Action { public: explicit TurnOnAction(Switch *a_switch) : switch_(a_switch) {} - void play(Ts... x) override { this->switch_->turn_on(); } + void play(const Ts &...x) override { this->switch_->turn_on(); } protected: Switch *switch_; @@ -21,7 +21,7 @@ template class TurnOffAction : public Action { public: explicit TurnOffAction(Switch *a_switch) : switch_(a_switch) {} - void play(Ts... x) override { this->switch_->turn_off(); } + void play(const Ts &...x) override { this->switch_->turn_off(); } protected: Switch *switch_; @@ -31,7 +31,24 @@ template class ToggleAction : public Action { public: explicit ToggleAction(Switch *a_switch) : switch_(a_switch) {} - void play(Ts... x) override { this->switch_->toggle(); } + void play(const Ts &...x) override { this->switch_->toggle(); } + + protected: + Switch *switch_; +}; + +template class ControlAction : public Action { + public: + explicit ControlAction(Switch *a_switch) : switch_(a_switch) {} + + TEMPLATABLE_VALUE(bool, state) + + void play(const Ts &...x) override { + auto state = this->state_.optional_value(x...); + if (state.has_value()) { + this->switch_->control(*state); + } + } protected: Switch *switch_; @@ -40,13 +57,20 @@ template class ToggleAction : public Action { template class SwitchCondition : public Condition { public: SwitchCondition(Switch *parent, bool state) : parent_(parent), state_(state) {} - bool check(Ts... x) override { return this->parent_->state == this->state_; } + bool check(const Ts &...x) override { return this->parent_->state == this->state_; } protected: Switch *parent_; bool state_; }; +class SwitchStateTrigger : public Trigger { + public: + SwitchStateTrigger(Switch *a_switch) { + a_switch->add_on_state_callback([this](bool state) { this->trigger(state); }); + } +}; + class SwitchTurnOnTrigger : public Trigger<> { public: SwitchTurnOnTrigger(Switch *a_switch) { @@ -74,7 +98,7 @@ template class SwitchPublishAction : public Action { SwitchPublishAction(Switch *a_switch) : switch_(a_switch) {} TEMPLATABLE_VALUE(bool, state) - void play(Ts... x) override { this->switch_->publish_state(this->state_.value(x...)); } + void play(const Ts &...x) override { this->switch_->publish_state(this->state_.value(x...)); } protected: Switch *switch_; diff --git a/esphome/components/switch/switch.cpp b/esphome/components/switch/switch.cpp index c204895755..3c3a437ff3 100644 --- a/esphome/components/switch/switch.cpp +++ b/esphome/components/switch/switch.cpp @@ -1,4 +1,6 @@ #include "switch.h" +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" namespace esphome { @@ -8,6 +10,14 @@ static const char *const TAG = "switch"; Switch::Switch() : state(false) {} +void Switch::control(bool target_state) { + ESP_LOGV(TAG, "'%s' Control: %s", this->get_name().c_str(), ONOFF(target_state)); + if (target_state) { + this->turn_on(); + } else { + this->turn_off(); + } +} void Switch::turn_on() { ESP_LOGD(TAG, "'%s' Turning ON.", this->get_name().c_str()); this->write_state(!this->inverted_); @@ -24,7 +34,7 @@ optional Switch::get_initial_state() { if (!(restore_mode & RESTORE_MODE_PERSISTENT_MASK)) return {}; - this->rtc_ = global_preferences->make_preference(this->get_object_id_hash()); + this->rtc_ = global_preferences->make_preference(this->get_preference_hash()); bool initial_state; if (!this->rtc_.load(&initial_state)) return {}; @@ -54,6 +64,9 @@ void Switch::publish_state(bool state) { ESP_LOGD(TAG, "'%s': Sending state %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); +#endif } bool Switch::assumed_state() { return false; } @@ -83,8 +96,8 @@ void log_switch(const char *tag, const char *prefix, const char *type, Switch *o LOG_STR_ARG(onoff)); // Add optional fields separately - if (!obj->get_icon().empty()) { - ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj->get_icon().c_str()); + if (!obj->get_icon_ref().empty()) { + ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj->get_icon_ref().c_str()); } if (obj->assumed_state()) { ESP_LOGCONFIG(tag, "%s Assumed State: YES", prefix); @@ -92,8 +105,8 @@ void log_switch(const char *tag, const char *prefix, const char *type, Switch *o if (obj->is_inverted()) { ESP_LOGCONFIG(tag, "%s Inverted: YES", prefix); } - if (!obj->get_device_class().empty()) { - ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj->get_device_class().c_str()); + if (!obj->get_device_class_ref().empty()) { + ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj->get_device_class_ref().c_str()); } } } diff --git a/esphome/components/switch/switch.h b/esphome/components/switch/switch.h index b999296564..6371e35292 100644 --- a/esphome/components/switch/switch.h +++ b/esphome/components/switch/switch.h @@ -55,6 +55,14 @@ class Switch : public EntityBase, public EntityBase_DeviceClass { /// The current reported state of the binary sensor. bool state; + /** Control this switch using a boolean state value. + * + * This method provides a unified interface for setting the switch state based on a boolean parameter. + * It automatically calls turn_on() when state is true or turn_off() when state is false. + * + * @param target_state The desired state: true to turn the switch ON, false to turn it OFF. + */ + void control(bool target_state); /** Turn this switch on. This is called by the front-end. * * For implementing switches, please override write_state. diff --git a/esphome/components/sx126x/__init__.py b/esphome/components/sx126x/__init__.py index b6aeaf072c..1eb83b7a33 100644 --- a/esphome/components/sx126x/__init__.py +++ b/esphome/components/sx126x/__init__.py @@ -3,7 +3,7 @@ import esphome.codegen as cg from esphome.components import spi import esphome.config_validation as cv from esphome.const import CONF_BUSY_PIN, CONF_DATA, CONF_FREQUENCY, CONF_ID -from esphome.core import TimePeriod +from esphome.core import ID, TimePeriod MULTI_CONF = True CODEOWNERS = ["@swoboda1337"] @@ -15,6 +15,10 @@ CONF_BANDWIDTH = "bandwidth" CONF_BITRATE = "bitrate" CONF_CODING_RATE = "coding_rate" CONF_CRC_ENABLE = "crc_enable" +CONF_CRC_INVERTED = "crc_inverted" +CONF_CRC_SIZE = "crc_size" +CONF_CRC_POLYNOMIAL = "crc_polynomial" +CONF_CRC_INITIAL = "crc_initial" CONF_DEVIATION = "deviation" CONF_DIO1_PIN = "dio1_pin" CONF_HW_VERSION = "hw_version" @@ -185,11 +189,19 @@ CONFIG_SCHEMA = ( cv.GenerateID(): cv.declare_id(SX126x), cv.Optional(CONF_BANDWIDTH, default="125_0kHz"): cv.enum(BW), cv.Optional(CONF_BITRATE, default=4800): cv.int_range(min=600, max=300000), - cv.Required(CONF_BUSY_PIN): pins.internal_gpio_input_pin_schema, + cv.Required(CONF_BUSY_PIN): pins.gpio_input_pin_schema, cv.Optional(CONF_CODING_RATE, default="CR_4_5"): cv.enum(CODING_RATE), cv.Optional(CONF_CRC_ENABLE, default=False): cv.boolean, + cv.Optional(CONF_CRC_INVERTED, default=True): cv.boolean, + cv.Optional(CONF_CRC_SIZE, default=2): cv.int_range(min=1, max=2), + cv.Optional(CONF_CRC_POLYNOMIAL, default=0x1021): cv.All( + cv.hex_int, cv.Range(min=0, max=0xFFFF) + ), + cv.Optional(CONF_CRC_INITIAL, default=0x1D0F): cv.All( + cv.hex_int, cv.Range(min=0, max=0xFFFF) + ), cv.Optional(CONF_DEVIATION, default=5000): cv.int_range(min=0, max=100000), - cv.Required(CONF_DIO1_PIN): pins.internal_gpio_input_pin_schema, + cv.Required(CONF_DIO1_PIN): pins.gpio_input_pin_schema, cv.Required(CONF_FREQUENCY): cv.int_range(min=137000000, max=1020000000), cv.Required(CONF_HW_VERSION): cv.one_of( "sx1261", "sx1262", "sx1268", "llcc68", lower=True @@ -201,7 +213,7 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_PAYLOAD_LENGTH, default=0): cv.int_range(min=0, max=256), cv.Optional(CONF_PREAMBLE_DETECT, default=2): cv.int_range(min=0, max=4), cv.Optional(CONF_PREAMBLE_SIZE, default=8): cv.int_range(min=1, max=65535), - cv.Required(CONF_RST_PIN): pins.internal_gpio_output_pin_schema, + cv.Required(CONF_RST_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_RX_START, default=True): cv.boolean, cv.Required(CONF_RF_SWITCH): cv.boolean, cv.Optional(CONF_SHAPING, default="NONE"): cv.enum(SHAPING), @@ -251,6 +263,10 @@ async def to_code(config): cg.add(var.set_shaping(config[CONF_SHAPING])) cg.add(var.set_bitrate(config[CONF_BITRATE])) cg.add(var.set_crc_enable(config[CONF_CRC_ENABLE])) + cg.add(var.set_crc_inverted(config[CONF_CRC_INVERTED])) + cg.add(var.set_crc_size(config[CONF_CRC_SIZE])) + cg.add(var.set_crc_polynomial(config[CONF_CRC_POLYNOMIAL])) + cg.add(var.set_crc_initial(config[CONF_CRC_INITIAL])) cg.add(var.set_payload_length(config[CONF_PAYLOAD_LENGTH])) cg.add(var.set_preamble_size(config[CONF_PREAMBLE_SIZE])) cg.add(var.set_preamble_detect(config[CONF_PREAMBLE_DETECT])) @@ -313,5 +329,8 @@ async def send_packet_action_to_code(config, action_id, template_arg, args): templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8)) cg.add(var.set_data_template(templ)) else: - cg.add(var.set_data_static(data)) + # Generate static array in flash to avoid RAM copy + arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8) + arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data)) + cg.add(var.set_data_static(arr, len(data))) return var diff --git a/esphome/components/sx126x/automation.h b/esphome/components/sx126x/automation.h index 520ef99718..2282c583cb 100644 --- a/esphome/components/sx126x/automation.h +++ b/esphome/components/sx126x/automation.h @@ -9,53 +9,59 @@ namespace sx126x { template class RunImageCalAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->run_image_cal(); } + void play(const Ts &...x) override { this->parent_->run_image_cal(); } }; template class SendPacketAction : public Action, public Parented { public: - void set_data_template(std::function(Ts...)> func) { - this->data_func_ = func; - this->static_ = false; + void set_data_template(std::vector (*func)(Ts...)) { + this->data_.func = func; + this->len_ = -1; // Sentinel value indicates template mode } - void set_data_static(const std::vector &data) { - this->data_static_ = data; - this->static_ = true; + void set_data_static(const uint8_t *data, size_t len) { + this->data_.data = data; + this->len_ = len; // Length >= 0 indicates static mode } - void play(Ts... x) override { - if (this->static_) { - this->parent_->transmit_packet(this->data_static_); + void play(const Ts &...x) override { + std::vector data; + if (this->len_ >= 0) { + // Static mode: copy from flash to vector + data.assign(this->data_.data, this->data_.data + this->len_); } else { - this->parent_->transmit_packet(this->data_func_(x...)); + // Template mode: call function + data = this->data_.func(x...); } + this->parent_->transmit_packet(data); } protected: - bool static_{false}; - std::function(Ts...)> data_func_{}; - std::vector data_static_{}; + ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length + union Data { + std::vector (*func)(Ts...); // Function pointer (stateless lambdas) + const uint8_t *data; // Pointer to static data in flash + } data_; }; template class SetModeTxAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->set_mode_tx(); } + void play(const Ts &...x) override { this->parent_->set_mode_tx(); } }; template class SetModeRxAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->set_mode_rx(); } + void play(const Ts &...x) override { this->parent_->set_mode_rx(); } }; template class SetModeSleepAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->set_mode_sleep(); } + void play(const Ts &...x) override { this->parent_->set_mode_sleep(); } }; template class SetModeStandbyAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->set_mode_standby(STDBY_XOSC); } + void play(const Ts &...x) override { this->parent_->set_mode_standby(STDBY_XOSC); } }; } // namespace sx126x diff --git a/esphome/components/sx126x/sx126x.cpp b/esphome/components/sx126x/sx126x.cpp index cae047d168..bb59f26b79 100644 --- a/esphome/components/sx126x/sx126x.cpp +++ b/esphome/components/sx126x/sx126x.cpp @@ -217,7 +217,7 @@ void SX126x::configure() { this->write_opcode_(RADIO_SET_MODULATIONPARAMS, buf, 4); // set packet params and sync word - this->set_packet_params_(this->payload_length_); + this->set_packet_params_(this->get_max_packet_size()); if (this->sync_value_.size() == 2) { this->write_register_(REG_LORA_SYNCWORD, this->sync_value_.data(), this->sync_value_.size()); } @@ -235,8 +235,18 @@ void SX126x::configure() { buf[7] = (fdev >> 0) & 0xFF; this->write_opcode_(RADIO_SET_MODULATIONPARAMS, buf, 8); + // set crc params + if (this->crc_enable_) { + buf[0] = this->crc_initial_ >> 8; + buf[1] = this->crc_initial_ & 0xFF; + this->write_register_(REG_CRC_INITIAL, buf, 2); + buf[0] = this->crc_polynomial_ >> 8; + buf[1] = this->crc_polynomial_ & 0xFF; + this->write_register_(REG_CRC_POLYNOMIAL, buf, 2); + } + // set packet params and sync word - this->set_packet_params_(this->payload_length_); + this->set_packet_params_(this->get_max_packet_size()); if (!this->sync_value_.empty()) { this->write_register_(REG_GFSK_SYNCWORD, this->sync_value_.data(), this->sync_value_.size()); } @@ -274,9 +284,13 @@ void SX126x::set_packet_params_(uint8_t payload_length) { buf[2] = (this->preamble_detect_ > 0) ? ((this->preamble_detect_ - 1) | 0x04) : 0x00; buf[3] = this->sync_value_.size() * 8; buf[4] = 0x00; - buf[5] = 0x00; + buf[5] = (this->payload_length_ > 0) ? 0x00 : 0x01; buf[6] = payload_length; - buf[7] = this->crc_enable_ ? 0x06 : 0x01; + if (this->crc_enable_) { + buf[7] = (this->crc_inverted_ ? 0x04 : 0x00) + (this->crc_size_ & 0x02); + } else { + buf[7] = 0x01; + } buf[8] = 0x00; this->write_opcode_(RADIO_SET_PACKETPARAMS, buf, 9); } @@ -314,6 +328,9 @@ SX126xError SX126x::transmit_packet(const std::vector &packet) { buf[0] = 0xFF; buf[1] = 0xFF; this->write_opcode_(RADIO_CLR_IRQSTATUS, buf, 2); + if (this->payload_length_ == 0) { + this->set_packet_params_(this->get_max_packet_size()); + } if (this->rx_start_) { this->set_mode_rx(); } else { diff --git a/esphome/components/sx126x/sx126x.h b/esphome/components/sx126x/sx126x.h index fd5c37942d..850d7d4c77 100644 --- a/esphome/components/sx126x/sx126x.h +++ b/esphome/components/sx126x/sx126x.h @@ -64,11 +64,15 @@ class SX126x : public Component, void dump_config() override; void set_bandwidth(SX126xBw bandwidth) { this->bandwidth_ = bandwidth; } void set_bitrate(uint32_t bitrate) { this->bitrate_ = bitrate; } - void set_busy_pin(InternalGPIOPin *busy_pin) { this->busy_pin_ = busy_pin; } + void set_busy_pin(GPIOPin *busy_pin) { this->busy_pin_ = busy_pin; } void set_coding_rate(uint8_t coding_rate) { this->coding_rate_ = coding_rate; } void set_crc_enable(bool crc_enable) { this->crc_enable_ = crc_enable; } + void set_crc_inverted(bool crc_inverted) { this->crc_inverted_ = crc_inverted; } + void set_crc_size(uint8_t crc_size) { this->crc_size_ = crc_size; } + void set_crc_polynomial(uint16_t crc_polynomial) { this->crc_polynomial_ = crc_polynomial; } + void set_crc_initial(uint16_t crc_initial) { this->crc_initial_ = crc_initial; } void set_deviation(uint32_t deviation) { this->deviation_ = deviation; } - void set_dio1_pin(InternalGPIOPin *dio1_pin) { this->dio1_pin_ = dio1_pin; } + void set_dio1_pin(GPIOPin *dio1_pin) { this->dio1_pin_ = dio1_pin; } void set_frequency(uint32_t frequency) { this->frequency_ = frequency; } void set_hw_version(const std::string &hw_version) { this->hw_version_ = hw_version; } void set_mode_rx(); @@ -81,7 +85,7 @@ class SX126x : public Component, void set_payload_length(uint8_t payload_length) { this->payload_length_ = payload_length; } void set_preamble_detect(uint16_t preamble_detect) { this->preamble_detect_ = preamble_detect; } void set_preamble_size(uint16_t preamble_size) { this->preamble_size_ = preamble_size; } - void set_rst_pin(InternalGPIOPin *rst_pin) { this->rst_pin_ = rst_pin; } + void set_rst_pin(GPIOPin *rst_pin) { this->rst_pin_ = rst_pin; } void set_rx_start(bool rx_start) { this->rx_start_ = rx_start; } void set_rf_switch(bool rf_switch) { this->rf_switch_ = rf_switch; } void set_shaping(uint8_t shaping) { this->shaping_ = shaping; } @@ -111,13 +115,18 @@ class SX126x : public Component, std::vector listeners_; std::vector packet_; std::vector sync_value_; - InternalGPIOPin *busy_pin_{nullptr}; - InternalGPIOPin *dio1_pin_{nullptr}; - InternalGPIOPin *rst_pin_{nullptr}; + GPIOPin *busy_pin_{nullptr}; + GPIOPin *dio1_pin_{nullptr}; + GPIOPin *rst_pin_{nullptr}; std::string hw_version_; char version_[16]; SX126xBw bandwidth_{SX126X_BW_125000}; uint32_t bitrate_{0}; + bool crc_enable_{false}; + bool crc_inverted_{false}; + uint8_t crc_size_{0}; + uint16_t crc_polynomial_{0}; + uint16_t crc_initial_{0}; uint32_t deviation_{0}; uint32_t frequency_{0}; uint32_t payload_length_{0}; @@ -131,7 +140,6 @@ class SX126x : public Component, uint8_t shaping_{0}; uint8_t spreading_factor_{0}; int8_t pa_power_{0}; - bool crc_enable_{false}; bool rx_start_{false}; bool rf_switch_{false}; }; diff --git a/esphome/components/sx126x/sx126x_reg.h b/esphome/components/sx126x/sx126x_reg.h index 3b12d822b5..143f4a05da 100644 --- a/esphome/components/sx126x/sx126x_reg.h +++ b/esphome/components/sx126x/sx126x_reg.h @@ -53,6 +53,8 @@ enum SX126xOpCode : uint8_t { enum SX126xRegister : uint16_t { REG_VERSION_STRING = 0x0320, + REG_CRC_INITIAL = 0x06BC, + REG_CRC_POLYNOMIAL = 0x06BE, REG_GFSK_SYNCWORD = 0x06C0, REG_LORA_SYNCWORD = 0x0740, REG_OCP = 0x08E7, diff --git a/esphome/components/sx127x/__init__.py b/esphome/components/sx127x/__init__.py index 33b556db07..77cb61f7f8 100644 --- a/esphome/components/sx127x/__init__.py +++ b/esphome/components/sx127x/__init__.py @@ -3,6 +3,7 @@ import esphome.codegen as cg from esphome.components import spi import esphome.config_validation as cv from esphome.const import CONF_DATA, CONF_FREQUENCY, CONF_ID +from esphome.core import ID MULTI_CONF = True CODEOWNERS = ["@swoboda1337"] @@ -321,5 +322,8 @@ async def send_packet_action_to_code(config, action_id, template_arg, args): templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8)) cg.add(var.set_data_template(templ)) else: - cg.add(var.set_data_static(data)) + # Generate static array in flash to avoid RAM copy + arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8) + arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data)) + cg.add(var.set_data_static(arr, len(data))) return var diff --git a/esphome/components/sx127x/automation.h b/esphome/components/sx127x/automation.h index 2b9c261de1..fb0367fcca 100644 --- a/esphome/components/sx127x/automation.h +++ b/esphome/components/sx127x/automation.h @@ -9,53 +9,59 @@ namespace sx127x { template class RunImageCalAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->run_image_cal(); } + void play(const Ts &...x) override { this->parent_->run_image_cal(); } }; template class SendPacketAction : public Action, public Parented { public: - void set_data_template(std::function(Ts...)> func) { - this->data_func_ = func; - this->static_ = false; + void set_data_template(std::vector (*func)(Ts...)) { + this->data_.func = func; + this->len_ = -1; // Sentinel value indicates template mode } - void set_data_static(const std::vector &data) { - this->data_static_ = data; - this->static_ = true; + void set_data_static(const uint8_t *data, size_t len) { + this->data_.data = data; + this->len_ = len; // Length >= 0 indicates static mode } - void play(Ts... x) override { - if (this->static_) { - this->parent_->transmit_packet(this->data_static_); + void play(const Ts &...x) override { + std::vector data; + if (this->len_ >= 0) { + // Static mode: copy from flash to vector + data.assign(this->data_.data, this->data_.data + this->len_); } else { - this->parent_->transmit_packet(this->data_func_(x...)); + // Template mode: call function + data = this->data_.func(x...); } + this->parent_->transmit_packet(data); } protected: - bool static_{false}; - std::function(Ts...)> data_func_{}; - std::vector data_static_{}; + ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length + union Data { + std::vector (*func)(Ts...); // Function pointer (stateless lambdas) + const uint8_t *data; // Pointer to static data in flash + } data_; }; template class SetModeTxAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->set_mode_tx(); } + void play(const Ts &...x) override { this->parent_->set_mode_tx(); } }; template class SetModeRxAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->set_mode_rx(); } + void play(const Ts &...x) override { this->parent_->set_mode_rx(); } }; template class SetModeSleepAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->set_mode_sleep(); } + void play(const Ts &...x) override { this->parent_->set_mode_sleep(); } }; template class SetModeStandbyAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->set_mode_standby(); } + void play(const Ts &...x) override { this->parent_->set_mode_standby(); } }; } // namespace sx127x diff --git a/esphome/components/sx1509/__init__.py b/esphome/components/sx1509/__init__.py index 67dc924903..b61b92fd1e 100644 --- a/esphome/components/sx1509/__init__.py +++ b/esphome/components/sx1509/__init__.py @@ -25,7 +25,7 @@ CONF_SCAN_TIME = "scan_time" CONF_DEBOUNCE_TIME = "debounce_time" CONF_SX1509_ID = "sx1509_id" -AUTO_LOAD = ["key_provider"] +AUTO_LOAD = ["key_provider", "gpio_expander"] DEPENDENCIES = ["i2c"] MULTI_CONF = True diff --git a/esphome/components/sx1509/sx1509.cpp b/esphome/components/sx1509/sx1509.cpp index 2bf6701dd2..746ec9cda3 100644 --- a/esphome/components/sx1509/sx1509.cpp +++ b/esphome/components/sx1509/sx1509.cpp @@ -39,6 +39,9 @@ void SX1509Component::dump_config() { } void SX1509Component::loop() { + // Reset cache at the start of each loop + this->reset_pin_cache_(); + if (this->has_keypad_) { if (millis() - this->last_loop_timestamp_ < min_loop_period_) return; @@ -73,18 +76,20 @@ void SX1509Component::loop() { } } -bool SX1509Component::digital_read(uint8_t pin) { +bool SX1509Component::digital_read_hw(uint8_t pin) { + // Always read all pins when any input pin is accessed + return this->read_byte_16(REG_DATA_B, &this->input_mask_); +} + +bool SX1509Component::digital_read_cache(uint8_t pin) { + // Return cached value for input pins, false for output pins if (this->ddr_mask_ & (1 << pin)) { - uint16_t temp_reg_data; - if (!this->read_byte_16(REG_DATA_B, &temp_reg_data)) - return false; - if (temp_reg_data & (1 << pin)) - return true; + return (this->input_mask_ & (1 << pin)) != 0; } return false; } -void SX1509Component::digital_write(uint8_t pin, bool bit_value) { +void SX1509Component::digital_write_hw(uint8_t pin, bool bit_value) { if ((~this->ddr_mask_) & (1 << pin)) { // If the pin is an output, write high/low uint16_t temp_reg_data = 0; diff --git a/esphome/components/sx1509/sx1509.h b/esphome/components/sx1509/sx1509.h index c0e86aa8a1..2afd0d0e4e 100644 --- a/esphome/components/sx1509/sx1509.h +++ b/esphome/components/sx1509/sx1509.h @@ -2,6 +2,7 @@ #include "esphome/components/i2c/i2c.h" #include "esphome/components/key_provider/key_provider.h" +#include "esphome/components/gpio_expander/cached_gpio.h" #include "esphome/core/component.h" #include "esphome/core/hal.h" #include "sx1509_gpio_pin.h" @@ -30,7 +31,10 @@ class SX1509Processor { class SX1509KeyTrigger : public Trigger {}; -class SX1509Component : public Component, public i2c::I2CDevice, public key_provider::KeyProvider { +class SX1509Component : public Component, + public i2c::I2CDevice, + public gpio_expander::CachedGpioExpander, + public key_provider::KeyProvider { public: SX1509Component() = default; @@ -39,11 +43,9 @@ class SX1509Component : public Component, public i2c::I2CDevice, public key_prov float get_setup_priority() const override { return setup_priority::HARDWARE; } void loop() override; - bool digital_read(uint8_t pin); uint16_t read_key_data(); void set_pin_value(uint8_t pin, uint8_t i_on) { this->write_byte(REG_I_ON[pin], i_on); }; void pin_mode(uint8_t pin, gpio::Flags flags); - void digital_write(uint8_t pin, bool bit_value); uint32_t get_clock() { return this->clk_x_; }; void set_rows_cols(uint8_t rows, uint8_t cols) { this->rows_ = rows; @@ -61,10 +63,15 @@ class SX1509Component : public Component, public i2c::I2CDevice, public key_prov void setup_led_driver(uint8_t pin); protected: + // Virtual methods from CachedGpioExpander + bool digital_read_hw(uint8_t pin) override; + bool digital_read_cache(uint8_t pin) override; + void digital_write_hw(uint8_t pin, bool value) override; + uint32_t clk_x_ = 2000000; uint8_t frequency_ = 0; uint16_t ddr_mask_ = 0x00; - uint16_t input_mask_ = 0x00; + uint16_t input_mask_ = 0x00; // Cache for input values (16-bit for all pins) uint16_t port_mask_ = 0x00; uint16_t output_state_ = 0x00; bool has_keypad_ = false; diff --git a/esphome/components/tca9548a/tca9548a.cpp b/esphome/components/tca9548a/tca9548a.cpp index edd8af9a27..1de3c49108 100644 --- a/esphome/components/tca9548a/tca9548a.cpp +++ b/esphome/components/tca9548a/tca9548a.cpp @@ -6,23 +6,15 @@ namespace tca9548a { static const char *const TAG = "tca9548a"; -i2c::ErrorCode TCA9548AChannel::readv(uint8_t address, i2c::ReadBuffer *buffers, size_t cnt) { +i2c::ErrorCode TCA9548AChannel::write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count, + uint8_t *read_buffer, size_t read_count) { auto err = this->parent_->switch_to_channel(channel_); if (err != i2c::ERROR_OK) return err; - err = this->parent_->bus_->readv(address, buffers, cnt); + err = this->parent_->bus_->write_readv(address, write_buffer, write_count, read_buffer, read_count); this->parent_->disable_all_channels(); return err; } -i2c::ErrorCode TCA9548AChannel::writev(uint8_t address, i2c::WriteBuffer *buffers, size_t cnt, bool stop) { - auto err = this->parent_->switch_to_channel(channel_); - if (err != i2c::ERROR_OK) - return err; - err = this->parent_->bus_->writev(address, buffers, cnt, stop); - this->parent_->disable_all_channels(); - return err; -} - void TCA9548AComponent::setup() { uint8_t status = 0; if (this->read(&status, 1) != i2c::ERROR_OK) { diff --git a/esphome/components/tca9548a/tca9548a.h b/esphome/components/tca9548a/tca9548a.h index 08f1674d11..0fb9ada99a 100644 --- a/esphome/components/tca9548a/tca9548a.h +++ b/esphome/components/tca9548a/tca9548a.h @@ -14,8 +14,8 @@ class TCA9548AChannel : public i2c::I2CBus { void set_channel(uint8_t channel) { channel_ = channel; } void set_parent(TCA9548AComponent *parent) { parent_ = parent; } - i2c::ErrorCode readv(uint8_t address, i2c::ReadBuffer *buffers, size_t cnt) override; - i2c::ErrorCode writev(uint8_t address, i2c::WriteBuffer *buffers, size_t cnt, bool stop) override; + i2c::ErrorCode write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count, uint8_t *read_buffer, + size_t read_count) override; protected: uint8_t channel_; diff --git a/esphome/components/tca9555/tca9555.cpp b/esphome/components/tca9555/tca9555.cpp index b4a04d5b0b..c3449ce254 100644 --- a/esphome/components/tca9555/tca9555.cpp +++ b/esphome/components/tca9555/tca9555.cpp @@ -50,7 +50,7 @@ bool TCA9555Component::read_gpio_outputs_() { return false; uint8_t data[2]; if (!this->read_bytes(TCA9555_OUTPUT_PORT_REGISTER_0, data, 2)) { - this->status_set_warning("Failed to read output register"); + this->status_set_warning(LOG_STR("Failed to read output register")); return false; } this->output_mask_ = (uint16_t(data[1]) << 8) | (uint16_t(data[0]) << 0); @@ -64,7 +64,7 @@ bool TCA9555Component::read_gpio_modes_() { uint8_t data[2]; bool success = this->read_bytes(TCA9555_CONFIGURATION_PORT_0, data, 2); if (!success) { - this->status_set_warning("Failed to read mode register"); + this->status_set_warning(LOG_STR("Failed to read mode register")); return false; } this->mode_mask_ = (uint16_t(data[1]) << 8) | (uint16_t(data[0]) << 0); @@ -79,7 +79,7 @@ bool TCA9555Component::digital_read_hw(uint8_t pin) { uint8_t bank_number = pin < 8 ? 0 : 1; uint8_t register_to_read = bank_number ? TCA9555_INPUT_PORT_REGISTER_1 : TCA9555_INPUT_PORT_REGISTER_0; if (!this->read_bytes(register_to_read, &data, 1)) { - this->status_set_warning("Failed to read input register"); + this->status_set_warning(LOG_STR("Failed to read input register")); return false; } uint8_t second_half = this->input_mask_ >> 8; @@ -108,7 +108,7 @@ void TCA9555Component::digital_write_hw(uint8_t pin, bool value) { data[0] = this->output_mask_; data[1] = this->output_mask_ >> 8; if (!this->write_bytes(TCA9555_OUTPUT_PORT_REGISTER_0, data, 2)) { - this->status_set_warning("Failed to write output register"); + this->status_set_warning(LOG_STR("Failed to write output register")); return; } @@ -123,7 +123,7 @@ bool TCA9555Component::write_gpio_modes_() { data[0] = this->mode_mask_; data[1] = this->mode_mask_ >> 8; if (!this->write_bytes(TCA9555_CONFIGURATION_PORT_0, data, 2)) { - this->status_set_warning("Failed to write mode register"); + this->status_set_warning(LOG_STR("Failed to write mode register")); return false; } this->status_clear_warning(); diff --git a/esphome/components/tee501/tee501.cpp b/esphome/components/tee501/tee501.cpp index 460f446865..d6513dbbe0 100644 --- a/esphome/components/tee501/tee501.cpp +++ b/esphome/components/tee501/tee501.cpp @@ -9,10 +9,10 @@ static const char *const TAG = "tee501"; void TEE501Component::setup() { uint8_t address[] = {0x70, 0x29}; - this->write(address, 2, false); uint8_t identification[9]; this->read(identification, 9); - if (identification[8] != calc_crc8_(identification, 0, 7)) { + this->write_read(address, sizeof address, identification, sizeof identification); + if (identification[8] != crc8(identification, 8, 0xFF, 0x31, true)) { this->error_code_ = CRC_CHECK_FAILED; this->mark_failed(); return; @@ -41,11 +41,11 @@ void TEE501Component::dump_config() { float TEE501Component::get_setup_priority() const { return setup_priority::DATA; } void TEE501Component::update() { uint8_t address_1[] = {0x2C, 0x1B}; - this->write(address_1, 2, true); + this->write(address_1, 2); this->set_timeout(50, [this]() { uint8_t i2c_response[3]; this->read(i2c_response, 3); - if (i2c_response[2] != calc_crc8_(i2c_response, 0, 1)) { + if (i2c_response[2] != crc8(i2c_response, 2, 0xFF, 0x31, true)) { this->error_code_ = CRC_CHECK_FAILED; this->status_set_warning(); return; @@ -62,24 +62,5 @@ void TEE501Component::update() { }); } -unsigned char TEE501Component::calc_crc8_(const unsigned char buf[], unsigned char from, unsigned char to) { - unsigned char crc_val = 0xFF; - unsigned char i = 0; - unsigned char j = 0; - for (i = from; i <= to; i++) { - int cur_val = buf[i]; - for (j = 0; j < 8; j++) { - if (((crc_val ^ cur_val) & 0x80) != 0) // If MSBs are not equal - { - crc_val = ((crc_val << 1) ^ 0x31); - } else { - crc_val = (crc_val << 1); - } - cur_val = cur_val << 1; - } - } - return crc_val; -} - } // namespace tee501 } // namespace esphome diff --git a/esphome/components/tee501/tee501.h b/esphome/components/tee501/tee501.h index fc655e58c9..2437ac92eb 100644 --- a/esphome/components/tee501/tee501.h +++ b/esphome/components/tee501/tee501.h @@ -1,8 +1,8 @@ #pragma once -#include "esphome/core/component.h" -#include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/core/component.h" namespace esphome { namespace tee501 { @@ -16,8 +16,6 @@ class TEE501Component : public sensor::Sensor, public PollingComponent, public i void update() override; protected: - unsigned char calc_crc8_(const unsigned char buf[], unsigned char from, unsigned char to); - enum ErrorCode { NONE = 0, COMMUNICATION_FAILED, CRC_CHECK_FAILED } error_code_{NONE}; }; diff --git a/esphome/components/template/alarm_control_panel/__init__.py b/esphome/components/template/alarm_control_panel/__init__.py index 5d2421fcbc..256c7f276a 100644 --- a/esphome/components/template/alarm_control_panel/__init__.py +++ b/esphome/components/template/alarm_control_panel/__init__.py @@ -137,7 +137,11 @@ async def to_code(config): cg.add(var.set_arming_night_time(config[CONF_ARMING_NIGHT_TIME])) supports_arm_night = True - for sensor in config.get(CONF_BINARY_SENSORS, []): + if sensors := config.get(CONF_BINARY_SENSORS, []): + # Initialize FixedVector with the exact number of sensors + cg.add(var.init_sensors(len(sensors))) + + for sensor in sensors: bs = await cg.get_variable(sensor[CONF_INPUT]) flags = BinarySensorFlags[FLAG_NORMAL] diff --git a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp index 11a148830d..f025435261 100644 --- a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp @@ -20,79 +20,72 @@ void TemplateAlarmControlPanel::add_sensor(binary_sensor::BinarySensor *sensor, // Save the flags and type. Assign a store index for the per sensor data type. SensorDataStore sd; sd.last_chime_state = false; - this->sensor_map_[sensor].flags = flags; - this->sensor_map_[sensor].type = type; + AlarmSensor alarm_sensor; + alarm_sensor.sensor = sensor; + alarm_sensor.info.flags = flags; + alarm_sensor.info.type = type; + alarm_sensor.info.store_index = this->next_store_index_++; + this->sensors_.push_back(alarm_sensor); this->sensor_data_.push_back(sd); - this->sensor_map_[sensor].store_index = this->next_store_index_++; }; + +static const LogString *sensor_type_to_string(AlarmSensorType type) { + switch (type) { + case ALARM_SENSOR_TYPE_INSTANT: + return LOG_STR("instant"); + case ALARM_SENSOR_TYPE_DELAYED_FOLLOWER: + return LOG_STR("delayed_follower"); + case ALARM_SENSOR_TYPE_INSTANT_ALWAYS: + return LOG_STR("instant_always"); + case ALARM_SENSOR_TYPE_DELAYED: + default: + return LOG_STR("delayed"); + } +} #endif void TemplateAlarmControlPanel::dump_config() { - ESP_LOGCONFIG(TAG, "TemplateAlarmControlPanel:"); ESP_LOGCONFIG(TAG, + "TemplateAlarmControlPanel:\n" " Current State: %s\n" - " Number of Codes: %u", - LOG_STR_ARG(alarm_control_panel_state_to_string(this->current_state_)), this->codes_.size()); - if (!this->codes_.empty()) - ESP_LOGCONFIG(TAG, " Requires Code To Arm: %s", YESNO(this->requires_code_to_arm_)); - ESP_LOGCONFIG(TAG, " Arming Away Time: %" PRIu32 "s", (this->arming_away_time_ / 1000)); - if (this->arming_home_time_ != 0) - ESP_LOGCONFIG(TAG, " Arming Home Time: %" PRIu32 "s", (this->arming_home_time_ / 1000)); - if (this->arming_night_time_ != 0) - ESP_LOGCONFIG(TAG, " Arming Night Time: %" PRIu32 "s", (this->arming_night_time_ / 1000)); - ESP_LOGCONFIG(TAG, + " Number of Codes: %zu\n" + " Requires Code To Arm: %s\n" + " Arming Away Time: %" PRIu32 "s\n" + " Arming Home Time: %" PRIu32 "s\n" + " Arming Night Time: %" PRIu32 "s\n" " Pending Time: %" PRIu32 "s\n" " Trigger Time: %" PRIu32 "s\n" " Supported Features: %" PRIu32, - (this->pending_time_ / 1000), (this->trigger_time_ / 1000), this->get_supported_features()); + LOG_STR_ARG(alarm_control_panel_state_to_string(this->current_state_)), this->codes_.size(), + YESNO(!this->codes_.empty() && this->requires_code_to_arm_), (this->arming_away_time_ / 1000), + (this->arming_home_time_ / 1000), (this->arming_night_time_ / 1000), (this->pending_time_ / 1000), + (this->trigger_time_ / 1000), this->get_supported_features()); #ifdef USE_BINARY_SENSOR - for (auto sensor_info : this->sensor_map_) { - ESP_LOGCONFIG(TAG, " Binary Sensor:"); + for (const auto &alarm_sensor : this->sensors_) { + const uint16_t flags = alarm_sensor.info.flags; ESP_LOGCONFIG(TAG, + " Binary Sensor:\n" " Name: %s\n" + " Type: %s\n" " Armed home bypass: %s\n" " Armed night bypass: %s\n" " Auto bypass: %s\n" " Chime mode: %s", - sensor_info.first->get_name().c_str(), - TRUEFALSE(sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME), - TRUEFALSE(sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT), - TRUEFALSE(sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_AUTO), - TRUEFALSE(sensor_info.second.flags & BINARY_SENSOR_MODE_CHIME)); - const char *sensor_type; - switch (sensor_info.second.type) { - case ALARM_SENSOR_TYPE_INSTANT: - sensor_type = "instant"; - break; - case ALARM_SENSOR_TYPE_DELAYED_FOLLOWER: - sensor_type = "delayed_follower"; - break; - case ALARM_SENSOR_TYPE_INSTANT_ALWAYS: - sensor_type = "instant_always"; - break; - case ALARM_SENSOR_TYPE_DELAYED: - default: - sensor_type = "delayed"; - } - ESP_LOGCONFIG(TAG, " Sensor type: %s", sensor_type); + alarm_sensor.sensor->get_name().c_str(), LOG_STR_ARG(sensor_type_to_string(alarm_sensor.info.type)), + TRUEFALSE(flags & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME), + TRUEFALSE(flags & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT), + TRUEFALSE(flags & BINARY_SENSOR_MODE_BYPASS_AUTO), TRUEFALSE(flags & BINARY_SENSOR_MODE_CHIME)); } #endif } void TemplateAlarmControlPanel::setup() { - switch (this->restore_mode_) { - case ALARM_CONTROL_PANEL_ALWAYS_DISARMED: - this->current_state_ = ACP_STATE_DISARMED; - break; - case ALARM_CONTROL_PANEL_RESTORE_DEFAULT_DISARMED: { - uint8_t value; - this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); - if (this->pref_.load(&value)) { - this->current_state_ = static_cast(value); - } else { - this->current_state_ = ACP_STATE_DISARMED; - } - break; + this->current_state_ = ACP_STATE_DISARMED; + if (this->restore_mode_ == ALARM_CONTROL_PANEL_RESTORE_DEFAULT_DISARMED) { + uint8_t value; + this->pref_ = global_preferences->make_preference(this->get_preference_hash()); + if (this->pref_.load(&value)) { + this->current_state_ = static_cast(value); } } this->desired_state_ = this->current_state_; @@ -119,86 +112,85 @@ void TemplateAlarmControlPanel::loop() { this->publish_state(ACP_STATE_TRIGGERED); return; } - auto future_state = this->current_state_; + auto next_state = this->current_state_; // reset triggered if all clear if (this->current_state_ == ACP_STATE_TRIGGERED && this->trigger_time_ > 0 && (millis() - this->last_update_) > this->trigger_time_) { - future_state = this->desired_state_; + next_state = this->desired_state_; } - bool delayed_sensor_not_ready = false; - bool instant_sensor_not_ready = false; + bool delayed_sensor_faulted = false; + bool instant_sensor_faulted = false; #ifdef USE_BINARY_SENSOR - // Test all of the sensors in the list regardless of the alarm panel state - for (auto sensor_info : this->sensor_map_) { + // Test all of the sensors regardless of the alarm panel state + for (const auto &alarm_sensor : this->sensors_) { + const auto &info = alarm_sensor.info; + auto *sensor = alarm_sensor.sensor; // Check for chime zones - if ((sensor_info.second.flags & BINARY_SENSOR_MODE_CHIME)) { + if (info.flags & BINARY_SENSOR_MODE_CHIME) { // Look for the transition from closed to open - if ((!this->sensor_data_[sensor_info.second.store_index].last_chime_state) && (sensor_info.first->state)) { + if ((!this->sensor_data_[info.store_index].last_chime_state) && (sensor->state)) { // Must be disarmed to chime if (this->current_state_ == ACP_STATE_DISARMED) { this->chime_callback_.call(); } } // Record the sensor state change - this->sensor_data_[sensor_info.second.store_index].last_chime_state = sensor_info.first->state; + this->sensor_data_[info.store_index].last_chime_state = sensor->state; } - // Check for triggered sensors - if (sensor_info.first->state) { // Sensor triggered? + // Check for faulted sensors + if (sensor->state) { // Skip if auto bypassed if (std::count(this->bypassed_sensor_indicies_.begin(), this->bypassed_sensor_indicies_.end(), - sensor_info.second.store_index) == 1) { + info.store_index) == 1) { continue; } // Skip if bypass armed home - if (this->current_state_ == ACP_STATE_ARMED_HOME && - (sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME)) { + if ((this->current_state_ == ACP_STATE_ARMED_HOME) && (info.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME)) { continue; } // Skip if bypass armed night - if (this->current_state_ == ACP_STATE_ARMED_NIGHT && - (sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT)) { + if ((this->current_state_ == ACP_STATE_ARMED_NIGHT) && (info.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT)) { continue; } - switch (sensor_info.second.type) { - case ALARM_SENSOR_TYPE_INSTANT: - instant_sensor_not_ready = true; - break; + switch (info.type) { case ALARM_SENSOR_TYPE_INSTANT_ALWAYS: - instant_sensor_not_ready = true; - future_state = ACP_STATE_TRIGGERED; + next_state = ACP_STATE_TRIGGERED; + [[fallthrough]]; + case ALARM_SENSOR_TYPE_INSTANT: + instant_sensor_faulted = true; break; case ALARM_SENSOR_TYPE_DELAYED_FOLLOWER: // Look to see if we are in the pending state if (this->current_state_ == ACP_STATE_PENDING) { - delayed_sensor_not_ready = true; + delayed_sensor_faulted = true; } else { - instant_sensor_not_ready = true; + instant_sensor_faulted = true; } break; case ALARM_SENSOR_TYPE_DELAYED: default: - delayed_sensor_not_ready = true; + delayed_sensor_faulted = true; } } } - // Update all sensors not ready flag - this->sensors_ready_ = ((!instant_sensor_not_ready) && (!delayed_sensor_not_ready)); + // Update all sensors ready flag + bool sensors_ready = !(instant_sensor_faulted || delayed_sensor_faulted); // Call the ready state change callback if there was a change - if (this->sensors_ready_ != this->sensors_ready_last_) { + if (this->sensors_ready_ != sensors_ready) { + this->sensors_ready_ = sensors_ready; this->ready_callback_.call(); - this->sensors_ready_last_ = this->sensors_ready_; } #endif - if (this->is_state_armed(future_state) && (!this->sensors_ready_)) { + if (this->is_state_armed(next_state) && (!this->sensors_ready_)) { // Instant sensors - if (instant_sensor_not_ready) { + if (instant_sensor_faulted) { this->publish_state(ACP_STATE_TRIGGERED); - } else if (delayed_sensor_not_ready) { + } else if (delayed_sensor_faulted) { // Delayed sensors if ((this->pending_time_ > 0) && (this->current_state_ != ACP_STATE_TRIGGERED)) { this->publish_state(ACP_STATE_PENDING); @@ -206,8 +198,8 @@ void TemplateAlarmControlPanel::loop() { this->publish_state(ACP_STATE_TRIGGERED); } } - } else if (future_state != this->current_state_) { - this->publish_state(future_state); + } else if (next_state != this->current_state_) { + this->publish_state(next_state); } } @@ -234,8 +226,6 @@ uint32_t TemplateAlarmControlPanel::get_supported_features() const { return features; } -bool TemplateAlarmControlPanel::get_requires_code() const { return !this->codes_.empty(); } - void TemplateAlarmControlPanel::arm_(optional code, alarm_control_panel::AlarmControlPanelState state, uint32_t delay) { if (this->current_state_ != ACP_STATE_DISARMED) { @@ -257,11 +247,11 @@ void TemplateAlarmControlPanel::arm_(optional code, alarm_control_p void TemplateAlarmControlPanel::bypass_before_arming() { #ifdef USE_BINARY_SENSOR - for (auto sensor_info : this->sensor_map_) { - // Check for sensors left on and set to bypass automatically and remove them from monitoring - if ((sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_AUTO) && (sensor_info.first->state)) { - ESP_LOGW(TAG, "'%s' is left on and will be automatically bypassed", sensor_info.first->get_name().c_str()); - this->bypassed_sensor_indicies_.push_back(sensor_info.second.store_index); + for (const auto &alarm_sensor : this->sensors_) { + // Check for faulted bypass_auto sensors and remove them from monitoring + if ((alarm_sensor.info.flags & BINARY_SENSOR_MODE_BYPASS_AUTO) && (alarm_sensor.sensor->state)) { + ESP_LOGW(TAG, "'%s' is faulted and will be automatically bypassed", alarm_sensor.sensor->get_name().c_str()); + this->bypassed_sensor_indicies_.push_back(alarm_sensor.info.store_index); } } #endif diff --git a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h index c3b28e8efa..80ce34b8ae 100644 --- a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h @@ -1,11 +1,12 @@ #pragma once #include -#include +#include #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/defines.h" +#include "esphome/core/helpers.h" #include "esphome/components/alarm_control_panel/alarm_control_panel.h" @@ -49,24 +50,38 @@ struct SensorInfo { uint8_t store_index; }; -class TemplateAlarmControlPanel : public alarm_control_panel::AlarmControlPanel, public Component { +#ifdef USE_BINARY_SENSOR +struct AlarmSensor { + binary_sensor::BinarySensor *sensor; + SensorInfo info; +}; +#endif + +class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControlPanel, public Component { public: TemplateAlarmControlPanel(); void dump_config() override; void setup() override; void loop() override; uint32_t get_supported_features() const override; - bool get_requires_code() const override; + bool get_requires_code() const override { return !this->codes_.empty(); } bool get_requires_code_to_arm() const override { return this->requires_code_to_arm_; } bool get_all_sensors_ready() { return this->sensors_ready_; }; void set_restore_mode(TemplateAlarmControlPanelRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } void bypass_before_arming(); #ifdef USE_BINARY_SENSOR + /** Initialize the sensors vector with the specified capacity. + * + * @param capacity The number of sensors to allocate space for. + */ + void init_sensors(size_t capacity) { this->sensors_.init(capacity); } + /** Add a binary_sensor to the alarm_panel. * * @param sensor The BinarySensor instance. - * @param ignore_when_home if this should be ignored when armed_home mode + * @param flags The OR of BinarySensorFlags for the sensor. + * @param type The sensor type which determines its triggering behaviour. */ void add_sensor(binary_sensor::BinarySensor *sensor, uint16_t flags = 0, AlarmSensorType type = ALARM_SENSOR_TYPE_DELAYED); @@ -121,8 +136,8 @@ class TemplateAlarmControlPanel : public alarm_control_panel::AlarmControlPanel, protected: void control(const alarm_control_panel::AlarmControlPanelCall &call) override; #ifdef USE_BINARY_SENSOR - // This maps a binary sensor to its type and attribute bits - std::map sensor_map_; + // List of binary sensors with their alarm-specific info + FixedVector sensors_; // a list of automatically bypassed sensors std::vector bypassed_sensor_indicies_; #endif @@ -147,7 +162,6 @@ class TemplateAlarmControlPanel : public alarm_control_panel::AlarmControlPanel, bool supports_arm_home_ = false; bool supports_arm_night_ = false; bool sensors_ready_ = false; - bool sensors_ready_last_ = false; uint8_t next_store_index_ = 0; // check if the code is valid bool is_code_valid_(optional code); diff --git a/esphome/components/template/binary_sensor/__init__.py b/esphome/components/template/binary_sensor/__init__.py index c93876380d..9d4208dcca 100644 --- a/esphome/components/template/binary_sensor/__init__.py +++ b/esphome/components/template/binary_sensor/__init__.py @@ -38,8 +38,14 @@ async def to_code(config): condition = await automation.build_condition( condition, cg.TemplateArguments(), [] ) + # Generate a stateless lambda that calls condition.check() + # capture="" is safe because condition is a global variable in generated C++ code + # and doesn't need to be captured. This allows implicit conversion to function pointer. template_ = LambdaExpression( - f"return {condition.check()};", [], return_type=cg.optional.template(bool) + f"return {condition.check()};", + [], + return_type=cg.optional.template(bool), + capture="", ) cg.add(var.set_template(template_)) diff --git a/esphome/components/template/binary_sensor/template_binary_sensor.cpp b/esphome/components/template/binary_sensor/template_binary_sensor.cpp index d1fb618695..806aed49b1 100644 --- a/esphome/components/template/binary_sensor/template_binary_sensor.cpp +++ b/esphome/components/template/binary_sensor/template_binary_sensor.cpp @@ -6,17 +6,21 @@ namespace template_ { static const char *const TAG = "template.binary_sensor"; -void TemplateBinarySensor::setup() { this->loop(); } +void TemplateBinarySensor::setup() { + if (!this->f_.has_value()) { + this->disable_loop(); + } else { + this->loop(); + } +} void TemplateBinarySensor::loop() { - if (this->f_ == nullptr) - return; - auto s = this->f_(); if (s.has_value()) { this->publish_state(*s); } } + void TemplateBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "Template Binary Sensor", this); } } // namespace template_ diff --git a/esphome/components/template/binary_sensor/template_binary_sensor.h b/esphome/components/template/binary_sensor/template_binary_sensor.h index 5e5624d82e..0af709b097 100644 --- a/esphome/components/template/binary_sensor/template_binary_sensor.h +++ b/esphome/components/template/binary_sensor/template_binary_sensor.h @@ -1,14 +1,15 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/template_lambda.h" #include "esphome/components/binary_sensor/binary_sensor.h" namespace esphome { namespace template_ { -class TemplateBinarySensor : public Component, public binary_sensor::BinarySensor { +class TemplateBinarySensor final : public Component, public binary_sensor::BinarySensor { public: - void set_template(std::function()> &&f) { this->f_ = f; } + template void set_template(F &&f) { this->f_.set(std::forward(f)); } void setup() override; void loop() override; @@ -17,7 +18,7 @@ class TemplateBinarySensor : public Component, public binary_sensor::BinarySenso float get_setup_priority() const override { return setup_priority::HARDWARE; } protected: - std::function()> f_{nullptr}; + TemplateLambda f_; }; } // namespace template_ diff --git a/esphome/components/template/button/template_button.h b/esphome/components/template/button/template_button.h index 68e976f64b..5bda82c58f 100644 --- a/esphome/components/template/button/template_button.h +++ b/esphome/components/template/button/template_button.h @@ -5,7 +5,7 @@ namespace esphome { namespace template_ { -class TemplateButton : public button::Button { +class TemplateButton final : public button::Button { public: // Implements the abstract `press_action` but the `on_press` trigger already handles the press. void press_action() override{}; diff --git a/esphome/components/template/cover/template_cover.cpp b/esphome/components/template/cover/template_cover.cpp index 84c687536e..a87f28ccec 100644 --- a/esphome/components/template/cover/template_cover.cpp +++ b/esphome/components/template/cover/template_cover.cpp @@ -33,28 +33,27 @@ void TemplateCover::setup() { break; } } + if (!this->state_f_.has_value() && !this->tilt_f_.has_value()) + this->disable_loop(); } void TemplateCover::loop() { bool changed = false; - if (this->state_f_.has_value()) { - auto s = (*this->state_f_)(); - if (s.has_value()) { - auto pos = clamp(*s, 0.0f, 1.0f); - if (pos != this->position) { - this->position = pos; - changed = true; - } + auto s = this->state_f_(); + if (s.has_value()) { + auto pos = clamp(*s, 0.0f, 1.0f); + if (pos != this->position) { + this->position = pos; + changed = true; } } - if (this->tilt_f_.has_value()) { - auto s = (*this->tilt_f_)(); - if (s.has_value()) { - auto tilt = clamp(*s, 0.0f, 1.0f); - if (tilt != this->tilt) { - this->tilt = tilt; - changed = true; - } + + auto tilt = this->tilt_f_(); + if (tilt.has_value()) { + auto tilt_val = clamp(*tilt, 0.0f, 1.0f); + if (tilt_val != this->tilt) { + this->tilt = tilt_val; + changed = true; } } @@ -63,7 +62,6 @@ void TemplateCover::loop() { } void TemplateCover::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } void TemplateCover::set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } -void TemplateCover::set_state_lambda(std::function()> &&f) { this->state_f_ = f; } float TemplateCover::get_setup_priority() const { return setup_priority::HARDWARE; } Trigger<> *TemplateCover::get_open_trigger() const { return this->open_trigger_; } Trigger<> *TemplateCover::get_close_trigger() const { return this->close_trigger_; } @@ -124,7 +122,6 @@ CoverTraits TemplateCover::get_traits() { } Trigger *TemplateCover::get_position_trigger() const { return this->position_trigger_; } Trigger *TemplateCover::get_tilt_trigger() const { return this->tilt_trigger_; } -void TemplateCover::set_tilt_lambda(std::function()> &&tilt_f) { this->tilt_f_ = tilt_f; } void TemplateCover::set_has_stop(bool has_stop) { this->has_stop_ = has_stop; } void TemplateCover::set_has_toggle(bool has_toggle) { this->has_toggle_ = has_toggle; } void TemplateCover::set_has_position(bool has_position) { this->has_position_ = has_position; } diff --git a/esphome/components/template/cover/template_cover.h b/esphome/components/template/cover/template_cover.h index 958c94b0a6..125c67bb86 100644 --- a/esphome/components/template/cover/template_cover.h +++ b/esphome/components/template/cover/template_cover.h @@ -2,6 +2,7 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" +#include "esphome/core/template_lambda.h" #include "esphome/components/cover/cover.h" namespace esphome { @@ -13,11 +14,12 @@ enum TemplateCoverRestoreMode { COVER_RESTORE_AND_CALL, }; -class TemplateCover : public cover::Cover, public Component { +class TemplateCover final : public cover::Cover, public Component { public: TemplateCover(); - void set_state_lambda(std::function()> &&f); + template void set_state_lambda(F &&f) { this->state_f_.set(std::forward(f)); } + template void set_tilt_lambda(F &&f) { this->tilt_f_.set(std::forward(f)); } Trigger<> *get_open_trigger() const; Trigger<> *get_close_trigger() const; Trigger<> *get_stop_trigger() const; @@ -26,7 +28,6 @@ class TemplateCover : public cover::Cover, public Component { Trigger *get_tilt_trigger() const; void set_optimistic(bool optimistic); void set_assumed_state(bool assumed_state); - void set_tilt_lambda(std::function()> &&tilt_f); void set_has_stop(bool has_stop); void set_has_position(bool has_position); void set_has_tilt(bool has_tilt); @@ -45,8 +46,8 @@ class TemplateCover : public cover::Cover, public Component { void stop_prev_trigger_(); TemplateCoverRestoreMode restore_mode_{COVER_RESTORE}; - optional()>> state_f_; - optional()>> tilt_f_; + TemplateLambda state_f_; + TemplateLambda tilt_f_; bool assumed_state_{false}; bool optimistic_{false}; Trigger<> *open_trigger_; diff --git a/esphome/components/template/datetime/template_date.cpp b/esphome/components/template/datetime/template_date.cpp index 01e15e532e..3f6626e847 100644 --- a/esphome/components/template/datetime/template_date.cpp +++ b/esphome/components/template/datetime/template_date.cpp @@ -20,7 +20,7 @@ void TemplateDate::setup() { } else { datetime::DateEntityRestoreState temp; this->pref_ = - global_preferences->make_preference(194434030U ^ this->get_object_id_hash()); + global_preferences->make_preference(194434030U ^ this->get_preference_hash()); if (this->pref_.load(&temp)) { temp.apply(this); return; @@ -40,14 +40,13 @@ void TemplateDate::update() { if (!this->f_.has_value()) return; - auto val = (*this->f_)(); - if (!val.has_value()) - return; - - this->year_ = val->year; - this->month_ = val->month; - this->day_ = val->day_of_month; - this->publish_state(); + auto val = this->f_(); + if (val.has_value()) { + this->year_ = val->year; + this->month_ = val->month; + this->day_ = val->day_of_month; + this->publish_state(); + } } void TemplateDate::control(const datetime::DateCall &call) { diff --git a/esphome/components/template/datetime/template_date.h b/esphome/components/template/datetime/template_date.h index 185c7ed49d..fe64b0ba14 100644 --- a/esphome/components/template/datetime/template_date.h +++ b/esphome/components/template/datetime/template_date.h @@ -9,13 +9,14 @@ #include "esphome/core/component.h" #include "esphome/core/preferences.h" #include "esphome/core/time.h" +#include "esphome/core/template_lambda.h" namespace esphome { namespace template_ { -class TemplateDate : public datetime::DateEntity, public PollingComponent { +class TemplateDate final : public datetime::DateEntity, public PollingComponent { public: - void set_template(std::function()> &&f) { this->f_ = f; } + template void set_template(F &&f) { this->f_.set(std::forward(f)); } void setup() override; void update() override; @@ -35,7 +36,7 @@ class TemplateDate : public datetime::DateEntity, public PollingComponent { ESPTime initial_value_{}; bool restore_value_{false}; Trigger *set_trigger_ = new Trigger(); - optional()>> f_; + TemplateLambda f_; ESPPreferenceObject pref_; }; diff --git a/esphome/components/template/datetime/template_datetime.cpp b/esphome/components/template/datetime/template_datetime.cpp index 3ab74e197f..62f842a7ad 100644 --- a/esphome/components/template/datetime/template_datetime.cpp +++ b/esphome/components/template/datetime/template_datetime.cpp @@ -19,8 +19,8 @@ void TemplateDateTime::setup() { state = this->initial_value_; } else { datetime::DateTimeEntityRestoreState temp; - this->pref_ = global_preferences->make_preference(194434090U ^ - this->get_object_id_hash()); + this->pref_ = global_preferences->make_preference( + 194434090U ^ this->get_preference_hash()); if (this->pref_.load(&temp)) { temp.apply(this); return; @@ -43,17 +43,16 @@ void TemplateDateTime::update() { if (!this->f_.has_value()) return; - auto val = (*this->f_)(); - if (!val.has_value()) - return; - - this->year_ = val->year; - this->month_ = val->month; - this->day_ = val->day_of_month; - this->hour_ = val->hour; - this->minute_ = val->minute; - this->second_ = val->second; - this->publish_state(); + auto val = this->f_(); + if (val.has_value()) { + this->year_ = val->year; + this->month_ = val->month; + this->day_ = val->day_of_month; + this->hour_ = val->hour; + this->minute_ = val->minute; + this->second_ = val->second; + this->publish_state(); + } } void TemplateDateTime::control(const datetime::DateTimeCall &call) { diff --git a/esphome/components/template/datetime/template_datetime.h b/esphome/components/template/datetime/template_datetime.h index ef80ded89a..c44bd85265 100644 --- a/esphome/components/template/datetime/template_datetime.h +++ b/esphome/components/template/datetime/template_datetime.h @@ -9,13 +9,14 @@ #include "esphome/core/component.h" #include "esphome/core/preferences.h" #include "esphome/core/time.h" +#include "esphome/core/template_lambda.h" namespace esphome { namespace template_ { -class TemplateDateTime : public datetime::DateTimeEntity, public PollingComponent { +class TemplateDateTime final : public datetime::DateTimeEntity, public PollingComponent { public: - void set_template(std::function()> &&f) { this->f_ = f; } + template void set_template(F &&f) { this->f_.set(std::forward(f)); } void setup() override; void update() override; @@ -35,7 +36,7 @@ class TemplateDateTime : public datetime::DateTimeEntity, public PollingComponen ESPTime initial_value_{}; bool restore_value_{false}; Trigger *set_trigger_ = new Trigger(); - optional()>> f_; + TemplateLambda f_; ESPPreferenceObject pref_; }; diff --git a/esphome/components/template/datetime/template_time.cpp b/esphome/components/template/datetime/template_time.cpp index 0e4d734d16..dab28d01cc 100644 --- a/esphome/components/template/datetime/template_time.cpp +++ b/esphome/components/template/datetime/template_time.cpp @@ -20,7 +20,7 @@ void TemplateTime::setup() { } else { datetime::TimeEntityRestoreState temp; this->pref_ = - global_preferences->make_preference(194434060U ^ this->get_object_id_hash()); + global_preferences->make_preference(194434060U ^ this->get_preference_hash()); if (this->pref_.load(&temp)) { temp.apply(this); return; @@ -40,14 +40,13 @@ void TemplateTime::update() { if (!this->f_.has_value()) return; - auto val = (*this->f_)(); - if (!val.has_value()) - return; - - this->hour_ = val->hour; - this->minute_ = val->minute; - this->second_ = val->second; - this->publish_state(); + auto val = this->f_(); + if (val.has_value()) { + this->hour_ = val->hour; + this->minute_ = val->minute; + this->second_ = val->second; + this->publish_state(); + } } void TemplateTime::control(const datetime::TimeCall &call) { diff --git a/esphome/components/template/datetime/template_time.h b/esphome/components/template/datetime/template_time.h index 4a7c0098ec..0c95330d27 100644 --- a/esphome/components/template/datetime/template_time.h +++ b/esphome/components/template/datetime/template_time.h @@ -9,13 +9,14 @@ #include "esphome/core/component.h" #include "esphome/core/preferences.h" #include "esphome/core/time.h" +#include "esphome/core/template_lambda.h" namespace esphome { namespace template_ { -class TemplateTime : public datetime::TimeEntity, public PollingComponent { +class TemplateTime final : public datetime::TimeEntity, public PollingComponent { public: - void set_template(std::function()> &&f) { this->f_ = f; } + template void set_template(F &&f) { this->f_.set(std::forward(f)); } void setup() override; void update() override; @@ -35,7 +36,7 @@ class TemplateTime : public datetime::TimeEntity, public PollingComponent { ESPTime initial_value_{}; bool restore_value_{false}; Trigger *set_trigger_ = new Trigger(); - optional()>> f_; + TemplateLambda f_; ESPPreferenceObject pref_; }; diff --git a/esphome/components/template/event/template_event.h b/esphome/components/template/event/template_event.h index 251ae9299b..5467a64141 100644 --- a/esphome/components/template/event/template_event.h +++ b/esphome/components/template/event/template_event.h @@ -6,7 +6,7 @@ namespace esphome { namespace template_ { -class TemplateEvent : public Component, public event::Event {}; +class TemplateEvent final : public Component, public event::Event {}; } // namespace template_ } // namespace esphome diff --git a/esphome/components/template/fan/template_fan.cpp b/esphome/components/template/fan/template_fan.cpp index 5f4a2ae8f7..eba4c673b5 100644 --- a/esphome/components/template/fan/template_fan.cpp +++ b/esphome/components/template/fan/template_fan.cpp @@ -29,7 +29,7 @@ void TemplateFan::control(const fan::FanCall &call) { this->oscillating = *call.get_oscillating(); if (call.get_direction().has_value() && this->has_direction_) this->direction = *call.get_direction(); - this->preset_mode = call.get_preset_mode(); + this->set_preset_mode_(call.get_preset_mode()); this->publish_state(); } diff --git a/esphome/components/template/fan/template_fan.h b/esphome/components/template/fan/template_fan.h index 7f5305ca48..052b385b93 100644 --- a/esphome/components/template/fan/template_fan.h +++ b/esphome/components/template/fan/template_fan.h @@ -1,14 +1,12 @@ #pragma once -#include - #include "esphome/core/component.h" #include "esphome/components/fan/fan.h" namespace esphome { namespace template_ { -class TemplateFan : public Component, public fan::Fan { +class TemplateFan final : public Component, public fan::Fan { public: TemplateFan() {} void setup() override; @@ -16,7 +14,7 @@ class TemplateFan : public Component, public fan::Fan { void set_has_direction(bool has_direction) { this->has_direction_ = has_direction; } void set_has_oscillating(bool has_oscillating) { this->has_oscillating_ = has_oscillating; } void set_speed_count(int count) { this->speed_count_ = count; } - void set_preset_modes(const std::set &presets) { this->preset_modes_ = presets; } + void set_preset_modes(std::initializer_list presets) { this->preset_modes_ = presets; } fan::FanTraits get_traits() override { return this->traits_; } protected: @@ -26,7 +24,7 @@ class TemplateFan : public Component, public fan::Fan { bool has_direction_{false}; int speed_count_{0}; fan::FanTraits traits_; - std::set preset_modes_{}; + std::vector preset_modes_{}; }; } // namespace template_ diff --git a/esphome/components/template/lock/automation.h b/esphome/components/template/lock/automation.h index 6124546592..bd110b7b0c 100644 --- a/esphome/components/template/lock/automation.h +++ b/esphome/components/template/lock/automation.h @@ -11,7 +11,7 @@ template class TemplateLockPublishAction : public Action, public: TEMPLATABLE_VALUE(lock::LockState, state) - void play(Ts... x) override { this->parent_->publish_state(this->state_.value(x...)); } + void play(const Ts &...x) override { this->parent_->publish_state(this->state_.value(x...)); } }; } // namespace template_ diff --git a/esphome/components/template/lock/template_lock.cpp b/esphome/components/template/lock/template_lock.cpp index 87ba1046eb..8ed87b9736 100644 --- a/esphome/components/template/lock/template_lock.cpp +++ b/esphome/components/template/lock/template_lock.cpp @@ -11,14 +11,16 @@ static const char *const TAG = "template.lock"; TemplateLock::TemplateLock() : lock_trigger_(new Trigger<>()), unlock_trigger_(new Trigger<>()), open_trigger_(new Trigger<>()) {} -void TemplateLock::loop() { +void TemplateLock::setup() { if (!this->f_.has_value()) - return; - auto val = (*this->f_)(); - if (!val.has_value()) - return; + this->disable_loop(); +} - this->publish_state(*val); +void TemplateLock::loop() { + auto val = this->f_(); + if (val.has_value()) { + this->publish_state(*val); + } } void TemplateLock::control(const lock::LockCall &call) { if (this->prev_trigger_ != nullptr) { @@ -45,7 +47,6 @@ void TemplateLock::open_latch() { this->open_trigger_->trigger(); } void TemplateLock::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } -void TemplateLock::set_state_lambda(std::function()> &&f) { this->f_ = f; } float TemplateLock::get_setup_priority() const { return setup_priority::HARDWARE; } Trigger<> *TemplateLock::get_lock_trigger() const { return this->lock_trigger_; } Trigger<> *TemplateLock::get_unlock_trigger() const { return this->unlock_trigger_; } diff --git a/esphome/components/template/lock/template_lock.h b/esphome/components/template/lock/template_lock.h index 4f798eca81..ac10794e4d 100644 --- a/esphome/components/template/lock/template_lock.h +++ b/esphome/components/template/lock/template_lock.h @@ -2,18 +2,20 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" +#include "esphome/core/template_lambda.h" #include "esphome/components/lock/lock.h" namespace esphome { namespace template_ { -class TemplateLock : public lock::Lock, public Component { +class TemplateLock final : public lock::Lock, public Component { public: TemplateLock(); + void setup() override; void dump_config() override; - void set_state_lambda(std::function()> &&f); + template void set_state_lambda(F &&f) { this->f_.set(std::forward(f)); } Trigger<> *get_lock_trigger() const; Trigger<> *get_unlock_trigger() const; Trigger<> *get_open_trigger() const; @@ -26,7 +28,7 @@ class TemplateLock : public lock::Lock, public Component { void control(const lock::LockCall &call) override; void open_latch() override; - optional()>> f_; + TemplateLambda f_; bool optimistic_{false}; Trigger<> *lock_trigger_; Trigger<> *unlock_trigger_; diff --git a/esphome/components/template/number/template_number.cpp b/esphome/components/template/number/template_number.cpp index aaf5b27a71..145a89a2f7 100644 --- a/esphome/components/template/number/template_number.cpp +++ b/esphome/components/template/number/template_number.cpp @@ -14,7 +14,7 @@ void TemplateNumber::setup() { if (!this->restore_value_) { value = this->initial_value_; } else { - this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); + this->pref_ = global_preferences->make_preference(this->get_preference_hash()); if (!this->pref_.load(&value)) { if (!std::isnan(this->initial_value_)) { value = this->initial_value_; @@ -30,11 +30,10 @@ void TemplateNumber::update() { if (!this->f_.has_value()) return; - auto val = (*this->f_)(); - if (!val.has_value()) - return; - - this->publish_state(*val); + auto val = this->f_(); + if (val.has_value()) { + this->publish_state(*val); + } } void TemplateNumber::control(float value) { diff --git a/esphome/components/template/number/template_number.h b/esphome/components/template/number/template_number.h index 9a82e44339..876ec96b3b 100644 --- a/esphome/components/template/number/template_number.h +++ b/esphome/components/template/number/template_number.h @@ -4,13 +4,14 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/preferences.h" +#include "esphome/core/template_lambda.h" namespace esphome { namespace template_ { -class TemplateNumber : public number::Number, public PollingComponent { +class TemplateNumber final : public number::Number, public PollingComponent { public: - void set_template(std::function()> &&f) { this->f_ = f; } + template void set_template(F &&f) { this->f_.set(std::forward(f)); } void setup() override; void update() override; @@ -28,7 +29,7 @@ class TemplateNumber : public number::Number, public PollingComponent { float initial_value_{NAN}; bool restore_value_{false}; Trigger *set_trigger_ = new Trigger(); - optional()>> f_; + TemplateLambda f_; ESPPreferenceObject pref_; }; diff --git a/esphome/components/template/output/template_output.h b/esphome/components/template/output/template_output.h index 90de801a5c..9ecfc446b9 100644 --- a/esphome/components/template/output/template_output.h +++ b/esphome/components/template/output/template_output.h @@ -7,7 +7,7 @@ namespace esphome { namespace template_ { -class TemplateBinaryOutput : public output::BinaryOutput { +class TemplateBinaryOutput final : public output::BinaryOutput { public: Trigger *get_trigger() const { return trigger_; } @@ -17,7 +17,7 @@ class TemplateBinaryOutput : public output::BinaryOutput { Trigger *trigger_ = new Trigger(); }; -class TemplateFloatOutput : public output::FloatOutput { +class TemplateFloatOutput final : public output::FloatOutput { public: Trigger *get_trigger() const { return trigger_; } diff --git a/esphome/components/template/select/__init__.py b/esphome/components/template/select/__init__.py index 3282092d63..0e9c240547 100644 --- a/esphome/components/template/select/__init__.py +++ b/esphome/components/template/select/__init__.py @@ -73,11 +73,18 @@ async def to_code(config): cg.add(var.set_template(template_)) else: - cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) - cg.add(var.set_initial_option(config[CONF_INITIAL_OPTION])) + # Only set if non-default to avoid bloating setup() function + if config[CONF_OPTIMISTIC]: + cg.add(var.set_optimistic(True)) + initial_option_index = config[CONF_OPTIONS].index(config[CONF_INITIAL_OPTION]) + # Only set if non-zero to avoid bloating setup() function + # (initial_option_index_ is zero-initialized in the header) + if initial_option_index != 0: + cg.add(var.set_initial_option_index(initial_option_index)) - if CONF_RESTORE_VALUE in config: - cg.add(var.set_restore_value(config[CONF_RESTORE_VALUE])) + # Only set if True (default is False) + if config.get(CONF_RESTORE_VALUE): + cg.add(var.set_restore_value(True)) if CONF_SET_ACTION in config: await automation.build_automation( diff --git a/esphome/components/template/select/template_select.cpp b/esphome/components/template/select/template_select.cpp index 6ec29c8ef0..112f24e919 100644 --- a/esphome/components/template/select/template_select.cpp +++ b/esphome/components/template/select/template_select.cpp @@ -10,54 +10,45 @@ void TemplateSelect::setup() { if (this->f_.has_value()) return; - std::string value; - if (!this->restore_value_) { - value = this->initial_option_; - ESP_LOGD(TAG, "State from initial: %s", value.c_str()); - } else { - size_t index; - this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); - if (!this->pref_.load(&index)) { - value = this->initial_option_; - ESP_LOGD(TAG, "State from initial (could not load stored index): %s", value.c_str()); - } else if (!this->has_index(index)) { - value = this->initial_option_; - ESP_LOGD(TAG, "State from initial (restored index %d out of bounds): %s", index, value.c_str()); + size_t index = this->initial_option_index_; + if (this->restore_value_) { + this->pref_ = global_preferences->make_preference(this->get_preference_hash()); + size_t restored_index; + if (this->pref_.load(&restored_index) && this->has_index(restored_index)) { + index = restored_index; + ESP_LOGD(TAG, "State from restore: %s", this->option_at(index)); } else { - value = this->at(index).value(); - ESP_LOGD(TAG, "State from restore: %s", value.c_str()); + ESP_LOGD(TAG, "State from initial (could not load or invalid stored index): %s", this->option_at(index)); } + } else { + ESP_LOGD(TAG, "State from initial: %s", this->option_at(index)); } - this->publish_state(value); + this->publish_state(index); } void TemplateSelect::update() { if (!this->f_.has_value()) return; - auto val = (*this->f_)(); - if (!val.has_value()) - return; - - if (!this->has_option(*val)) { - ESP_LOGE(TAG, "Lambda returned an invalid option: %s", (*val).c_str()); - return; + auto val = this->f_(); + if (val.has_value()) { + if (!this->has_option(*val)) { + ESP_LOGE(TAG, "Lambda returned an invalid option: %s", (*val).c_str()); + return; + } + this->publish_state(*val); } - - this->publish_state(*val); } -void TemplateSelect::control(const std::string &value) { - this->set_trigger_->trigger(value); +void TemplateSelect::control(size_t index) { + this->set_trigger_->trigger(std::string(this->option_at(index))); if (this->optimistic_) - this->publish_state(value); + this->publish_state(index); - if (this->restore_value_) { - auto index = this->index_of(value); - this->pref_.save(&index.value()); - } + if (this->restore_value_) + this->pref_.save(&index); } void TemplateSelect::dump_config() { @@ -69,7 +60,7 @@ void TemplateSelect::dump_config() { " Optimistic: %s\n" " Initial Option: %s\n" " Restore Value: %s", - YESNO(this->optimistic_), this->initial_option_.c_str(), YESNO(this->restore_value_)); + YESNO(this->optimistic_), this->option_at(this->initial_option_index_), YESNO(this->restore_value_)); } } // namespace template_ diff --git a/esphome/components/template/select/template_select.h b/esphome/components/template/select/template_select.h index 2f00765c3d..cb5b546976 100644 --- a/esphome/components/template/select/template_select.h +++ b/esphome/components/template/select/template_select.h @@ -4,13 +4,14 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/preferences.h" +#include "esphome/core/template_lambda.h" namespace esphome { namespace template_ { -class TemplateSelect : public select::Select, public PollingComponent { +class TemplateSelect final : public select::Select, public PollingComponent { public: - void set_template(std::function()> &&f) { this->f_ = f; } + template void set_template(F &&f) { this->f_.set(std::forward(f)); } void setup() override; void update() override; @@ -19,16 +20,16 @@ class TemplateSelect : public select::Select, public PollingComponent { Trigger *get_set_trigger() const { return this->set_trigger_; } void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } - void set_initial_option(const std::string &initial_option) { this->initial_option_ = initial_option; } + void set_initial_option_index(size_t initial_option_index) { this->initial_option_index_ = initial_option_index; } void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; } protected: - void control(const std::string &value) override; + void control(size_t index) override; bool optimistic_ = false; - std::string initial_option_; + size_t initial_option_index_{0}; bool restore_value_ = false; Trigger *set_trigger_ = new Trigger(); - optional()>> f_; + TemplateLambda f_; ESPPreferenceObject pref_; }; diff --git a/esphome/components/template/sensor/template_sensor.cpp b/esphome/components/template/sensor/template_sensor.cpp index f2d0e7363e..1558ea9b15 100644 --- a/esphome/components/template/sensor/template_sensor.cpp +++ b/esphome/components/template/sensor/template_sensor.cpp @@ -11,13 +11,14 @@ void TemplateSensor::update() { if (!this->f_.has_value()) return; - auto val = (*this->f_)(); + auto val = this->f_(); if (val.has_value()) { this->publish_state(*val); } } + float TemplateSensor::get_setup_priority() const { return setup_priority::HARDWARE; } -void TemplateSensor::set_template(std::function()> &&f) { this->f_ = f; } + void TemplateSensor::dump_config() { LOG_SENSOR("", "Template Sensor", this); LOG_UPDATE_INTERVAL(this); diff --git a/esphome/components/template/sensor/template_sensor.h b/esphome/components/template/sensor/template_sensor.h index 2630cb0b14..3ca965dde3 100644 --- a/esphome/components/template/sensor/template_sensor.h +++ b/esphome/components/template/sensor/template_sensor.h @@ -1,14 +1,15 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/template_lambda.h" #include "esphome/components/sensor/sensor.h" namespace esphome { namespace template_ { -class TemplateSensor : public sensor::Sensor, public PollingComponent { +class TemplateSensor final : public sensor::Sensor, public PollingComponent { public: - void set_template(std::function()> &&f); + template void set_template(F &&f) { this->f_.set(std::forward(f)); } void update() override; @@ -17,7 +18,7 @@ class TemplateSensor : public sensor::Sensor, public PollingComponent { float get_setup_priority() const override; protected: - optional()>> f_; + TemplateLambda f_; }; } // namespace template_ diff --git a/esphome/components/template/switch/template_switch.cpp b/esphome/components/template/switch/template_switch.cpp index fa236f6364..95e8692da5 100644 --- a/esphome/components/template/switch/template_switch.cpp +++ b/esphome/components/template/switch/template_switch.cpp @@ -9,13 +9,10 @@ static const char *const TAG = "template.switch"; TemplateSwitch::TemplateSwitch() : turn_on_trigger_(new Trigger<>()), turn_off_trigger_(new Trigger<>()) {} void TemplateSwitch::loop() { - if (!this->f_.has_value()) - return; - auto s = (*this->f_)(); - if (!s.has_value()) - return; - - this->publish_state(*s); + auto s = this->f_(); + if (s.has_value()) { + this->publish_state(*s); + } } void TemplateSwitch::write_state(bool state) { if (this->prev_trigger_ != nullptr) { @@ -35,11 +32,13 @@ void TemplateSwitch::write_state(bool state) { } void TemplateSwitch::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } bool TemplateSwitch::assumed_state() { return this->assumed_state_; } -void TemplateSwitch::set_state_lambda(std::function()> &&f) { this->f_ = f; } float TemplateSwitch::get_setup_priority() const { return setup_priority::HARDWARE - 2.0f; } Trigger<> *TemplateSwitch::get_turn_on_trigger() const { return this->turn_on_trigger_; } Trigger<> *TemplateSwitch::get_turn_off_trigger() const { return this->turn_off_trigger_; } void TemplateSwitch::setup() { + if (!this->f_.has_value()) + this->disable_loop(); + optional initial_state = this->get_initial_state_with_restore_mode(); if (initial_state.has_value()) { diff --git a/esphome/components/template/switch/template_switch.h b/esphome/components/template/switch/template_switch.h index bfe9ac25d6..35c18af448 100644 --- a/esphome/components/template/switch/template_switch.h +++ b/esphome/components/template/switch/template_switch.h @@ -2,19 +2,20 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" +#include "esphome/core/template_lambda.h" #include "esphome/components/switch/switch.h" namespace esphome { namespace template_ { -class TemplateSwitch : public switch_::Switch, public Component { +class TemplateSwitch final : public switch_::Switch, public Component { public: TemplateSwitch(); void setup() override; void dump_config() override; - void set_state_lambda(std::function()> &&f); + template void set_state_lambda(F &&f) { this->f_.set(std::forward(f)); } Trigger<> *get_turn_on_trigger() const; Trigger<> *get_turn_off_trigger() const; void set_optimistic(bool optimistic); @@ -28,7 +29,7 @@ class TemplateSwitch : public switch_::Switch, public Component { void write_state(bool state) override; - optional()>> f_; + TemplateLambda f_; bool optimistic_{false}; bool assumed_state_{false}; Trigger<> *turn_on_trigger_; diff --git a/esphome/components/template/text/template_text.cpp b/esphome/components/template/text/template_text.cpp index f5df7287c5..a917c72a14 100644 --- a/esphome/components/template/text/template_text.cpp +++ b/esphome/components/template/text/template_text.cpp @@ -7,15 +7,13 @@ namespace template_ { static const char *const TAG = "template.text"; void TemplateText::setup() { - if (!(this->f_ == nullptr)) { - if (this->f_.has_value()) - return; - } + if (this->f_.has_value()) + return; std::string value = this->initial_value_; if (!this->pref_) { ESP_LOGD(TAG, "State from initial: %s", value.c_str()); } else { - uint32_t key = this->get_object_id_hash(); + uint32_t key = this->get_preference_hash(); key += this->traits.get_min_length() << 2; key += this->traits.get_max_length() << 4; key += fnv1_hash(this->traits.get_pattern()) << 6; @@ -26,17 +24,13 @@ void TemplateText::setup() { } void TemplateText::update() { - if (this->f_ == nullptr) - return; - if (!this->f_.has_value()) return; - auto val = (*this->f_)(); - if (!val.has_value()) - return; - - this->publish_state(*val); + auto val = this->f_(); + if (val.has_value()) { + this->publish_state(*val); + } } void TemplateText::control(const std::string &value) { diff --git a/esphome/components/template/text/template_text.h b/esphome/components/template/text/template_text.h index bcfc54a2ba..1a0a66ed5b 100644 --- a/esphome/components/template/text/template_text.h +++ b/esphome/components/template/text/template_text.h @@ -4,6 +4,7 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/preferences.h" +#include "esphome/core/template_lambda.h" namespace esphome { namespace template_ { @@ -59,9 +60,9 @@ template class TextSaver : public TemplateTextSaverBase { } }; -class TemplateText : public text::Text, public PollingComponent { +class TemplateText final : public text::Text, public PollingComponent { public: - void set_template(std::function()> &&f) { this->f_ = f; } + template void set_template(F &&f) { this->f_.set(std::forward(f)); } void setup() override; void update() override; @@ -78,7 +79,7 @@ class TemplateText : public text::Text, public PollingComponent { bool optimistic_ = false; std::string initial_value_; Trigger *set_trigger_ = new Trigger(); - optional()>> f_{nullptr}; + TemplateLambda f_{}; TemplateTextSaverBase *pref_ = nullptr; }; diff --git a/esphome/components/template/text_sensor/template_text_sensor.cpp b/esphome/components/template/text_sensor/template_text_sensor.cpp index 885ad47bbf..024d0093a2 100644 --- a/esphome/components/template/text_sensor/template_text_sensor.cpp +++ b/esphome/components/template/text_sensor/template_text_sensor.cpp @@ -10,13 +10,14 @@ void TemplateTextSensor::update() { if (!this->f_.has_value()) return; - auto val = (*this->f_)(); + auto val = this->f_(); if (val.has_value()) { this->publish_state(*val); } } + float TemplateTextSensor::get_setup_priority() const { return setup_priority::HARDWARE; } -void TemplateTextSensor::set_template(std::function()> &&f) { this->f_ = f; } + void TemplateTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Template Sensor", this); } } // namespace template_ diff --git a/esphome/components/template/text_sensor/template_text_sensor.h b/esphome/components/template/text_sensor/template_text_sensor.h index 07a2bd96fc..da5c518c7f 100644 --- a/esphome/components/template/text_sensor/template_text_sensor.h +++ b/esphome/components/template/text_sensor/template_text_sensor.h @@ -2,14 +2,15 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" +#include "esphome/core/template_lambda.h" #include "esphome/components/text_sensor/text_sensor.h" namespace esphome { namespace template_ { -class TemplateTextSensor : public text_sensor::TextSensor, public PollingComponent { +class TemplateTextSensor final : public text_sensor::TextSensor, public PollingComponent { public: - void set_template(std::function()> &&f); + template void set_template(F &&f) { this->f_.set(std::forward(f)); } void update() override; @@ -18,7 +19,7 @@ class TemplateTextSensor : public text_sensor::TextSensor, public PollingCompone void dump_config() override; protected: - optional()>> f_{}; + TemplateLambda f_{}; }; } // namespace template_ diff --git a/esphome/components/template/valve/automation.h b/esphome/components/template/valve/automation.h index af9b070c60..e3f394ac7c 100644 --- a/esphome/components/template/valve/automation.h +++ b/esphome/components/template/valve/automation.h @@ -11,7 +11,7 @@ template class TemplateValvePublishAction : public Action TEMPLATABLE_VALUE(float, position) TEMPLATABLE_VALUE(valve::ValveOperation, current_operation) - void play(Ts... x) override { + void play(const Ts &...x) override { if (this->position_.has_value()) this->parent_->position = this->position_.value(x...); if (this->current_operation_.has_value()) diff --git a/esphome/components/template/valve/template_valve.cpp b/esphome/components/template/valve/template_valve.cpp index 5fa14a2de7..b91b32473e 100644 --- a/esphome/components/template/valve/template_valve.cpp +++ b/esphome/components/template/valve/template_valve.cpp @@ -33,19 +33,19 @@ void TemplateValve::setup() { break; } } + if (!this->state_f_.has_value()) + this->disable_loop(); } void TemplateValve::loop() { bool changed = false; - if (this->state_f_.has_value()) { - auto s = (*this->state_f_)(); - if (s.has_value()) { - auto pos = clamp(*s, 0.0f, 1.0f); - if (pos != this->position) { - this->position = pos; - changed = true; - } + auto s = this->state_f_(); + if (s.has_value()) { + auto pos = clamp(*s, 0.0f, 1.0f); + if (pos != this->position) { + this->position = pos; + changed = true; } } @@ -55,7 +55,6 @@ void TemplateValve::loop() { void TemplateValve::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } void TemplateValve::set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } -void TemplateValve::set_state_lambda(std::function()> &&f) { this->state_f_ = f; } float TemplateValve::get_setup_priority() const { return setup_priority::HARDWARE; } Trigger<> *TemplateValve::get_open_trigger() const { return this->open_trigger_; } diff --git a/esphome/components/template/valve/template_valve.h b/esphome/components/template/valve/template_valve.h index 5e3fb6aff3..c452648193 100644 --- a/esphome/components/template/valve/template_valve.h +++ b/esphome/components/template/valve/template_valve.h @@ -2,6 +2,7 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" +#include "esphome/core/template_lambda.h" #include "esphome/components/valve/valve.h" namespace esphome { @@ -13,11 +14,11 @@ enum TemplateValveRestoreMode { VALVE_RESTORE_AND_CALL, }; -class TemplateValve : public valve::Valve, public Component { +class TemplateValve final : public valve::Valve, public Component { public: TemplateValve(); - void set_state_lambda(std::function()> &&f); + template void set_state_lambda(F &&f) { this->state_f_.set(std::forward(f)); } Trigger<> *get_open_trigger() const; Trigger<> *get_close_trigger() const; Trigger<> *get_stop_trigger() const; @@ -42,7 +43,7 @@ class TemplateValve : public valve::Valve, public Component { void stop_prev_trigger_(); TemplateValveRestoreMode restore_mode_{VALVE_NO_RESTORE}; - optional()>> state_f_; + TemplateLambda state_f_; bool assumed_state_{false}; bool optimistic_{false}; Trigger<> *open_trigger_; diff --git a/esphome/components/text/__init__.py b/esphome/components/text/__init__.py index aa831d1f06..9ceea0dfdf 100644 --- a/esphome/components/text/__init__.py +++ b/esphome/components/text/__init__.py @@ -13,7 +13,7 @@ from esphome.const import ( CONF_VALUE, CONF_WEB_SERVER, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass @@ -84,11 +84,6 @@ def text_schema( return _TEXT_SCHEMA.extend(schema) -# Remove before 2025.11.0 -TEXT_SCHEMA = text_schema() -TEXT_SCHEMA.add_extra(cv.deprecated_schema_constant("text")) - - async def setup_text_core_( var, config, @@ -149,7 +144,7 @@ async def new_text( return var -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(text_ns.using) diff --git a/esphome/components/text/automation.h b/esphome/components/text/automation.h index f20a4f433b..e7667fe491 100644 --- a/esphome/components/text/automation.h +++ b/esphome/components/text/automation.h @@ -19,7 +19,7 @@ template class TextSetAction : public Action { explicit TextSetAction(Text *text) : text_(text) {} TEMPLATABLE_VALUE(std::string, value) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->text_->make_call(); call.set_value(this->value_.value(x...)); call.perform(); diff --git a/esphome/components/text/text.cpp b/esphome/components/text/text.cpp index 654893d4e4..933d82c85c 100644 --- a/esphome/components/text/text.cpp +++ b/esphome/components/text/text.cpp @@ -1,4 +1,6 @@ #include "text.h" +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" namespace esphome { @@ -16,6 +18,9 @@ void Text::publish_state(const std::string &state) { ESP_LOGD(TAG, "'%s': Sending state %s", this->get_name().c_str(), state.c_str()); } this->state_callback_.call(state); +#if defined(USE_TEXT) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_text_update(this); +#endif } void Text::add_on_state_callback(std::function &&callback) { diff --git a/esphome/components/text/text.h b/esphome/components/text/text.h index 3cc0cefc3e..74d08eda8a 100644 --- a/esphome/components/text/text.h +++ b/esphome/components/text/text.h @@ -12,8 +12,8 @@ namespace text { #define LOG_TEXT(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - if (!(obj)->get_icon().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \ + if (!(obj)->get_icon_ref().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon_ref().c_str()); \ } \ } diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index e4aa701a7b..0d22400a8e 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -20,7 +20,7 @@ from esphome.const import ( DEVICE_CLASS_EMPTY, DEVICE_CLASS_TIMESTAMP, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass from esphome.util import Registry @@ -57,6 +57,7 @@ validate_filters = cv.validate_registry("filter", FILTER_REGISTRY) # Filters Filter = text_sensor_ns.class_("Filter") LambdaFilter = text_sensor_ns.class_("LambdaFilter", Filter) +StatelessLambdaFilter = text_sensor_ns.class_("StatelessLambdaFilter", Filter) ToUpperFilter = text_sensor_ns.class_("ToUpperFilter", Filter) ToLowerFilter = text_sensor_ns.class_("ToLowerFilter", Filter) AppendFilter = text_sensor_ns.class_("AppendFilter", Filter) @@ -70,7 +71,7 @@ async def lambda_filter_to_code(config, filter_id): lambda_ = await cg.process_lambda( config, [(cg.std_string, "x")], return_type=cg.optional.template(cg.std_string) ) - return cg.new_Pvariable(filter_id, lambda_) + return automation.new_lambda_pvariable(filter_id, lambda_, StatelessLambdaFilter) @FILTER_REGISTRY.register("to_upper", ToUpperFilter, {}) @@ -110,17 +111,28 @@ def validate_mapping(value): "substitute", SubstituteFilter, cv.ensure_list(validate_mapping) ) async def substitute_filter_to_code(config, filter_id): - from_strings = [conf[CONF_FROM] for conf in config] - to_strings = [conf[CONF_TO] for conf in config] - return cg.new_Pvariable(filter_id, from_strings, to_strings) + substitutions = [ + cg.StructInitializer( + cg.MockObj("Substitution", "esphome::text_sensor::"), + ("from", conf[CONF_FROM]), + ("to", conf[CONF_TO]), + ) + for conf in config + ] + return cg.new_Pvariable(filter_id, substitutions) @FILTER_REGISTRY.register("map", MapFilter, cv.ensure_list(validate_mapping)) async def map_filter_to_code(config, filter_id): - map_ = cg.std_ns.class_("map").template(cg.std_string, cg.std_string) - return cg.new_Pvariable( - filter_id, map_([(item[CONF_FROM], item[CONF_TO]) for item in config]) - ) + mappings = [ + cg.StructInitializer( + cg.MockObj("Substitution", "esphome::text_sensor::"), + ("from", conf[CONF_FROM]), + ("to", conf[CONF_TO]), + ) + for conf in config + ] + return cg.new_Pvariable(filter_id, mappings) validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") @@ -181,11 +193,6 @@ def text_sensor_schema( return _TEXT_SENSOR_SCHEMA.extend(schema) -# Remove before 2025.11.0 -TEXT_SENSOR_SCHEMA = text_sensor_schema() -TEXT_SENSOR_SCHEMA.add_extra(cv.deprecated_schema_constant("text_sensor")) - - async def build_filters(config): return await cg.build_registry_list(FILTER_REGISTRY, config) @@ -230,7 +237,7 @@ async def new_text_sensor(config, *args): return var -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(text_sensor_ns.using) diff --git a/esphome/components/text_sensor/automation.h b/esphome/components/text_sensor/automation.h index d7286845e0..709c54c140 100644 --- a/esphome/components/text_sensor/automation.h +++ b/esphome/components/text_sensor/automation.h @@ -29,7 +29,7 @@ template class TextSensorStateCondition : public Conditionparent_->state == this->state_.value(x...); } + bool check(const Ts &...x) override { return this->parent_->state == this->state_.value(x...); } protected: TextSensor *parent_; @@ -40,7 +40,7 @@ template class TextSensorPublishAction : public Action { TextSensorPublishAction(TextSensor *sensor) : sensor_(sensor) {} TEMPLATABLE_VALUE(std::string, state) - void play(Ts... x) override { this->sensor_->publish_state(this->state_.value(x...)); } + void play(const Ts &...x) override { this->sensor_->publish_state(this->state_.value(x...)); } protected: TextSensor *sensor_; diff --git a/esphome/components/text_sensor/filter.cpp b/esphome/components/text_sensor/filter.cpp index 80edae2b6c..40a37febee 100644 --- a/esphome/components/text_sensor/filter.cpp +++ b/esphome/components/text_sensor/filter.cpp @@ -62,19 +62,31 @@ optional AppendFilter::new_value(std::string value) { return value optional PrependFilter::new_value(std::string value) { return this->prefix_ + value; } // Substitute +SubstituteFilter::SubstituteFilter(const std::initializer_list &substitutions) + : substitutions_(substitutions) {} + optional SubstituteFilter::new_value(std::string value) { - std::size_t pos; - for (size_t i = 0; i < this->from_strings_.size(); i++) { - while ((pos = value.find(this->from_strings_[i])) != std::string::npos) - value.replace(pos, this->from_strings_[i].size(), this->to_strings_[i]); + for (const auto &sub : this->substitutions_) { + std::size_t pos = 0; + while ((pos = value.find(sub.from, pos)) != std::string::npos) { + value.replace(pos, sub.from.size(), sub.to); + // Advance past the replacement to avoid infinite loop when + // the replacement contains the search pattern (e.g., f -> foo) + pos += sub.to.size(); + } } return value; } // Map +MapFilter::MapFilter(const std::initializer_list &mappings) : mappings_(mappings) {} + optional MapFilter::new_value(std::string value) { - auto item = mappings_.find(value); - return item == mappings_.end() ? value : item->second; + for (const auto &mapping : this->mappings_) { + if (mapping.from == value) + return mapping.to; + } + return value; // Pass through if no match } } // namespace text_sensor diff --git a/esphome/components/text_sensor/filter.h b/esphome/components/text_sensor/filter.h index 2de9010b88..85acac5c8d 100644 --- a/esphome/components/text_sensor/filter.h +++ b/esphome/components/text_sensor/filter.h @@ -2,10 +2,6 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" -#include -#include -#include -#include namespace esphome { namespace text_sensor { @@ -66,6 +62,21 @@ class LambdaFilter : public Filter { lambda_filter_t lambda_filter_; }; +/** Optimized lambda filter for stateless lambdas (no capture). + * + * Uses function pointer instead of std::function to reduce memory overhead. + * Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function). + */ +class StatelessLambdaFilter : public Filter { + public: + explicit StatelessLambdaFilter(optional (*lambda_filter)(std::string)) : lambda_filter_(lambda_filter) {} + + optional new_value(std::string value) override { return this->lambda_filter_(value); } + + protected: + optional (*lambda_filter_)(std::string); +}; + /// A simple filter that converts all text to uppercase class ToUpperFilter : public Filter { public: @@ -98,26 +109,52 @@ class PrependFilter : public Filter { std::string prefix_; }; +struct Substitution { + std::string from; + std::string to; +}; + /// A simple filter that replaces a substring with another substring class SubstituteFilter : public Filter { public: - SubstituteFilter(std::vector from_strings, std::vector to_strings) - : from_strings_(std::move(from_strings)), to_strings_(std::move(to_strings)) {} + explicit SubstituteFilter(const std::initializer_list &substitutions); optional new_value(std::string value) override; protected: - std::vector from_strings_; - std::vector to_strings_; + FixedVector substitutions_; }; -/// A filter that maps values from one set to another +/** A filter that maps values from one set to another + * + * Uses linear search instead of std::map for typical small datasets (2-20 mappings). + * Linear search on contiguous memory is faster than red-black tree lookups when: + * - Dataset is small (< ~30 items) + * - Memory is contiguous (cache-friendly, better CPU cache utilization) + * - No pointer chasing overhead (tree node traversal) + * - String comparison cost dominates lookup time + * + * Benchmark results (see benchmark_map_filter.cpp): + * - 2 mappings: Linear 1.26x faster than std::map + * - 5 mappings: Linear 2.25x faster than std::map + * - 10 mappings: Linear 1.83x faster than std::map + * - 20 mappings: Linear 1.59x faster than std::map + * - 30 mappings: Linear 1.09x faster than std::map + * - 40 mappings: std::map 1.27x faster than Linear (break-even) + * + * Benefits over std::map: + * - ~2KB smaller flash (no red-black tree code) + * - ~24-32 bytes less RAM per mapping (no tree node overhead) + * - Faster for typical ESPHome usage (2-10 mappings common, 20+ rare) + * + * Break-even point: ~35-40 mappings, but ESPHome configs rarely exceed 20 + */ class MapFilter : public Filter { public: - MapFilter(std::map mappings) : mappings_(std::move(mappings)) {} + explicit MapFilter(const std::initializer_list &mappings); optional new_value(std::string value) override; protected: - std::map mappings_; + FixedVector mappings_; }; } // namespace text_sensor diff --git a/esphome/components/text_sensor/text_sensor.cpp b/esphome/components/text_sensor/text_sensor.cpp index 72b540b84c..a7bcf19967 100644 --- a/esphome/components/text_sensor/text_sensor.cpp +++ b/esphome/components/text_sensor/text_sensor.cpp @@ -1,4 +1,6 @@ #include "text_sensor.h" +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" namespace esphome { @@ -6,6 +8,22 @@ namespace text_sensor { static const char *const TAG = "text_sensor"; +void log_text_sensor(const char *tag, const char *prefix, const char *type, TextSensor *obj) { + if (obj == nullptr) { + return; + } + + ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str()); + + if (!obj->get_device_class_ref().empty()) { + ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj->get_device_class_ref().c_str()); + } + + if (!obj->get_icon_ref().empty()) { + ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj->get_icon_ref().c_str()); + } +} + void TextSensor::publish_state(const std::string &state) { this->raw_state = state; if (this->raw_callback_) { @@ -35,12 +53,12 @@ void TextSensor::add_filter(Filter *filter) { } filter->initialize(this, nullptr); } -void TextSensor::add_filters(const std::vector &filters) { +void TextSensor::add_filters(std::initializer_list filters) { for (Filter *filter : filters) { this->add_filter(filter); } } -void TextSensor::set_filters(const std::vector &filters) { +void TextSensor::set_filters(std::initializer_list filters) { this->clear_filters(); this->add_filters(filters); } @@ -68,6 +86,9 @@ void TextSensor::internal_send_state_to_frontend(const std::string &state) { this->set_has_state(true); ESP_LOGD(TAG, "'%s': Sending state '%s'", this->name_.c_str(), state.c_str()); this->callback_.call(state); +#if defined(USE_TEXT_SENSOR) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_text_sensor_update(this); +#endif } } // namespace text_sensor diff --git a/esphome/components/text_sensor/text_sensor.h b/esphome/components/text_sensor/text_sensor.h index b54f75155b..db2e857ae3 100644 --- a/esphome/components/text_sensor/text_sensor.h +++ b/esphome/components/text_sensor/text_sensor.h @@ -5,22 +5,15 @@ #include "esphome/core/helpers.h" #include "esphome/components/text_sensor/filter.h" -#include +#include #include namespace esphome { namespace text_sensor { -#define LOG_TEXT_SENSOR(prefix, type, obj) \ - if ((obj) != nullptr) { \ - ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - if (!(obj)->get_device_class().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class().c_str()); \ - } \ - if (!(obj)->get_icon().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \ - } \ - } +void log_text_sensor(const char *tag, const char *prefix, const char *type, TextSensor *obj); + +#define LOG_TEXT_SENSOR(prefix, type, obj) log_text_sensor(TAG, prefix, LOG_STR_LITERAL(type), obj) #define SUB_TEXT_SENSOR(name) \ protected: \ @@ -44,10 +37,10 @@ class TextSensor : public EntityBase, public EntityBase_DeviceClass { void add_filter(Filter *filter); /// Add a list of vectors to the back of the filter chain. - void add_filters(const std::vector &filters); + void add_filters(std::initializer_list filters); /// Clear the filters and replace them by filters. - void set_filters(const std::vector &filters); + void set_filters(std::initializer_list filters); /// Clear the entire filter chain. void clear_filters(); diff --git a/esphome/components/thermopro_ble/__init__.py b/esphome/components/thermopro_ble/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/thermopro_ble/sensor.py b/esphome/components/thermopro_ble/sensor.py new file mode 100644 index 0000000000..de63229621 --- /dev/null +++ b/esphome/components/thermopro_ble/sensor.py @@ -0,0 +1,97 @@ +import esphome.codegen as cg +from esphome.components import esp32_ble_tracker, sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_BATTERY_LEVEL, + CONF_EXTERNAL_TEMPERATURE, + CONF_HUMIDITY, + CONF_ID, + CONF_MAC_ADDRESS, + CONF_SIGNAL_STRENGTH, + CONF_TEMPERATURE, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_TEMPERATURE, + ENTITY_CATEGORY_DIAGNOSTIC, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_DECIBEL_MILLIWATT, + UNIT_PERCENT, +) + +CODEOWNERS = ["@sittner"] + +DEPENDENCIES = ["esp32_ble_tracker"] + +thermopro_ble_ns = cg.esphome_ns.namespace("thermopro_ble") +ThermoProBLE = thermopro_ble_ns.class_( + "ThermoProBLE", esp32_ble_tracker.ESPBTDeviceListener, cg.Component +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ThermoProBLE), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_EXTERNAL_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SIGNAL_STRENGTH): sensor.sensor_schema( + unit_of_measurement=UNIT_DECIBEL_MILLIWATT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ) + .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await esp32_ble_tracker.register_ble_device(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + + if temperature_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature_config) + cg.add(var.set_temperature(sens)) + if external_temperature_config := config.get(CONF_EXTERNAL_TEMPERATURE): + sens = await sensor.new_sensor(external_temperature_config) + cg.add(var.set_external_temperature(sens)) + if humidity_config := config.get(CONF_HUMIDITY): + sens = await sensor.new_sensor(humidity_config) + cg.add(var.set_humidity(sens)) + if battery_level_config := config.get(CONF_BATTERY_LEVEL): + sens = await sensor.new_sensor(battery_level_config) + cg.add(var.set_battery_level(sens)) + if signal_strength_config := config.get(CONF_SIGNAL_STRENGTH): + sens = await sensor.new_sensor(signal_strength_config) + cg.add(var.set_signal_strength(sens)) diff --git a/esphome/components/thermopro_ble/thermopro_ble.cpp b/esphome/components/thermopro_ble/thermopro_ble.cpp new file mode 100644 index 0000000000..4b43c9b39e --- /dev/null +++ b/esphome/components/thermopro_ble/thermopro_ble.cpp @@ -0,0 +1,204 @@ +#include "thermopro_ble.h" +#include "esphome/core/log.h" + +#ifdef USE_ESP32 + +namespace esphome::thermopro_ble { + +// this size must be large enough to hold the largest data frame +// of all supported devices +static constexpr std::size_t MAX_DATA_SIZE = 24; + +struct DeviceParserMapping { + const char *prefix; + DeviceParser parser; +}; + +static float tp96_battery(uint16_t voltage); + +static optional parse_tp972(const uint8_t *data, std::size_t data_size); +static optional parse_tp96(const uint8_t *data, std::size_t data_size); +static optional parse_tp3(const uint8_t *data, std::size_t data_size); + +static const char *const TAG = "thermopro_ble"; + +static const struct DeviceParserMapping DEVICE_PARSER_MAP[] = { + {"TP972", parse_tp972}, {"TP970", parse_tp96}, {"TP96", parse_tp96}, {"TP3", parse_tp3}}; + +void ThermoProBLE::dump_config() { + ESP_LOGCONFIG(TAG, "ThermoPro BLE"); + LOG_SENSOR(" ", "Temperature", this->temperature_); + LOG_SENSOR(" ", "External temperature", this->external_temperature_); + LOG_SENSOR(" ", "Humidity", this->humidity_); + LOG_SENSOR(" ", "Battery Level", this->battery_level_); +} + +bool ThermoProBLE::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + // check for matching mac address + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + + // check for valid device type + update_device_type_(device.get_name()); + if (this->device_parser_ == nullptr) { + ESP_LOGVV(TAG, "parse_device(): invalid device type."); + return false; + } + + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + // publish signal strength + float signal_strength = float(device.get_rssi()); + if (this->signal_strength_ != nullptr) + this->signal_strength_->publish_state(signal_strength); + + bool success = false; + for (auto &service_data : device.get_manufacturer_datas()) { + // check maximum data size + std::size_t data_size = service_data.data.size() + 2; + if (data_size > MAX_DATA_SIZE) { + ESP_LOGVV(TAG, "parse_device(): maximum data size exceeded!"); + continue; + } + + // reconstruct whole record from 2 byte uuid and data + esp_bt_uuid_t uuid = service_data.uuid.get_uuid(); + uint8_t data[MAX_DATA_SIZE] = {static_cast(uuid.uuid.uuid16), static_cast(uuid.uuid.uuid16 >> 8)}; + std::copy(service_data.data.begin(), service_data.data.end(), std::begin(data) + 2); + + // dispatch data to parser + optional result = this->device_parser_(data, data_size); + if (!result.has_value()) { + continue; + } + + // publish sensor values + if (result->temperature.has_value() && this->temperature_ != nullptr) + this->temperature_->publish_state(*result->temperature); + if (result->external_temperature.has_value() && this->external_temperature_ != nullptr) + this->external_temperature_->publish_state(*result->external_temperature); + if (result->humidity.has_value() && this->humidity_ != nullptr) + this->humidity_->publish_state(*result->humidity); + if (result->battery_level.has_value() && this->battery_level_ != nullptr) + this->battery_level_->publish_state(*result->battery_level); + + success = true; + } + + return success; +} + +void ThermoProBLE::update_device_type_(const std::string &device_name) { + // check for changed device name (should only happen on initial call) + if (this->device_name_ == device_name) { + return; + } + + // remember device name + this->device_name_ = device_name; + + // try to find device parser + for (const auto &mapping : DEVICE_PARSER_MAP) { + if (device_name.starts_with(mapping.prefix)) { + this->device_parser_ = mapping.parser; + return; + } + } + + // device type unknown + this->device_parser_ = nullptr; + ESP_LOGVV(TAG, "update_device_type_(): unknown device type %s.", device_name.c_str()); +} + +static inline uint16_t read_uint16(const uint8_t *data, std::size_t offset) { + return static_cast(data[offset + 0]) | (static_cast(data[offset + 1]) << 8); +} + +static inline int16_t read_int16(const uint8_t *data, std::size_t offset) { + return static_cast(read_uint16(data, offset)); +} + +static inline uint32_t read_uint32(const uint8_t *data, std::size_t offset) { + return static_cast(data[offset + 0]) | (static_cast(data[offset + 1]) << 8) | + (static_cast(data[offset + 2]) << 16) | (static_cast(data[offset + 3]) << 24); +} + +// Battery calculation used with permission from: +// https://github.com/Bluetooth-Devices/thermopro-ble/blob/main/src/thermopro_ble/parser.py +// +// TP96x battery values appear to be a voltage reading, probably in millivolts. +// This means that calculating battery life from it is a non-linear function. +// Examining the curve, it looked fairly close to a curve from the tanh function. +// So, I created a script to use Tensorflow to optimize an equation in the format +// A*tanh(B*x+C)+D +// Where A,B,C,D are the variables to optimize for. This yielded the below function +static float tp96_battery(uint16_t voltage) { + float level = 52.317286f * tanh(static_cast(voltage) / 273.624277936f - 8.76485439394f) + 51.06925f; + return std::max(0.0f, std::min(level, 100.0f)); +} + +static optional parse_tp972(const uint8_t *data, std::size_t data_size) { + if (data_size != 23) { + ESP_LOGVV(TAG, "parse_tp972(): payload has wrong size of %d (!= 23)!", data_size); + return {}; + } + + ParseResult result; + + // ambient temperature, 2 bytes, 16-bit unsigned integer, -54 °C offset + result.external_temperature = static_cast(read_uint16(data, 1)) - 54.0f; + + // battery level, 2 bytes, 16-bit unsigned integer, voltage (convert to percentage) + result.battery_level = tp96_battery(read_uint16(data, 3)); + + // internal temperature, 4 bytes, float, -54 °C offset + result.temperature = static_cast(read_uint32(data, 9)) - 54.0f; + + return result; +} + +static optional parse_tp96(const uint8_t *data, std::size_t data_size) { + if (data_size != 7) { + ESP_LOGVV(TAG, "parse_tp96(): payload has wrong size of %d (!= 7)!", data_size); + return {}; + } + + ParseResult result; + + // internal temperature, 2 bytes, 16-bit unsigned integer, -30 °C offset + result.temperature = static_cast(read_uint16(data, 1)) - 30.0f; + + // battery level, 2 bytes, 16-bit unsigned integer, voltage (convert to percentage) + result.battery_level = tp96_battery(read_uint16(data, 3)); + + // ambient temperature, 2 bytes, 16-bit unsigned integer, -30 °C offset + result.external_temperature = static_cast(read_uint16(data, 5)) - 30.0f; + + return result; +} + +static optional parse_tp3(const uint8_t *data, std::size_t data_size) { + if (data_size < 6) { + ESP_LOGVV(TAG, "parse_tp3(): payload has wrong size of %d (< 6)!", data_size); + return {}; + } + + ParseResult result; + + // temperature, 2 bytes, 16-bit signed integer, 0.1 °C + result.temperature = static_cast(read_int16(data, 1)) * 0.1f; + + // humidity, 1 byte, 8-bit unsigned integer, 1.0 % + result.humidity = static_cast(data[3]); + + // battery level, 2 bits (0-2) + result.battery_level = static_cast(data[4] & 0x3) * 50.0; + + return result; +} + +} // namespace esphome::thermopro_ble + +#endif diff --git a/esphome/components/thermopro_ble/thermopro_ble.h b/esphome/components/thermopro_ble/thermopro_ble.h new file mode 100644 index 0000000000..38bed82102 --- /dev/null +++ b/esphome/components/thermopro_ble/thermopro_ble.h @@ -0,0 +1,49 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" + +#ifdef USE_ESP32 + +namespace esphome::thermopro_ble { + +struct ParseResult { + optional temperature; + optional external_temperature; + optional humidity; + optional battery_level; +}; + +using DeviceParser = optional (*)(const uint8_t *data, std::size_t data_size); + +class ThermoProBLE : public Component, public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { this->address_ = address; }; + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + void dump_config() override; + void set_signal_strength(sensor::Sensor *signal_strength) { this->signal_strength_ = signal_strength; } + void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; } + void set_external_temperature(sensor::Sensor *external_temperature) { + this->external_temperature_ = external_temperature; + } + void set_humidity(sensor::Sensor *humidity) { this->humidity_ = humidity; } + void set_battery_level(sensor::Sensor *battery_level) { this->battery_level_ = battery_level; } + + protected: + uint64_t address_; + std::string device_name_; + DeviceParser device_parser_{nullptr}; + sensor::Sensor *signal_strength_{nullptr}; + sensor::Sensor *temperature_{nullptr}; + sensor::Sensor *external_temperature_{nullptr}; + sensor::Sensor *humidity_{nullptr}; + sensor::Sensor *battery_level_{nullptr}; + + void update_device_type_(const std::string &device_name); +}; + +} // namespace esphome::thermopro_ble + +#endif diff --git a/esphome/components/thermostat/climate.py b/esphome/components/thermostat/climate.py index 5d0d9442e8..a3c155aac0 100644 --- a/esphome/components/thermostat/climate.py +++ b/esphome/components/thermostat/climate.py @@ -32,6 +32,7 @@ from esphome.const import ( CONF_FAN_WITH_COOLING, CONF_FAN_WITH_HEATING, CONF_HEAT_ACTION, + CONF_HEAT_COOL_MODE, CONF_HEAT_DEADBAND, CONF_HEAT_MODE, CONF_HEAT_OVERRUN, @@ -70,9 +71,14 @@ from esphome.const import ( CONF_VISUAL, ) -CONF_PRESET_CHANGE = "preset_change" CONF_DEFAULT_PRESET = "default_preset" +CONF_HUMIDITY_CONTROL_DEHUMIDIFY_ACTION = "humidity_control_dehumidify_action" +CONF_HUMIDITY_CONTROL_HUMIDIFY_ACTION = "humidity_control_humidify_action" +CONF_HUMIDITY_CONTROL_OFF_ACTION = "humidity_control_off_action" +CONF_HUMIDITY_HYSTERESIS = "humidity_hysteresis" CONF_ON_BOOT_RESTORE_FROM = "on_boot_restore_from" +CONF_PRESET_CHANGE = "preset_change" +CONF_TARGET_HUMIDITY_CHANGE_ACTION = "target_humidity_change_action" CODEOWNERS = ["@kbx81"] @@ -150,7 +156,7 @@ def generate_comparable_preset(config, name): def validate_thermostat(config): # verify corresponding action(s) exist(s) for any defined climate mode or action requirements = { - CONF_AUTO_MODE: [ + CONF_HEAT_COOL_MODE: [ CONF_COOL_ACTION, CONF_HEAT_ACTION, CONF_MIN_COOLING_OFF_TIME, @@ -240,6 +246,14 @@ def validate_thermostat(config): CONF_MAX_HEATING_RUN_TIME, CONF_SUPPLEMENTAL_HEATING_ACTION, ], + CONF_HUMIDITY_CONTROL_DEHUMIDIFY_ACTION: [ + CONF_HUMIDITY_CONTROL_OFF_ACTION, + CONF_HUMIDITY_SENSOR, + ], + CONF_HUMIDITY_CONTROL_HUMIDIFY_ACTION: [ + CONF_HUMIDITY_CONTROL_OFF_ACTION, + CONF_HUMIDITY_SENSOR, + ], } for config_trigger, req_triggers in requirements.items(): for req_trigger in req_triggers: @@ -337,7 +351,7 @@ def validate_thermostat(config): # Warn about using the removed CONF_DEFAULT_MODE and advise users if CONF_DEFAULT_MODE in config and config[CONF_DEFAULT_MODE] is not None: raise cv.Invalid( - f"{CONF_DEFAULT_MODE} is no longer valid. Please switch to using presets and specify a {CONF_DEFAULT_PRESET}." + f"{CONF_DEFAULT_MODE} is no longer valid. Please switch to using presets and specify a {CONF_DEFAULT_PRESET}" ) default_mode = config[CONF_DEFAULT_MODE] @@ -540,6 +554,9 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_FAN_ONLY_MODE): automation.validate_automation( single=True ), + cv.Optional(CONF_HEAT_COOL_MODE): automation.validate_automation( + single=True + ), cv.Optional(CONF_HEAT_MODE): automation.validate_automation(single=True), cv.Optional(CONF_OFF_MODE): automation.validate_automation(single=True), cv.Optional(CONF_FAN_MODE_ON_ACTION): automation.validate_automation( @@ -584,9 +601,24 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_SWING_VERTICAL_ACTION): automation.validate_automation( single=True ), + cv.Optional( + CONF_TARGET_HUMIDITY_CHANGE_ACTION + ): automation.validate_automation(single=True), cv.Optional( CONF_TARGET_TEMPERATURE_CHANGE_ACTION ): automation.validate_automation(single=True), + cv.Exclusive( + CONF_HUMIDITY_CONTROL_DEHUMIDIFY_ACTION, + group_of_exclusion="humidity_control", + ): automation.validate_automation(single=True), + cv.Exclusive( + CONF_HUMIDITY_CONTROL_HUMIDIFY_ACTION, + group_of_exclusion="humidity_control", + ): automation.validate_automation(single=True), + cv.Optional( + CONF_HUMIDITY_CONTROL_OFF_ACTION + ): automation.validate_automation(single=True), + cv.Optional(CONF_HUMIDITY_HYSTERESIS, default=1.0): cv.percentage, cv.Optional(CONF_DEFAULT_MODE, default=None): cv.valid, cv.Optional(CONF_DEFAULT_PRESET): cv.templatable(cv.string), cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature, @@ -644,7 +676,6 @@ async def to_code(config): var = await climate.new_climate(config) await cg.register_component(var, config) - heat_cool_mode_available = CONF_HEAT_ACTION in config and CONF_COOL_ACTION in config two_points_available = CONF_HEAT_ACTION in config and ( CONF_COOL_ACTION in config or (config[CONF_FAN_ONLY_COOLING] and CONF_FAN_ONLY_ACTION in config) @@ -739,11 +770,6 @@ async def to_code(config): var.get_idle_action_trigger(), [], config[CONF_IDLE_ACTION] ) - if heat_cool_mode_available is True: - cg.add(var.set_supports_heat_cool(True)) - else: - cg.add(var.set_supports_heat_cool(False)) - if CONF_COOL_ACTION in config: await automation.build_automation( var.get_cool_action_trigger(), [], config[CONF_COOL_ACTION] @@ -780,6 +806,7 @@ async def to_code(config): await automation.build_automation( var.get_auto_mode_trigger(), [], config[CONF_AUTO_MODE] ) + cg.add(var.set_supports_auto(True)) if CONF_COOL_MODE in config: await automation.build_automation( var.get_cool_mode_trigger(), [], config[CONF_COOL_MODE] @@ -800,6 +827,11 @@ async def to_code(config): var.get_heat_mode_trigger(), [], config[CONF_HEAT_MODE] ) cg.add(var.set_supports_heat(True)) + if CONF_HEAT_COOL_MODE in config: + await automation.build_automation( + var.get_heat_cool_mode_trigger(), [], config[CONF_HEAT_COOL_MODE] + ) + cg.add(var.set_supports_heat_cool(True)) if CONF_OFF_MODE in config: await automation.build_automation( var.get_off_mode_trigger(), [], config[CONF_OFF_MODE] @@ -878,14 +910,45 @@ async def to_code(config): config[CONF_SWING_VERTICAL_ACTION], ) cg.add(var.set_supports_swing_mode_vertical(True)) + if CONF_TARGET_HUMIDITY_CHANGE_ACTION in config: + await automation.build_automation( + var.get_humidity_change_trigger(), + [], + config[CONF_TARGET_HUMIDITY_CHANGE_ACTION], + ) if CONF_TARGET_TEMPERATURE_CHANGE_ACTION in config: await automation.build_automation( var.get_temperature_change_trigger(), [], config[CONF_TARGET_TEMPERATURE_CHANGE_ACTION], ) + if CONF_HUMIDITY_CONTROL_DEHUMIDIFY_ACTION in config: + cg.add(var.set_supports_dehumidification(True)) + await automation.build_automation( + var.get_humidity_control_dehumidify_action_trigger(), + [], + config[CONF_HUMIDITY_CONTROL_DEHUMIDIFY_ACTION], + ) + if CONF_HUMIDITY_CONTROL_HUMIDIFY_ACTION in config: + cg.add(var.set_supports_humidification(True)) + await automation.build_automation( + var.get_humidity_control_humidify_action_trigger(), + [], + config[CONF_HUMIDITY_CONTROL_HUMIDIFY_ACTION], + ) + if CONF_HUMIDITY_CONTROL_OFF_ACTION in config: + await automation.build_automation( + var.get_humidity_control_off_action_trigger(), + [], + config[CONF_HUMIDITY_CONTROL_OFF_ACTION], + ) + cg.add(var.set_humidity_hysteresis(config[CONF_HUMIDITY_HYSTERESIS])) if CONF_PRESET in config: + # Separate standard and custom presets, and build preset config variables + standard_presets: list[tuple[cg.MockObj, cg.MockObj]] = [] + custom_presets: list[tuple[str, cg.MockObj]] = [] + for preset_config in config[CONF_PRESET]: name = preset_config[CONF_NAME] standard_preset = None @@ -928,9 +991,39 @@ async def to_code(config): ) if standard_preset is not None: - cg.add(var.set_preset_config(standard_preset, preset_target_variable)) + standard_presets.append((standard_preset, preset_target_variable)) else: - cg.add(var.set_custom_preset_config(name, preset_target_variable)) + custom_presets.append((name, preset_target_variable)) + + # Build initializer list for standard presets + if standard_presets: + cg.add( + var.set_preset_config( + [ + cg.StructInitializer( + thermostat_ns.struct("ThermostatPresetEntry"), + ("preset", preset), + ("config", preset_var), + ) + for preset, preset_var in standard_presets + ] + ) + ) + + # Build initializer list for custom presets + if custom_presets: + cg.add( + var.set_custom_preset_config( + [ + cg.StructInitializer( + thermostat_ns.struct("ThermostatCustomPresetEntry"), + ("name", cg.RawExpression(f'"{name}"')), + ("config", preset_var), + ) + for name, preset_var in custom_presets + ] + ) + ) if CONF_DEFAULT_PRESET in config: default_preset_name = config[CONF_DEFAULT_PRESET] diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index 404e585aff..e79eed4055 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -1,4 +1,6 @@ #include "thermostat_climate.h" +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" namespace esphome { @@ -9,11 +11,11 @@ static const char *const TAG = "thermostat.climate"; void ThermostatClimate::setup() { if (this->use_startup_delay_) { // start timers so that no actions are called for a moment - this->start_timer_(thermostat::TIMER_COOLING_OFF); - this->start_timer_(thermostat::TIMER_FANNING_OFF); - this->start_timer_(thermostat::TIMER_HEATING_OFF); + this->start_timer_(thermostat::THERMOSTAT_TIMER_COOLING_OFF); + this->start_timer_(thermostat::THERMOSTAT_TIMER_FANNING_OFF); + this->start_timer_(thermostat::THERMOSTAT_TIMER_HEATING_OFF); if (this->supports_fan_only_action_uses_fan_mode_timer_) - this->start_timer_(thermostat::TIMER_FAN_MODE); + this->start_timer_(thermostat::THERMOSTAT_TIMER_FAN_MODE); } // add a callback so that whenever the sensor state changes we can take action this->sensor_->add_on_state_callback([this](float state) { @@ -30,6 +32,7 @@ void ThermostatClimate::setup() { if (this->humidity_sensor_ != nullptr) { this->humidity_sensor_->add_on_state_callback([this](float state) { this->current_humidity = state; + this->switch_to_humidity_control_action_(this->compute_humidity_control_action_()); this->publish_state(); }); this->current_humidity = this->humidity_sensor_->state; @@ -50,7 +53,7 @@ void ThermostatClimate::setup() { if (use_default_preset) { if (this->default_preset_ != climate::ClimatePreset::CLIMATE_PRESET_NONE) { this->change_preset_(this->default_preset_); - } else if (!this->default_custom_preset_.empty()) { + } else if (this->default_custom_preset_ != nullptr) { this->change_custom_preset_(this->default_custom_preset_); } } @@ -64,7 +67,7 @@ void ThermostatClimate::setup() { void ThermostatClimate::loop() { for (auto &timer : this->timer_) { - if (timer.active && (timer.started + timer.time < millis())) { + if (timer.active && (timer.started + timer.time < App.get_loop_component_start_time())) { timer.active = false; timer.func(); } @@ -82,6 +85,8 @@ void ThermostatClimate::refresh() { this->switch_to_supplemental_action_(this->compute_supplemental_action_()); this->switch_to_fan_mode_(this->fan_mode.value(), false); this->switch_to_swing_mode_(this->swing_mode, false); + this->switch_to_humidity_control_action_(this->compute_humidity_control_action_()); + this->check_humidity_change_trigger_(); this->check_temperature_change_trigger_(); this->publish_state(); } @@ -127,26 +132,40 @@ bool ThermostatClimate::hysteresis_valid() { return true; } +bool ThermostatClimate::humidity_hysteresis_valid() { + return !std::isnan(this->humidity_hysteresis_) && this->humidity_hysteresis_ >= 0.0f && + this->humidity_hysteresis_ < 100.0f; +} + +bool ThermostatClimate::limit_setpoints_for_heat_cool() { + return this->mode == climate::CLIMATE_MODE_HEAT_COOL || + (this->mode == climate::CLIMATE_MODE_AUTO && this->supports_heat_cool_); +} + void ThermostatClimate::validate_target_temperature() { if (std::isnan(this->target_temperature)) { + // default to the midpoint between visual min and max this->target_temperature = ((this->get_traits().get_visual_max_temperature() - this->get_traits().get_visual_min_temperature()) / 2) + this->get_traits().get_visual_min_temperature(); } else { // target_temperature must be between the visual minimum and the visual maximum - if (this->target_temperature < this->get_traits().get_visual_min_temperature()) - this->target_temperature = this->get_traits().get_visual_min_temperature(); - if (this->target_temperature > this->get_traits().get_visual_max_temperature()) - this->target_temperature = this->get_traits().get_visual_max_temperature(); + this->target_temperature = clamp(this->target_temperature, this->get_traits().get_visual_min_temperature(), + this->get_traits().get_visual_max_temperature()); } } -void ThermostatClimate::validate_target_temperatures() { - if (this->supports_two_points_) { +void ThermostatClimate::validate_target_temperatures(const bool pin_target_temperature_high) { + if (!this->supports_two_points_) { + this->validate_target_temperature(); + } else if (pin_target_temperature_high) { + // if target_temperature_high is set less than target_temperature_low, move down target_temperature_low this->validate_target_temperature_low(); this->validate_target_temperature_high(); } else { - this->validate_target_temperature(); + // if target_temperature_low is set greater than target_temperature_high, move up target_temperature_high + this->validate_target_temperature_high(); + this->validate_target_temperature_low(); } } @@ -154,18 +173,13 @@ void ThermostatClimate::validate_target_temperature_low() { if (std::isnan(this->target_temperature_low)) { this->target_temperature_low = this->get_traits().get_visual_min_temperature(); } else { - // target_temperature_low must not be lower than the visual minimum - if (this->target_temperature_low < this->get_traits().get_visual_min_temperature()) - this->target_temperature_low = this->get_traits().get_visual_min_temperature(); - // target_temperature_low must not be greater than the visual maximum minus set_point_minimum_differential_ - if (this->target_temperature_low > - this->get_traits().get_visual_max_temperature() - this->set_point_minimum_differential_) { - this->target_temperature_low = - this->get_traits().get_visual_max_temperature() - this->set_point_minimum_differential_; - } - // if target_temperature_low is set greater than target_temperature_high, move up target_temperature_high - if (this->target_temperature_low > this->target_temperature_high - this->set_point_minimum_differential_) - this->target_temperature_high = this->target_temperature_low + this->set_point_minimum_differential_; + float target_temperature_low_upper_limit = + this->limit_setpoints_for_heat_cool() + ? clamp(this->target_temperature_high - this->set_point_minimum_differential_, + this->get_traits().get_visual_min_temperature(), this->get_traits().get_visual_max_temperature()) + : this->get_traits().get_visual_max_temperature(); + this->target_temperature_low = clamp(this->target_temperature_low, this->get_traits().get_visual_min_temperature(), + target_temperature_low_upper_limit); } } @@ -173,122 +187,152 @@ void ThermostatClimate::validate_target_temperature_high() { if (std::isnan(this->target_temperature_high)) { this->target_temperature_high = this->get_traits().get_visual_max_temperature(); } else { - // target_temperature_high must not be lower than the visual maximum - if (this->target_temperature_high > this->get_traits().get_visual_max_temperature()) - this->target_temperature_high = this->get_traits().get_visual_max_temperature(); - // target_temperature_high must not be lower than the visual minimum plus set_point_minimum_differential_ - if (this->target_temperature_high < - this->get_traits().get_visual_min_temperature() + this->set_point_minimum_differential_) { - this->target_temperature_high = - this->get_traits().get_visual_min_temperature() + this->set_point_minimum_differential_; - } - // if target_temperature_high is set less than target_temperature_low, move down target_temperature_low - if (this->target_temperature_high < this->target_temperature_low + this->set_point_minimum_differential_) - this->target_temperature_low = this->target_temperature_high - this->set_point_minimum_differential_; + float target_temperature_high_lower_limit = + this->limit_setpoints_for_heat_cool() + ? clamp(this->target_temperature_low + this->set_point_minimum_differential_, + this->get_traits().get_visual_min_temperature(), this->get_traits().get_visual_max_temperature()) + : this->get_traits().get_visual_min_temperature(); + this->target_temperature_high = clamp(this->target_temperature_high, target_temperature_high_lower_limit, + this->get_traits().get_visual_max_temperature()); + } +} + +void ThermostatClimate::validate_target_humidity() { + if (std::isnan(this->target_humidity)) { + this->target_humidity = + (this->get_traits().get_visual_max_humidity() - this->get_traits().get_visual_min_humidity()) / 2.0f; + } else { + this->target_humidity = clamp(this->target_humidity, this->get_traits().get_visual_min_humidity(), + this->get_traits().get_visual_max_humidity()); } } void ThermostatClimate::control(const climate::ClimateCall &call) { + bool target_temperature_high_changed = false; + if (call.get_preset().has_value()) { // setup_complete_ blocks modifying/resetting the temps immediately after boot if (this->setup_complete_) { - this->change_preset_(*call.get_preset()); + this->change_preset_(call.get_preset().value()); } else { - this->preset = *call.get_preset(); + this->preset = call.get_preset().value(); } } - if (call.get_custom_preset().has_value()) { + if (call.has_custom_preset()) { // setup_complete_ blocks modifying/resetting the temps immediately after boot if (this->setup_complete_) { - this->change_custom_preset_(*call.get_custom_preset()); + this->change_custom_preset_(call.get_custom_preset()); } else { - this->custom_preset = *call.get_custom_preset(); + // Use the base class method which handles pointer lookup internally + this->set_custom_preset_(call.get_custom_preset()); } } - if (call.get_mode().has_value()) - this->mode = *call.get_mode(); - if (call.get_fan_mode().has_value()) - this->fan_mode = *call.get_fan_mode(); - if (call.get_swing_mode().has_value()) - this->swing_mode = *call.get_swing_mode(); + if (call.get_mode().has_value()) { + this->mode = call.get_mode().value(); + } + if (call.get_fan_mode().has_value()) { + this->fan_mode = call.get_fan_mode().value(); + } + if (call.get_swing_mode().has_value()) { + this->swing_mode = call.get_swing_mode().value(); + } if (this->supports_two_points_) { if (call.get_target_temperature_low().has_value()) { - this->target_temperature_low = *call.get_target_temperature_low(); - validate_target_temperature_low(); + this->target_temperature_low = call.get_target_temperature_low().value(); } if (call.get_target_temperature_high().has_value()) { - this->target_temperature_high = *call.get_target_temperature_high(); - validate_target_temperature_high(); + target_temperature_high_changed = this->target_temperature_high != call.get_target_temperature_high().value(); + this->target_temperature_high = call.get_target_temperature_high().value(); } + // ensure the two set points are valid and adjust one of them if necessary + this->validate_target_temperatures(target_temperature_high_changed || + (this->prev_mode_ == climate::CLIMATE_MODE_COOL)); } else { if (call.get_target_temperature().has_value()) { - this->target_temperature = *call.get_target_temperature(); - validate_target_temperature(); + this->target_temperature = call.get_target_temperature().value(); + this->validate_target_temperature(); } } + if (call.get_target_humidity().has_value()) { + this->target_humidity = call.get_target_humidity().value(); + this->validate_target_humidity(); + } // make any changes happen - refresh(); + this->refresh(); } climate::ClimateTraits ThermostatClimate::traits() { auto traits = climate::ClimateTraits(); - traits.set_supports_current_temperature(true); - if (this->humidity_sensor_ != nullptr) - traits.set_supports_current_humidity(true); - if (supports_auto_) + traits.add_feature_flags(climate::CLIMATE_SUPPORTS_ACTION | climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE); + + if (this->supports_two_points_) + traits.add_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE); + + if (this->humidity_sensor_ != nullptr) + traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY); + + if (this->supports_humidification_ || this->supports_dehumidification_) + traits.add_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY); + + if (this->supports_auto_) traits.add_supported_mode(climate::CLIMATE_MODE_AUTO); - if (supports_heat_cool_) + if (this->supports_heat_cool_) traits.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL); - if (supports_cool_) + if (this->supports_cool_) traits.add_supported_mode(climate::CLIMATE_MODE_COOL); - if (supports_dry_) + if (this->supports_dry_) traits.add_supported_mode(climate::CLIMATE_MODE_DRY); - if (supports_fan_only_) + if (this->supports_fan_only_) traits.add_supported_mode(climate::CLIMATE_MODE_FAN_ONLY); - if (supports_heat_) + if (this->supports_heat_) traits.add_supported_mode(climate::CLIMATE_MODE_HEAT); - if (supports_fan_mode_on_) + if (this->supports_fan_mode_on_) traits.add_supported_fan_mode(climate::CLIMATE_FAN_ON); - if (supports_fan_mode_off_) + if (this->supports_fan_mode_off_) traits.add_supported_fan_mode(climate::CLIMATE_FAN_OFF); - if (supports_fan_mode_auto_) + if (this->supports_fan_mode_auto_) traits.add_supported_fan_mode(climate::CLIMATE_FAN_AUTO); - if (supports_fan_mode_low_) + if (this->supports_fan_mode_low_) traits.add_supported_fan_mode(climate::CLIMATE_FAN_LOW); - if (supports_fan_mode_medium_) + if (this->supports_fan_mode_medium_) traits.add_supported_fan_mode(climate::CLIMATE_FAN_MEDIUM); - if (supports_fan_mode_high_) + if (this->supports_fan_mode_high_) traits.add_supported_fan_mode(climate::CLIMATE_FAN_HIGH); - if (supports_fan_mode_middle_) + if (this->supports_fan_mode_middle_) traits.add_supported_fan_mode(climate::CLIMATE_FAN_MIDDLE); - if (supports_fan_mode_focus_) + if (this->supports_fan_mode_focus_) traits.add_supported_fan_mode(climate::CLIMATE_FAN_FOCUS); - if (supports_fan_mode_diffuse_) + if (this->supports_fan_mode_diffuse_) traits.add_supported_fan_mode(climate::CLIMATE_FAN_DIFFUSE); - if (supports_fan_mode_quiet_) + if (this->supports_fan_mode_quiet_) traits.add_supported_fan_mode(climate::CLIMATE_FAN_QUIET); - if (supports_swing_mode_both_) + if (this->supports_swing_mode_both_) traits.add_supported_swing_mode(climate::CLIMATE_SWING_BOTH); - if (supports_swing_mode_horizontal_) + if (this->supports_swing_mode_horizontal_) traits.add_supported_swing_mode(climate::CLIMATE_SWING_HORIZONTAL); - if (supports_swing_mode_off_) + if (this->supports_swing_mode_off_) traits.add_supported_swing_mode(climate::CLIMATE_SWING_OFF); - if (supports_swing_mode_vertical_) + if (this->supports_swing_mode_vertical_) traits.add_supported_swing_mode(climate::CLIMATE_SWING_VERTICAL); - for (auto &it : this->preset_config_) { - traits.add_supported_preset(it.first); - } - for (auto &it : this->custom_preset_config_) { - traits.add_supported_custom_preset(it.first); + for (const auto &entry : this->preset_config_) { + traits.add_supported_preset(entry.preset); + } + + // Extract custom preset names from the custom_preset_config_ vector + if (!this->custom_preset_config_.empty()) { + std::vector custom_preset_names; + custom_preset_names.reserve(this->custom_preset_config_.size()); + for (const auto &entry : this->custom_preset_config_) { + custom_preset_names.push_back(entry.name); + } + traits.set_supported_custom_presets(custom_preset_names); } - traits.set_supports_two_point_target_temperature(this->supports_two_points_); - traits.set_supports_action(true); return traits; } @@ -299,14 +343,15 @@ climate::ClimateAction ThermostatClimate::compute_action_(const bool ignore_time return climate::CLIMATE_ACTION_OFF; } // do not change the action if an "ON" timer is running - if ((!ignore_timers) && - (timer_active_(thermostat::TIMER_IDLE_ON) || timer_active_(thermostat::TIMER_COOLING_ON) || - timer_active_(thermostat::TIMER_FANNING_ON) || timer_active_(thermostat::TIMER_HEATING_ON))) { + if ((!ignore_timers) && (this->timer_active_(thermostat::THERMOSTAT_TIMER_IDLE_ON) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_COOLING_ON) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_FANNING_ON) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_HEATING_ON))) { return this->action; } // ensure set point(s) is/are valid before computing the action - this->validate_target_temperatures(); + this->validate_target_temperatures(this->prev_mode_ == climate::CLIMATE_MODE_COOL); // everything has been validated so we can now safely compute the action switch (this->mode) { // if the climate mode is OFF then the climate action must be OFF @@ -340,6 +385,22 @@ climate::ClimateAction ThermostatClimate::compute_action_(const bool ignore_time target_action = climate::CLIMATE_ACTION_HEATING; } break; + case climate::CLIMATE_MODE_AUTO: + if (this->supports_two_points_) { + if (this->cooling_required_() && this->heating_required_()) { + // this is bad and should never happen, so just stop. + // target_action = climate::CLIMATE_ACTION_IDLE; + } else if (this->cooling_required_()) { + target_action = climate::CLIMATE_ACTION_COOLING; + } else if (this->heating_required_()) { + target_action = climate::CLIMATE_ACTION_HEATING; + } + } else if (this->supports_cool_ && this->cooling_required_()) { + target_action = climate::CLIMATE_ACTION_COOLING; + } else if (this->supports_heat_ && this->heating_required_()) { + target_action = climate::CLIMATE_ACTION_HEATING; + } + break; default: break; } @@ -362,7 +423,7 @@ climate::ClimateAction ThermostatClimate::compute_supplemental_action_() { } // ensure set point(s) is/are valid before computing the action - this->validate_target_temperatures(); + this->validate_target_temperatures(this->prev_mode_ == climate::CLIMATE_MODE_COOL); // everything has been validated so we can now safely compute the action switch (this->mode) { // if the climate mode is OFF then the climate action must be OFF @@ -396,6 +457,28 @@ climate::ClimateAction ThermostatClimate::compute_supplemental_action_() { return target_action; } +HumidificationAction ThermostatClimate::compute_humidity_control_action_() { + auto target_action = THERMOSTAT_HUMIDITY_CONTROL_ACTION_OFF; + // if hysteresis value or current_humidity is not valid, we go to OFF + if (std::isnan(this->current_humidity) || !this->humidity_hysteresis_valid()) { + return THERMOSTAT_HUMIDITY_CONTROL_ACTION_OFF; + } + + // ensure set point is valid before computing the action + this->validate_target_humidity(); + // everything has been validated so we can now safely compute the action + if (this->dehumidification_required_() && this->humidification_required_()) { + // this is bad and should never happen, so just stop. + // target_action = THERMOSTAT_HUMIDITY_CONTROL_ACTION_OFF; + } else if (this->supports_dehumidification_ && this->dehumidification_required_()) { + target_action = THERMOSTAT_HUMIDITY_CONTROL_ACTION_DEHUMIDIFY; + } else if (this->supports_humidification_ && this->humidification_required_()) { + target_action = THERMOSTAT_HUMIDITY_CONTROL_ACTION_HUMIDIFY; + } + + return target_action; +} + void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool publish_state) { // setup_complete_ helps us ensure an action is called immediately after boot if ((action == this->action) && this->setup_complete_) { @@ -420,18 +503,18 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool pu case climate::CLIMATE_ACTION_OFF: case climate::CLIMATE_ACTION_IDLE: if (this->idle_action_ready_()) { - this->start_timer_(thermostat::TIMER_IDLE_ON); + this->start_timer_(thermostat::THERMOSTAT_TIMER_IDLE_ON); if (this->action == climate::CLIMATE_ACTION_COOLING) - this->start_timer_(thermostat::TIMER_COOLING_OFF); + this->start_timer_(thermostat::THERMOSTAT_TIMER_COOLING_OFF); if (this->action == climate::CLIMATE_ACTION_FAN) { if (this->supports_fan_only_action_uses_fan_mode_timer_) { - this->start_timer_(thermostat::TIMER_FAN_MODE); + this->start_timer_(thermostat::THERMOSTAT_TIMER_FAN_MODE); } else { - this->start_timer_(thermostat::TIMER_FANNING_OFF); + this->start_timer_(thermostat::THERMOSTAT_TIMER_FANNING_OFF); } } if (this->action == climate::CLIMATE_ACTION_HEATING) - this->start_timer_(thermostat::TIMER_HEATING_OFF); + this->start_timer_(thermostat::THERMOSTAT_TIMER_HEATING_OFF); // trig = this->idle_action_trigger_; ESP_LOGVV(TAG, "Switching to IDLE/OFF action"); this->cooling_max_runtime_exceeded_ = false; @@ -441,10 +524,10 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool pu break; case climate::CLIMATE_ACTION_COOLING: if (this->cooling_action_ready_()) { - this->start_timer_(thermostat::TIMER_COOLING_ON); - this->start_timer_(thermostat::TIMER_COOLING_MAX_RUN_TIME); + this->start_timer_(thermostat::THERMOSTAT_TIMER_COOLING_ON); + this->start_timer_(thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME); if (this->supports_fan_with_cooling_) { - this->start_timer_(thermostat::TIMER_FANNING_ON); + this->start_timer_(thermostat::THERMOSTAT_TIMER_FANNING_ON); trig_fan = this->fan_only_action_trigger_; } this->cooling_max_runtime_exceeded_ = false; @@ -455,10 +538,10 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool pu break; case climate::CLIMATE_ACTION_HEATING: if (this->heating_action_ready_()) { - this->start_timer_(thermostat::TIMER_HEATING_ON); - this->start_timer_(thermostat::TIMER_HEATING_MAX_RUN_TIME); + this->start_timer_(thermostat::THERMOSTAT_TIMER_HEATING_ON); + this->start_timer_(thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME); if (this->supports_fan_with_heating_) { - this->start_timer_(thermostat::TIMER_FANNING_ON); + this->start_timer_(thermostat::THERMOSTAT_TIMER_FANNING_ON); trig_fan = this->fan_only_action_trigger_; } this->heating_max_runtime_exceeded_ = false; @@ -470,9 +553,9 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool pu case climate::CLIMATE_ACTION_FAN: if (this->fanning_action_ready_()) { if (this->supports_fan_only_action_uses_fan_mode_timer_) { - this->start_timer_(thermostat::TIMER_FAN_MODE); + this->start_timer_(thermostat::THERMOSTAT_TIMER_FAN_MODE); } else { - this->start_timer_(thermostat::TIMER_FANNING_ON); + this->start_timer_(thermostat::THERMOSTAT_TIMER_FANNING_ON); } trig = this->fan_only_action_trigger_; ESP_LOGVV(TAG, "Switching to FAN_ONLY action"); @@ -481,8 +564,8 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool pu break; case climate::CLIMATE_ACTION_DRYING: if (this->drying_action_ready_()) { - this->start_timer_(thermostat::TIMER_COOLING_ON); - this->start_timer_(thermostat::TIMER_FANNING_ON); + this->start_timer_(thermostat::THERMOSTAT_TIMER_COOLING_ON); + this->start_timer_(thermostat::THERMOSTAT_TIMER_FANNING_ON); trig = this->dry_action_trigger_; ESP_LOGVV(TAG, "Switching to DRYING action"); action_ready = true; @@ -525,14 +608,14 @@ void ThermostatClimate::switch_to_supplemental_action_(climate::ClimateAction ac switch (action) { case climate::CLIMATE_ACTION_OFF: case climate::CLIMATE_ACTION_IDLE: - this->cancel_timer_(thermostat::TIMER_COOLING_MAX_RUN_TIME); - this->cancel_timer_(thermostat::TIMER_HEATING_MAX_RUN_TIME); + this->cancel_timer_(thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME); + this->cancel_timer_(thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME); break; case climate::CLIMATE_ACTION_COOLING: - this->cancel_timer_(thermostat::TIMER_COOLING_MAX_RUN_TIME); + this->cancel_timer_(thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME); break; case climate::CLIMATE_ACTION_HEATING: - this->cancel_timer_(thermostat::TIMER_HEATING_MAX_RUN_TIME); + this->cancel_timer_(thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME); break; default: return; @@ -547,15 +630,15 @@ void ThermostatClimate::trigger_supplemental_action_() { switch (this->supplemental_action_) { case climate::CLIMATE_ACTION_COOLING: - if (!this->timer_active_(thermostat::TIMER_COOLING_MAX_RUN_TIME)) { - this->start_timer_(thermostat::TIMER_COOLING_MAX_RUN_TIME); + if (!this->timer_active_(thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME)) { + this->start_timer_(thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME); } trig = this->supplemental_cool_action_trigger_; ESP_LOGVV(TAG, "Calling supplemental COOLING action"); break; case climate::CLIMATE_ACTION_HEATING: - if (!this->timer_active_(thermostat::TIMER_HEATING_MAX_RUN_TIME)) { - this->start_timer_(thermostat::TIMER_HEATING_MAX_RUN_TIME); + if (!this->timer_active_(thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME)) { + this->start_timer_(thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME); } trig = this->supplemental_heat_action_trigger_; ESP_LOGVV(TAG, "Calling supplemental HEATING action"); @@ -569,6 +652,44 @@ void ThermostatClimate::trigger_supplemental_action_() { } } +void ThermostatClimate::switch_to_humidity_control_action_(HumidificationAction action) { + // setup_complete_ helps us ensure an action is called immediately after boot + if ((action == this->humidification_action) && this->setup_complete_) { + // already in target mode + return; + } + + Trigger<> *trig = this->humidity_control_off_action_trigger_; + switch (action) { + case THERMOSTAT_HUMIDITY_CONTROL_ACTION_OFF: + // trig = this->humidity_control_off_action_trigger_; + ESP_LOGVV(TAG, "Switching to HUMIDIFICATION_OFF action"); + break; + case THERMOSTAT_HUMIDITY_CONTROL_ACTION_DEHUMIDIFY: + trig = this->humidity_control_dehumidify_action_trigger_; + ESP_LOGVV(TAG, "Switching to DEHUMIDIFY action"); + break; + case THERMOSTAT_HUMIDITY_CONTROL_ACTION_HUMIDIFY: + trig = this->humidity_control_humidify_action_trigger_; + ESP_LOGVV(TAG, "Switching to HUMIDIFY action"); + break; + case THERMOSTAT_HUMIDITY_CONTROL_ACTION_NONE: + default: + action = THERMOSTAT_HUMIDITY_CONTROL_ACTION_OFF; + // trig = this->humidity_control_off_action_trigger_; + } + + if (this->prev_humidity_control_trigger_ != nullptr) { + this->prev_humidity_control_trigger_->stop_action(); + this->prev_humidity_control_trigger_ = nullptr; + } + this->humidification_action = action; + this->prev_humidity_control_trigger_ = trig; + if (trig != nullptr) { + trig->trigger(); + } +} + void ThermostatClimate::switch_to_fan_mode_(climate::ClimateFanMode fan_mode, bool publish_state) { // setup_complete_ helps us ensure an action is called immediately after boot if ((fan_mode == this->prev_fan_mode_) && this->setup_complete_) { @@ -633,7 +754,7 @@ void ThermostatClimate::switch_to_fan_mode_(climate::ClimateFanMode fan_mode, bo this->prev_fan_mode_trigger_->stop_action(); this->prev_fan_mode_trigger_ = nullptr; } - this->start_timer_(thermostat::TIMER_FAN_MODE); + this->start_timer_(thermostat::THERMOSTAT_TIMER_FAN_MODE); if (trig != nullptr) { trig->trigger(); } @@ -653,13 +774,13 @@ void ThermostatClimate::switch_to_mode_(climate::ClimateMode mode, bool publish_ this->prev_mode_trigger_->stop_action(); this->prev_mode_trigger_ = nullptr; } - Trigger<> *trig = this->auto_mode_trigger_; + Trigger<> *trig = this->off_mode_trigger_; switch (mode) { - case climate::CLIMATE_MODE_OFF: - trig = this->off_mode_trigger_; + case climate::CLIMATE_MODE_AUTO: + trig = this->auto_mode_trigger_; break; case climate::CLIMATE_MODE_HEAT_COOL: - // trig = this->auto_mode_trigger_; + trig = this->heat_cool_mode_trigger_; break; case climate::CLIMATE_MODE_COOL: trig = this->cool_mode_trigger_; @@ -673,11 +794,12 @@ void ThermostatClimate::switch_to_mode_(climate::ClimateMode mode, bool publish_ case climate::CLIMATE_MODE_DRY: trig = this->dry_mode_trigger_; break; + case climate::CLIMATE_MODE_OFF: default: // we cannot report an invalid mode back to HA (even if it asked for one) // and must assume some valid value - mode = climate::CLIMATE_MODE_HEAT_COOL; - // trig = this->auto_mode_trigger_; + mode = climate::CLIMATE_MODE_OFF; + // trig = this->off_mode_trigger_; } if (trig != nullptr) { trig->trigger(); @@ -685,8 +807,9 @@ void ThermostatClimate::switch_to_mode_(climate::ClimateMode mode, bool publish_ this->mode = mode; this->prev_mode_ = mode; this->prev_mode_trigger_ = trig; - if (publish_state) + if (publish_state) { this->publish_state(); + } } void ThermostatClimate::switch_to_swing_mode_(climate::ClimateSwingMode swing_mode, bool publish_state) { @@ -732,35 +855,44 @@ void ThermostatClimate::switch_to_swing_mode_(climate::ClimateSwingMode swing_mo bool ThermostatClimate::idle_action_ready_() { if (this->supports_fan_only_action_uses_fan_mode_timer_) { - return !(this->timer_active_(thermostat::TIMER_COOLING_ON) || this->timer_active_(thermostat::TIMER_FAN_MODE) || - this->timer_active_(thermostat::TIMER_HEATING_ON)); + return !(this->timer_active_(thermostat::THERMOSTAT_TIMER_COOLING_ON) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_FAN_MODE) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_HEATING_ON)); } - return !(this->timer_active_(thermostat::TIMER_COOLING_ON) || this->timer_active_(thermostat::TIMER_FANNING_ON) || - this->timer_active_(thermostat::TIMER_HEATING_ON)); + return !(this->timer_active_(thermostat::THERMOSTAT_TIMER_COOLING_ON) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_FANNING_ON) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_HEATING_ON)); } bool ThermostatClimate::cooling_action_ready_() { - return !(this->timer_active_(thermostat::TIMER_IDLE_ON) || this->timer_active_(thermostat::TIMER_FANNING_OFF) || - this->timer_active_(thermostat::TIMER_COOLING_OFF) || this->timer_active_(thermostat::TIMER_HEATING_ON)); + return !(this->timer_active_(thermostat::THERMOSTAT_TIMER_IDLE_ON) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_FANNING_OFF) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_COOLING_OFF) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_HEATING_ON)); } bool ThermostatClimate::drying_action_ready_() { - return !(this->timer_active_(thermostat::TIMER_IDLE_ON) || this->timer_active_(thermostat::TIMER_FANNING_OFF) || - this->timer_active_(thermostat::TIMER_COOLING_OFF) || this->timer_active_(thermostat::TIMER_HEATING_ON)); + return !(this->timer_active_(thermostat::THERMOSTAT_TIMER_IDLE_ON) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_FANNING_OFF) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_COOLING_OFF) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_HEATING_ON)); } -bool ThermostatClimate::fan_mode_ready_() { return !(this->timer_active_(thermostat::TIMER_FAN_MODE)); } +bool ThermostatClimate::fan_mode_ready_() { return !(this->timer_active_(thermostat::THERMOSTAT_TIMER_FAN_MODE)); } bool ThermostatClimate::fanning_action_ready_() { if (this->supports_fan_only_action_uses_fan_mode_timer_) { - return !(this->timer_active_(thermostat::TIMER_FAN_MODE)); + return !(this->timer_active_(thermostat::THERMOSTAT_TIMER_FAN_MODE)); } - return !(this->timer_active_(thermostat::TIMER_IDLE_ON) || this->timer_active_(thermostat::TIMER_FANNING_OFF)); + return !(this->timer_active_(thermostat::THERMOSTAT_TIMER_IDLE_ON) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_FANNING_OFF)); } bool ThermostatClimate::heating_action_ready_() { - return !(this->timer_active_(thermostat::TIMER_IDLE_ON) || this->timer_active_(thermostat::TIMER_COOLING_ON) || - this->timer_active_(thermostat::TIMER_FANNING_OFF) || this->timer_active_(thermostat::TIMER_HEATING_OFF)); + return !(this->timer_active_(thermostat::THERMOSTAT_TIMER_IDLE_ON) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_COOLING_ON) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_FANNING_OFF) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_HEATING_OFF)); } void ThermostatClimate::start_timer_(const ThermostatClimateTimerIndex timer_index) { @@ -849,6 +981,20 @@ void ThermostatClimate::idle_on_timer_callback_() { this->switch_to_supplemental_action_(this->compute_supplemental_action_()); } +void ThermostatClimate::check_humidity_change_trigger_() { + if ((this->prev_target_humidity_ == this->target_humidity) && this->setup_complete_) { + return; // nothing changed, no reason to trigger + } else { + // save the new temperature so we can check it again later; the trigger will fire below + this->prev_target_humidity_ = this->target_humidity; + } + // trigger the action + Trigger<> *trig = this->humidity_change_trigger_; + if (trig != nullptr) { + trig->trigger(); + } +} + void ThermostatClimate::check_temperature_change_trigger_() { if (this->supports_two_points_) { // setup_complete_ helps us ensure an action is called immediately after boot @@ -958,51 +1104,71 @@ bool ThermostatClimate::supplemental_heating_required_() { (this->supplemental_action_ == climate::CLIMATE_ACTION_HEATING)); } -void ThermostatClimate::dump_preset_config_(const char *preset_name, const ThermostatClimateTargetTempConfig &config, - bool is_default_preset) { - ESP_LOGCONFIG(TAG, " %s Is Default: %s", preset_name, YESNO(is_default_preset)); +bool ThermostatClimate::dehumidification_required_() { + if (this->current_humidity > this->target_humidity + this->humidity_hysteresis_) { + // if the current humidity exceeds the target + hysteresis, dehumidification is required + return true; + } else if (this->current_humidity < this->target_humidity - this->humidity_hysteresis_) { + // if the current humidity is less than the target - hysteresis, dehumidification should stop + return false; + } + // if we get here, the current humidity is between target + hysteresis and target - hysteresis, + // so the action should not change + return this->humidification_action == THERMOSTAT_HUMIDITY_CONTROL_ACTION_DEHUMIDIFY; +} +bool ThermostatClimate::humidification_required_() { + if (this->current_humidity < this->target_humidity - this->humidity_hysteresis_) { + // if the current humidity is below the target - hysteresis, humidification is required + return true; + } else if (this->current_humidity > this->target_humidity + this->humidity_hysteresis_) { + // if the current humidity is above the target + hysteresis, humidification should stop + return false; + } + // if we get here, the current humidity is between target - hysteresis and target + hysteresis, + // so the action should not change + return this->humidification_action == THERMOSTAT_HUMIDITY_CONTROL_ACTION_HUMIDIFY; +} + +void ThermostatClimate::dump_preset_config_(const char *preset_name, const ThermostatClimateTargetTempConfig &config) { if (this->supports_heat_) { - if (this->supports_two_points_) { - ESP_LOGCONFIG(TAG, " %s Default Target Temperature Low: %.1f°C", preset_name, - config.default_temperature_low); - } else { - ESP_LOGCONFIG(TAG, " %s Default Target Temperature Low: %.1f°C", preset_name, config.default_temperature); - } + ESP_LOGCONFIG(TAG, " Default Target Temperature Low: %.1f°C", + this->supports_two_points_ ? config.default_temperature_low : config.default_temperature); } if ((this->supports_cool_) || (this->supports_fan_only_)) { - if (this->supports_two_points_) { - ESP_LOGCONFIG(TAG, " %s Default Target Temperature High: %.1f°C", preset_name, - config.default_temperature_high); - } else { - ESP_LOGCONFIG(TAG, " %s Default Target Temperature High: %.1f°C", preset_name, config.default_temperature); - } + ESP_LOGCONFIG(TAG, " Default Target Temperature High: %.1f°C", + this->supports_two_points_ ? config.default_temperature_high : config.default_temperature); } if (config.mode_.has_value()) { - ESP_LOGCONFIG(TAG, " %s Default Mode: %s", preset_name, - LOG_STR_ARG(climate::climate_mode_to_string(*config.mode_))); + ESP_LOGCONFIG(TAG, " Default Mode: %s", LOG_STR_ARG(climate::climate_mode_to_string(*config.mode_))); } if (config.fan_mode_.has_value()) { - ESP_LOGCONFIG(TAG, " %s Default Fan Mode: %s", preset_name, + ESP_LOGCONFIG(TAG, " Default Fan Mode: %s", LOG_STR_ARG(climate::climate_fan_mode_to_string(*config.fan_mode_))); } if (config.swing_mode_.has_value()) { - ESP_LOGCONFIG(TAG, " %s Default Swing Mode: %s", preset_name, + ESP_LOGCONFIG(TAG, " Default Swing Mode: %s", LOG_STR_ARG(climate::climate_swing_mode_to_string(*config.swing_mode_))); } } void ThermostatClimate::change_preset_(climate::ClimatePreset preset) { - auto config = this->preset_config_.find(preset); + // Linear search through preset configurations + const ThermostatClimateTargetTempConfig *config = nullptr; + for (const auto &entry : this->preset_config_) { + if (entry.preset == preset) { + config = &entry.config; + break; + } + } - if (config != this->preset_config_.end()) { + if (config != nullptr) { ESP_LOGV(TAG, "Preset %s requested", LOG_STR_ARG(climate::climate_preset_to_string(preset))); - if (this->change_preset_internal_(config->second) || (!this->preset.has_value()) || - this->preset.value() != preset) { + if (this->change_preset_internal_(*config) || (!this->preset.has_value()) || this->preset.value() != preset) { // Fire any preset changed trigger if defined Trigger<> *trig = this->preset_change_trigger_; - this->preset = preset; + this->set_preset_(preset); if (trig != nullptr) { trig->trigger(); } @@ -1012,36 +1178,43 @@ void ThermostatClimate::change_preset_(climate::ClimatePreset preset) { } else { ESP_LOGI(TAG, "No changes required to apply preset %s", LOG_STR_ARG(climate::climate_preset_to_string(preset))); } - this->custom_preset.reset(); - this->preset = preset; } else { ESP_LOGW(TAG, "Preset %s not configured; ignoring", LOG_STR_ARG(climate::climate_preset_to_string(preset))); } } -void ThermostatClimate::change_custom_preset_(const std::string &custom_preset) { - auto config = this->custom_preset_config_.find(custom_preset); +void ThermostatClimate::change_custom_preset_(const char *custom_preset) { + // Linear search through custom preset configurations + const ThermostatClimateTargetTempConfig *config = nullptr; + for (const auto &entry : this->custom_preset_config_) { + if (strcmp(entry.name, custom_preset) == 0) { + config = &entry.config; + break; + } + } - if (config != this->custom_preset_config_.end()) { - ESP_LOGV(TAG, "Custom preset %s requested", custom_preset.c_str()); - if (this->change_preset_internal_(config->second) || (!this->custom_preset.has_value()) || - this->custom_preset.value() != custom_preset) { + if (config != nullptr) { + ESP_LOGV(TAG, "Custom preset %s requested", custom_preset); + if (this->change_preset_internal_(*config) || !this->has_custom_preset() || + strcmp(this->get_custom_preset(), custom_preset) != 0) { // Fire any preset changed trigger if defined Trigger<> *trig = this->preset_change_trigger_; - this->custom_preset = custom_preset; + // Use the base class method which handles pointer lookup and preset reset internally + this->set_custom_preset_(custom_preset); if (trig != nullptr) { trig->trigger(); } this->refresh(); - ESP_LOGI(TAG, "Custom preset %s applied", custom_preset.c_str()); + ESP_LOGI(TAG, "Custom preset %s applied", custom_preset); } else { - ESP_LOGI(TAG, "No changes required to apply custom preset %s", custom_preset.c_str()); + ESP_LOGI(TAG, "No changes required to apply custom preset %s", custom_preset); + // Note: set_custom_preset_() above handles preset.reset() and custom_preset_ assignment internally. + // The old code had these lines here unconditionally, which was a bug (double assignment, state modification + // even when no changes were needed). Now properly handled by the protected setter with mutual exclusion. } - this->preset.reset(); - this->custom_preset = custom_preset; } else { - ESP_LOGW(TAG, "Custom preset %s not configured; ignoring", custom_preset.c_str()); + ESP_LOGW(TAG, "Custom preset %s not configured; ignoring", custom_preset); } } @@ -1087,14 +1260,12 @@ bool ThermostatClimate::change_preset_internal_(const ThermostatClimateTargetTem return something_changed; } -void ThermostatClimate::set_preset_config(climate::ClimatePreset preset, - const ThermostatClimateTargetTempConfig &config) { - this->preset_config_[preset] = config; +void ThermostatClimate::set_preset_config(std::initializer_list presets) { + this->preset_config_ = presets; } -void ThermostatClimate::set_custom_preset_config(const std::string &name, - const ThermostatClimateTargetTempConfig &config) { - this->custom_preset_config_[name] = config; +void ThermostatClimate::set_custom_preset_config(std::initializer_list presets) { + this->custom_preset_config_ = presets; } ThermostatClimate::ThermostatClimate() @@ -1106,6 +1277,7 @@ ThermostatClimate::ThermostatClimate() heat_action_trigger_(new Trigger<>()), supplemental_heat_action_trigger_(new Trigger<>()), heat_mode_trigger_(new Trigger<>()), + heat_cool_mode_trigger_(new Trigger<>()), auto_mode_trigger_(new Trigger<>()), idle_action_trigger_(new Trigger<>()), off_mode_trigger_(new Trigger<>()), @@ -1125,11 +1297,23 @@ ThermostatClimate::ThermostatClimate() swing_mode_off_trigger_(new Trigger<>()), swing_mode_horizontal_trigger_(new Trigger<>()), swing_mode_vertical_trigger_(new Trigger<>()), + humidity_change_trigger_(new Trigger<>()), temperature_change_trigger_(new Trigger<>()), - preset_change_trigger_(new Trigger<>()) {} + preset_change_trigger_(new Trigger<>()), + humidity_control_dehumidify_action_trigger_(new Trigger<>()), + humidity_control_humidify_action_trigger_(new Trigger<>()), + humidity_control_off_action_trigger_(new Trigger<>()) {} -void ThermostatClimate::set_default_preset(const std::string &custom_preset) { - this->default_custom_preset_ = custom_preset; +void ThermostatClimate::set_default_preset(const char *custom_preset) { + // Find the preset in custom_preset_config_ and store pointer from there + for (const auto &entry : this->custom_preset_config_) { + if (strcmp(entry.name, custom_preset) == 0) { + this->default_custom_preset_ = entry.name; + return; + } + } + // If not found, it will be caught during validation + this->default_custom_preset_ = nullptr; } void ThermostatClimate::set_default_preset(climate::ClimatePreset preset) { this->default_preset_ = preset; } @@ -1147,49 +1331,52 @@ void ThermostatClimate::set_heat_overrun(float overrun) { this->heating_overrun_ void ThermostatClimate::set_supplemental_cool_delta(float delta) { this->supplemental_cool_delta_ = delta; } void ThermostatClimate::set_supplemental_heat_delta(float delta) { this->supplemental_heat_delta_ = delta; } void ThermostatClimate::set_cooling_maximum_run_time_in_sec(uint32_t time) { - this->timer_[thermostat::TIMER_COOLING_MAX_RUN_TIME].time = + this->timer_[thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME].time = 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); } void ThermostatClimate::set_cooling_minimum_off_time_in_sec(uint32_t time) { - this->timer_[thermostat::TIMER_COOLING_OFF].time = + this->timer_[thermostat::THERMOSTAT_TIMER_COOLING_OFF].time = 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); } void ThermostatClimate::set_cooling_minimum_run_time_in_sec(uint32_t time) { - this->timer_[thermostat::TIMER_COOLING_ON].time = + this->timer_[thermostat::THERMOSTAT_TIMER_COOLING_ON].time = 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); } void ThermostatClimate::set_fan_mode_minimum_switching_time_in_sec(uint32_t time) { - this->timer_[thermostat::TIMER_FAN_MODE].time = + this->timer_[thermostat::THERMOSTAT_TIMER_FAN_MODE].time = 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); } void ThermostatClimate::set_fanning_minimum_off_time_in_sec(uint32_t time) { - this->timer_[thermostat::TIMER_FANNING_OFF].time = + this->timer_[thermostat::THERMOSTAT_TIMER_FANNING_OFF].time = 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); } void ThermostatClimate::set_fanning_minimum_run_time_in_sec(uint32_t time) { - this->timer_[thermostat::TIMER_FANNING_ON].time = + this->timer_[thermostat::THERMOSTAT_TIMER_FANNING_ON].time = 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); } void ThermostatClimate::set_heating_maximum_run_time_in_sec(uint32_t time) { - this->timer_[thermostat::TIMER_HEATING_MAX_RUN_TIME].time = + this->timer_[thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME].time = 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); } void ThermostatClimate::set_heating_minimum_off_time_in_sec(uint32_t time) { - this->timer_[thermostat::TIMER_HEATING_OFF].time = + this->timer_[thermostat::THERMOSTAT_TIMER_HEATING_OFF].time = 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); } void ThermostatClimate::set_heating_minimum_run_time_in_sec(uint32_t time) { - this->timer_[thermostat::TIMER_HEATING_ON].time = + this->timer_[thermostat::THERMOSTAT_TIMER_HEATING_ON].time = 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); } void ThermostatClimate::set_idle_minimum_time_in_sec(uint32_t time) { - this->timer_[thermostat::TIMER_IDLE_ON].time = + this->timer_[thermostat::THERMOSTAT_TIMER_IDLE_ON].time = 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); } void ThermostatClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } void ThermostatClimate::set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; } +void ThermostatClimate::set_humidity_hysteresis(float humidity_hysteresis) { + this->humidity_hysteresis_ = std::clamp(humidity_hysteresis, 0.0f, 100.0f); +} void ThermostatClimate::set_use_startup_delay(bool use_startup_delay) { this->use_startup_delay_ = use_startup_delay; } void ThermostatClimate::set_supports_heat_cool(bool supports_heat_cool) { this->supports_heat_cool_ = supports_heat_cool; @@ -1257,6 +1444,18 @@ void ThermostatClimate::set_supports_swing_mode_vertical(bool supports_swing_mod void ThermostatClimate::set_supports_two_points(bool supports_two_points) { this->supports_two_points_ = supports_two_points; } +void ThermostatClimate::set_supports_dehumidification(bool supports_dehumidification) { + this->supports_dehumidification_ = supports_dehumidification; + if (supports_dehumidification) { + this->supports_humidification_ = false; + } +} +void ThermostatClimate::set_supports_humidification(bool supports_humidification) { + this->supports_humidification_ = supports_humidification; + if (supports_humidification) { + this->supports_dehumidification_ = false; + } +} Trigger<> *ThermostatClimate::get_cool_action_trigger() const { return this->cool_action_trigger_; } Trigger<> *ThermostatClimate::get_supplemental_cool_action_trigger() const { @@ -1274,6 +1473,7 @@ Trigger<> *ThermostatClimate::get_cool_mode_trigger() const { return this->cool_ Trigger<> *ThermostatClimate::get_dry_mode_trigger() const { return this->dry_mode_trigger_; } Trigger<> *ThermostatClimate::get_fan_only_mode_trigger() const { return this->fan_only_mode_trigger_; } Trigger<> *ThermostatClimate::get_heat_mode_trigger() const { return this->heat_mode_trigger_; } +Trigger<> *ThermostatClimate::get_heat_cool_mode_trigger() const { return this->heat_cool_mode_trigger_; } Trigger<> *ThermostatClimate::get_off_mode_trigger() const { return this->off_mode_trigger_; } Trigger<> *ThermostatClimate::get_fan_mode_on_trigger() const { return this->fan_mode_on_trigger_; } Trigger<> *ThermostatClimate::get_fan_mode_off_trigger() const { return this->fan_mode_off_trigger_; } @@ -1289,70 +1489,85 @@ Trigger<> *ThermostatClimate::get_swing_mode_both_trigger() const { return this- Trigger<> *ThermostatClimate::get_swing_mode_off_trigger() const { return this->swing_mode_off_trigger_; } Trigger<> *ThermostatClimate::get_swing_mode_horizontal_trigger() const { return this->swing_mode_horizontal_trigger_; } Trigger<> *ThermostatClimate::get_swing_mode_vertical_trigger() const { return this->swing_mode_vertical_trigger_; } +Trigger<> *ThermostatClimate::get_humidity_change_trigger() const { return this->humidity_change_trigger_; } Trigger<> *ThermostatClimate::get_temperature_change_trigger() const { return this->temperature_change_trigger_; } Trigger<> *ThermostatClimate::get_preset_change_trigger() const { return this->preset_change_trigger_; } +Trigger<> *ThermostatClimate::get_humidity_control_dehumidify_action_trigger() const { + return this->humidity_control_dehumidify_action_trigger_; +} +Trigger<> *ThermostatClimate::get_humidity_control_humidify_action_trigger() const { + return this->humidity_control_humidify_action_trigger_; +} +Trigger<> *ThermostatClimate::get_humidity_control_off_action_trigger() const { + return this->humidity_control_off_action_trigger_; +} void ThermostatClimate::dump_config() { LOG_CLIMATE("", "Thermostat", this); + ESP_LOGCONFIG(TAG, + " On boot, restore from: %s\n" + " Use Start-up Delay: %s", + this->on_boot_restore_from_ == thermostat::DEFAULT_PRESET ? "DEFAULT_PRESET" : "MEMORY", + YESNO(this->use_startup_delay_)); if (this->supports_two_points_) { ESP_LOGCONFIG(TAG, " Minimum Set Point Differential: %.1f°C", this->set_point_minimum_differential_); } - ESP_LOGCONFIG(TAG, " Use Start-up Delay: %s", YESNO(this->use_startup_delay_)); if (this->supports_cool_) { ESP_LOGCONFIG(TAG, " Cooling Parameters:\n" " Deadband: %.1f°C\n" - " Overrun: %.1f°C", - this->cooling_deadband_, this->cooling_overrun_); - if ((this->supplemental_cool_delta_ > 0) || (this->timer_duration_(thermostat::TIMER_COOLING_MAX_RUN_TIME) > 0)) { - ESP_LOGCONFIG(TAG, - " Supplemental Delta: %.1f°C\n" - " Maximum Run Time: %" PRIu32 "s", - this->supplemental_cool_delta_, - this->timer_duration_(thermostat::TIMER_COOLING_MAX_RUN_TIME) / 1000); - } - ESP_LOGCONFIG(TAG, + " Overrun: %.1f°C\n" " Minimum Off Time: %" PRIu32 "s\n" " Minimum Run Time: %" PRIu32 "s", - this->timer_duration_(thermostat::TIMER_COOLING_OFF) / 1000, - this->timer_duration_(thermostat::TIMER_COOLING_ON) / 1000); + this->cooling_deadband_, this->cooling_overrun_, + this->timer_duration_(thermostat::THERMOSTAT_TIMER_COOLING_OFF) / 1000, + this->timer_duration_(thermostat::THERMOSTAT_TIMER_COOLING_ON) / 1000); + if ((this->supplemental_cool_delta_ > 0) || + (this->timer_duration_(thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME) > 0)) { + ESP_LOGCONFIG(TAG, + " Maximum Run Time: %" PRIu32 "s\n" + " Supplemental Delta: %.1f°C", + this->timer_duration_(thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME) / 1000, + this->supplemental_cool_delta_); + } } if (this->supports_heat_) { ESP_LOGCONFIG(TAG, " Heating Parameters:\n" " Deadband: %.1f°C\n" - " Overrun: %.1f°C", - this->heating_deadband_, this->heating_overrun_); - if ((this->supplemental_heat_delta_ > 0) || (this->timer_duration_(thermostat::TIMER_HEATING_MAX_RUN_TIME) > 0)) { - ESP_LOGCONFIG(TAG, - " Supplemental Delta: %.1f°C\n" - " Maximum Run Time: %" PRIu32 "s", - this->supplemental_heat_delta_, - this->timer_duration_(thermostat::TIMER_HEATING_MAX_RUN_TIME) / 1000); - } - ESP_LOGCONFIG(TAG, + " Overrun: %.1f°C\n" " Minimum Off Time: %" PRIu32 "s\n" " Minimum Run Time: %" PRIu32 "s", - this->timer_duration_(thermostat::TIMER_HEATING_OFF) / 1000, - this->timer_duration_(thermostat::TIMER_HEATING_ON) / 1000); + this->heating_deadband_, this->heating_overrun_, + this->timer_duration_(thermostat::THERMOSTAT_TIMER_HEATING_OFF) / 1000, + this->timer_duration_(thermostat::THERMOSTAT_TIMER_HEATING_ON) / 1000); + if ((this->supplemental_heat_delta_ > 0) || + (this->timer_duration_(thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME) > 0)) { + ESP_LOGCONFIG(TAG, + " Maximum Run Time: %" PRIu32 "s\n" + " Supplemental Delta: %.1f°C", + this->timer_duration_(thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME) / 1000, + this->supplemental_heat_delta_); + } } if (this->supports_fan_only_) { ESP_LOGCONFIG(TAG, - " Fanning Minimum Off Time: %" PRIu32 "s\n" - " Fanning Minimum Run Time: %" PRIu32 "s", - this->timer_duration_(thermostat::TIMER_FANNING_OFF) / 1000, - this->timer_duration_(thermostat::TIMER_FANNING_ON) / 1000); + " Fan Parameters:\n" + " Minimum Off Time: %" PRIu32 "s\n" + " Minimum Run Time: %" PRIu32 "s", + this->timer_duration_(thermostat::THERMOSTAT_TIMER_FANNING_OFF) / 1000, + this->timer_duration_(thermostat::THERMOSTAT_TIMER_FANNING_ON) / 1000); } if (this->supports_fan_mode_on_ || this->supports_fan_mode_off_ || this->supports_fan_mode_auto_ || this->supports_fan_mode_low_ || this->supports_fan_mode_medium_ || this->supports_fan_mode_high_ || this->supports_fan_mode_middle_ || this->supports_fan_mode_focus_ || this->supports_fan_mode_diffuse_ || this->supports_fan_mode_quiet_) { ESP_LOGCONFIG(TAG, " Minimum Fan Mode Switching Time: %" PRIu32 "s", - this->timer_duration_(thermostat::TIMER_FAN_MODE) / 1000); + this->timer_duration_(thermostat::THERMOSTAT_TIMER_FAN_MODE) / 1000); } - ESP_LOGCONFIG(TAG, " Minimum Idle Time: %" PRIu32 "s", this->timer_[thermostat::TIMER_IDLE_ON].time / 1000); ESP_LOGCONFIG(TAG, + " Minimum Idle Time: %" PRIu32 "s\n" " Supported MODES:\n" " AUTO: %s\n" " HEAT/COOL: %s\n" @@ -1362,8 +1577,9 @@ void ThermostatClimate::dump_config() { " FAN_ONLY: %s\n" " FAN_ONLY_ACTION_USES_FAN_MODE_TIMER: %s\n" " FAN_ONLY_COOLING: %s", - YESNO(this->supports_auto_), YESNO(this->supports_heat_cool_), YESNO(this->supports_heat_), - YESNO(this->supports_cool_), YESNO(this->supports_dry_), YESNO(this->supports_fan_only_), + this->timer_[thermostat::THERMOSTAT_TIMER_IDLE_ON].time / 1000, YESNO(this->supports_auto_), + YESNO(this->supports_heat_cool_), YESNO(this->supports_heat_), YESNO(this->supports_cool_), + YESNO(this->supports_dry_), YESNO(this->supports_fan_only_), YESNO(this->supports_fan_only_action_uses_fan_mode_timer_), YESNO(this->supports_fan_only_cooling_)); if (this->supports_cool_) { ESP_LOGCONFIG(TAG, " FAN_WITH_COOLING: %s", YESNO(this->supports_fan_with_cooling_)); @@ -1382,40 +1598,50 @@ void ThermostatClimate::dump_config() { " MIDDLE: %s\n" " FOCUS: %s\n" " DIFFUSE: %s\n" - " QUIET: %s", - YESNO(this->supports_fan_mode_on_), YESNO(this->supports_fan_mode_off_), - YESNO(this->supports_fan_mode_auto_), YESNO(this->supports_fan_mode_low_), - YESNO(this->supports_fan_mode_medium_), YESNO(this->supports_fan_mode_high_), - YESNO(this->supports_fan_mode_middle_), YESNO(this->supports_fan_mode_focus_), - YESNO(this->supports_fan_mode_diffuse_), YESNO(this->supports_fan_mode_quiet_)); - ESP_LOGCONFIG(TAG, + " QUIET: %s\n" " Supported SWING MODES:\n" " BOTH: %s\n" " OFF: %s\n" " HORIZONTAL: %s\n" " VERTICAL: %s\n" - " Supports TWO SET POINTS: %s", + " Supports TWO SET POINTS: %s\n" + " Supported Humidity Parameters:\n" + " CURRENT: %s\n" + " TARGET: %s\n" + " DEHUMIDIFICATION: %s\n" + " HUMIDIFICATION: %s", + YESNO(this->supports_fan_mode_on_), YESNO(this->supports_fan_mode_off_), + YESNO(this->supports_fan_mode_auto_), YESNO(this->supports_fan_mode_low_), + YESNO(this->supports_fan_mode_medium_), YESNO(this->supports_fan_mode_high_), + YESNO(this->supports_fan_mode_middle_), YESNO(this->supports_fan_mode_focus_), + YESNO(this->supports_fan_mode_diffuse_), YESNO(this->supports_fan_mode_quiet_), YESNO(this->supports_swing_mode_both_), YESNO(this->supports_swing_mode_off_), YESNO(this->supports_swing_mode_horizontal_), YESNO(this->supports_swing_mode_vertical_), - YESNO(this->supports_two_points_)); + YESNO(this->supports_two_points_), + YESNO(this->get_traits().has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)), + YESNO(this->supports_dehumidification_ || this->supports_humidification_), + YESNO(this->supports_dehumidification_), YESNO(this->supports_humidification_)); - ESP_LOGCONFIG(TAG, " Supported PRESETS: "); - for (auto &it : this->preset_config_) { - const auto *preset_name = LOG_STR_ARG(climate::climate_preset_to_string(it.first)); - - ESP_LOGCONFIG(TAG, " Supports %s: %s", preset_name, YESNO(true)); - this->dump_preset_config_(preset_name, it.second, it.first == this->default_preset_); + if (!this->preset_config_.empty()) { + ESP_LOGCONFIG(TAG, " Supported PRESETS:"); + for (const auto &entry : this->preset_config_) { + const auto *preset_name = LOG_STR_ARG(climate::climate_preset_to_string(entry.preset)); + ESP_LOGCONFIG(TAG, " %s:%s", preset_name, entry.preset == this->default_preset_ ? " (default)" : ""); + this->dump_preset_config_(preset_name, entry.config); + } } - ESP_LOGCONFIG(TAG, " Supported CUSTOM PRESETS: "); - for (auto &it : this->custom_preset_config_) { - const auto *preset_name = it.first.c_str(); - - ESP_LOGCONFIG(TAG, " Supports %s: %s", preset_name, YESNO(true)); - this->dump_preset_config_(preset_name, it.second, it.first == this->default_custom_preset_); + if (!this->custom_preset_config_.empty()) { + ESP_LOGCONFIG(TAG, " Supported CUSTOM PRESETS:"); + for (const auto &entry : this->custom_preset_config_) { + const auto *preset_name = entry.name; + ESP_LOGCONFIG(TAG, " %s:%s", preset_name, + (this->default_custom_preset_ != nullptr && strcmp(entry.name, this->default_custom_preset_) == 0) + ? " (default)" + : ""); + this->dump_preset_config_(preset_name, entry.config); + } } - ESP_LOGCONFIG(TAG, " On boot, restore from: %s", - this->on_boot_restore_from_ == thermostat::DEFAULT_PRESET ? "DEFAULT_PRESET" : "MEMORY"); } ThermostatClimateTargetTempConfig::ThermostatClimateTargetTempConfig() = default; diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h index 007d7297d5..69d2307b1c 100644 --- a/esphome/components/thermostat/thermostat_climate.h +++ b/esphome/components/thermostat/thermostat_climate.h @@ -3,27 +3,35 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/hal.h" +#include "esphome/core/helpers.h" #include "esphome/components/climate/climate.h" #include "esphome/components/sensor/sensor.h" +#include #include -#include -#include namespace esphome { namespace thermostat { +enum HumidificationAction : uint8_t { + THERMOSTAT_HUMIDITY_CONTROL_ACTION_OFF = 0, + THERMOSTAT_HUMIDITY_CONTROL_ACTION_DEHUMIDIFY = 1, + THERMOSTAT_HUMIDITY_CONTROL_ACTION_HUMIDIFY = 2, + THERMOSTAT_HUMIDITY_CONTROL_ACTION_NONE, +}; + enum ThermostatClimateTimerIndex : uint8_t { - TIMER_COOLING_MAX_RUN_TIME = 0, - TIMER_COOLING_OFF = 1, - TIMER_COOLING_ON = 2, - TIMER_FAN_MODE = 3, - TIMER_FANNING_OFF = 4, - TIMER_FANNING_ON = 5, - TIMER_HEATING_MAX_RUN_TIME = 6, - TIMER_HEATING_OFF = 7, - TIMER_HEATING_ON = 8, - TIMER_IDLE_ON = 9, + THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME = 0, + THERMOSTAT_TIMER_COOLING_OFF = 1, + THERMOSTAT_TIMER_COOLING_ON = 2, + THERMOSTAT_TIMER_FAN_MODE = 3, + THERMOSTAT_TIMER_FANNING_OFF = 4, + THERMOSTAT_TIMER_FANNING_ON = 5, + THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME = 6, + THERMOSTAT_TIMER_HEATING_OFF = 7, + THERMOSTAT_TIMER_HEATING_ON = 8, + THERMOSTAT_TIMER_IDLE_ON = 9, + THERMOSTAT_TIMER_COUNT = 10, }; enum OnBootRestoreFrom : uint8_t { @@ -32,6 +40,10 @@ enum OnBootRestoreFrom : uint8_t { }; struct ThermostatClimateTimer { + ThermostatClimateTimer() = default; + ThermostatClimateTimer(bool active, uint32_t time, uint32_t started, std::function func) + : active(active), time(time), started(started), func(std::move(func)) {} + bool active; uint32_t time; uint32_t started; @@ -60,14 +72,29 @@ struct ThermostatClimateTargetTempConfig { optional mode_{}; }; +/// Entry for standard preset lookup +struct ThermostatPresetEntry { + climate::ClimatePreset preset; + ThermostatClimateTargetTempConfig config; +}; + +/// Entry for custom preset lookup +struct ThermostatCustomPresetEntry { + const char *name; + ThermostatClimateTargetTempConfig config; +}; + class ThermostatClimate : public climate::Climate, public Component { public: + using PresetEntry = ThermostatPresetEntry; + using CustomPresetEntry = ThermostatCustomPresetEntry; + ThermostatClimate(); void setup() override; void dump_config() override; void loop() override; - void set_default_preset(const std::string &custom_preset); + void set_default_preset(const char *custom_preset); void set_default_preset(climate::ClimatePreset preset); void set_on_boot_restore_from(OnBootRestoreFrom on_boot_restore_from); void set_set_point_minimum_differential(float differential); @@ -89,6 +116,7 @@ class ThermostatClimate : public climate::Climate, public Component { void set_idle_minimum_time_in_sec(uint32_t time); void set_sensor(sensor::Sensor *sensor); void set_humidity_sensor(sensor::Sensor *humidity_sensor); + void set_humidity_hysteresis(float humidity_hysteresis); void set_use_startup_delay(bool use_startup_delay); void set_supports_auto(bool supports_auto); void set_supports_heat_cool(bool supports_heat_cool); @@ -114,10 +142,12 @@ class ThermostatClimate : public climate::Climate, public Component { void set_supports_swing_mode_horizontal(bool supports_swing_mode_horizontal); void set_supports_swing_mode_off(bool supports_swing_mode_off); void set_supports_swing_mode_vertical(bool supports_swing_mode_vertical); + void set_supports_dehumidification(bool supports_dehumidification); + void set_supports_humidification(bool supports_humidification); void set_supports_two_points(bool supports_two_points); - void set_preset_config(climate::ClimatePreset preset, const ThermostatClimateTargetTempConfig &config); - void set_custom_preset_config(const std::string &name, const ThermostatClimateTargetTempConfig &config); + void set_preset_config(std::initializer_list presets); + void set_custom_preset_config(std::initializer_list presets); Trigger<> *get_cool_action_trigger() const; Trigger<> *get_supplemental_cool_action_trigger() const; @@ -131,6 +161,7 @@ class ThermostatClimate : public climate::Climate, public Component { Trigger<> *get_dry_mode_trigger() const; Trigger<> *get_fan_only_mode_trigger() const; Trigger<> *get_heat_mode_trigger() const; + Trigger<> *get_heat_cool_mode_trigger() const; Trigger<> *get_off_mode_trigger() const; Trigger<> *get_fan_mode_on_trigger() const; Trigger<> *get_fan_mode_off_trigger() const; @@ -146,8 +177,12 @@ class ThermostatClimate : public climate::Climate, public Component { Trigger<> *get_swing_mode_horizontal_trigger() const; Trigger<> *get_swing_mode_off_trigger() const; Trigger<> *get_swing_mode_vertical_trigger() const; + Trigger<> *get_humidity_change_trigger() const; Trigger<> *get_temperature_change_trigger() const; Trigger<> *get_preset_change_trigger() const; + Trigger<> *get_humidity_control_dehumidify_action_trigger() const; + Trigger<> *get_humidity_control_humidify_action_trigger() const; + Trigger<> *get_humidity_control_off_action_trigger() const; /// Get current hysteresis values float cool_deadband(); float cool_overrun(); @@ -163,11 +198,17 @@ class ThermostatClimate : public climate::Climate, public Component { /// Returns the fan mode that is locked in (check fan_mode_change_delayed(), first!) climate::ClimateFanMode locked_fan_mode(); /// Set point and hysteresis validation - bool hysteresis_valid(); // returns true if valid + bool hysteresis_valid(); // returns true if valid + bool humidity_hysteresis_valid(); // returns true if valid + bool limit_setpoints_for_heat_cool(); // returns true if set points should be further limited within visual range void validate_target_temperature(); - void validate_target_temperatures(); + void validate_target_temperatures(bool pin_target_temperature_high); void validate_target_temperature_low(); void validate_target_temperature_high(); + void validate_target_humidity(); + + /// The current humidification action + HumidificationAction humidification_action{THERMOSTAT_HUMIDITY_CONTROL_ACTION_NONE}; protected: /// Override control to change settings of the climate device. @@ -176,7 +217,7 @@ class ThermostatClimate : public climate::Climate, public Component { /// Change to a provided preset setting; will reset temperature, mode, fan, and swing modes accordingly void change_preset_(climate::ClimatePreset preset); /// Change to a provided custom preset setting; will reset temperature, mode, fan, and swing modes accordingly - void change_custom_preset_(const std::string &custom_preset); + void change_custom_preset_(const char *custom_preset); /// Applies the temperature, mode, fan, and swing modes of the provided config. /// This is agnostic of custom vs built in preset @@ -189,11 +230,13 @@ class ThermostatClimate : public climate::Climate, public Component { /// Re-compute the required action of this climate controller. climate::ClimateAction compute_action_(bool ignore_timers = false); climate::ClimateAction compute_supplemental_action_(); + HumidificationAction compute_humidity_control_action_(); /// Switch the climate device to the given climate action. void switch_to_action_(climate::ClimateAction action, bool publish_state = true); void switch_to_supplemental_action_(climate::ClimateAction action); void trigger_supplemental_action_(); + void switch_to_humidity_control_action_(HumidificationAction action); /// Switch the climate device to the given climate fan mode. void switch_to_fan_mode_(climate::ClimateFanMode fan_mode, bool publish_state = true); @@ -204,6 +247,9 @@ class ThermostatClimate : public climate::Climate, public Component { /// Switch the climate device to the given climate swing mode. void switch_to_swing_mode_(climate::ClimateSwingMode swing_mode, bool publish_state = true); + /// Check if the humidity change trigger should be called. + void check_humidity_change_trigger_(); + /// Check if the temperature change trigger should be called. void check_temperature_change_trigger_(); @@ -240,13 +286,31 @@ class ThermostatClimate : public climate::Climate, public Component { bool heating_required_(); bool supplemental_cooling_required_(); bool supplemental_heating_required_(); + bool dehumidification_required_(); + bool humidification_required_(); - void dump_preset_config_(const char *preset_name, const ThermostatClimateTargetTempConfig &config, - bool is_default_preset); + void dump_preset_config_(const char *preset_name, const ThermostatClimateTargetTempConfig &config); /// Minimum allowable duration in seconds for action timers const uint8_t min_timer_duration_{1}; + /// Store previously-known states + /// + /// These are used to determine when a trigger/action needs to be called + climate::ClimateFanMode prev_fan_mode_{climate::CLIMATE_FAN_ON}; + climate::ClimateMode prev_mode_{climate::CLIMATE_MODE_OFF}; + climate::ClimateSwingMode prev_swing_mode_{climate::CLIMATE_SWING_OFF}; + + /// The current supplemental action + climate::ClimateAction supplemental_action_{climate::CLIMATE_ACTION_OFF}; + + /// Default standard preset to use on start up + climate::ClimatePreset default_preset_{}; + + /// If set to DEFAULT_PRESET then the default preset is always used. When MEMORY prior + /// state will attempt to be restored if possible + OnBootRestoreFrom on_boot_restore_from_{OnBootRestoreFrom::MEMORY}; + /// Whether the controller supports auto/cooling/drying/fanning/heating. /// /// A false value for any given attribute means that the controller has no such action @@ -302,6 +366,12 @@ class ThermostatClimate : public climate::Climate, public Component { /// A false value means that the controller has no such support. bool supports_two_points_{false}; + /// Whether the controller supports dehumidification and/or humidification + /// + /// A false value means that the controller has no such support. + bool supports_dehumidification_{false}; + bool supports_humidification_{false}; + /// Flags indicating if maximum allowable run time was exceeded bool cooling_max_runtime_exceeded_{false}; bool heating_max_runtime_exceeded_{false}; @@ -312,9 +382,10 @@ class ThermostatClimate : public climate::Climate, public Component { /// setup_complete_ blocks modifying/resetting the temps immediately after boot bool setup_complete_{false}; - /// Store previously-known temperatures + /// Store previously-known humidity and temperatures /// - /// These are used to determine when the temperature change trigger/action needs to be called + /// These are used to determine when a temperature/humidity has changed + float prev_target_humidity_{NAN}; float prev_target_temperature_{NAN}; float prev_target_temperature_low_{NAN}; float prev_target_temperature_high_{NAN}; @@ -328,6 +399,9 @@ class ThermostatClimate : public climate::Climate, public Component { float heating_deadband_{0}; float heating_overrun_{0}; + /// Hysteresis values used for computing humidification action + float humidity_hysteresis_{0}; + /// Maximum allowable temperature deltas before engaging supplemental cooling/heating actions float supplemental_cool_delta_{0}; float supplemental_heat_delta_{0}; @@ -362,9 +436,15 @@ class ThermostatClimate : public climate::Climate, public Component { Trigger<> *supplemental_heat_action_trigger_{nullptr}; Trigger<> *heat_mode_trigger_{nullptr}; + /// The trigger to call when the controller should switch to heat/cool mode. + /// + /// In heat/cool mode, the controller will enable heating/cooling as necessary and switch + /// to idle when the temperature is within the thresholds/set points. + Trigger<> *heat_cool_mode_trigger_{nullptr}; + /// The trigger to call when the controller should switch to auto mode. /// - /// In auto mode, the controller will enable heating/cooling as necessary and switch + /// In auto mode, the controller will enable heating/cooling as supported/necessary and switch /// to idle when the temperature is within the thresholds/set points. Trigger<> *auto_mode_trigger_{nullptr}; @@ -423,12 +503,24 @@ class ThermostatClimate : public climate::Climate, public Component { /// The trigger to call when the controller should switch the swing mode to "vertical". Trigger<> *swing_mode_vertical_trigger_{nullptr}; + /// The trigger to call when the target humidity changes. + Trigger<> *humidity_change_trigger_{nullptr}; + /// The trigger to call when the target temperature(s) change(es). Trigger<> *temperature_change_trigger_{nullptr}; /// The trigger to call when the preset mode changes Trigger<> *preset_change_trigger_{nullptr}; + /// The trigger to call when dehumidification is required + Trigger<> *humidity_control_dehumidify_action_trigger_{nullptr}; + + /// The trigger to call when humidification is required + Trigger<> *humidity_control_humidify_action_trigger_{nullptr}; + + /// The trigger to call when (de)humidification should stop + Trigger<> *humidity_control_off_action_trigger_{nullptr}; + /// A reference to the trigger that was previously active. /// /// This is so that the previous trigger can be stopped before enabling a new one @@ -437,42 +529,29 @@ class ThermostatClimate : public climate::Climate, public Component { Trigger<> *prev_fan_mode_trigger_{nullptr}; Trigger<> *prev_mode_trigger_{nullptr}; Trigger<> *prev_swing_mode_trigger_{nullptr}; - - /// If set to DEFAULT_PRESET then the default preset is always used. When MEMORY prior - /// state will attempt to be restored if possible - OnBootRestoreFrom on_boot_restore_from_{OnBootRestoreFrom::MEMORY}; - - /// Store previously-known states - /// - /// These are used to determine when a trigger/action needs to be called - climate::ClimateAction supplemental_action_{climate::CLIMATE_ACTION_OFF}; - climate::ClimateFanMode prev_fan_mode_{climate::CLIMATE_FAN_ON}; - climate::ClimateMode prev_mode_{climate::CLIMATE_MODE_OFF}; - climate::ClimateSwingMode prev_swing_mode_{climate::CLIMATE_SWING_OFF}; - - /// Default standard preset to use on start up - climate::ClimatePreset default_preset_{}; - /// Default custom preset to use on start up - std::string default_custom_preset_{}; + Trigger<> *prev_humidity_control_trigger_{nullptr}; /// Climate action timers - std::vector timer_{ - {false, 0, 0, std::bind(&ThermostatClimate::cooling_max_run_time_timer_callback_, this)}, - {false, 0, 0, std::bind(&ThermostatClimate::cooling_off_timer_callback_, this)}, - {false, 0, 0, std::bind(&ThermostatClimate::cooling_on_timer_callback_, this)}, - {false, 0, 0, std::bind(&ThermostatClimate::fan_mode_timer_callback_, this)}, - {false, 0, 0, std::bind(&ThermostatClimate::fanning_off_timer_callback_, this)}, - {false, 0, 0, std::bind(&ThermostatClimate::fanning_on_timer_callback_, this)}, - {false, 0, 0, std::bind(&ThermostatClimate::heating_max_run_time_timer_callback_, this)}, - {false, 0, 0, std::bind(&ThermostatClimate::heating_off_timer_callback_, this)}, - {false, 0, 0, std::bind(&ThermostatClimate::heating_on_timer_callback_, this)}, - {false, 0, 0, std::bind(&ThermostatClimate::idle_on_timer_callback_, this)}, + std::array timer_{ + ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::cooling_max_run_time_timer_callback_, this)), + ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::cooling_off_timer_callback_, this)), + ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::cooling_on_timer_callback_, this)), + ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::fan_mode_timer_callback_, this)), + ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::fanning_off_timer_callback_, this)), + ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::fanning_on_timer_callback_, this)), + ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::heating_max_run_time_timer_callback_, this)), + ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::heating_off_timer_callback_, this)), + ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::heating_on_timer_callback_, this)), + ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::idle_on_timer_callback_, this)), }; /// The set of standard preset configurations this thermostat supports (Eg. AWAY, ECO, etc) - std::map preset_config_{}; + FixedVector preset_config_{}; /// The set of custom preset configurations this thermostat supports (eg. "My Custom Preset") - std::map custom_preset_config_{}; + FixedVector custom_preset_config_{}; + /// Default custom preset to use on start up (pointer to entry in custom_preset_config_) + private: + const char *default_custom_preset_{nullptr}; }; } // namespace thermostat diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py index 63d4ba17f2..a20d79b857 100644 --- a/esphome/components/time/__init__.py +++ b/esphome/components/time/__init__.py @@ -26,11 +26,11 @@ from esphome.const import ( CONF_TIMEZONE, CONF_TRIGGER_ID, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority _LOGGER = logging.getLogger(__name__) -CODEOWNERS = ["@OttoWinter"] +CODEOWNERS = ["@esphome/core"] IS_PLATFORM_COMPONENT = True time_ns = cg.esphome_ns.namespace("time") @@ -340,7 +340,7 @@ async def register_time(time_var, config): await setup_time_core_(time_var, config) -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): if CORE.using_zephyr: zephyr_add_prj_conf("POSIX_CLOCK", True) diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp index 42c564659f..175cee0c1f 100644 --- a/esphome/components/time/real_time_clock.cpp +++ b/esphome/components/time/real_time_clock.cpp @@ -23,6 +23,13 @@ namespace time { static const char *const TAG = "time"; RealTimeClock::RealTimeClock() = default; + +void RealTimeClock::dump_config() { +#ifdef USE_TIME_TIMEZONE + ESP_LOGCONFIG(TAG, "Timezone: '%s'", this->timezone_.c_str()); +#endif +} + void RealTimeClock::synchronize_epoch_(uint32_t epoch) { ESP_LOGVV(TAG, "Got epoch %" PRIu32, epoch); // Update UTC epoch time. diff --git a/esphome/components/time/real_time_clock.h b/esphome/components/time/real_time_clock.h index 4b98a88975..2f17bd86d6 100644 --- a/esphome/components/time/real_time_clock.h +++ b/esphome/components/time/real_time_clock.h @@ -27,6 +27,14 @@ class RealTimeClock : public PollingComponent { this->apply_timezone_(); } + /// Set the time zone from raw buffer, only if it differs from the current one. + void set_timezone(const char *tz, size_t len) { + if (this->timezone_.length() != len || memcmp(this->timezone_.c_str(), tz, len) != 0) { + this->timezone_.assign(tz, len); + this->apply_timezone_(); + } + } + /// Get the time zone currently in use. std::string get_timezone() { return this->timezone_; } #endif @@ -44,6 +52,8 @@ class RealTimeClock : public PollingComponent { this->time_sync_callback_.add(std::move(callback)); }; + void dump_config() override; + protected: /// Report a unix epoch as current time. void synchronize_epoch_(uint32_t epoch); @@ -59,7 +69,7 @@ class RealTimeClock : public PollingComponent { template class TimeHasTimeCondition : public Condition { public: TimeHasTimeCondition(RealTimeClock *parent) : parent_(parent) {} - bool check(Ts... x) override { return this->parent_->now().is_valid(); } + bool check(const Ts &...x) override { return this->parent_->now().is_valid(); } protected: RealTimeClock *parent_; diff --git a/esphome/components/tinyusb/__init__.py b/esphome/components/tinyusb/__init__.py new file mode 100644 index 0000000000..72afc18387 --- /dev/null +++ b/esphome/components/tinyusb/__init__.py @@ -0,0 +1,60 @@ +import esphome.codegen as cg +from esphome.components import esp32 +from esphome.components.esp32 import add_idf_component, add_idf_sdkconfig_option +from esphome.components.esp32.const import ( + VARIANT_ESP32P4, + VARIANT_ESP32S2, + VARIANT_ESP32S3, +) +import esphome.config_validation as cv +from esphome.const import CONF_ID + +CODEOWNERS = ["@kbx81"] +CONFLICTS_WITH = ["usb_host"] + +CONF_USB_LANG_ID = "usb_lang_id" +CONF_USB_MANUFACTURER_STR = "usb_manufacturer_str" +CONF_USB_PRODUCT_ID = "usb_product_id" +CONF_USB_PRODUCT_STR = "usb_product_str" +CONF_USB_SERIAL_STR = "usb_serial_str" +CONF_USB_VENDOR_ID = "usb_vendor_id" + +tinyusb_ns = cg.esphome_ns.namespace("tinyusb") +TinyUSB = tinyusb_ns.class_("TinyUSB", cg.Component) + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(TinyUSB), + cv.Optional(CONF_USB_PRODUCT_ID, default=0x4001): cv.uint16_t, + cv.Optional(CONF_USB_VENDOR_ID, default=0x303A): cv.uint16_t, + cv.Optional(CONF_USB_LANG_ID, default=0x0409): cv.uint16_t, + cv.Optional(CONF_USB_MANUFACTURER_STR, default="ESPHome"): cv.string, + cv.Optional(CONF_USB_PRODUCT_STR, default="ESPHome"): cv.string, + cv.Optional(CONF_USB_SERIAL_STR, default=""): cv.string, + } + ).extend(cv.COMPONENT_SCHEMA), + esp32.only_on_variant( + supported=[VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3], + ), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + # Set USB device descriptor properties + cg.add(var.set_usb_desc_product_id(config[CONF_USB_PRODUCT_ID])) + cg.add(var.set_usb_desc_vendor_id(config[CONF_USB_VENDOR_ID])) + cg.add(var.set_usb_desc_lang_id(config[CONF_USB_LANG_ID])) + cg.add(var.set_usb_desc_manufacturer(config[CONF_USB_MANUFACTURER_STR])) + cg.add(var.set_usb_desc_product(config[CONF_USB_PRODUCT_STR])) + if config[CONF_USB_SERIAL_STR]: + cg.add(var.set_usb_desc_serial(config[CONF_USB_SERIAL_STR])) + + add_idf_component(name="espressif/esp_tinyusb", ref="1.7.6~1") + + add_idf_sdkconfig_option("CONFIG_TINYUSB_DESC_USE_ESPRESSIF_VID", False) + add_idf_sdkconfig_option("CONFIG_TINYUSB_DESC_USE_DEFAULT_PID", False) + add_idf_sdkconfig_option("CONFIG_TINYUSB_DESC_BCD_DEVICE", 0x0100) diff --git a/esphome/components/tinyusb/tinyusb_component.cpp b/esphome/components/tinyusb/tinyusb_component.cpp new file mode 100644 index 0000000000..a2057c90ce --- /dev/null +++ b/esphome/components/tinyusb/tinyusb_component.cpp @@ -0,0 +1,44 @@ +#if defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) +#include "tinyusb_component.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome::tinyusb { + +static const char *TAG = "tinyusb"; + +void TinyUSB::setup() { + // Use the device's MAC address as its serial number if no serial number is defined + if (this->string_descriptor_[SERIAL_NUMBER] == nullptr) { + static char mac_addr_buf[13]; + get_mac_address_into_buffer(mac_addr_buf); + this->string_descriptor_[SERIAL_NUMBER] = mac_addr_buf; + } + + this->tusb_cfg_ = { + .descriptor = &this->usb_descriptor_, + .string_descriptor = this->string_descriptor_, + .string_descriptor_count = SIZE, + .external_phy = false, + }; + + esp_err_t result = tinyusb_driver_install(&this->tusb_cfg_); + if (result != ESP_OK) { + this->mark_failed(); + } +} + +void TinyUSB::dump_config() { + ESP_LOGCONFIG(TAG, + "TinyUSB:\n" + " Product ID: 0x%04X\n" + " Vendor ID: 0x%04X\n" + " Manufacturer: '%s'\n" + " Product: '%s'\n" + " Serial: '%s'\n", + this->usb_descriptor_.idProduct, this->usb_descriptor_.idVendor, this->string_descriptor_[MANUFACTURER], + this->string_descriptor_[PRODUCT], this->string_descriptor_[SERIAL_NUMBER]); +} + +} // namespace esphome::tinyusb +#endif diff --git a/esphome/components/tinyusb/tinyusb_component.h b/esphome/components/tinyusb/tinyusb_component.h new file mode 100644 index 0000000000..56c286f455 --- /dev/null +++ b/esphome/components/tinyusb/tinyusb_component.h @@ -0,0 +1,72 @@ +#pragma once +#if defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) +#include "esphome/core/component.h" + +#include "tinyusb.h" +#include "tusb.h" + +namespace esphome::tinyusb { + +enum USBDStringDescriptor : uint8_t { + LANGUAGE_ID = 0, + MANUFACTURER = 1, + PRODUCT = 2, + SERIAL_NUMBER = 3, + INTERFACE = 4, + TERMINATOR = 5, + SIZE = 6, +}; + +static const char *DEFAULT_USB_STR = "ESPHome"; + +class TinyUSB : public Component { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::BUS; } + + void set_usb_desc_product_id(uint16_t product_id) { this->usb_descriptor_.idProduct = product_id; } + void set_usb_desc_vendor_id(uint16_t vendor_id) { this->usb_descriptor_.idVendor = vendor_id; } + void set_usb_desc_lang_id(uint16_t lang_id) { + this->usb_desc_lang_id_[0] = lang_id & 0xFF; + this->usb_desc_lang_id_[1] = lang_id >> 8; + } + void set_usb_desc_manufacturer(const char *usb_desc_manufacturer) { + this->string_descriptor_[MANUFACTURER] = usb_desc_manufacturer; + } + void set_usb_desc_product(const char *usb_desc_product) { this->string_descriptor_[PRODUCT] = usb_desc_product; } + void set_usb_desc_serial(const char *usb_desc_serial) { this->string_descriptor_[SERIAL_NUMBER] = usb_desc_serial; } + + protected: + char usb_desc_lang_id_[2] = {0x09, 0x04}; // defaults to english + + const char *string_descriptor_[SIZE] = { + this->usb_desc_lang_id_, // 0: supported language is English (0x0409) + DEFAULT_USB_STR, // 1: Manufacturer + DEFAULT_USB_STR, // 2: Product + nullptr, // 3: Serial Number + nullptr, // 4: Interface + nullptr, // 5: Terminator + }; + + tinyusb_config_t tusb_cfg_{}; + tusb_desc_device_t usb_descriptor_{ + .bLength = sizeof(tusb_desc_device_t), + .bDescriptorType = TUSB_DESC_DEVICE, + .bcdUSB = 0x0200, + .bDeviceClass = TUSB_CLASS_MISC, + .bDeviceSubClass = MISC_SUBCLASS_COMMON, + .bDeviceProtocol = MISC_PROTOCOL_IAD, + .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE, + .idVendor = 0x303A, + .idProduct = 0x4001, + .bcdDevice = CONFIG_TINYUSB_DESC_BCD_DEVICE, + .iManufacturer = 1, + .iProduct = 2, + .iSerialNumber = 3, + .bNumConfigurations = 1, + }; +}; + +} // namespace esphome::tinyusb +#endif diff --git a/esphome/components/tlc59208f/tlc59208f_output.cpp b/esphome/components/tlc59208f/tlc59208f_output.cpp index a524f92f75..85311a877c 100644 --- a/esphome/components/tlc59208f/tlc59208f_output.cpp +++ b/esphome/components/tlc59208f/tlc59208f_output.cpp @@ -74,7 +74,8 @@ void TLC59208FOutput::setup() { ESP_LOGV(TAG, " Resetting all devices on the bus"); // Reset all devices on the bus - if (this->bus_->write(TLC59208F_SWRST_ADDR >> 1, TLC59208F_SWRST_SEQ, 2) != i2c::ERROR_OK) { + if (this->bus_->write_readv(TLC59208F_SWRST_ADDR >> 1, TLC59208F_SWRST_SEQ, sizeof TLC59208F_SWRST_SEQ, nullptr, 0) != + i2c::ERROR_OK) { ESP_LOGE(TAG, "RESET failed"); this->mark_failed(); return; diff --git a/esphome/components/tm1621/tm1621.h b/esphome/components/tm1621/tm1621.h index b9f330e96e..fe923417a6 100644 --- a/esphome/components/tm1621/tm1621.h +++ b/esphome/components/tm1621/tm1621.h @@ -3,13 +3,14 @@ #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/hal.h" +#include "esphome/components/display/display.h" namespace esphome { namespace tm1621 { class TM1621Display; -using tm1621_writer_t = std::function; +using tm1621_writer_t = display::DisplayWriter; class TM1621Display : public PollingComponent { public: @@ -59,7 +60,7 @@ class TM1621Display : public PollingComponent { GPIOPin *cs_pin_; GPIOPin *read_pin_; GPIOPin *write_pin_; - optional writer_{}; + tm1621_writer_t writer_{}; char row_[2][12]; uint8_t state_; uint8_t device_; diff --git a/esphome/components/tm1637/tm1637.h b/esphome/components/tm1637/tm1637.h index d44680c623..b9e96119e9 100644 --- a/esphome/components/tm1637/tm1637.h +++ b/esphome/components/tm1637/tm1637.h @@ -4,6 +4,7 @@ #include "esphome/core/defines.h" #include "esphome/core/hal.h" #include "esphome/core/time.h" +#include "esphome/components/display/display.h" #include @@ -19,7 +20,7 @@ class TM1637Display; class TM1637Key; #endif -using tm1637_writer_t = std::function; +using tm1637_writer_t = display::DisplayWriter; class TM1637Display : public PollingComponent { public: @@ -78,7 +79,7 @@ class TM1637Display : public PollingComponent { uint8_t length_; bool inverted_; bool on_{true}; - optional writer_{}; + tm1637_writer_t writer_{}; uint8_t buffer_[6] = {0}; #ifdef USE_BINARY_SENSOR std::vector tm1637_keys_{}; diff --git a/esphome/components/tm1638/tm1638.h b/esphome/components/tm1638/tm1638.h index add72cfdf3..f6b2922ecf 100644 --- a/esphome/components/tm1638/tm1638.h +++ b/esphome/components/tm1638/tm1638.h @@ -5,6 +5,7 @@ #include "esphome/core/defines.h" #include "esphome/core/hal.h" #include "esphome/core/time.h" +#include "esphome/components/display/display.h" #include @@ -18,7 +19,7 @@ class KeyListener { class TM1638Component; -using tm1638_writer_t = std::function; +using tm1638_writer_t = display::DisplayWriter; class TM1638Component : public PollingComponent { public: @@ -70,7 +71,7 @@ class TM1638Component : public PollingComponent { GPIOPin *stb_pin_; GPIOPin *dio_pin_; uint8_t *buffer_ = new uint8_t[8]; - optional writer_{}; + tm1638_writer_t writer_{}; std::vector listeners_{}; }; diff --git a/esphome/components/tm1651/tm1651.h b/esphome/components/tm1651/tm1651.h index 7079910adf..83e74c5f33 100644 --- a/esphome/components/tm1651/tm1651.h +++ b/esphome/components/tm1651/tm1651.h @@ -61,7 +61,7 @@ template class SetBrightnessAction : public Action, publi public: TEMPLATABLE_VALUE(uint8_t, brightness) - void play(Ts... x) override { + void play(const Ts &...x) override { auto brightness = this->brightness_.value(x...); this->parent_->set_brightness(brightness); } @@ -71,7 +71,7 @@ template class SetLevelAction : public Action, public Par public: TEMPLATABLE_VALUE(uint8_t, level) - void play(Ts... x) override { + void play(const Ts &...x) override { auto level = this->level_.value(x...); this->parent_->set_level(level); } @@ -81,7 +81,7 @@ template class SetLevelPercentAction : public Action, pub public: TEMPLATABLE_VALUE(uint8_t, level_percent) - void play(Ts... x) override { + void play(const Ts &...x) override { auto level_percent = this->level_percent_.value(x...); this->parent_->set_level_percent(level_percent); } @@ -89,12 +89,12 @@ template class SetLevelPercentAction : public Action, pub template class TurnOnAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->turn_on(); } + void play(const Ts &...x) override { this->parent_->turn_on(); } }; template class TurnOffAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->turn_off(); } + void play(const Ts &...x) override { this->parent_->turn_off(); } }; } // namespace tm1651 diff --git a/esphome/components/tmp1075/tmp1075.cpp b/esphome/components/tmp1075/tmp1075.cpp index 831f905bd2..1d9b384c66 100644 --- a/esphome/components/tmp1075/tmp1075.cpp +++ b/esphome/components/tmp1075/tmp1075.cpp @@ -32,7 +32,7 @@ void TMP1075Sensor::update() { uint16_t regvalue; if (!read_byte_16(REG_TEMP, ®value)) { ESP_LOGW(TAG, "'%s' - unable to read temperature register", this->name_.c_str()); - this->status_set_warning("can't read"); + this->status_set_warning(LOG_STR("can't read")); return; } this->status_clear_warning(); diff --git a/esphome/components/tormatic/tormatic_cover.cpp b/esphome/components/tormatic/tormatic_cover.cpp index be412d62a8..ef93964a28 100644 --- a/esphome/components/tormatic/tormatic_cover.cpp +++ b/esphome/components/tormatic/tormatic_cover.cpp @@ -251,7 +251,7 @@ void Tormatic::stop_at_target_() { // Read a GateStatus from the unit. The unit only sends messages in response to // status requests or commands, so a message needs to be sent first. optional Tormatic::read_gate_status_() { - if (this->available() < sizeof(MessageHeader)) { + if (this->available() < static_cast(sizeof(MessageHeader))) { return {}; } diff --git a/esphome/components/toshiba/climate.py b/esphome/components/toshiba/climate.py index b8e390dd66..bdb17923fa 100644 --- a/esphome/components/toshiba/climate.py +++ b/esphome/components/toshiba/climate.py @@ -14,6 +14,7 @@ MODELS = { "GENERIC": Model.MODEL_GENERIC, "RAC-PT1411HWRU-C": Model.MODEL_RAC_PT1411HWRU_C, "RAC-PT1411HWRU-F": Model.MODEL_RAC_PT1411HWRU_F, + "RAS-2819T": Model.MODEL_RAS_2819T, } CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(ToshibaClimate).extend( diff --git a/esphome/components/toshiba/toshiba.cpp b/esphome/components/toshiba/toshiba.cpp index ff4241a81f..5efa70d6b4 100644 --- a/esphome/components/toshiba/toshiba.cpp +++ b/esphome/components/toshiba/toshiba.cpp @@ -1,4 +1,5 @@ #include "toshiba.h" +#include "esphome/components/remote_base/toshiba_ac_protocol.h" #include @@ -97,6 +98,282 @@ const std::vector RAC_PT1411HWRU_TEMPERATURE_F{0x10, 0x30, 0x00, 0x20, 0x22, 0x06, 0x26, 0x07, 0x05, 0x25, 0x04, 0x24, 0x0C, 0x2C, 0x0D, 0x2D, 0x09, 0x08, 0x28, 0x0A, 0x2A, 0x0B}; +// RAS-2819T protocol constants +const uint16_t RAS_2819T_HEADER1 = 0xC23D; +const uint8_t RAS_2819T_HEADER2 = 0xD5; +const uint8_t RAS_2819T_MESSAGE_LENGTH = 6; + +// RAS-2819T fan speed codes for rc_code_1 (bytes 2-3) +const uint16_t RAS_2819T_FAN_AUTO = 0xBF40; +const uint16_t RAS_2819T_FAN_QUIET = 0xFF00; +const uint16_t RAS_2819T_FAN_LOW = 0x9F60; +const uint16_t RAS_2819T_FAN_MEDIUM = 0x5FA0; +const uint16_t RAS_2819T_FAN_HIGH = 0x3FC0; + +// RAS-2819T fan speed codes for rc_code_2 (byte 1) +const uint8_t RAS_2819T_FAN2_AUTO = 0x66; +const uint8_t RAS_2819T_FAN2_QUIET = 0x01; +const uint8_t RAS_2819T_FAN2_LOW = 0x28; +const uint8_t RAS_2819T_FAN2_MEDIUM = 0x3C; +const uint8_t RAS_2819T_FAN2_HIGH = 0x50; + +// RAS-2819T second packet suffix bytes for rc_code_2 (bytes 3-5) +// These are fixed patterns, not actual checksums +struct Ras2819tPacketSuffix { + uint8_t byte3; + uint8_t byte4; + uint8_t byte5; +}; +const Ras2819tPacketSuffix RAS_2819T_SUFFIX_AUTO{0x00, 0x02, 0x3D}; +const Ras2819tPacketSuffix RAS_2819T_SUFFIX_QUIET{0x00, 0x02, 0xD8}; +const Ras2819tPacketSuffix RAS_2819T_SUFFIX_LOW{0x00, 0x02, 0xFF}; +const Ras2819tPacketSuffix RAS_2819T_SUFFIX_MEDIUM{0x00, 0x02, 0x13}; +const Ras2819tPacketSuffix RAS_2819T_SUFFIX_HIGH{0x00, 0x02, 0x27}; + +// RAS-2819T swing toggle command +const uint64_t RAS_2819T_SWING_TOGGLE = 0xC23D6B94E01F; + +// RAS-2819T single-packet commands +const uint64_t RAS_2819T_POWER_OFF_COMMAND = 0xC23D7B84E01F; + +// RAS-2819T known valid command patterns for validation +const std::array RAS_2819T_VALID_SINGLE_COMMANDS = { + RAS_2819T_POWER_OFF_COMMAND, // Power off + RAS_2819T_SWING_TOGGLE, // Swing toggle +}; + +const uint16_t RAS_2819T_VALID_HEADER1 = 0xC23D; +const uint8_t RAS_2819T_VALID_HEADER2 = 0xD5; + +const uint8_t RAS_2819T_DRY_BYTE2 = 0x1F; +const uint8_t RAS_2819T_DRY_BYTE3 = 0xE0; +const uint8_t RAS_2819T_DRY_TEMP_OFFSET = 0x24; + +const uint8_t RAS_2819T_AUTO_BYTE2 = 0x1F; +const uint8_t RAS_2819T_AUTO_BYTE3 = 0xE0; +const uint8_t RAS_2819T_AUTO_TEMP_OFFSET = 0x08; + +const uint8_t RAS_2819T_FAN_ONLY_TEMP = 0xE4; +const uint8_t RAS_2819T_FAN_ONLY_TEMP_INV = 0x1B; + +const uint8_t RAS_2819T_HEAT_TEMP_OFFSET = 0x0C; + +// RAS-2819T second packet fixed values +const uint8_t RAS_2819T_AUTO_DRY_FAN_BYTE = 0x65; +const uint8_t RAS_2819T_AUTO_DRY_SUFFIX = 0x3A; +const uint8_t RAS_2819T_HEAT_SUFFIX = 0x3B; + +// RAS-2819T temperature codes for 18-30°C +static const uint8_t RAS_2819T_TEMP_CODES[] = { + 0x10, // 18°C + 0x30, // 19°C + 0x20, // 20°C + 0x60, // 21°C + 0x70, // 22°C + 0x50, // 23°C + 0x40, // 24°C + 0xC0, // 25°C + 0xD0, // 26°C + 0x90, // 27°C + 0x80, // 28°C + 0xA0, // 29°C + 0xB0 // 30°C +}; + +// Helper functions for RAS-2819T protocol +// +// ===== RAS-2819T PROTOCOL DOCUMENTATION ===== +// +// The RAS-2819T uses a two-packet IR protocol with some exceptions for simple commands. +// +// PACKET STRUCTURE: +// All packets are 6 bytes (48 bits) transmitted with standard Toshiba timing. +// +// TWO-PACKET COMMANDS (Mode/Temperature/Fan changes): +// +// First Packet (rc_code_1): [C2 3D] [FAN_HI FAN_LO] [TEMP] [~TEMP] +// Byte 0-1: Header (always 0xC23D) +// Byte 2-3: Fan speed encoding (varies by mode, see fan tables below) +// Byte 4: Temperature + mode encoding +// Byte 5: Bitwise complement of temperature byte +// +// Second Packet (rc_code_2): [D5] [FAN2] [00] [SUF1] [SUF2] [SUF3] +// Byte 0: Header (always 0xD5) +// Byte 1: Fan speed secondary encoding +// Byte 2: Always 0x00 +// Byte 3-5: Fixed suffix pattern (depends on fan speed and mode) +// +// TEMPERATURE ENCODING: +// Base temp codes: 18°C=0x10, 19°C=0x30, 20°C=0x20, 21°C=0x60, 22°C=0x70, +// 23°C=0x50, 24°C=0x40, 25°C=0xC0, 26°C=0xD0, 27°C=0x90, +// 28°C=0x80, 29°C=0xA0, 30°C=0xB0 +// Mode offsets added to base temp: +// COOL: No offset +// HEAT: +0x0C (e.g., 24°C heat = 0x40 | 0x0C = 0x4C) +// AUTO: +0x08 (e.g., 24°C auto = 0x40 | 0x08 = 0x48) +// DRY: +0x24 (e.g., 24°C dry = 0x40 | 0x24 = 0x64) +// +// FAN SPEED ENCODING (First packet bytes 2-3): +// AUTO: 0xBF40, QUIET: 0xFF00, LOW: 0x9F60, MEDIUM: 0x5FA0, HIGH: 0x3FC0 +// Special cases: AUTO/DRY modes use 0x1FE0 instead +// +// SINGLE-PACKET COMMANDS: +// Power Off: 0xC23D7B84E01F (6 bytes, no second packet) +// Swing Toggle: 0xC23D6B94E01F (6 bytes, no second packet) +// +// MODE DETECTION (from first packet): +// - Check bytes 2-3: if 0x7B84 → OFF mode +// - Check bytes 2-3: if 0x1FE0 → AUTO/DRY/low-temp-COOL (distinguish by temp code) +// - Otherwise: COOL/HEAT/FAN_ONLY (distinguish by temp code and byte 5) + +/** + * Get fan speed encoding for RAS-2819T first packet (rc_code_1, bytes 2-3) + */ +static uint16_t get_ras_2819t_fan_code(climate::ClimateFanMode fan_mode) { + switch (fan_mode) { + case climate::CLIMATE_FAN_QUIET: + return RAS_2819T_FAN_QUIET; + case climate::CLIMATE_FAN_LOW: + return RAS_2819T_FAN_LOW; + case climate::CLIMATE_FAN_MEDIUM: + return RAS_2819T_FAN_MEDIUM; + case climate::CLIMATE_FAN_HIGH: + return RAS_2819T_FAN_HIGH; + case climate::CLIMATE_FAN_AUTO: + default: + return RAS_2819T_FAN_AUTO; + } +} + +/** + * Get fan speed encoding for RAS-2819T rc_code_2 packet (second packet) + */ +struct Ras2819tSecondPacketCodes { + uint8_t fan_byte; + Ras2819tPacketSuffix suffix; +}; + +static Ras2819tSecondPacketCodes get_ras_2819t_second_packet_codes(climate::ClimateFanMode fan_mode) { + switch (fan_mode) { + case climate::CLIMATE_FAN_QUIET: + return {RAS_2819T_FAN2_QUIET, RAS_2819T_SUFFIX_QUIET}; + case climate::CLIMATE_FAN_LOW: + return {RAS_2819T_FAN2_LOW, RAS_2819T_SUFFIX_LOW}; + case climate::CLIMATE_FAN_MEDIUM: + return {RAS_2819T_FAN2_MEDIUM, RAS_2819T_SUFFIX_MEDIUM}; + case climate::CLIMATE_FAN_HIGH: + return {RAS_2819T_FAN2_HIGH, RAS_2819T_SUFFIX_HIGH}; + case climate::CLIMATE_FAN_AUTO: + default: + return {RAS_2819T_FAN2_AUTO, RAS_2819T_SUFFIX_AUTO}; + } +} + +/** + * Get temperature code for RAS-2819T protocol + */ +static uint8_t get_ras_2819t_temp_code(float temperature) { + int temp_index = static_cast(temperature) - 18; + if (temp_index < 0 || temp_index >= static_cast(sizeof(RAS_2819T_TEMP_CODES))) { + ESP_LOGW(TAG, "Temperature %.1f°C out of range [18-30°C], defaulting to 24°C", temperature); + return 0x40; // Default to 24°C + } + + return RAS_2819T_TEMP_CODES[temp_index]; +} + +/** + * Decode temperature from RAS-2819T temp code + */ +static float decode_ras_2819t_temperature(uint8_t temp_code) { + uint8_t base_temp_code = temp_code & 0xF0; + + // Find the code in the temperature array + for (size_t temp_index = 0; temp_index < sizeof(RAS_2819T_TEMP_CODES); temp_index++) { + if (RAS_2819T_TEMP_CODES[temp_index] == base_temp_code) { + return static_cast(temp_index + 18); // 18°C is the minimum + } + } + + ESP_LOGW(TAG, "Unknown temp code: 0x%02X, defaulting to 24°C", base_temp_code); + return 24.0f; // Default to 24°C +} + +/** + * Decode fan speed from RAS-2819T IR codes + */ +static climate::ClimateFanMode decode_ras_2819t_fan_mode(uint16_t fan_code) { + switch (fan_code) { + case RAS_2819T_FAN_QUIET: + return climate::CLIMATE_FAN_QUIET; + case RAS_2819T_FAN_LOW: + return climate::CLIMATE_FAN_LOW; + case RAS_2819T_FAN_MEDIUM: + return climate::CLIMATE_FAN_MEDIUM; + case RAS_2819T_FAN_HIGH: + return climate::CLIMATE_FAN_HIGH; + case RAS_2819T_FAN_AUTO: + default: + return climate::CLIMATE_FAN_AUTO; + } +} + +/** + * Validate RAS-2819T IR command structure and content + */ +static bool is_valid_ras_2819t_command(uint64_t rc_code_1, uint64_t rc_code_2 = 0) { + // Check header of first packet + uint16_t header1 = (rc_code_1 >> 32) & 0xFFFF; + if (header1 != RAS_2819T_VALID_HEADER1) { + return false; + } + + // Single packet commands + if (rc_code_2 == 0) { + for (uint64_t valid_cmd : RAS_2819T_VALID_SINGLE_COMMANDS) { + if (rc_code_1 == valid_cmd) { + return true; + } + } + // Additional validation for unknown single packets + return false; + } + + // Two-packet commands - validate second packet header + uint8_t header2 = (rc_code_2 >> 40) & 0xFF; + if (header2 != RAS_2819T_VALID_HEADER2) { + return false; + } + + // Validate temperature complement in first packet (byte 4 should be ~byte 5) + uint8_t temp_byte = (rc_code_1 >> 8) & 0xFF; + uint8_t temp_complement = rc_code_1 & 0xFF; + if (temp_byte != static_cast(~temp_complement)) { + return false; + } + + // Validate fan speed combinations make sense + uint16_t fan_code = (rc_code_1 >> 16) & 0xFFFF; + uint8_t fan2_byte = (rc_code_2 >> 32) & 0xFF; + + // Check if fan codes are from known valid patterns + bool valid_fan_combo = false; + if (fan_code == RAS_2819T_FAN_AUTO && fan2_byte == RAS_2819T_FAN2_AUTO) + valid_fan_combo = true; + if (fan_code == RAS_2819T_FAN_QUIET && fan2_byte == RAS_2819T_FAN2_QUIET) + valid_fan_combo = true; + if (fan_code == RAS_2819T_FAN_LOW && fan2_byte == RAS_2819T_FAN2_LOW) + valid_fan_combo = true; + if (fan_code == RAS_2819T_FAN_MEDIUM && fan2_byte == RAS_2819T_FAN2_MEDIUM) + valid_fan_combo = true; + if (fan_code == RAS_2819T_FAN_HIGH && fan2_byte == RAS_2819T_FAN2_HIGH) + valid_fan_combo = true; + if (fan_code == 0x1FE0 && fan2_byte == RAS_2819T_AUTO_DRY_FAN_BYTE) + valid_fan_combo = true; // AUTO/DRY + + return valid_fan_combo; +} + void ToshibaClimate::setup() { if (this->sensor_) { this->sensor_->add_on_state_callback([this](float state) { @@ -126,16 +403,43 @@ void ToshibaClimate::setup() { this->minimum_temperature_ = this->temperature_min_(); this->maximum_temperature_ = this->temperature_max_(); this->swing_modes_ = this->toshiba_swing_modes_(); + + // Ensure swing mode is always initialized to a valid value + if (this->swing_modes_.empty() || !this->swing_modes_.count(this->swing_mode)) { + // No swing support for this model or current swing mode not supported, reset to OFF + this->swing_mode = climate::CLIMATE_SWING_OFF; + } + + // Ensure mode is valid - ESPHome should only use standard climate modes + if (this->mode != climate::CLIMATE_MODE_OFF && this->mode != climate::CLIMATE_MODE_HEAT && + this->mode != climate::CLIMATE_MODE_COOL && this->mode != climate::CLIMATE_MODE_HEAT_COOL && + this->mode != climate::CLIMATE_MODE_DRY && this->mode != climate::CLIMATE_MODE_FAN_ONLY) { + ESP_LOGW(TAG, "Invalid mode detected during setup, resetting to OFF"); + this->mode = climate::CLIMATE_MODE_OFF; + } + + // Ensure fan mode is valid + if (!this->fan_mode.has_value()) { + ESP_LOGW(TAG, "Fan mode not set during setup, defaulting to AUTO"); + this->fan_mode = climate::CLIMATE_FAN_AUTO; + } + // Never send nan to HA if (std::isnan(this->target_temperature)) this->target_temperature = 24; + // Log final state for debugging HA errors + ESP_LOGV(TAG, "Setup complete - Mode: %d, Fan: %s, Swing: %d, Temp: %.1f", static_cast(this->mode), + this->fan_mode.has_value() ? std::to_string(static_cast(this->fan_mode.value())).c_str() : "NONE", + static_cast(this->swing_mode), this->target_temperature); } void ToshibaClimate::transmit_state() { if (this->model_ == MODEL_RAC_PT1411HWRU_C || this->model_ == MODEL_RAC_PT1411HWRU_F) { - transmit_rac_pt1411hwru_(); + this->transmit_rac_pt1411hwru_(); + } else if (this->model_ == MODEL_RAS_2819T) { + this->transmit_ras_2819t_(); } else { - transmit_generic_(); + this->transmit_generic_(); } } @@ -230,7 +534,7 @@ void ToshibaClimate::transmit_generic_() { auto transmit = this->transmitter_->transmit(); auto *data = transmit.get_data(); - encode_(data, message, message_length, 1); + this->encode_(data, message, message_length, 1); transmit.perform(); } @@ -348,15 +652,12 @@ void ToshibaClimate::transmit_rac_pt1411hwru_() { message[11] += message[index]; } } - ESP_LOGV(TAG, "*** Generated codes: 0x%.2X%.2X%.2X%.2X%.2X%.2X 0x%.2X%.2X%.2X%.2X%.2X%.2X", message[0], message[1], - message[2], message[3], message[4], message[5], message[6], message[7], message[8], message[9], message[10], - message[11]); // load first block of IR code and repeat it once - encode_(data, &message[0], RAC_PT1411HWRU_MESSAGE_LENGTH, 1); + this->encode_(data, &message[0], RAC_PT1411HWRU_MESSAGE_LENGTH, 1); // load second block of IR code, if present if (message[6] != 0) { - encode_(data, &message[6], RAC_PT1411HWRU_MESSAGE_LENGTH, 0); + this->encode_(data, &message[6], RAC_PT1411HWRU_MESSAGE_LENGTH, 0); } transmit.perform(); @@ -366,19 +667,19 @@ void ToshibaClimate::transmit_rac_pt1411hwru_() { data->space(TOSHIBA_PACKET_SPACE); switch (this->swing_mode) { case climate::CLIMATE_SWING_VERTICAL: - encode_(data, &RAC_PT1411HWRU_SWING_VERTICAL[0], RAC_PT1411HWRU_MESSAGE_LENGTH, 1); + this->encode_(data, &RAC_PT1411HWRU_SWING_VERTICAL[0], RAC_PT1411HWRU_MESSAGE_LENGTH, 1); break; case climate::CLIMATE_SWING_OFF: default: - encode_(data, &RAC_PT1411HWRU_SWING_OFF[0], RAC_PT1411HWRU_MESSAGE_LENGTH, 1); + this->encode_(data, &RAC_PT1411HWRU_SWING_OFF[0], RAC_PT1411HWRU_MESSAGE_LENGTH, 1); } data->space(TOSHIBA_PACKET_SPACE); transmit.perform(); if (this->sensor_) { - transmit_rac_pt1411hwru_temp_(true, false); + this->transmit_rac_pt1411hwru_temp_(true, false); } } @@ -430,15 +731,217 @@ void ToshibaClimate::transmit_rac_pt1411hwru_temp_(const bool cs_state, const bo // Byte 5: Footer lower/bitwise complement of byte 4 message[5] = ~message[4]; - ESP_LOGV(TAG, "*** Generated code: 0x%.2X%.2X%.2X%.2X%.2X%.2X", message[0], message[1], message[2], message[3], - message[4], message[5]); // load IR code and repeat it once - encode_(data, message, RAC_PT1411HWRU_MESSAGE_LENGTH, 1); + this->encode_(data, message, RAC_PT1411HWRU_MESSAGE_LENGTH, 1); transmit.perform(); } } +void ToshibaClimate::transmit_ras_2819t_() { + // Handle swing mode transmission for RAS-2819T + // Note: RAS-2819T uses a toggle command, so we need to track state changes + + // Check if ONLY swing mode changed (and no other climate parameters) + bool swing_changed = (this->swing_mode != this->last_swing_mode_); + bool mode_changed = (this->mode != this->last_mode_); + bool fan_changed = (this->fan_mode != this->last_fan_mode_); + bool temp_changed = (abs(this->target_temperature - this->last_target_temperature_) > 0.1f); + + bool only_swing_changed = swing_changed && !mode_changed && !fan_changed && !temp_changed; + + if (only_swing_changed) { + // Send ONLY swing toggle command (like the physical remote does) + auto swing_transmit = this->transmitter_->transmit(); + auto *swing_data = swing_transmit.get_data(); + + // Convert toggle command to bytes for transmission + uint8_t swing_message[RAS_2819T_MESSAGE_LENGTH]; + swing_message[0] = (RAS_2819T_SWING_TOGGLE >> 40) & 0xFF; + swing_message[1] = (RAS_2819T_SWING_TOGGLE >> 32) & 0xFF; + swing_message[2] = (RAS_2819T_SWING_TOGGLE >> 24) & 0xFF; + swing_message[3] = (RAS_2819T_SWING_TOGGLE >> 16) & 0xFF; + swing_message[4] = (RAS_2819T_SWING_TOGGLE >> 8) & 0xFF; + swing_message[5] = RAS_2819T_SWING_TOGGLE & 0xFF; + + // Use single packet transmission WITH repeat (like regular commands) + this->encode_(swing_data, swing_message, RAS_2819T_MESSAGE_LENGTH, 1); + swing_transmit.perform(); + + // Update all state tracking + this->last_swing_mode_ = this->swing_mode; + this->last_mode_ = this->mode; + this->last_fan_mode_ = this->fan_mode; + this->last_target_temperature_ = this->target_temperature; + + // Immediately publish the state change to Home Assistant + this->publish_state(); + + return; // Exit early - don't send climate command + } + + // If we get here, send the regular climate command (temperature/mode/fan) + uint8_t message1[RAS_2819T_MESSAGE_LENGTH] = {0}; + uint8_t message2[RAS_2819T_MESSAGE_LENGTH] = {0}; + float temperature = + clamp(this->target_temperature, TOSHIBA_RAS_2819T_TEMP_C_MIN, TOSHIBA_RAS_2819T_TEMP_C_MAX); + + // Build first packet (RAS_2819T_HEADER1 + 4 bytes) + message1[0] = (RAS_2819T_HEADER1 >> 8) & 0xFF; + message1[1] = RAS_2819T_HEADER1 & 0xFF; + + // Handle OFF mode + if (this->mode == climate::CLIMATE_MODE_OFF) { + // Extract bytes from power off command constant + message1[2] = (RAS_2819T_POWER_OFF_COMMAND >> 24) & 0xFF; + message1[3] = (RAS_2819T_POWER_OFF_COMMAND >> 16) & 0xFF; + message1[4] = (RAS_2819T_POWER_OFF_COMMAND >> 8) & 0xFF; + message1[5] = RAS_2819T_POWER_OFF_COMMAND & 0xFF; + // No second packet for OFF + } else { + // Get temperature and fan encoding + uint8_t temp_code = get_ras_2819t_temp_code(temperature); + + // Get fan speed encoding for rc_code_1 + climate::ClimateFanMode effective_fan_mode = this->fan_mode.value(); + + // Dry mode only supports AUTO fan speed + if (this->mode == climate::CLIMATE_MODE_DRY) { + effective_fan_mode = climate::CLIMATE_FAN_AUTO; + if (this->fan_mode.value() != climate::CLIMATE_FAN_AUTO) { + ESP_LOGW(TAG, "Dry mode only supports AUTO fan speed, forcing AUTO"); + } + } + + uint16_t fan_code = get_ras_2819t_fan_code(effective_fan_mode); + + // Mode and temperature encoding + switch (this->mode) { + case climate::CLIMATE_MODE_COOL: + // All cooling temperatures support fan speed control + message1[2] = (fan_code >> 8) & 0xFF; + message1[3] = fan_code & 0xFF; + message1[4] = temp_code; + message1[5] = ~temp_code; + break; + + case climate::CLIMATE_MODE_HEAT: + // Heating supports fan speed control + message1[2] = (fan_code >> 8) & 0xFF; + message1[3] = fan_code & 0xFF; + // Heat mode adds offset to temperature code + message1[4] = temp_code | RAS_2819T_HEAT_TEMP_OFFSET; + message1[5] = ~(temp_code | RAS_2819T_HEAT_TEMP_OFFSET); + break; + + case climate::CLIMATE_MODE_HEAT_COOL: + // Auto mode uses fixed encoding + message1[2] = RAS_2819T_AUTO_BYTE2; + message1[3] = RAS_2819T_AUTO_BYTE3; + message1[4] = temp_code | RAS_2819T_AUTO_TEMP_OFFSET; + message1[5] = ~(temp_code | RAS_2819T_AUTO_TEMP_OFFSET); + break; + + case climate::CLIMATE_MODE_DRY: + // Dry mode uses fixed encoding and forces AUTO fan + message1[2] = RAS_2819T_DRY_BYTE2; + message1[3] = RAS_2819T_DRY_BYTE3; + message1[4] = temp_code | RAS_2819T_DRY_TEMP_OFFSET; + message1[5] = ~message1[4]; + break; + + case climate::CLIMATE_MODE_FAN_ONLY: + // Fan only mode supports fan speed control + message1[2] = (fan_code >> 8) & 0xFF; + message1[3] = fan_code & 0xFF; + message1[4] = RAS_2819T_FAN_ONLY_TEMP; + message1[5] = RAS_2819T_FAN_ONLY_TEMP_INV; + break; + + default: + // Default case supports fan speed control + message1[2] = (fan_code >> 8) & 0xFF; + message1[3] = fan_code & 0xFF; + message1[4] = temp_code; + message1[5] = ~temp_code; + break; + } + + // Build second packet (RAS_2819T_HEADER2 + 4 bytes) + message2[0] = RAS_2819T_HEADER2; + + // Get fan speed encoding for rc_code_2 + Ras2819tSecondPacketCodes second_packet_codes = get_ras_2819t_second_packet_codes(effective_fan_mode); + + // Determine header byte 2 and fan encoding based on mode + switch (this->mode) { + case climate::CLIMATE_MODE_COOL: + message2[1] = second_packet_codes.fan_byte; + message2[2] = 0x00; + message2[3] = second_packet_codes.suffix.byte3; + message2[4] = second_packet_codes.suffix.byte4; + message2[5] = second_packet_codes.suffix.byte5; + break; + + case climate::CLIMATE_MODE_HEAT: + message2[1] = second_packet_codes.fan_byte; + message2[2] = 0x00; + message2[3] = second_packet_codes.suffix.byte3; + message2[4] = 0x00; + message2[5] = RAS_2819T_HEAT_SUFFIX; + break; + + case climate::CLIMATE_MODE_HEAT_COOL: + case climate::CLIMATE_MODE_DRY: + // Auto/Dry modes use fixed values regardless of fan setting + message2[1] = RAS_2819T_AUTO_DRY_FAN_BYTE; + message2[2] = 0x00; + message2[3] = 0x00; + message2[4] = 0x00; + message2[5] = RAS_2819T_AUTO_DRY_SUFFIX; + break; + + case climate::CLIMATE_MODE_FAN_ONLY: + message2[1] = second_packet_codes.fan_byte; + message2[2] = 0x00; + message2[3] = second_packet_codes.suffix.byte3; + message2[4] = 0x00; + message2[5] = RAS_2819T_HEAT_SUFFIX; + break; + + default: + message2[1] = second_packet_codes.fan_byte; + message2[2] = 0x00; + message2[3] = second_packet_codes.suffix.byte3; + message2[4] = second_packet_codes.suffix.byte4; + message2[5] = second_packet_codes.suffix.byte5; + break; + } + } + + // Log final messages being transmitted + + // Transmit using proper Toshiba protocol timing + auto transmit = this->transmitter_->transmit(); + auto *data = transmit.get_data(); + + // Use existing Toshiba encode function for proper timing + this->encode_(data, message1, RAS_2819T_MESSAGE_LENGTH, 1); + + if (this->mode != climate::CLIMATE_MODE_OFF) { + // Send second packet with gap + this->encode_(data, message2, RAS_2819T_MESSAGE_LENGTH, 0); + } + + transmit.perform(); + + // Update all state tracking after successful transmission + this->last_swing_mode_ = this->swing_mode; + this->last_mode_ = this->mode; + this->last_fan_mode_ = this->fan_mode; + this->last_target_temperature_ = this->target_temperature; +} + uint8_t ToshibaClimate::is_valid_rac_pt1411hwru_header_(const uint8_t *message) { const std::vector header{RAC_PT1411HWRU_MESSAGE_HEADER0, RAC_PT1411HWRU_CS_HEADER, RAC_PT1411HWRU_SWING_HEADER}; @@ -464,11 +967,11 @@ bool ToshibaClimate::compare_rac_pt1411hwru_packets_(const uint8_t *message1, co bool ToshibaClimate::is_valid_rac_pt1411hwru_message_(const uint8_t *message) { uint8_t checksum = 0; - switch (is_valid_rac_pt1411hwru_header_(message)) { + switch (this->is_valid_rac_pt1411hwru_header_(message)) { case RAC_PT1411HWRU_MESSAGE_HEADER0: case RAC_PT1411HWRU_CS_HEADER: case RAC_PT1411HWRU_SWING_HEADER: - if (is_valid_rac_pt1411hwru_header_(message) && (message[2] == static_cast(~message[3])) && + if (this->is_valid_rac_pt1411hwru_header_(message) && (message[2] == static_cast(~message[3])) && (message[4] == static_cast(~message[5]))) { return true; } @@ -490,7 +993,103 @@ bool ToshibaClimate::is_valid_rac_pt1411hwru_message_(const uint8_t *message) { return false; } +bool ToshibaClimate::process_ras_2819t_command_(const remote_base::ToshibaAcData &toshiba_data) { + // Check for power-off command (single packet) + if (toshiba_data.rc_code_2 == 0 && toshiba_data.rc_code_1 == RAS_2819T_POWER_OFF_COMMAND) { + this->mode = climate::CLIMATE_MODE_OFF; + ESP_LOGI(TAG, "Mode: OFF"); + this->publish_state(); + return true; + } + + // Check for swing toggle command (single packet) + if (toshiba_data.rc_code_2 == 0 && toshiba_data.rc_code_1 == RAS_2819T_SWING_TOGGLE) { + // Toggle swing mode + if (this->swing_mode == climate::CLIMATE_SWING_VERTICAL) { + this->swing_mode = climate::CLIMATE_SWING_OFF; + ESP_LOGI(TAG, "Swing: OFF"); + } else { + this->swing_mode = climate::CLIMATE_SWING_VERTICAL; + ESP_LOGI(TAG, "Swing: VERTICAL"); + } + this->publish_state(); + return true; + } + + // Handle regular two-packet commands (mode/temperature/fan changes) + if (toshiba_data.rc_code_2 != 0) { + // Convert to byte array for easier processing + uint8_t message1[6], message2[6]; + for (uint8_t i = 0; i < 6; i++) { + message1[i] = (toshiba_data.rc_code_1 >> (40 - i * 8)) & 0xFF; + message2[i] = (toshiba_data.rc_code_2 >> (40 - i * 8)) & 0xFF; + } + + // Decode the protocol using message1 (rc_code_1) + uint8_t temp_code = message1[4]; + + // Decode mode - check bytes 2-3 pattern and temperature code + if ((message1[2] == 0x7B) && (message1[3] == 0x84)) { + // OFF mode has specific pattern + this->mode = climate::CLIMATE_MODE_OFF; + ESP_LOGI(TAG, "Mode: OFF"); + } else if ((message1[2] == 0x1F) && (message1[3] == 0xE0)) { + // 0x1FE0 pattern is used for AUTO, DRY, and low-temp COOL + if ((temp_code & 0x0F) == 0x08) { + this->mode = climate::CLIMATE_MODE_HEAT_COOL; + ESP_LOGI(TAG, "Mode: AUTO"); + } else if ((temp_code & 0x0F) == 0x04) { + this->mode = climate::CLIMATE_MODE_DRY; + ESP_LOGI(TAG, "Mode: DRY"); + } else { + this->mode = climate::CLIMATE_MODE_COOL; + ESP_LOGI(TAG, "Mode: COOL (low temp)"); + } + } else { + // Variable fan speed patterns - decode by temperature code + if ((temp_code & 0x0F) == 0x0C) { + this->mode = climate::CLIMATE_MODE_HEAT; + ESP_LOGI(TAG, "Mode: HEAT"); + } else if (message1[5] == 0x1B) { + this->mode = climate::CLIMATE_MODE_FAN_ONLY; + ESP_LOGI(TAG, "Mode: FAN_ONLY"); + } else { + this->mode = climate::CLIMATE_MODE_COOL; + ESP_LOGI(TAG, "Mode: COOL"); + } + } + + // Decode fan speed from rc_code_1 + uint16_t fan_code = (message1[2] << 8) | message1[3]; + this->fan_mode = decode_ras_2819t_fan_mode(fan_code); + + // Decode temperature + if (this->mode != climate::CLIMATE_MODE_OFF && this->mode != climate::CLIMATE_MODE_FAN_ONLY) { + this->target_temperature = decode_ras_2819t_temperature(temp_code); + } + + this->publish_state(); + return true; + } else { + ESP_LOGD(TAG, "Unknown single-packet RAS-2819T command: 0x%" PRIX64, toshiba_data.rc_code_1); + return false; + } +} + bool ToshibaClimate::on_receive(remote_base::RemoteReceiveData data) { + // Try modern ToshibaAcProtocol decoder first (handles RAS-2819T and potentially others) + remote_base::ToshibaAcProtocol toshiba_protocol; + auto decode_result = toshiba_protocol.decode(data); + + if (decode_result.has_value()) { + auto toshiba_data = decode_result.value(); + // Validate and process RAS-2819T commands + if (is_valid_ras_2819t_command(toshiba_data.rc_code_1, toshiba_data.rc_code_2)) { + return this->process_ras_2819t_command_(toshiba_data); + } + } + + // Fall back to generic processing for older protocols uint8_t message[18] = {0}; uint8_t message_length = TOSHIBA_HEADER_LENGTH, temperature_code = 0; @@ -499,11 +1098,11 @@ bool ToshibaClimate::on_receive(remote_base::RemoteReceiveData data) { return false; } // Read incoming bits into buffer - if (!decode_(&data, message, message_length)) { + if (!this->decode_(&data, message, message_length)) { return false; } // Determine incoming message protocol version and/or length - if (is_valid_rac_pt1411hwru_header_(message)) { + if (this->is_valid_rac_pt1411hwru_header_(message)) { // We already received four bytes message_length = RAC_PT1411HWRU_MESSAGE_LENGTH - 4; } else if ((message[0] ^ message[1] ^ message[2]) != message[3]) { @@ -514,11 +1113,11 @@ bool ToshibaClimate::on_receive(remote_base::RemoteReceiveData data) { message_length = message[2] + 2; } // Decode the remaining bytes - if (!decode_(&data, &message[4], message_length)) { + if (!this->decode_(&data, &message[4], message_length)) { return false; } // If this is a RAC-PT1411HWRU message, we expect the first packet a second time and also possibly a third packet - if (is_valid_rac_pt1411hwru_header_(message)) { + if (this->is_valid_rac_pt1411hwru_header_(message)) { // There is always a space between packets if (!data.expect_item(TOSHIBA_BIT_MARK, TOSHIBA_GAP_SPACE)) { return false; @@ -527,7 +1126,7 @@ bool ToshibaClimate::on_receive(remote_base::RemoteReceiveData data) { if (!data.expect_item(TOSHIBA_HEADER_MARK, TOSHIBA_HEADER_SPACE)) { return false; } - if (!decode_(&data, &message[6], RAC_PT1411HWRU_MESSAGE_LENGTH)) { + if (!this->decode_(&data, &message[6], RAC_PT1411HWRU_MESSAGE_LENGTH)) { return false; } // If this is a RAC-PT1411HWRU message, there may also be a third packet. @@ -535,25 +1134,25 @@ bool ToshibaClimate::on_receive(remote_base::RemoteReceiveData data) { if (data.expect_item(TOSHIBA_BIT_MARK, TOSHIBA_GAP_SPACE)) { // Validate header 3 data.expect_item(TOSHIBA_HEADER_MARK, TOSHIBA_HEADER_SPACE); - if (decode_(&data, &message[12], RAC_PT1411HWRU_MESSAGE_LENGTH)) { - if (!is_valid_rac_pt1411hwru_message_(&message[12])) { + if (this->decode_(&data, &message[12], RAC_PT1411HWRU_MESSAGE_LENGTH)) { + if (!this->is_valid_rac_pt1411hwru_message_(&message[12])) { // If a third packet was received but the checksum is not valid, fail return false; } } } - if (!compare_rac_pt1411hwru_packets_(&message[0], &message[6])) { + if (!this->compare_rac_pt1411hwru_packets_(&message[0], &message[6])) { // If the first two packets don't match each other, fail return false; } - if (!is_valid_rac_pt1411hwru_message_(&message[0])) { + if (!this->is_valid_rac_pt1411hwru_message_(&message[0])) { // If the first packet isn't valid, fail return false; } } // Header has been verified, now determine protocol version and set the climate component properties - switch (is_valid_rac_pt1411hwru_header_(message)) { + switch (this->is_valid_rac_pt1411hwru_header_(message)) { // Power, temperature, mode, fan speed case RAC_PT1411HWRU_MESSAGE_HEADER0: // Get the mode @@ -608,7 +1207,7 @@ bool ToshibaClimate::on_receive(remote_base::RemoteReceiveData data) { break; } // Get the target temperature - if (is_valid_rac_pt1411hwru_message_(&message[12])) { + if (this->is_valid_rac_pt1411hwru_message_(&message[12])) { temperature_code = (message[4] >> 4) | (message[14] & RAC_PT1411HWRU_FLAG_FRAC) | (message[15] & RAC_PT1411HWRU_FLAG_NEG); if (message[15] & RAC_PT1411HWRU_FLAG_FAH) { diff --git a/esphome/components/toshiba/toshiba.h b/esphome/components/toshiba/toshiba.h index 83e85c34db..ee1dec5cc9 100644 --- a/esphome/components/toshiba/toshiba.h +++ b/esphome/components/toshiba/toshiba.h @@ -1,6 +1,7 @@ #pragma once #include "esphome/components/climate_ir/climate_ir.h" +#include "esphome/components/remote_base/toshiba_ac_protocol.h" namespace esphome { namespace toshiba { @@ -10,6 +11,7 @@ enum Model { MODEL_GENERIC = 0, // Temperature range is from 17 to 30 MODEL_RAC_PT1411HWRU_C = 1, // Temperature range is from 16 to 30 MODEL_RAC_PT1411HWRU_F = 2, // Temperature range is from 16 to 30 + MODEL_RAS_2819T = 3, // RAS-2819T protocol variant, temperature range 18 to 30 }; // Supported temperature ranges @@ -19,6 +21,8 @@ const float TOSHIBA_RAC_PT1411HWRU_TEMP_C_MIN = 16.0; const float TOSHIBA_RAC_PT1411HWRU_TEMP_C_MAX = 30.0; const float TOSHIBA_RAC_PT1411HWRU_TEMP_F_MIN = 60.0; const float TOSHIBA_RAC_PT1411HWRU_TEMP_F_MAX = 86.0; +const float TOSHIBA_RAS_2819T_TEMP_C_MIN = 18.0; +const float TOSHIBA_RAS_2819T_TEMP_C_MAX = 30.0; class ToshibaClimate : public climate_ir::ClimateIR { public: @@ -35,6 +39,9 @@ class ToshibaClimate : public climate_ir::ClimateIR { void transmit_generic_(); void transmit_rac_pt1411hwru_(); void transmit_rac_pt1411hwru_temp_(bool cs_state = true, bool cs_send_update = true); + void transmit_ras_2819t_(); + // Process RAS-2819T IR command data + bool process_ras_2819t_command_(const remote_base::ToshibaAcData &toshiba_data); // Returns the header if valid, else returns zero uint8_t is_valid_rac_pt1411hwru_header_(const uint8_t *message); // Returns true if message is a valid RAC-PT1411HWRU IR message, regardless if first or second packet @@ -43,16 +50,31 @@ class ToshibaClimate : public climate_ir::ClimateIR { bool compare_rac_pt1411hwru_packets_(const uint8_t *message1, const uint8_t *message2); bool on_receive(remote_base::RemoteReceiveData data) override; + private: + // RAS-2819T state tracking for swing mode optimization + climate::ClimateSwingMode last_swing_mode_{climate::CLIMATE_SWING_OFF}; + climate::ClimateMode last_mode_{climate::CLIMATE_MODE_OFF}; + optional last_fan_mode_{}; + float last_target_temperature_{24.0f}; + float temperature_min_() { - return (this->model_ == MODEL_GENERIC) ? TOSHIBA_GENERIC_TEMP_C_MIN : TOSHIBA_RAC_PT1411HWRU_TEMP_C_MIN; + if (this->model_ == MODEL_RAC_PT1411HWRU_C || this->model_ == MODEL_RAC_PT1411HWRU_F) + return TOSHIBA_RAC_PT1411HWRU_TEMP_C_MIN; + if (this->model_ == MODEL_RAS_2819T) + return TOSHIBA_RAS_2819T_TEMP_C_MIN; + return TOSHIBA_GENERIC_TEMP_C_MIN; // Default to GENERIC for unknown models } float temperature_max_() { - return (this->model_ == MODEL_GENERIC) ? TOSHIBA_GENERIC_TEMP_C_MAX : TOSHIBA_RAC_PT1411HWRU_TEMP_C_MAX; + if (this->model_ == MODEL_RAC_PT1411HWRU_C || this->model_ == MODEL_RAC_PT1411HWRU_F) + return TOSHIBA_RAC_PT1411HWRU_TEMP_C_MAX; + if (this->model_ == MODEL_RAS_2819T) + return TOSHIBA_RAS_2819T_TEMP_C_MAX; + return TOSHIBA_GENERIC_TEMP_C_MAX; // Default to GENERIC for unknown models } - std::set toshiba_swing_modes_() { + climate::ClimateSwingModeMask toshiba_swing_modes_() { return (this->model_ == MODEL_GENERIC) - ? std::set{} - : std::set{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}; + ? climate::ClimateSwingModeMask() + : climate::ClimateSwingModeMask{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}; } void encode_(remote_base::RemoteTransmitData *data, const uint8_t *message, uint8_t nbytes, uint8_t repeat); bool decode_(remote_base::RemoteReceiveData *data, uint8_t *message, uint8_t nbytes); diff --git a/esphome/components/total_daily_energy/total_daily_energy.cpp b/esphome/components/total_daily_energy/total_daily_energy.cpp index 7c316c495d..818696f99b 100644 --- a/esphome/components/total_daily_energy/total_daily_energy.cpp +++ b/esphome/components/total_daily_energy/total_daily_energy.cpp @@ -10,7 +10,7 @@ void TotalDailyEnergy::setup() { float initial_value = 0; if (this->restore_) { - this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); + this->pref_ = global_preferences->make_preference(this->get_preference_hash()); this->pref_.load(&initial_value); } this->publish_state_and_save(initial_value); diff --git a/esphome/components/touchscreen/__init__.py b/esphome/components/touchscreen/__init__.py index 01a271a34e..4a5c03ace4 100644 --- a/esphome/components/touchscreen/__init__.py +++ b/esphome/components/touchscreen/__init__.py @@ -13,7 +13,7 @@ from esphome.const import ( CONF_SWAP_XY, CONF_TRANSFORM, ) -from esphome.core import coroutine_with_priority +from esphome.core import CoroPriority, coroutine_with_priority CODEOWNERS = ["@jesserockz", "@nielsnl68"] DEPENDENCIES = ["display"] @@ -152,7 +152,7 @@ async def register_touchscreen(var, config): ) -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(touchscreen_ns.using) cg.add_define("USE_TOUCHSCREEN") diff --git a/esphome/components/tuya/climate/tuya_climate.cpp b/esphome/components/tuya/climate/tuya_climate.cpp index 7827a4e3ab..4d8fd4b310 100644 --- a/esphome/components/tuya/climate/tuya_climate.cpp +++ b/esphome/components/tuya/climate/tuya_climate.cpp @@ -67,7 +67,9 @@ void TuyaClimate::setup() { } if (this->eco_id_.has_value()) { this->parent_->register_listener(*this->eco_id_, [this](const TuyaDatapoint &datapoint) { + // Whether data type is BOOL or ENUM, it will still be a 1 or a 0, so the functions below are valid in both cases this->eco_ = datapoint.value_bool; + this->eco_type_ = datapoint.type; ESP_LOGV(TAG, "MCU reported eco is: %s", ONOFF(this->eco_)); this->compute_preset_(); this->compute_target_temperature_(); @@ -176,7 +178,11 @@ void TuyaClimate::control(const climate::ClimateCall &call) { if (this->eco_id_.has_value()) { const bool eco = preset == climate::CLIMATE_PRESET_ECO; ESP_LOGV(TAG, "Setting eco: %s", ONOFF(eco)); - this->parent_->set_boolean_datapoint_value(*this->eco_id_, eco); + if (this->eco_type_ == TuyaDatapointType::ENUM) { + this->parent_->set_enum_datapoint_value(*this->eco_id_, eco); + } else { + this->parent_->set_boolean_datapoint_value(*this->eco_id_, eco); + } } if (this->sleep_id_.has_value()) { const bool sleep = preset == climate::CLIMATE_PRESET_SLEEP; @@ -283,8 +289,11 @@ void TuyaClimate::control_fan_mode_(const climate::ClimateCall &call) { climate::ClimateTraits TuyaClimate::traits() { auto traits = climate::ClimateTraits(); - traits.set_supports_action(true); - traits.set_supports_current_temperature(this->current_temperature_id_.has_value()); + traits.add_feature_flags(climate::CLIMATE_SUPPORTS_ACTION); + if (this->current_temperature_id_.has_value()) { + traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE); + } + if (supports_heat_) traits.add_supported_mode(climate::CLIMATE_MODE_HEAT); if (supports_cool_) @@ -303,18 +312,12 @@ climate::ClimateTraits TuyaClimate::traits() { traits.add_supported_preset(climate::CLIMATE_PRESET_NONE); } if (this->swing_vertical_id_.has_value() && this->swing_horizontal_id_.has_value()) { - std::set supported_swing_modes = { - climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH, climate::CLIMATE_SWING_VERTICAL, - climate::CLIMATE_SWING_HORIZONTAL}; - traits.set_supported_swing_modes(std::move(supported_swing_modes)); + traits.set_supported_swing_modes({climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH, + climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_HORIZONTAL}); } else if (this->swing_vertical_id_.has_value()) { - std::set supported_swing_modes = {climate::CLIMATE_SWING_OFF, - climate::CLIMATE_SWING_VERTICAL}; - traits.set_supported_swing_modes(std::move(supported_swing_modes)); + traits.set_supported_swing_modes({climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}); } else if (this->swing_horizontal_id_.has_value()) { - std::set supported_swing_modes = {climate::CLIMATE_SWING_OFF, - climate::CLIMATE_SWING_HORIZONTAL}; - traits.set_supported_swing_modes(std::move(supported_swing_modes)); + traits.set_supported_swing_modes({climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL}); } if (fan_speed_id_) { diff --git a/esphome/components/tuya/climate/tuya_climate.h b/esphome/components/tuya/climate/tuya_climate.h index d6258c21e1..31bef57639 100644 --- a/esphome/components/tuya/climate/tuya_climate.h +++ b/esphome/components/tuya/climate/tuya_climate.h @@ -104,6 +104,7 @@ class TuyaClimate : public climate::Climate, public Component { optional eco_id_{}; optional sleep_id_{}; optional eco_temperature_{}; + TuyaDatapointType eco_type_{}; uint8_t active_state_; uint8_t fan_state_; optional swing_vertical_id_{}; diff --git a/esphome/components/tuya/number/tuya_number.cpp b/esphome/components/tuya/number/tuya_number.cpp index 68a7f8f2a7..44b22167de 100644 --- a/esphome/components/tuya/number/tuya_number.cpp +++ b/esphome/components/tuya/number/tuya_number.cpp @@ -8,7 +8,7 @@ static const char *const TAG = "tuya.number"; void TuyaNumber::setup() { if (this->restore_value_) { - this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); + this->pref_ = global_preferences->make_preference(this->get_preference_hash()); } this->parent_->register_listener(this->number_id_, [this](const TuyaDatapoint &datapoint) { diff --git a/esphome/components/tuya/select/tuya_select.cpp b/esphome/components/tuya/select/tuya_select.cpp index 07b0ff2815..9d46e4c8ca 100644 --- a/esphome/components/tuya/select/tuya_select.cpp +++ b/esphome/components/tuya/select/tuya_select.cpp @@ -10,7 +10,6 @@ void TuyaSelect::setup() { this->parent_->register_listener(this->select_id_, [this](const TuyaDatapoint &datapoint) { uint8_t enum_value = datapoint.value_enum; ESP_LOGV(TAG, "MCU reported select %u value %u", this->select_id_, enum_value); - auto options = this->traits.get_options(); auto mappings = this->mappings_; auto it = std::find(mappings.cbegin(), mappings.cend(), enum_value); if (it == mappings.end()) { @@ -18,28 +17,21 @@ void TuyaSelect::setup() { return; } size_t mapping_idx = std::distance(mappings.cbegin(), it); - auto value = this->at(mapping_idx); - this->publish_state(value.value()); + this->publish_state(mapping_idx); }); } -void TuyaSelect::control(const std::string &value) { +void TuyaSelect::control(size_t index) { if (this->optimistic_) - this->publish_state(value); + this->publish_state(index); - auto idx = this->index_of(value); - if (idx.has_value()) { - uint8_t mapping = this->mappings_.at(idx.value()); - ESP_LOGV(TAG, "Setting %u datapoint value to %u:%s", this->select_id_, mapping, value.c_str()); - if (this->is_int_) { - this->parent_->set_integer_datapoint_value(this->select_id_, mapping); - } else { - this->parent_->set_enum_datapoint_value(this->select_id_, mapping); - } - return; + uint8_t mapping = this->mappings_.at(index); + ESP_LOGV(TAG, "Setting %u datapoint value to %u:%s", this->select_id_, mapping, this->option_at(index)); + if (this->is_int_) { + this->parent_->set_integer_datapoint_value(this->select_id_, mapping); + } else { + this->parent_->set_enum_datapoint_value(this->select_id_, mapping); } - - ESP_LOGW(TAG, "Invalid value %s", value.c_str()); } void TuyaSelect::dump_config() { @@ -49,9 +41,9 @@ void TuyaSelect::dump_config() { " Data type: %s\n" " Options are:", this->select_id_, this->is_int_ ? "int" : "enum"); - auto options = this->traits.get_options(); - for (auto i = 0; i < this->mappings_.size(); i++) { - ESP_LOGCONFIG(TAG, " %i: %s", this->mappings_.at(i), options.at(i).c_str()); + const auto &options = this->traits.get_options(); + for (size_t i = 0; i < this->mappings_.size(); i++) { + ESP_LOGCONFIG(TAG, " %i: %s", this->mappings_.at(i), options.at(i)); } } diff --git a/esphome/components/tuya/select/tuya_select.h b/esphome/components/tuya/select/tuya_select.h index 12d7b507d4..24505c9910 100644 --- a/esphome/components/tuya/select/tuya_select.h +++ b/esphome/components/tuya/select/tuya_select.h @@ -23,7 +23,7 @@ class TuyaSelect : public select::Select, public Component { void set_select_mappings(std::vector mappings) { this->mappings_ = std::move(mappings); } protected: - void control(const std::string &value) override; + void control(size_t index) override; Tuya *parent_; bool optimistic_ = false; diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index 1443d10254..12b14be9ff 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -215,12 +215,37 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff this->send_empty_command_(TuyaCommandType::DATAPOINT_QUERY); } break; - case TuyaCommandType::WIFI_RESET: - ESP_LOGE(TAG, "WIFI_RESET is not handled"); - break; case TuyaCommandType::WIFI_SELECT: - ESP_LOGE(TAG, "WIFI_SELECT is not handled"); + case TuyaCommandType::WIFI_RESET: { + const bool is_select = (len >= 1); + // Send WIFI_SELECT ACK + TuyaCommand ack; + ack.cmd = is_select ? TuyaCommandType::WIFI_SELECT : TuyaCommandType::WIFI_RESET; + ack.payload.clear(); + this->send_command_(ack); + // Establish pairing mode for correct first WIFI_STATE byte, EZ (0x00) default + uint8_t first = 0x00; + const char *mode_str = "EZ"; + if (is_select && buffer[0] == 0x01) { + first = 0x01; + mode_str = "AP"; + } + // Send WIFI_STATE response, MCU exits pairing mode + TuyaCommand st; + st.cmd = TuyaCommandType::WIFI_STATE; + st.payload.resize(1); + st.payload[0] = first; + this->send_command_(st); + st.payload[0] = 0x02; + this->send_command_(st); + st.payload[0] = 0x03; + this->send_command_(st); + st.payload[0] = 0x04; + this->send_command_(st); + ESP_LOGI(TAG, "%s received (%s), replied with WIFI_STATE confirming connection established", + is_select ? "WIFI_SELECT" : "WIFI_RESET", mode_str); break; + } case TuyaCommandType::DATAPOINT_DELIVER: break; case TuyaCommandType::DATAPOINT_REPORT_ASYNC: diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index 7d4c6360fe..7b0d9726b8 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -1,3 +1,5 @@ +from logging import getLogger +import math import re from esphome import automation, pins @@ -14,9 +16,9 @@ from esphome.const import ( CONF_DIRECTION, CONF_DUMMY_RECEIVER, CONF_DUMMY_RECEIVER_ID, + CONF_FLOW_CONTROL_PIN, CONF_ID, CONF_INVERT, - CONF_INVERTED, CONF_LAMBDA, CONF_NUMBER, CONF_PORT, @@ -30,18 +32,17 @@ from esphome.const import ( PLATFORM_HOST, PlatformFramework, ) -from esphome.core import CORE +from esphome.core import CORE, ID import esphome.final_validate as fv from esphome.yaml_util import make_data_base +_LOGGER = getLogger(__name__) + CODEOWNERS = ["@esphome/core"] uart_ns = cg.esphome_ns.namespace("uart") UARTComponent = uart_ns.class_("UARTComponent") IDFUARTComponent = uart_ns.class_("IDFUARTComponent", UARTComponent, cg.Component) -ESP32ArduinoUARTComponent = uart_ns.class_( - "ESP32ArduinoUARTComponent", UARTComponent, cg.Component -) ESP8266UartComponent = uart_ns.class_( "ESP8266UartComponent", UARTComponent, cg.Component ) @@ -53,7 +54,6 @@ HostUartComponent = uart_ns.class_("HostUartComponent", UARTComponent, cg.Compon NATIVE_UART_CLASSES = ( str(IDFUARTComponent), - str(ESP32ArduinoUARTComponent), str(ESP8266UartComponent), str(RP2040UartComponent), str(LibreTinyUARTComponent), @@ -119,20 +119,6 @@ def validate_rx_pin(value): return value -def validate_invert_esp32(config): - if ( - CORE.is_esp32 - and CORE.using_arduino - and CONF_TX_PIN in config - and CONF_RX_PIN in config - and config[CONF_TX_PIN][CONF_INVERTED] != config[CONF_RX_PIN][CONF_INVERTED] - ): - raise cv.Invalid( - "Different invert values for TX and RX pin are not supported for ESP32 when using Arduino." - ) - return config - - def validate_host_config(config): if CORE.is_host: if CONF_TX_PIN in config or CONF_RX_PIN in config: @@ -147,14 +133,26 @@ def validate_host_config(config): return config +def validate_rx_buffer_size(config): + if CORE.is_esp32: + # ESP32 UART hardware FIFO is 128 bytes (LP UART is 16 bytes, but we use 128 as safe minimum) + # rx_buffer_size must be greater than the hardware FIFO length + min_buffer_size = 128 + if config[CONF_RX_BUFFER_SIZE] <= min_buffer_size: + _LOGGER.warning( + "UART rx_buffer_size (%d bytes) is too small and must be greater than the hardware " + "FIFO size (%d bytes). The buffer size will be automatically adjusted at runtime.", + config[CONF_RX_BUFFER_SIZE], + min_buffer_size, + ) + return config + + def _uart_declare_type(value): if CORE.is_esp8266: return cv.declare_id(ESP8266UartComponent)(value) if CORE.is_esp32: - if CORE.using_arduino: - return cv.declare_id(ESP32ArduinoUARTComponent)(value) - if CORE.using_esp_idf: - return cv.declare_id(IDFUARTComponent)(value) + return cv.declare_id(IDFUARTComponent)(value) if CORE.is_rp2040: return cv.declare_id(RP2040UartComponent)(value) if CORE.is_libretiny: @@ -174,6 +172,8 @@ UART_PARITY_OPTIONS = { CONF_STOP_BITS = "stop_bits" CONF_DATA_BITS = "data_bits" CONF_PARITY = "parity" +CONF_RX_FULL_THRESHOLD = "rx_full_threshold" +CONF_RX_TIMEOUT = "rx_timeout" UARTDirection = uart_ns.enum("UARTDirection") UART_DIRECTIONS = { @@ -241,8 +241,17 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_BAUD_RATE): cv.int_range(min=1), cv.Optional(CONF_TX_PIN): pins.internal_gpio_output_pin_schema, cv.Optional(CONF_RX_PIN): validate_rx_pin, + cv.Optional(CONF_FLOW_CONTROL_PIN): cv.All( + cv.only_on_esp32, pins.internal_gpio_output_pin_schema + ), cv.Optional(CONF_PORT): cv.All(validate_port, cv.only_on(PLATFORM_HOST)), cv.Optional(CONF_RX_BUFFER_SIZE, default=256): cv.validate_bytes, + cv.Optional(CONF_RX_FULL_THRESHOLD): cv.All( + cv.only_on_esp32, cv.validate_bytes, cv.int_range(min=1, max=120) + ), + cv.SplitDefault(CONF_RX_TIMEOUT, esp32=2): cv.All( + cv.only_on_esp32, cv.validate_bytes, cv.int_range(min=0, max=92) + ), cv.Optional(CONF_STOP_BITS, default=1): cv.one_of(1, 2, int=True), cv.Optional(CONF_DATA_BITS, default=8): cv.int_range(min=5, max=8), cv.Optional(CONF_PARITY, default="NONE"): cv.enum( @@ -255,8 +264,8 @@ CONFIG_SCHEMA = cv.All( } ).extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key(CONF_TX_PIN, CONF_RX_PIN, CONF_PORT), - validate_invert_esp32, validate_host_config, + validate_rx_buffer_size, ) @@ -298,9 +307,27 @@ async def to_code(config): if CONF_RX_PIN in config: rx_pin = await cg.gpio_pin_expression(config[CONF_RX_PIN]) cg.add(var.set_rx_pin(rx_pin)) + if CONF_FLOW_CONTROL_PIN in config: + flow_control_pin = await cg.gpio_pin_expression(config[CONF_FLOW_CONTROL_PIN]) + cg.add(var.set_flow_control_pin(flow_control_pin)) if CONF_PORT in config: cg.add(var.set_name(config[CONF_PORT])) cg.add(var.set_rx_buffer_size(config[CONF_RX_BUFFER_SIZE])) + if CORE.is_esp32: + if CONF_RX_FULL_THRESHOLD not in config: + # Calculate rx_full_threshold to be 10ms + bytelength = config[CONF_DATA_BITS] + config[CONF_STOP_BITS] + 1 + if config[CONF_PARITY] != "NONE": + bytelength += 1 + config[CONF_RX_FULL_THRESHOLD] = max( + 1, + min( + 120, + math.floor((config[CONF_BAUD_RATE] / (bytelength * 1000 / 10)) - 1), + ), + ) + cg.add(var.set_rx_full_threshold(config[CONF_RX_FULL_THRESHOLD])) + cg.add(var.set_rx_timeout(config[CONF_RX_TIMEOUT])) cg.add(var.set_stop_bits(config[CONF_STOP_BITS])) cg.add(var.set_data_bits(config[CONF_DATA_BITS])) cg.add(var.set_parity(config[CONF_PARITY])) @@ -339,7 +366,7 @@ def final_validate_device_schema( def validate_pin(opt, device): def validator(value): - if opt in device: + if opt in device and not CORE.testing_mode: raise cv.Invalid( f"The uart {opt} is used both by {name} and {device[opt]}, " f"but can only be used by one. Please create a new uart bus for {name}." @@ -438,14 +465,19 @@ async def uart_write_to_code(config, action_id, template_arg, args): templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8)) cg.add(var.set_data_template(templ)) else: - cg.add(var.set_data_static(data)) + # Generate static array in flash to avoid RAM copy + arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8) + arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data)) + cg.add(var.set_data_static(arr, len(data))) return var FILTER_SOURCE_FILES = filter_source_files_from_platform( { - "uart_component_esp32_arduino.cpp": {PlatformFramework.ESP32_ARDUINO}, - "uart_component_esp_idf.cpp": {PlatformFramework.ESP32_IDF}, + "uart_component_esp_idf.cpp": { + PlatformFramework.ESP32_IDF, + PlatformFramework.ESP32_ARDUINO, + }, "uart_component_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, "uart_component_host.cpp": {PlatformFramework.HOST_NATIVE}, "uart_component_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO}, diff --git a/esphome/components/uart/automation.h b/esphome/components/uart/automation.h index b6a50ea22d..c2eb308eb8 100644 --- a/esphome/components/uart/automation.h +++ b/esphome/components/uart/automation.h @@ -10,28 +10,35 @@ namespace uart { template class UARTWriteAction : public Action, public Parented { public: - void set_data_template(std::function(Ts...)> func) { - this->data_func_ = func; - this->static_ = false; - } - void set_data_static(const std::vector &data) { - this->data_static_ = data; - this->static_ = true; + void set_data_template(std::vector (*func)(Ts...)) { + // Stateless lambdas (generated by ESPHome) implicitly convert to function pointers + this->code_.func = func; + this->len_ = -1; // Sentinel value indicates template mode } - void play(Ts... x) override { - if (this->static_) { - this->parent_->write_array(this->data_static_); + // Store pointer to static data in flash (no RAM copy) + void set_data_static(const uint8_t *data, size_t len) { + this->code_.data = data; + this->len_ = len; // Length >= 0 indicates static mode + } + + void play(const Ts &...x) override { + if (this->len_ >= 0) { + // Static mode: use pointer and length + this->parent_->write_array(this->code_.data, static_cast(this->len_)); } else { - auto val = this->data_func_(x...); + // Template mode: call function + auto val = this->code_.func(x...); this->parent_->write_array(val); } } protected: - bool static_{false}; - std::function(Ts...)> data_func_{}; - std::vector data_static_{}; + ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length + union Code { + std::vector (*func)(Ts...); // Function pointer (stateless lambdas) + const uint8_t *data; // Pointer to static data in flash + } code_; }; } // namespace uart diff --git a/esphome/components/uart/button/__init__.py b/esphome/components/uart/button/__init__.py index 5b811de07d..95fe21271d 100644 --- a/esphome/components/uart/button/__init__.py +++ b/esphome/components/uart/button/__init__.py @@ -33,4 +33,4 @@ async def to_code(config): data = config[CONF_DATA] if isinstance(data, bytes): data = [HexInt(x) for x in data] - cg.add(var.set_data(data)) + cg.add(var.set_data(cg.ArrayInitializer(*data))) diff --git a/esphome/components/uart/button/uart_button.h b/esphome/components/uart/button/uart_button.h index 2d600b199a..8c7d762a05 100644 --- a/esphome/components/uart/button/uart_button.h +++ b/esphome/components/uart/button/uart_button.h @@ -11,7 +11,8 @@ namespace uart { class UARTButton : public button::Button, public UARTDevice, public Component { public: - void set_data(const std::vector &data) { this->data_ = data; } + void set_data(std::vector &&data) { this->data_ = std::move(data); } + void set_data(std::initializer_list data) { this->data_ = std::vector(data); } void dump_config() override; diff --git a/esphome/components/uart/switch/__init__.py b/esphome/components/uart/switch/__init__.py index b25e070461..290bbed5d3 100644 --- a/esphome/components/uart/switch/__init__.py +++ b/esphome/components/uart/switch/__init__.py @@ -44,16 +44,16 @@ async def to_code(config): if data_on := data.get(CONF_TURN_ON): if isinstance(data_on, bytes): data_on = [HexInt(x) for x in data_on] - cg.add(var.set_data_on(data_on)) + cg.add(var.set_data_on(cg.ArrayInitializer(*data_on))) if data_off := data.get(CONF_TURN_OFF): if isinstance(data_off, bytes): data_off = [HexInt(x) for x in data_off] - cg.add(var.set_data_off(data_off)) + cg.add(var.set_data_off(cg.ArrayInitializer(*data_off))) else: data = config[CONF_DATA] if isinstance(data, bytes): data = [HexInt(x) for x in data] - cg.add(var.set_data_on(data)) + cg.add(var.set_data_on(cg.ArrayInitializer(*data))) cg.add(var.set_single_state(True)) if CONF_SEND_EVERY in config: cg.add(var.set_send_every(config[CONF_SEND_EVERY])) diff --git a/esphome/components/uart/switch/uart_switch.h b/esphome/components/uart/switch/uart_switch.h index 4ef5b6da4b..909307d57e 100644 --- a/esphome/components/uart/switch/uart_switch.h +++ b/esphome/components/uart/switch/uart_switch.h @@ -14,8 +14,10 @@ class UARTSwitch : public switch_::Switch, public UARTDevice, public Component { public: void loop() override; - void set_data_on(const std::vector &data) { this->data_on_ = data; } - void set_data_off(const std::vector &data) { this->data_off_ = data; } + void set_data_on(std::vector &&data) { this->data_on_ = std::move(data); } + void set_data_on(std::initializer_list data) { this->data_on_ = std::vector(data); } + void set_data_off(std::vector &&data) { this->data_off_ = std::move(data); } + void set_data_off(std::initializer_list data) { this->data_off_ = std::vector(data); } void set_send_every(uint32_t send_every) { this->send_every_ = send_every; } void set_single_state(bool single) { this->single_state_ = single; } diff --git a/esphome/components/uart/uart.h b/esphome/components/uart/uart.h index dc6962fbae..e2912db122 100644 --- a/esphome/components/uart/uart.h +++ b/esphome/components/uart/uart.h @@ -18,6 +18,12 @@ class UARTDevice { void write_byte(uint8_t data) { this->parent_->write_byte(data); } + void set_rx_full_threshold(size_t rx_full_threshold) { this->parent_->set_rx_full_threshold(rx_full_threshold); } + void set_rx_full_threshold_ms(size_t time) { this->parent_->set_rx_full_threshold_ms(time); } + size_t get_rx_full_threshold() { return this->parent_->get_rx_full_threshold(); } + void set_rx_timeout(size_t rx_timeout) { this->parent_->set_rx_timeout(rx_timeout); } + size_t get_rx_timeout() { return this->parent_->get_rx_timeout(); } + void write_array(const uint8_t *data, size_t len) { this->parent_->write_array(data, len); } void write_array(const std::vector &data) { this->parent_->write_array(data); } template void write_array(const std::array &data) { diff --git a/esphome/components/uart/uart_component.cpp b/esphome/components/uart/uart_component.cpp index 09b8c975ab..8f670275d4 100644 --- a/esphome/components/uart/uart_component.cpp +++ b/esphome/components/uart/uart_component.cpp @@ -20,5 +20,13 @@ bool UARTComponent::check_read_timeout_(size_t len) { return true; } +void UARTComponent::set_rx_full_threshold_ms(uint8_t time) { + uint8_t bytelength = this->data_bits_ + this->stop_bits_ + 1; + if (this->parity_ != UARTParityOptions::UART_CONFIG_PARITY_NONE) + bytelength += 1; + int32_t val = clamp((this->baud_rate_ / (bytelength * 1000 / time)) - 1, 1, 120); + this->set_rx_full_threshold(val); +} + } // namespace uart } // namespace esphome diff --git a/esphome/components/uart/uart_component.h b/esphome/components/uart/uart_component.h index a57910c1a1..452688b3e9 100644 --- a/esphome/components/uart/uart_component.h +++ b/esphome/components/uart/uart_component.h @@ -6,6 +6,7 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" +#include "esphome/core/helpers.h" #ifdef USE_UART_DEBUGGER #include "esphome/core/automation.h" #endif @@ -82,6 +83,10 @@ class UARTComponent { // @param rx_pin Pointer to the internal GPIO pin used for reception. void set_rx_pin(InternalGPIOPin *rx_pin) { this->rx_pin_ = rx_pin; } + // Sets the flow control pin for the UART bus. + // @param flow_control_pin Pointer to the internal GPIO pin used for flow control. + void set_flow_control_pin(InternalGPIOPin *flow_control_pin) { this->flow_control_pin_ = flow_control_pin; } + // Sets the size of the RX buffer. // @param rx_buffer_size Size of the RX buffer in bytes. void set_rx_buffer_size(size_t rx_buffer_size) { this->rx_buffer_size_ = rx_buffer_size; } @@ -90,6 +95,26 @@ class UARTComponent { // @return Size of the RX buffer in bytes. size_t get_rx_buffer_size() { return this->rx_buffer_size_; } + // Sets the RX FIFO full interrupt threshold. + // @param rx_full_threshold RX full interrupt threshold in bytes. + virtual void set_rx_full_threshold(size_t rx_full_threshold) {} + + // Sets the RX FIFO full interrupt threshold. + // @param time RX full interrupt threshold in ms. + void set_rx_full_threshold_ms(uint8_t time); + + // Gets the RX FIFO full interrupt threshold. + // @return RX full interrupt threshold in bytes. + size_t get_rx_full_threshold() { return this->rx_full_threshold_; } + + // Sets the RX timeout interrupt threshold. + // @param rx_timeout RX timeout interrupt threshold (unit: time of sending one byte). + virtual void set_rx_timeout(size_t rx_timeout) {} + + // Gets the RX timeout interrupt threshold. + // @return RX timeout interrupt threshold (unit: time of sending one byte). + size_t get_rx_timeout() { return this->rx_timeout_; } + // Sets the number of stop bits used in UART communication. // @param stop_bits Number of stop bits. void set_stop_bits(uint8_t stop_bits) { this->stop_bits_ = stop_bits; } @@ -161,7 +186,10 @@ class UARTComponent { InternalGPIOPin *tx_pin_; InternalGPIOPin *rx_pin_; + InternalGPIOPin *flow_control_pin_; size_t rx_buffer_size_; + size_t rx_full_threshold_{1}; + size_t rx_timeout_{0}; uint32_t baud_rate_; uint8_t stop_bits_; uint8_t data_bits_; diff --git a/esphome/components/uart/uart_component_esp32_arduino.cpp b/esphome/components/uart/uart_component_esp32_arduino.cpp deleted file mode 100644 index 4a1c326789..0000000000 --- a/esphome/components/uart/uart_component_esp32_arduino.cpp +++ /dev/null @@ -1,214 +0,0 @@ -#ifdef USE_ESP32_FRAMEWORK_ARDUINO -#include "uart_component_esp32_arduino.h" -#include "esphome/core/application.h" -#include "esphome/core/defines.h" -#include "esphome/core/helpers.h" -#include "esphome/core/log.h" - -#ifdef USE_LOGGER -#include "esphome/components/logger/logger.h" -#endif - -namespace esphome { -namespace uart { -static const char *const TAG = "uart.arduino_esp32"; - -static const uint32_t UART_PARITY_EVEN = 0 << 0; -static const uint32_t UART_PARITY_ODD = 1 << 0; -static const uint32_t UART_PARITY_ENABLE = 1 << 1; -static const uint32_t UART_NB_BIT_5 = 0 << 2; -static const uint32_t UART_NB_BIT_6 = 1 << 2; -static const uint32_t UART_NB_BIT_7 = 2 << 2; -static const uint32_t UART_NB_BIT_8 = 3 << 2; -static const uint32_t UART_NB_STOP_BIT_1 = 1 << 4; -static const uint32_t UART_NB_STOP_BIT_2 = 3 << 4; -static const uint32_t UART_TICK_APB_CLOCK = 1 << 27; - -uint32_t ESP32ArduinoUARTComponent::get_config() { - uint32_t config = 0; - - /* - * All bits numbers below come from - * framework-arduinoespressif32/cores/esp32/esp32-hal-uart.h - * And more specifically conf0 union in uart_dev_t. - * - * Below is bit used from conf0 union. - * : - * parity:0 0:even 1:odd - * parity_en:1 Set this bit to enable uart parity check. - * bit_num:2-4 0:5bits 1:6bits 2:7bits 3:8bits - * stop_bit_num:4-6 stop bit. 1:1bit 2:1.5bits 3:2bits - * tick_ref_always_on:27 select the clock.1:apb clock:ref_tick - */ - - if (this->parity_ == UART_CONFIG_PARITY_EVEN) { - config |= UART_PARITY_EVEN | UART_PARITY_ENABLE; - } else if (this->parity_ == UART_CONFIG_PARITY_ODD) { - config |= UART_PARITY_ODD | UART_PARITY_ENABLE; - } - - switch (this->data_bits_) { - case 5: - config |= UART_NB_BIT_5; - break; - case 6: - config |= UART_NB_BIT_6; - break; - case 7: - config |= UART_NB_BIT_7; - break; - case 8: - config |= UART_NB_BIT_8; - break; - } - - if (this->stop_bits_ == 1) { - config |= UART_NB_STOP_BIT_1; - } else { - config |= UART_NB_STOP_BIT_2; - } - - config |= UART_TICK_APB_CLOCK; - - return config; -} - -void ESP32ArduinoUARTComponent::setup() { - // Use Arduino HardwareSerial UARTs if all used pins match the ones - // preconfigured by the platform. For example if RX disabled but TX pin - // is 1 we still want to use Serial. - bool is_default_tx, is_default_rx; -#ifdef CONFIG_IDF_TARGET_ESP32C3 - is_default_tx = tx_pin_ == nullptr || tx_pin_->get_pin() == 21; - is_default_rx = rx_pin_ == nullptr || rx_pin_->get_pin() == 20; -#else - is_default_tx = tx_pin_ == nullptr || tx_pin_->get_pin() == 1; - is_default_rx = rx_pin_ == nullptr || rx_pin_->get_pin() == 3; -#endif - static uint8_t next_uart_num = 0; - if (is_default_tx && is_default_rx && next_uart_num == 0) { -#if ARDUINO_USB_CDC_ON_BOOT - this->hw_serial_ = &Serial0; -#else - this->hw_serial_ = &Serial; -#endif - next_uart_num++; - } else { -#ifdef USE_LOGGER - bool logger_uses_hardware_uart = true; - -#ifdef USE_LOGGER_USB_CDC - if (logger::global_logger->get_uart() == logger::UART_SELECTION_USB_CDC) { - // this is not a hardware UART, ignore it - logger_uses_hardware_uart = false; - } -#endif // USE_LOGGER_USB_CDC - -#ifdef USE_LOGGER_USB_SERIAL_JTAG - if (logger::global_logger->get_uart() == logger::UART_SELECTION_USB_SERIAL_JTAG) { - // this is not a hardware UART, ignore it - logger_uses_hardware_uart = false; - } -#endif // USE_LOGGER_USB_SERIAL_JTAG - - if (logger_uses_hardware_uart && logger::global_logger->get_baud_rate() > 0 && - logger::global_logger->get_uart() == next_uart_num) { - next_uart_num++; - } -#endif // USE_LOGGER - - if (next_uart_num >= SOC_UART_NUM) { - ESP_LOGW(TAG, "Maximum number of UART components created already."); - this->mark_failed(); - return; - } - - this->number_ = next_uart_num; - this->hw_serial_ = new HardwareSerial(next_uart_num++); // NOLINT(cppcoreguidelines-owning-memory) - } - - this->load_settings(false); -} - -void ESP32ArduinoUARTComponent::load_settings(bool dump_config) { - int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1; - int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1; - bool invert = false; - if (tx_pin_ != nullptr && tx_pin_->is_inverted()) - invert = true; - if (rx_pin_ != nullptr && rx_pin_->is_inverted()) - invert = true; - this->hw_serial_->setRxBufferSize(this->rx_buffer_size_); - this->hw_serial_->begin(this->baud_rate_, get_config(), rx, tx, invert); - if (dump_config) { - ESP_LOGCONFIG(TAG, "UART %u was reloaded.", this->number_); - this->dump_config(); - } -} - -void ESP32ArduinoUARTComponent::dump_config() { - ESP_LOGCONFIG(TAG, "UART Bus %d:", this->number_); - LOG_PIN(" TX Pin: ", tx_pin_); - LOG_PIN(" RX Pin: ", rx_pin_); - if (this->rx_pin_ != nullptr) { - ESP_LOGCONFIG(TAG, " RX Buffer Size: %u", this->rx_buffer_size_); - } - ESP_LOGCONFIG(TAG, - " Baud Rate: %u baud\n" - " Data Bits: %u\n" - " Parity: %s\n" - " Stop bits: %u", - this->baud_rate_, this->data_bits_, LOG_STR_ARG(parity_to_str(this->parity_)), this->stop_bits_); - this->check_logger_conflict(); -} - -void ESP32ArduinoUARTComponent::write_array(const uint8_t *data, size_t len) { - this->hw_serial_->write(data, len); -#ifdef USE_UART_DEBUGGER - for (size_t i = 0; i < len; i++) { - this->debug_callback_.call(UART_DIRECTION_TX, data[i]); - } -#endif -} - -bool ESP32ArduinoUARTComponent::peek_byte(uint8_t *data) { - if (!this->check_read_timeout_()) - return false; - *data = this->hw_serial_->peek(); - return true; -} - -bool ESP32ArduinoUARTComponent::read_array(uint8_t *data, size_t len) { - if (!this->check_read_timeout_(len)) - return false; - this->hw_serial_->readBytes(data, len); -#ifdef USE_UART_DEBUGGER - for (size_t i = 0; i < len; i++) { - this->debug_callback_.call(UART_DIRECTION_RX, data[i]); - } -#endif - return true; -} - -int ESP32ArduinoUARTComponent::available() { return this->hw_serial_->available(); } -void ESP32ArduinoUARTComponent::flush() { - ESP_LOGVV(TAG, " Flushing"); - this->hw_serial_->flush(); -} - -void ESP32ArduinoUARTComponent::check_logger_conflict() { -#ifdef USE_LOGGER - if (this->hw_serial_ == nullptr || logger::global_logger->get_baud_rate() == 0) { - return; - } - - if (this->hw_serial_ == logger::global_logger->get_hw_serial()) { - ESP_LOGW(TAG, " You're using the same serial port for logging and the UART component. Please " - "disable logging over the serial port by setting logger->baud_rate to 0."); - } -#endif -} - -} // namespace uart -} // namespace esphome -#endif // USE_ESP32_FRAMEWORK_ARDUINO diff --git a/esphome/components/uart/uart_component_esp32_arduino.h b/esphome/components/uart/uart_component_esp32_arduino.h deleted file mode 100644 index de17d9718b..0000000000 --- a/esphome/components/uart/uart_component_esp32_arduino.h +++ /dev/null @@ -1,60 +0,0 @@ -#pragma once - -#ifdef USE_ESP32_FRAMEWORK_ARDUINO - -#include -#include -#include -#include "esphome/core/component.h" -#include "esphome/core/hal.h" -#include "esphome/core/log.h" -#include "uart_component.h" - -namespace esphome { -namespace uart { - -class ESP32ArduinoUARTComponent : public UARTComponent, public Component { - public: - void setup() override; - void dump_config() override; - float get_setup_priority() const override { return setup_priority::BUS; } - - void write_array(const uint8_t *data, size_t len) override; - - bool peek_byte(uint8_t *data) override; - bool read_array(uint8_t *data, size_t len) override; - - int available() override; - void flush() override; - - uint32_t get_config(); - - HardwareSerial *get_hw_serial() { return this->hw_serial_; } - uint8_t get_hw_serial_number() { return this->number_; } - - /** - * Load the UART with the current settings. - * @param dump_config (Optional, default `true`): True for displaying new settings or - * false to change it quitely - * - * Example: - * ```cpp - * id(uart1).load_settings(); - * ``` - * - * This will load the current UART interface with the latest settings (baud_rate, parity, etc). - */ - void load_settings(bool dump_config) override; - void load_settings() override { this->load_settings(true); } - - protected: - void check_logger_conflict() override; - - HardwareSerial *hw_serial_{nullptr}; - uint8_t number_{0}; -}; - -} // namespace uart -} // namespace esphome - -#endif // USE_ESP32_FRAMEWORK_ARDUINO diff --git a/esphome/components/uart/uart_component_esp8266.cpp b/esphome/components/uart/uart_component_esp8266.cpp index b2bf2bacf1..c84a877ef4 100644 --- a/esphome/components/uart/uart_component_esp8266.cpp +++ b/esphome/components/uart/uart_component_esp8266.cpp @@ -56,6 +56,21 @@ uint32_t ESP8266UartComponent::get_config() { } void ESP8266UartComponent::setup() { + auto setup_pin_if_needed = [](InternalGPIOPin *pin) { + if (!pin) { + return; + } + const auto mask = gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN; + if ((pin->get_flags() & mask) != gpio::Flags::FLAG_NONE) { + pin->setup(); + } + }; + + setup_pin_if_needed(this->rx_pin_); + if (this->rx_pin_ != this->tx_pin_) { + setup_pin_if_needed(this->tx_pin_); + } + // Use Arduino HardwareSerial UARTs if all used pins match the ones // preconfigured by the platform. For example if RX disabled but TX pin // is 1 we still want to use Serial. diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 6bb4b16819..61ca8c1c0c 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -1,4 +1,4 @@ -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include "uart_component_esp_idf.h" #include @@ -6,6 +6,9 @@ #include "esphome/core/defines.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include "esphome/core/gpio.h" +#include "driver/gpio.h" +#include "soc/gpio_num.h" #ifdef USE_LOGGER #include "esphome/components/logger/logger.h" @@ -88,24 +91,74 @@ void IDFUARTComponent::setup() { this->uart_num_ = static_cast(next_uart_num++); this->lock_ = xSemaphoreCreateMutex(); +#if (SOC_UART_LP_NUM >= 1) + size_t fifo_len = ((this->uart_num_ < SOC_UART_HP_NUM) ? SOC_UART_FIFO_LEN : SOC_LP_UART_FIFO_LEN); +#else + size_t fifo_len = SOC_UART_FIFO_LEN; +#endif + if (this->rx_buffer_size_ <= fifo_len) { + ESP_LOGW(TAG, "rx_buffer_size is too small, must be greater than %zu", fifo_len); + this->rx_buffer_size_ = fifo_len * 2; + } + xSemaphoreTake(this->lock_, portMAX_DELAY); - uart_config_t uart_config = this->get_config_(); - esp_err_t err = uart_param_config(this->uart_num_, &uart_config); + this->load_settings(false); + + xSemaphoreGive(this->lock_); +} + +void IDFUARTComponent::load_settings(bool dump_config) { + esp_err_t err; + + if (uart_is_driver_installed(this->uart_num_)) { + err = uart_driver_delete(this->uart_num_); + if (err != ESP_OK) { + ESP_LOGW(TAG, "uart_driver_delete failed: %s", esp_err_to_name(err)); + this->mark_failed(); + return; + } + } + err = uart_driver_install(this->uart_num_, // UART number + this->rx_buffer_size_, // RX ring buffer size + 0, // TX ring buffer size. If zero, driver will not use a TX buffer and TX function will + // block task until all data has been sent out + 20, // event queue size/depth + &this->uart_event_queue_, // event queue + 0 // Flags used to allocate the interrupt + ); if (err != ESP_OK) { - ESP_LOGW(TAG, "uart_param_config failed: %s", esp_err_to_name(err)); + ESP_LOGW(TAG, "uart_driver_install failed: %s", esp_err_to_name(err)); this->mark_failed(); return; } + auto setup_pin_if_needed = [](InternalGPIOPin *pin) { + if (!pin) { + return; + } + const auto mask = gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN; + if ((pin->get_flags() & mask) != gpio::Flags::FLAG_NONE) { + pin->setup(); + } + }; + + setup_pin_if_needed(this->rx_pin_); + if (this->rx_pin_ != this->tx_pin_) { + setup_pin_if_needed(this->tx_pin_); + } + int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1; int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1; + int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1; uint32_t invert = 0; - if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted()) + if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted()) { invert |= UART_SIGNAL_TXD_INV; - if (this->rx_pin_ != nullptr && this->rx_pin_->is_inverted()) + } + if (this->rx_pin_ != nullptr && this->rx_pin_->is_inverted()) { invert |= UART_SIGNAL_RXD_INV; + } err = uart_set_line_inverse(this->uart_num_, invert); if (err != ESP_OK) { @@ -114,47 +167,60 @@ void IDFUARTComponent::setup() { return; } - err = uart_set_pin(this->uart_num_, tx, rx, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); + err = uart_set_pin(this->uart_num_, tx, rx, flow_control, UART_PIN_NO_CHANGE); if (err != ESP_OK) { ESP_LOGW(TAG, "uart_set_pin failed: %s", esp_err_to_name(err)); this->mark_failed(); return; } - err = uart_driver_install(this->uart_num_, /* UART RX ring buffer size. */ this->rx_buffer_size_, - /* UART TX ring buffer size. If set to zero, driver will not use TX buffer, TX function will - block task until all data have been sent out.*/ - 0, - /* UART event queue size/depth. */ 20, &(this->uart_event_queue_), - /* Flags used to allocate the interrupt. */ 0); + err = uart_set_rx_full_threshold(this->uart_num_, this->rx_full_threshold_); if (err != ESP_OK) { - ESP_LOGW(TAG, "uart_driver_install failed: %s", esp_err_to_name(err)); + ESP_LOGW(TAG, "uart_set_rx_full_threshold failed: %s", esp_err_to_name(err)); this->mark_failed(); return; } - xSemaphoreGive(this->lock_); -} + err = uart_set_rx_timeout(this->uart_num_, this->rx_timeout_); + if (err != ESP_OK) { + ESP_LOGW(TAG, "uart_set_rx_timeout failed: %s", esp_err_to_name(err)); + this->mark_failed(); + return; + } + + auto mode = this->flow_control_pin_ != nullptr ? UART_MODE_RS485_HALF_DUPLEX : UART_MODE_UART; + err = uart_set_mode(this->uart_num_, mode); // per docs, must be called only after uart_driver_install() + if (err != ESP_OK) { + ESP_LOGW(TAG, "uart_set_mode failed: %s", esp_err_to_name(err)); + this->mark_failed(); + return; + } -void IDFUARTComponent::load_settings(bool dump_config) { uart_config_t uart_config = this->get_config_(); - esp_err_t err = uart_param_config(this->uart_num_, &uart_config); + err = uart_param_config(this->uart_num_, &uart_config); if (err != ESP_OK) { ESP_LOGW(TAG, "uart_param_config failed: %s", esp_err_to_name(err)); this->mark_failed(); return; - } else if (dump_config) { - ESP_LOGCONFIG(TAG, "UART %u was reloaded.", this->uart_num_); + } + + if (dump_config) { + ESP_LOGCONFIG(TAG, "Reloaded UART %u", this->uart_num_); this->dump_config(); } } void IDFUARTComponent::dump_config() { ESP_LOGCONFIG(TAG, "UART Bus %u:", this->uart_num_); - LOG_PIN(" TX Pin: ", tx_pin_); - LOG_PIN(" RX Pin: ", rx_pin_); + LOG_PIN(" TX Pin: ", this->tx_pin_); + LOG_PIN(" RX Pin: ", this->rx_pin_); + LOG_PIN(" Flow Control Pin: ", this->flow_control_pin_); if (this->rx_pin_ != nullptr) { - ESP_LOGCONFIG(TAG, " RX Buffer Size: %u", this->rx_buffer_size_); + ESP_LOGCONFIG(TAG, + " RX Buffer Size: %u\n" + " RX Full Threshold: %u\n" + " RX Timeout: %u", + this->rx_buffer_size_, this->rx_full_threshold_, this->rx_timeout_); } ESP_LOGCONFIG(TAG, " Baud Rate: %" PRIu32 " baud\n" @@ -165,10 +231,36 @@ void IDFUARTComponent::dump_config() { this->check_logger_conflict(); } +void IDFUARTComponent::set_rx_full_threshold(size_t rx_full_threshold) { + if (this->is_ready()) { + esp_err_t err = uart_set_rx_full_threshold(this->uart_num_, rx_full_threshold); + if (err != ESP_OK) { + ESP_LOGW(TAG, "uart_set_rx_full_threshold failed: %s", esp_err_to_name(err)); + return; + } + } + this->rx_full_threshold_ = rx_full_threshold; +} + +void IDFUARTComponent::set_rx_timeout(size_t rx_timeout) { + if (this->is_ready()) { + esp_err_t err = uart_set_rx_timeout(this->uart_num_, rx_timeout); + if (err != ESP_OK) { + ESP_LOGW(TAG, "uart_set_rx_timeout failed: %s", esp_err_to_name(err)); + return; + } + } + this->rx_timeout_ = rx_timeout; +} + void IDFUARTComponent::write_array(const uint8_t *data, size_t len) { xSemaphoreTake(this->lock_, portMAX_DELAY); - uart_write_bytes(this->uart_num_, data, len); + int32_t write_len = uart_write_bytes(this->uart_num_, data, len); xSemaphoreGive(this->lock_); + if (write_len != (int32_t) len) { + ESP_LOGW(TAG, "uart_write_bytes failed: %d != %zu", write_len, len); + this->mark_failed(); + } #ifdef USE_UART_DEBUGGER for (size_t i = 0; i < len; i++) { this->debug_callback_.call(UART_DIRECTION_TX, data[i]); @@ -197,6 +289,7 @@ bool IDFUARTComponent::peek_byte(uint8_t *data) { bool IDFUARTComponent::read_array(uint8_t *data, size_t len) { size_t length_to_read = len; + int32_t read_len = 0; if (!this->check_read_timeout_(len)) return false; xSemaphoreTake(this->lock_, portMAX_DELAY); @@ -207,25 +300,31 @@ bool IDFUARTComponent::read_array(uint8_t *data, size_t len) { this->has_peek_ = false; } if (length_to_read > 0) - uart_read_bytes(this->uart_num_, data, length_to_read, 20 / portTICK_PERIOD_MS); + read_len = uart_read_bytes(this->uart_num_, data, length_to_read, 20 / portTICK_PERIOD_MS); xSemaphoreGive(this->lock_); #ifdef USE_UART_DEBUGGER for (size_t i = 0; i < len; i++) { this->debug_callback_.call(UART_DIRECTION_RX, data[i]); } #endif - return true; + return read_len == (int32_t) length_to_read; } int IDFUARTComponent::available() { - size_t available; + size_t available = 0; + esp_err_t err; xSemaphoreTake(this->lock_, portMAX_DELAY); - uart_get_buffered_data_len(this->uart_num_, &available); - if (this->has_peek_) - available++; + err = uart_get_buffered_data_len(this->uart_num_, &available); xSemaphoreGive(this->lock_); + if (err != ESP_OK) { + ESP_LOGW(TAG, "uart_get_buffered_data_len failed: %s", esp_err_to_name(err)); + this->mark_failed(); + } + if (this->has_peek_) { + available++; + } return available; } diff --git a/esphome/components/uart/uart_component_esp_idf.h b/esphome/components/uart/uart_component_esp_idf.h index 215641ebe2..a2ba2aa968 100644 --- a/esphome/components/uart/uart_component_esp_idf.h +++ b/esphome/components/uart/uart_component_esp_idf.h @@ -1,6 +1,6 @@ #pragma once -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include #include "esphome/core/component.h" @@ -15,6 +15,9 @@ class IDFUARTComponent : public UARTComponent, public Component { void dump_config() override; float get_setup_priority() const override { return setup_priority::BUS; } + void set_rx_full_threshold(size_t rx_full_threshold) override; + void set_rx_timeout(size_t rx_timeout) override; + void write_array(const uint8_t *data, size_t len) override; bool peek_byte(uint8_t *data) override; @@ -55,4 +58,4 @@ class IDFUARTComponent : public UARTComponent, public Component { } // namespace uart } // namespace esphome -#endif // USE_ESP_IDF +#endif // USE_ESP32 diff --git a/esphome/components/uart/uart_component_libretiny.cpp b/esphome/components/uart/uart_component_libretiny.cpp index 8a7a301cfe..1e408b169b 100644 --- a/esphome/components/uart/uart_component_libretiny.cpp +++ b/esphome/components/uart/uart_component_libretiny.cpp @@ -51,28 +51,53 @@ void LibreTinyUARTComponent::setup() { bool tx_inverted = tx_pin_ != nullptr && tx_pin_->is_inverted(); bool rx_inverted = rx_pin_ != nullptr && rx_pin_->is_inverted(); + auto shouldFallbackToSoftwareSerial = [&]() -> bool { + auto hasFlags = [](InternalGPIOPin *pin, const gpio::Flags mask) -> bool { + return pin && (pin->get_flags() & mask) != gpio::Flags::FLAG_NONE; + }; + if (hasFlags(this->tx_pin_, gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN) || + hasFlags(this->rx_pin_, gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN)) { +#if LT_ARD_HAS_SOFTSERIAL + ESP_LOGI(TAG, "Pins has flags set. Using Software Serial"); + return true; +#else + ESP_LOGW(TAG, "Pin flags are set but not supported for hardware serial. Ignoring"); +#endif + } + return false; + }; + if (false) return; #if LT_HW_UART0 - else if ((tx_pin == -1 || tx_pin == PIN_SERIAL0_TX) && (rx_pin == -1 || rx_pin == PIN_SERIAL0_RX)) { + else if ((tx_pin == -1 || tx_pin == PIN_SERIAL0_TX) && (rx_pin == -1 || rx_pin == PIN_SERIAL0_RX) && + !shouldFallbackToSoftwareSerial()) { this->serial_ = &Serial0; this->hardware_idx_ = 0; } #endif #if LT_HW_UART1 - else if ((tx_pin == -1 || tx_pin == PIN_SERIAL1_TX) && (rx_pin == -1 || rx_pin == PIN_SERIAL1_RX)) { + else if ((tx_pin == -1 || tx_pin == PIN_SERIAL1_TX) && (rx_pin == -1 || rx_pin == PIN_SERIAL1_RX) && + !shouldFallbackToSoftwareSerial()) { this->serial_ = &Serial1; this->hardware_idx_ = 1; } #endif #if LT_HW_UART2 - else if ((tx_pin == -1 || tx_pin == PIN_SERIAL2_TX) && (rx_pin == -1 || rx_pin == PIN_SERIAL2_RX)) { + else if ((tx_pin == -1 || tx_pin == PIN_SERIAL2_TX) && (rx_pin == -1 || rx_pin == PIN_SERIAL2_RX) && + !shouldFallbackToSoftwareSerial()) { this->serial_ = &Serial2; this->hardware_idx_ = 2; } #endif else { #if LT_ARD_HAS_SOFTSERIAL + if (this->rx_pin_) { + this->rx_pin_->setup(); + } + if (this->tx_pin_ && this->rx_pin_ != this->tx_pin_) { + this->tx_pin_->setup(); + } this->serial_ = new SoftwareSerial(rx_pin, tx_pin, rx_inverted || tx_inverted); #else this->serial_ = &Serial; diff --git a/esphome/components/uart/uart_component_rp2040.cpp b/esphome/components/uart/uart_component_rp2040.cpp index ae3042fb77..cd3905b5c1 100644 --- a/esphome/components/uart/uart_component_rp2040.cpp +++ b/esphome/components/uart/uart_component_rp2040.cpp @@ -52,6 +52,21 @@ uint16_t RP2040UartComponent::get_config() { } void RP2040UartComponent::setup() { + auto setup_pin_if_needed = [](InternalGPIOPin *pin) { + if (!pin) { + return; + } + const auto mask = gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN; + if ((pin->get_flags() & mask) != gpio::Flags::FLAG_NONE) { + pin->setup(); + } + }; + + setup_pin_if_needed(this->rx_pin_); + if (this->rx_pin_ != this->tx_pin_) { + setup_pin_if_needed(this->tx_pin_); + } + uint16_t config = get_config(); constexpr uint32_t valid_tx_uart_0 = __bitset({0, 12, 16, 28}); diff --git a/esphome/components/udp/__init__.py b/esphome/components/udp/__init__.py index 6b1e4f8ed8..69abf4b989 100644 --- a/esphome/components/udp/__init__.py +++ b/esphome/components/udp/__init__.py @@ -12,7 +12,7 @@ from esphome.components.packet_transport import ( ) import esphome.config_validation as cv from esphome.const import CONF_DATA, CONF_ID, CONF_PORT, CONF_TRIGGER_ID -from esphome.core import Lambda +from esphome.core import ID, Lambda from esphome.cpp_generator import ExpressionStatement, MockObj CODEOWNERS = ["@clydebarrow"] @@ -158,5 +158,8 @@ async def udp_write_to_code(config, action_id, template_arg, args): templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8)) cg.add(var.set_data_template(templ)) else: - cg.add(var.set_data_static(data)) + # Generate static array in flash to avoid RAM copy + arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8) + arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data)) + cg.add(var.set_data_static(arr, len(data))) return var diff --git a/esphome/components/udp/automation.h b/esphome/components/udp/automation.h index f75e6d35bf..b66c2a9892 100644 --- a/esphome/components/udp/automation.h +++ b/esphome/components/udp/automation.h @@ -11,28 +11,33 @@ namespace udp { template class UDPWriteAction : public Action, public Parented { public: - void set_data_template(std::function(Ts...)> func) { - this->data_func_ = func; - this->static_ = false; - } - void set_data_static(const std::vector &data) { - this->data_static_ = data; - this->static_ = true; + void set_data_template(std::vector (*func)(Ts...)) { + this->data_.func = func; + this->len_ = -1; // Sentinel value indicates template mode } - void play(Ts... x) override { - if (this->static_) { - this->parent_->send_packet(this->data_static_); + void set_data_static(const uint8_t *data, size_t len) { + this->data_.data = data; + this->len_ = len; // Length >= 0 indicates static mode + } + + void play(const Ts &...x) override { + if (this->len_ >= 0) { + // Static mode: pass pointer directly to send_packet(const uint8_t *, size_t) + this->parent_->send_packet(this->data_.data, static_cast(this->len_)); } else { - auto val = this->data_func_(x...); + // Template mode: call function and pass vector to send_packet(const std::vector &) + auto val = this->data_.func(x...); this->parent_->send_packet(val); } } protected: - bool static_{false}; - std::function(Ts...)> data_func_{}; - std::vector data_static_{}; + ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length + union Data { + std::vector (*func)(Ts...); // Function pointer (stateless lambdas) + const uint8_t *data; // Pointer to static data in flash + } data_; }; } // namespace udp diff --git a/esphome/components/udp/udp_component.cpp b/esphome/components/udp/udp_component.cpp index 62a1189355..9105ced21e 100644 --- a/esphome/components/udp/udp_component.cpp +++ b/esphome/components/udp/udp_component.cpp @@ -21,19 +21,19 @@ void UDPComponent::setup() { if (this->should_broadcast_) { this->broadcast_socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); if (this->broadcast_socket_ == nullptr) { + this->status_set_error(LOG_STR("Could not create socket")); this->mark_failed(); - this->status_set_error("Could not create socket"); return; } int enable = 1; auto err = this->broadcast_socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)); if (err != 0) { - this->status_set_warning("Socket unable to set reuseaddr"); + this->status_set_warning(LOG_STR("Socket unable to set reuseaddr")); // we can still continue } err = this->broadcast_socket_->setsockopt(SOL_SOCKET, SO_BROADCAST, &enable, sizeof(int)); if (err != 0) { - this->status_set_warning("Socket unable to set broadcast"); + this->status_set_warning(LOG_STR("Socket unable to set broadcast")); } } // create listening socket if we either want to subscribe to providers, or need to listen @@ -41,21 +41,21 @@ void UDPComponent::setup() { if (this->should_listen_) { this->listen_socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); if (this->listen_socket_ == nullptr) { + this->status_set_error(LOG_STR("Could not create socket")); this->mark_failed(); - this->status_set_error("Could not create socket"); return; } auto err = this->listen_socket_->setblocking(false); if (err < 0) { ESP_LOGE(TAG, "Unable to set nonblocking: errno %d", errno); + this->status_set_error(LOG_STR("Unable to set nonblocking")); this->mark_failed(); - this->status_set_error("Unable to set nonblocking"); return; } int enable = 1; err = this->listen_socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(enable)); if (err != 0) { - this->status_set_warning("Socket unable to set reuseaddr"); + this->status_set_warning(LOG_STR("Socket unable to set reuseaddr")); // we can still continue } struct sockaddr_in server {}; @@ -73,8 +73,8 @@ void UDPComponent::setup() { err = this->listen_socket_->setsockopt(IPPROTO_IP, IP_ADD_MEMBERSHIP, &imreq, sizeof(imreq)); if (err < 0) { ESP_LOGE(TAG, "Failed to set IP_ADD_MEMBERSHIP. Error %d", errno); + this->status_set_error(LOG_STR("Failed to set IP_ADD_MEMBERSHIP")); this->mark_failed(); - this->status_set_error("Failed to set IP_ADD_MEMBERSHIP"); return; } } @@ -82,8 +82,8 @@ void UDPComponent::setup() { err = this->listen_socket_->bind((struct sockaddr *) &server, sizeof(server)); if (err != 0) { ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno); + this->status_set_error(LOG_STR("Unable to bind socket")); this->mark_failed(); - this->status_set_error("Unable to bind socket"); return; } } diff --git a/esphome/components/ufire_ec/ufire_ec.cpp b/esphome/components/ufire_ec/ufire_ec.cpp index 364a133776..0a57ecc67b 100644 --- a/esphome/components/ufire_ec/ufire_ec.cpp +++ b/esphome/components/ufire_ec/ufire_ec.cpp @@ -104,10 +104,10 @@ void UFireECComponent::write_data_(uint8_t reg, float data) { void UFireECComponent::dump_config() { ESP_LOGCONFIG(TAG, "uFire-EC"); LOG_I2C_DEVICE(this) - LOG_UPDATE_INTERVAL(this) - LOG_SENSOR(" ", "EC Sensor", this->ec_sensor_) - LOG_SENSOR(" ", "Temperature Sensor", this->temperature_sensor_) - LOG_SENSOR(" ", "Temperature Sensor external", this->temperature_sensor_external_) + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "EC Sensor", this->ec_sensor_); + LOG_SENSOR(" ", "Temperature Sensor", this->temperature_sensor_); + LOG_SENSOR(" ", "Temperature Sensor external", this->temperature_sensor_external_); ESP_LOGCONFIG(TAG, " Temperature Compensation: %f\n" " Temperature Coefficient: %f", diff --git a/esphome/components/ufire_ec/ufire_ec.h b/esphome/components/ufire_ec/ufire_ec.h index 3d436555a2..bfbed1b43e 100644 --- a/esphome/components/ufire_ec/ufire_ec.h +++ b/esphome/components/ufire_ec/ufire_ec.h @@ -65,7 +65,7 @@ template class UFireECCalibrateProbeAction : public Actionparent_->calibrate_probe(this->solution_.value(x...), this->temperature_.value(x...)); } @@ -77,7 +77,7 @@ template class UFireECResetAction : public Action { public: UFireECResetAction(UFireECComponent *parent) : parent_(parent) {} - void play(Ts... x) override { this->parent_->reset_board(); } + void play(const Ts &...x) override { this->parent_->reset_board(); } protected: UFireECComponent *parent_; diff --git a/esphome/components/ufire_ise/ufire_ise.cpp b/esphome/components/ufire_ise/ufire_ise.cpp index 503d993fb7..486a506391 100644 --- a/esphome/components/ufire_ise/ufire_ise.cpp +++ b/esphome/components/ufire_ise/ufire_ise.cpp @@ -141,10 +141,10 @@ void UFireISEComponent::write_data_(uint8_t reg, float data) { void UFireISEComponent::dump_config() { ESP_LOGCONFIG(TAG, "uFire-ISE"); LOG_I2C_DEVICE(this) - LOG_UPDATE_INTERVAL(this) - LOG_SENSOR(" ", "PH Sensor", this->ph_sensor_) - LOG_SENSOR(" ", "Temperature Sensor", this->temperature_sensor_) - LOG_SENSOR(" ", "Temperature Sensor external", this->temperature_sensor_external_) + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "PH Sensor", this->ph_sensor_); + LOG_SENSOR(" ", "Temperature Sensor", this->temperature_sensor_); + LOG_SENSOR(" ", "Temperature Sensor external", this->temperature_sensor_external_); } } // namespace ufire_ise diff --git a/esphome/components/ufire_ise/ufire_ise.h b/esphome/components/ufire_ise/ufire_ise.h index 01efdcdb55..fe9a6dfb9c 100644 --- a/esphome/components/ufire_ise/ufire_ise.h +++ b/esphome/components/ufire_ise/ufire_ise.h @@ -64,7 +64,7 @@ template class UFireISECalibrateProbeLowAction : public Actionparent_->calibrate_probe_low(this->solution_.value(x...)); } + void play(const Ts &...x) override { this->parent_->calibrate_probe_low(this->solution_.value(x...)); } protected: UFireISEComponent *parent_; @@ -75,7 +75,7 @@ template class UFireISECalibrateProbeHighAction : public Action< UFireISECalibrateProbeHighAction(UFireISEComponent *parent) : parent_(parent) {} TEMPLATABLE_VALUE(float, solution) - void play(Ts... x) override { this->parent_->calibrate_probe_high(this->solution_.value(x...)); } + void play(const Ts &...x) override { this->parent_->calibrate_probe_high(this->solution_.value(x...)); } protected: UFireISEComponent *parent_; @@ -85,7 +85,7 @@ template class UFireISEResetAction : public Action { public: UFireISEResetAction(UFireISEComponent *parent) : parent_(parent) {} - void play(Ts... x) override { this->parent_->reset_board(); } + void play(const Ts &...x) override { this->parent_->reset_board(); } protected: UFireISEComponent *parent_; diff --git a/esphome/components/update/__init__.py b/esphome/components/update/__init__.py index 50d8aaf139..7a381c85a8 100644 --- a/esphome/components/update/__init__.py +++ b/esphome/components/update/__init__.py @@ -14,7 +14,7 @@ from esphome.const import ( DEVICE_CLASS_FIRMWARE, ENTITY_CATEGORY_CONFIG, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass @@ -84,11 +84,6 @@ def update_schema( return _UPDATE_SCHEMA.extend(schema) -# Remove before 2025.11.0 -UPDATE_SCHEMA = update_schema() -UPDATE_SCHEMA.add_extra(cv.deprecated_schema_constant("update")) - - async def setup_update_core_(var, config): await setup_entity(var, config, "update") @@ -124,7 +119,7 @@ async def new_update(config): return var -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(update_ns.using) diff --git a/esphome/components/update/automation.h b/esphome/components/update/automation.h index df50f86a0c..8563b855fe 100644 --- a/esphome/components/update/automation.h +++ b/esphome/components/update/automation.h @@ -11,12 +11,12 @@ template class PerformAction : public Action, public Pare TEMPLATABLE_VALUE(bool, force) public: - void play(Ts... x) override { this->parent_->perform(this->force_.value(x...)); } + void play(const Ts &...x) override { this->parent_->perform(this->force_.value(x...)); } }; template class IsAvailableCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->state == UPDATE_STATE_AVAILABLE; } + bool check(const Ts &...x) override { return this->parent_->state == UPDATE_STATE_AVAILABLE; } }; } // namespace update diff --git a/esphome/components/update/update_entity.cpp b/esphome/components/update/update_entity.cpp index ce97fb1b77..567fc9fc8e 100644 --- a/esphome/components/update/update_entity.cpp +++ b/esphome/components/update/update_entity.cpp @@ -1,5 +1,6 @@ #include "update_entity.h" - +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" namespace esphome { @@ -32,6 +33,9 @@ void UpdateEntity::publish_state() { this->set_has_state(true); this->state_callback_.call(); +#if defined(USE_UPDATE) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_update(this); +#endif } } // namespace update diff --git a/esphome/components/uponor_smatrix/__init__.py b/esphome/components/uponor_smatrix/__init__.py index d4102d1026..9588b0df7f 100644 --- a/esphome/components/uponor_smatrix/__init__.py +++ b/esphome/components/uponor_smatrix/__init__.py @@ -17,6 +17,12 @@ UponorSmatrixDevice = uponor_smatrix_ns.class_( "UponorSmatrixDevice", cg.Parented.template(UponorSmatrixComponent) ) + +device_address = cv.All( + cv.hex_int, + cv.Range(min=0x1000000, max=0xFFFFFFFF, msg="Expected a 32 bit device address"), +) + CONF_UPONOR_SMATRIX_ID = "uponor_smatrix_id" CONF_TIME_DEVICE_ADDRESS = "time_device_address" @@ -24,9 +30,12 @@ CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(UponorSmatrixComponent), - cv.Optional(CONF_ADDRESS): cv.hex_uint16_t, + cv.Optional(CONF_ADDRESS): cv.invalid( + f"The '{CONF_ADDRESS}' option has been removed. " + "Use full 32 bit addresses in the device definitions instead." + ), cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock), - cv.Optional(CONF_TIME_DEVICE_ADDRESS): cv.hex_uint16_t, + cv.Optional(CONF_TIME_DEVICE_ADDRESS): device_address, } ) .extend(cv.COMPONENT_SCHEMA) @@ -47,7 +56,7 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( UPONOR_SMATRIX_DEVICE_SCHEMA = cv.Schema( { cv.GenerateID(CONF_UPONOR_SMATRIX_ID): cv.use_id(UponorSmatrixComponent), - cv.Required(CONF_ADDRESS): cv.hex_uint16_t, + cv.Required(CONF_ADDRESS): device_address, } ) @@ -58,17 +67,15 @@ async def to_code(config): await cg.register_component(var, config) await uart.register_uart_device(var, config) - if address := config.get(CONF_ADDRESS): - cg.add(var.set_system_address(address)) if time_id := config.get(CONF_TIME_ID): time_ = await cg.get_variable(time_id) cg.add(var.set_time_id(time_)) - if time_device_address := config.get(CONF_TIME_DEVICE_ADDRESS): - cg.add(var.set_time_device_address(time_device_address)) + if time_device_address := config.get(CONF_TIME_DEVICE_ADDRESS): + cg.add(var.set_time_device_address(time_device_address)) async def register_uponor_smatrix_device(var, config): parent = await cg.get_variable(config[CONF_UPONOR_SMATRIX_ID]) cg.add(var.set_parent(parent)) - cg.add(var.set_device_address(config[CONF_ADDRESS])) + cg.add(var.set_address(config[CONF_ADDRESS])) cg.add(parent.register_device(var)) diff --git a/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp b/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp index d7e672d8cf..4256b01c4e 100644 --- a/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp +++ b/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp @@ -10,7 +10,7 @@ static const char *const TAG = "uponor_smatrix.climate"; void UponorSmatrixClimate::dump_config() { LOG_CLIMATE("", "Uponor Smatrix Climate", this); - ESP_LOGCONFIG(TAG, " Device address: 0x%04X", this->address_); + ESP_LOGCONFIG(TAG, " Device address: 0x%08X", this->address_); } void UponorSmatrixClimate::loop() { @@ -30,10 +30,9 @@ void UponorSmatrixClimate::loop() { climate::ClimateTraits UponorSmatrixClimate::traits() { auto traits = climate::ClimateTraits(); - traits.set_supports_current_temperature(true); - traits.set_supports_current_humidity(true); + traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE | climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY | + climate::CLIMATE_SUPPORTS_ACTION); traits.set_supported_modes({climate::CLIMATE_MODE_HEAT}); - traits.set_supports_action(true); traits.set_supported_presets({climate::CLIMATE_PRESET_ECO}); traits.set_visual_min_temperature(this->min_temperature_); traits.set_visual_max_temperature(this->max_temperature_); @@ -58,7 +57,7 @@ void UponorSmatrixClimate::control(const climate::ClimateCall &call) { } void UponorSmatrixClimate::on_device_data(const UponorSmatrixData *data, size_t data_len) { - for (int i = 0; i < data_len; i++) { + for (size_t i = 0; i < data_len; i++) { switch (data[i].id) { case UPONOR_ID_TARGET_TEMP_MIN: this->min_temperature_ = raw_to_celsius(data[i].value); diff --git a/esphome/components/uponor_smatrix/sensor/uponor_smatrix_sensor.cpp b/esphome/components/uponor_smatrix/sensor/uponor_smatrix_sensor.cpp index 452660dc14..7ee12edcdb 100644 --- a/esphome/components/uponor_smatrix/sensor/uponor_smatrix_sensor.cpp +++ b/esphome/components/uponor_smatrix/sensor/uponor_smatrix_sensor.cpp @@ -9,7 +9,7 @@ static const char *const TAG = "uponor_smatrix.sensor"; void UponorSmatrixSensor::dump_config() { ESP_LOGCONFIG(TAG, "Uponor Smatrix Sensor\n" - " Device address: 0x%04X", + " Device address: 0x%08X", this->address_); LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); LOG_SENSOR(" ", "External Temperature", this->external_temperature_sensor_); @@ -18,7 +18,7 @@ void UponorSmatrixSensor::dump_config() { } void UponorSmatrixSensor::on_device_data(const UponorSmatrixData *data, size_t data_len) { - for (int i = 0; i < data_len; i++) { + for (size_t i = 0; i < data_len; i++) { switch (data[i].id) { case UPONOR_ID_ROOM_TEMP: if (this->temperature_sensor_ != nullptr) diff --git a/esphome/components/uponor_smatrix/uponor_smatrix.cpp b/esphome/components/uponor_smatrix/uponor_smatrix.cpp index a0017518bf..221f07c80e 100644 --- a/esphome/components/uponor_smatrix/uponor_smatrix.cpp +++ b/esphome/components/uponor_smatrix/uponor_smatrix.cpp @@ -18,11 +18,10 @@ void UponorSmatrixComponent::setup() { void UponorSmatrixComponent::dump_config() { ESP_LOGCONFIG(TAG, "Uponor Smatrix"); - ESP_LOGCONFIG(TAG, " System address: 0x%04X", this->address_); #ifdef USE_TIME if (this->time_id_ != nullptr) { ESP_LOGCONFIG(TAG, " Time synchronization: YES"); - ESP_LOGCONFIG(TAG, " Time master device address: 0x%04X", this->time_device_address_); + ESP_LOGCONFIG(TAG, " Time master device address: 0x%08X", this->time_device_address_); } #endif @@ -31,7 +30,7 @@ void UponorSmatrixComponent::dump_config() { if (!this->unknown_devices_.empty()) { ESP_LOGCONFIG(TAG, " Detected unknown device addresses:"); for (auto device_address : this->unknown_devices_) { - ESP_LOGCONFIG(TAG, " 0x%04X", device_address); + ESP_LOGCONFIG(TAG, " 0x%08X", device_address); } } } @@ -89,8 +88,7 @@ bool UponorSmatrixComponent::parse_byte_(uint8_t byte) { return false; } - uint16_t system_address = encode_uint16(packet[0], packet[1]); - uint16_t device_address = encode_uint16(packet[2], packet[3]); + uint32_t device_address = encode_uint32(packet[0], packet[1], packet[2], packet[3]); uint16_t crc = encode_uint16(packet[packet_len - 1], packet[packet_len - 2]); uint16_t computed_crc = crc16(packet, packet_len - 2); @@ -99,30 +97,20 @@ bool UponorSmatrixComponent::parse_byte_(uint8_t byte) { return false; } - ESP_LOGV(TAG, "Received packet: sys=%04X, dev=%04X, data=%s, crc=%04X", system_address, device_address, + ESP_LOGV(TAG, "Received packet: addr=%08X, data=%s, crc=%04X", device_address, format_hex(&packet[4], packet_len - 6).c_str(), crc); - // Detect or check system address - if (this->address_ == 0) { - ESP_LOGI(TAG, "Using detected system address 0x%04X", system_address); - this->address_ = system_address; - } else if (this->address_ != system_address) { - // This should never happen except if the system address was set or detected incorrectly, so warn the user. - ESP_LOGW(TAG, "Received packet from unknown system address 0x%04X", system_address); - return true; - } - // Handle packet size_t data_len = (packet_len - 6) / 3; if (data_len == 0) { if (packet[4] == UPONOR_ID_REQUEST) - ESP_LOGVV(TAG, "Ignoring request packet for device 0x%04X", device_address); + ESP_LOGVV(TAG, "Ignoring request packet for device 0x%08X", device_address); return true; } // Decode packet payload data for easy access UponorSmatrixData data[data_len]; - for (int i = 0; i < data_len; i++) { + for (size_t i = 0; i < data_len; i++) { data[i].id = packet[(i * 3) + 4]; data[i].value = encode_uint16(packet[(i * 3) + 5], packet[(i * 3) + 6]); } @@ -135,13 +123,13 @@ bool UponorSmatrixComponent::parse_byte_(uint8_t byte) { // thermostat sending both room temperature and time information. bool found_temperature = false; bool found_time = false; - for (int i = 0; i < data_len; i++) { + for (size_t i = 0; i < data_len; i++) { if (data[i].id == UPONOR_ID_ROOM_TEMP) found_temperature = true; if (data[i].id == UPONOR_ID_DATETIME1) found_time = true; if (found_temperature && found_time) { - ESP_LOGI(TAG, "Using detected time device address 0x%04X", device_address); + ESP_LOGI(TAG, "Using detected time device address 0x%08X", device_address); this->time_device_address_ = device_address; break; } @@ -160,7 +148,7 @@ bool UponorSmatrixComponent::parse_byte_(uint8_t byte) { // Log unknown device addresses if (!found && !this->unknown_devices_.count(device_address)) { - ESP_LOGI(TAG, "Received packet for unknown device address 0x%04X ", device_address); + ESP_LOGI(TAG, "Received packet for unknown device address 0x%08X ", device_address); this->unknown_devices_.insert(device_address); } @@ -168,20 +156,20 @@ bool UponorSmatrixComponent::parse_byte_(uint8_t byte) { return true; } -bool UponorSmatrixComponent::send(uint16_t device_address, const UponorSmatrixData *data, size_t data_len) { - if (this->address_ == 0 || device_address == 0 || data == nullptr || data_len == 0) +bool UponorSmatrixComponent::send(uint32_t device_address, const UponorSmatrixData *data, size_t data_len) { + if (device_address == 0 || data == nullptr || data_len == 0) return false; // Assemble packet for send queue. All fields are big-endian except for the little-endian checksum. std::vector packet; packet.reserve(6 + 3 * data_len); - packet.push_back(this->address_ >> 8); - packet.push_back(this->address_ >> 0); + packet.push_back(device_address >> 24); + packet.push_back(device_address >> 16); packet.push_back(device_address >> 8); packet.push_back(device_address >> 0); - for (int i = 0; i < data_len; i++) { + for (size_t i = 0; i < data_len; i++) { packet.push_back(data[i].id); packet.push_back(data[i].value >> 8); packet.push_back(data[i].value >> 0); diff --git a/esphome/components/uponor_smatrix/uponor_smatrix.h b/esphome/components/uponor_smatrix/uponor_smatrix.h index e3e19a12fc..bd760f0d77 100644 --- a/esphome/components/uponor_smatrix/uponor_smatrix.h +++ b/esphome/components/uponor_smatrix/uponor_smatrix.h @@ -71,23 +71,21 @@ class UponorSmatrixComponent : public uart::UARTDevice, public Component { void dump_config() override; void loop() override; - void set_system_address(uint16_t address) { this->address_ = address; } void register_device(UponorSmatrixDevice *device) { this->devices_.push_back(device); } - bool send(uint16_t device_address, const UponorSmatrixData *data, size_t data_len); + bool send(uint32_t device_address, const UponorSmatrixData *data, size_t data_len); #ifdef USE_TIME void set_time_id(time::RealTimeClock *time_id) { this->time_id_ = time_id; } - void set_time_device_address(uint16_t address) { this->time_device_address_ = address; } + void set_time_device_address(uint32_t address) { this->time_device_address_ = address; } void send_time() { this->send_time_requested_ = true; } #endif protected: bool parse_byte_(uint8_t byte); - uint16_t address_; std::vector devices_; - std::set unknown_devices_; + std::set unknown_devices_; std::vector rx_buffer_; std::queue> tx_queue_; @@ -96,7 +94,7 @@ class UponorSmatrixComponent : public uart::UARTDevice, public Component { #ifdef USE_TIME time::RealTimeClock *time_id_{nullptr}; - uint16_t time_device_address_; + uint32_t time_device_address_; bool send_time_requested_; bool do_send_time_(); #endif @@ -104,7 +102,7 @@ class UponorSmatrixComponent : public uart::UARTDevice, public Component { class UponorSmatrixDevice : public Parented { public: - void set_device_address(uint16_t address) { this->address_ = address; } + void set_address(uint32_t address) { this->address_ = address; } virtual void on_device_data(const UponorSmatrixData *data, size_t data_len) = 0; bool send(const UponorSmatrixData *data, size_t data_len) { @@ -113,7 +111,7 @@ class UponorSmatrixDevice : public Parented { protected: friend UponorSmatrixComponent; - uint16_t address_; + uint32_t address_; }; inline float raw_to_celsius(uint16_t raw) { diff --git a/esphome/components/usb_host/__init__.py b/esphome/components/usb_host/__init__.py index 0fe3310127..cccabcf646 100644 --- a/esphome/components/usb_host/__init__.py +++ b/esphome/components/usb_host/__init__.py @@ -1,5 +1,7 @@ import esphome.codegen as cg +from esphome.components import socket from esphome.components.esp32 import ( + VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3, add_idf_sdkconfig_option, @@ -8,8 +10,9 @@ from esphome.components.esp32 import ( import esphome.config_validation as cv from esphome.const import CONF_DEVICES, CONF_ID from esphome.cpp_types import Component +from esphome.types import ConfigType -AUTO_LOAD = ["bytebuffer"] +AUTO_LOAD = ["bytebuffer", "socket"] CODEOWNERS = ["@clydebarrow"] DEPENDENCIES = ["esp32"] usb_host_ns = cg.esphome_ns.namespace("usb_host") @@ -19,6 +22,7 @@ USBClient = usb_host_ns.class_("USBClient", Component) CONF_VID = "vid" CONF_PID = "pid" CONF_ENABLE_HUBS = "enable_hubs" +CONF_MAX_TRANSFER_REQUESTS = "max_transfer_requests" def usb_device_schema(cls=USBClient, vid: int = None, pid: [int] = None) -> cv.Schema: @@ -43,11 +47,14 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(USBHost), cv.Optional(CONF_ENABLE_HUBS, default=False): cv.boolean, + cv.Optional(CONF_MAX_TRANSFER_REQUESTS, default=16): cv.int_range( + min=1, max=32 + ), cv.Optional(CONF_DEVICES): cv.ensure_list(usb_device_schema()), } ), cv.only_with_esp_idf, - only_on_variant(supported=[VARIANT_ESP32S2, VARIANT_ESP32S3]), + only_on_variant(supported=[VARIANT_ESP32S2, VARIANT_ESP32S3, VARIANT_ESP32P4]), ) @@ -57,10 +64,19 @@ async def register_usb_client(config): return var -async def to_code(config): +async def to_code(config: ConfigType) -> None: add_idf_sdkconfig_option("CONFIG_USB_HOST_CONTROL_TRANSFER_MAX_SIZE", 1024) if config.get(CONF_ENABLE_HUBS): add_idf_sdkconfig_option("CONFIG_USB_HOST_HUBS_SUPPORTED", True) + + max_requests = config[CONF_MAX_TRANSFER_REQUESTS] + cg.add_define("USB_HOST_MAX_REQUESTS", max_requests) + + # USB uses the socket wake_loop_threadsafe() mechanism to wake the main loop from USB task + # This enables low-latency (~12μs) USB event processing instead of waiting for + # select() timeout (0-16ms). The wake socket is shared across all components. + socket.require_wake_loop_threadsafe() + var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) for device in config.get(CONF_DEVICES) or (): diff --git a/esphome/components/usb_host/usb_host.h b/esphome/components/usb_host/usb_host.h index c5466eb1f0..31bdde2df8 100644 --- a/esphome/components/usb_host/usb_host.h +++ b/esphome/components/usb_host/usb_host.h @@ -1,18 +1,48 @@ #pragma once // Should not be needed, but it's required to pass CI clang-tidy checks -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) +#include "esphome/core/defines.h" #include "esphome/core/component.h" #include #include "usb/usb_host.h" - -#include +#include +#include +#include "esphome/core/lock_free_queue.h" +#include "esphome/core/event_pool.h" +#include namespace esphome { namespace usb_host { +// THREADING MODEL: +// This component uses a dedicated USB task for event processing to prevent data loss. +// - USB Task (high priority): Handles USB events, executes transfer callbacks, releases transfer slots +// - Main Loop Task: Initiates transfers, processes device connect/disconnect events +// +// Thread-safe communication: +// - Lock-free queues for USB task -> main loop events (SPSC pattern) +// - Lock-free TransferRequest pool using atomic bitmask (MCMP pattern - multi-consumer, multi-producer) +// +// TransferRequest pool access pattern: +// - get_trq_() [allocate]: Called from BOTH USB task and main loop threads +// * USB task: via USB UART input callbacks that restart transfers immediately +// * Main loop: for output transfers and flow-controlled input restarts +// - release_trq() [deallocate]: Called from BOTH USB task and main loop threads +// * USB task: immediately after transfer callback completes (critical for preventing slot exhaustion) +// * Main loop: when transfer submission fails +// +// The multi-threaded allocation/deallocation is intentional for performance: +// - USB task can immediately restart input transfers and release slots without context switching +// - Main loop controls backpressure by deciding when to restart after consuming data +// The atomic bitmask ensures thread-safe allocation/deallocation without mutex blocking. + static const char *const TAG = "usb_host"; +// Forward declarations +struct TransferRequest; +class USBClient; + // constants for setup packet type static const uint8_t USB_RECIP_DEVICE = 0; static const uint8_t USB_RECIP_INTERFACE = 1; @@ -25,7 +55,21 @@ static const uint8_t USB_DIR_IN = 1 << 7; static const uint8_t USB_DIR_OUT = 0; static const size_t SETUP_PACKET_SIZE = 8; -static const size_t MAX_REQUESTS = 16; // maximum number of outstanding requests possible. +static constexpr size_t MAX_REQUESTS = USB_HOST_MAX_REQUESTS; // maximum number of outstanding requests possible. +static_assert(MAX_REQUESTS >= 1 && MAX_REQUESTS <= 32, "MAX_REQUESTS must be between 1 and 32"); + +// Select appropriate bitmask type for tracking allocation of TransferRequest slots. +// The bitmask must have at least as many bits as MAX_REQUESTS, so: +// - Use uint16_t for up to 16 requests (MAX_REQUESTS <= 16) +// - Use uint32_t for 17-32 requests (MAX_REQUESTS > 16) +// This is tied to the static_assert above, which enforces MAX_REQUESTS is between 1 and 32. +// If MAX_REQUESTS is increased above 32, this logic and the static_assert must be updated. +using trq_bitmask_t = std::conditional<(MAX_REQUESTS <= 16), uint16_t, uint32_t>::type; +static constexpr trq_bitmask_t ALL_REQUESTS_IN_USE = MAX_REQUESTS == 32 ? ~0 : (1 << MAX_REQUESTS) - 1; + +static constexpr size_t USB_EVENT_QUEUE_SIZE = 32; // Size of event queue between USB task and main loop +static constexpr size_t USB_TASK_STACK_SIZE = 4096; // Stack size for USB task (same as ESP-IDF USB examples) +static constexpr UBaseType_t USB_TASK_PRIORITY = 5; // Higher priority than main loop (tskIDLE_PRIORITY + 5) // used to report a transfer status struct TransferStatus { @@ -49,6 +93,26 @@ struct TransferRequest { USBClient *client; }; +enum EventType : uint8_t { + EVENT_DEVICE_NEW, + EVENT_DEVICE_GONE, +}; + +struct UsbEvent { + EventType type; + union { + struct { + uint8_t address; + } device_new; + struct { + usb_device_handle_t handle; + } device_gone; + } data; + + // Required for EventPool - no cleanup needed for POD types + void release() {} +}; + // callback function type. enum ClientState { @@ -63,33 +127,40 @@ class USBClient : public Component { friend class USBHost; public: - USBClient(uint16_t vid, uint16_t pid) : vid_(vid), pid_(pid) { init_pool(); } - - void init_pool() { - this->trq_pool_.clear(); - for (size_t i = 0; i != MAX_REQUESTS; i++) - this->trq_pool_.push_back(&this->requests_[i]); - } + USBClient(uint16_t vid, uint16_t pid) : vid_(vid), pid_(pid), trq_in_use_(0) {} void setup() override; void loop() override; // setup must happen after the host bus has been setup float get_setup_priority() const override { return setup_priority::IO; } void on_opened(uint8_t addr); void on_removed(usb_device_handle_t handle); - void control_transfer_callback(const usb_transfer_t *xfer) const; - void transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length); - void transfer_out(uint8_t ep_address, const transfer_cb_t &callback, const uint8_t *data, uint16_t length); + bool transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length); + bool transfer_out(uint8_t ep_address, const transfer_cb_t &callback, const uint8_t *data, uint16_t length); void dump_config() override; void release_trq(TransferRequest *trq); + trq_bitmask_t get_trq_in_use() const { return trq_in_use_; } bool control_transfer(uint8_t type, uint8_t request, uint16_t value, uint16_t index, const transfer_cb_t &callback, const std::vector &data = {}); + // Lock-free event queue and pool for USB task to main loop communication + // Must be public for access from static callbacks + LockFreeQueue event_queue; + EventPool event_pool; + protected: - bool register_(); - TransferRequest *get_trq_(); + TransferRequest *get_trq_(); // Lock-free allocation using atomic bitmask (multi-consumer safe) virtual void disconnect(); virtual void on_connected() {} - virtual void on_disconnected() { this->init_pool(); } + virtual void on_disconnected() { + // Reset all requests to available (all bits to 0) + this->trq_in_use_.store(0); + } + + // USB task management + static void usb_task_fn(void *arg); + [[noreturn]] void usb_task_loop() const; + + TaskHandle_t usb_task_handle_{nullptr}; usb_host_client_handle_t handle_{}; usb_device_handle_t device_handle_{}; @@ -97,7 +168,11 @@ class USBClient : public Component { int state_{USB_CLIENT_INIT}; uint16_t vid_{}; uint16_t pid_{}; - std::list trq_pool_{}; + // Lock-free pool management using atomic bitmask (no dynamic allocation) + // Bit i = 1: requests_[i] is in use, Bit i = 0: requests_[i] is available + // Supports multiple concurrent consumers and producers (both threads can allocate/deallocate) + // Bitmask type automatically selected: uint16_t for <= 16 slots, uint32_t for 17-32 slots + std::atomic trq_in_use_; TransferRequest requests_[MAX_REQUESTS]{}; }; class USBHost : public Component { diff --git a/esphome/components/usb_host/usb_host_client.cpp b/esphome/components/usb_host/usb_host_client.cpp index 4c0c12fa18..fe61353b5d 100644 --- a/esphome/components/usb_host/usb_host_client.cpp +++ b/esphome/components/usb_host/usb_host_client.cpp @@ -1,12 +1,14 @@ // Should not be needed, but it's required to pass CI clang-tidy checks -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) #include "usb_host.h" #include "esphome/core/log.h" #include "esphome/core/hal.h" +#include "esphome/core/application.h" #include "esphome/components/bytebuffer/bytebuffer.h" #include #include +#include namespace esphome { namespace usb_host { @@ -139,24 +141,45 @@ static std::string get_descriptor_string(const usb_str_desc_t *desc) { return {buffer}; } +// CALLBACK CONTEXT: USB task (called from usb_host_client_handle_events in USB task) static void client_event_cb(const usb_host_client_event_msg_t *event_msg, void *ptr) { auto *client = static_cast(ptr); + + // Allocate event from pool + UsbEvent *event = client->event_pool.allocate(); + if (event == nullptr) { + // No events available - increment counter for periodic logging + client->event_queue.increment_dropped_count(); + return; + } + + // Queue events to be processed in main loop switch (event_msg->event) { case USB_HOST_CLIENT_EVENT_NEW_DEV: { - auto addr = event_msg->new_dev.address; ESP_LOGD(TAG, "New device %d", event_msg->new_dev.address); - client->on_opened(addr); + event->type = EVENT_DEVICE_NEW; + event->data.device_new.address = event_msg->new_dev.address; break; } case USB_HOST_CLIENT_EVENT_DEV_GONE: { - client->on_removed(event_msg->dev_gone.dev_hdl); - ESP_LOGD(TAG, "Device gone %d", event_msg->new_dev.address); + ESP_LOGD(TAG, "Device gone"); + event->type = EVENT_DEVICE_GONE; + event->data.device_gone.handle = event_msg->dev_gone.dev_hdl; break; } default: ESP_LOGD(TAG, "Unknown event %d", event_msg->event); - break; + client->event_pool.release(event); + return; } + + // Push to lock-free queue (always succeeds since pool size == queue size) + client->event_queue.push(event); + + // Wake main loop immediately to process USB event instead of waiting for select() timeout +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + App.wake_loop_threadsafe(); +#endif } void USBClient::setup() { usb_host_client_config_t config{.is_synchronous = false, @@ -165,17 +188,62 @@ void USBClient::setup() { auto err = usb_host_client_register(&config, &this->handle_); if (err != ESP_OK) { ESP_LOGE(TAG, "client register failed: %s", esp_err_to_name(err)); - this->status_set_error("Client register failed"); + this->status_set_error(LOG_STR("Client register failed")); this->mark_failed(); return; } - for (auto *trq : this->trq_pool_) { - usb_host_transfer_alloc(64, 0, &trq->transfer); - trq->client = this; + // Pre-allocate USB transfer buffers for all slots at startup + // This avoids any dynamic allocation during runtime + for (auto &request : this->requests_) { + usb_host_transfer_alloc(64, 0, &request.transfer); + request.client = this; // Set once, never changes + } + + // Create and start USB task + xTaskCreate(usb_task_fn, "usb_task", + USB_TASK_STACK_SIZE, // Stack size + this, // Task parameter + USB_TASK_PRIORITY, // Priority (higher than main loop) + &this->usb_task_handle_); + + if (this->usb_task_handle_ == nullptr) { + ESP_LOGE(TAG, "Failed to create USB task"); + this->mark_failed(); + } +} + +void USBClient::usb_task_fn(void *arg) { + auto *client = static_cast(arg); + client->usb_task_loop(); +} +void USBClient::usb_task_loop() const { + while (true) { + usb_host_client_handle_events(this->handle_, portMAX_DELAY); } } void USBClient::loop() { + // Process any events from the USB task + UsbEvent *event; + while ((event = this->event_queue.pop()) != nullptr) { + switch (event->type) { + case EVENT_DEVICE_NEW: + this->on_opened(event->data.device_new.address); + break; + case EVENT_DEVICE_GONE: + this->on_removed(event->data.device_gone.handle); + break; + } + // Return event to pool for reuse + this->event_pool.release(event); + } + + // Log dropped events periodically + uint16_t dropped = this->event_queue.get_and_reset_dropped_count(); + if (dropped > 0) { + ESP_LOGW(TAG, "Dropped %u USB events due to queue overflow", dropped); + } + switch (this->state_) { case USB_CLIENT_OPEN: { int err; @@ -228,7 +296,6 @@ void USBClient::loop() { } default: - usb_host_client_handle_events(this->handle_, 0); break; } } @@ -245,6 +312,7 @@ void USBClient::on_removed(usb_device_handle_t handle) { } } +// CALLBACK CONTEXT: USB task (called from usb_host_client_handle_events in USB task) static void control_callback(const usb_transfer_t *xfer) { auto *trq = static_cast(xfer->context); trq->status.error_code = xfer->status; @@ -252,23 +320,53 @@ static void control_callback(const usb_transfer_t *xfer) { trq->status.endpoint = xfer->bEndpointAddress; trq->status.data = xfer->data_buffer; trq->status.data_len = xfer->actual_num_bytes; - if (trq->callback != nullptr) + + // Execute callback in USB task context + if (trq->callback != nullptr) { trq->callback(trq->status); + } + + // Release transfer slot immediately in USB task + // The release_trq() uses thread-safe atomic operations trq->client->release_trq(trq); } +// THREAD CONTEXT: Called from both USB task and main loop threads (multi-consumer) +// - USB task: USB UART input callbacks restart transfers for immediate data reception +// - Main loop: Output transfers and flow-controlled input restarts after consuming data +// +// THREAD SAFETY: Lock-free using atomic compare-and-swap on bitmask +// This multi-threaded access is intentional for performance - USB task can +// immediately restart transfers without waiting for main loop scheduling. TransferRequest *USBClient::get_trq_() { - if (this->trq_pool_.empty()) { - ESP_LOGE(TAG, "Too many requests queued"); - return nullptr; + trq_bitmask_t mask = this->trq_in_use_.load(std::memory_order_acquire); + + // Find first available slot (bit = 0) and try to claim it atomically + // We use a while loop to allow retrying the same slot after CAS failure + for (;;) { + if (mask == ALL_REQUESTS_IN_USE) { + ESP_LOGE(TAG, "All %zu transfer slots in use", MAX_REQUESTS); + return nullptr; + } + // find the least significant zero bit + trq_bitmask_t lsb = ~mask & (mask + 1); + + // Slot i appears available, try to claim it atomically + trq_bitmask_t desired = mask | lsb; + + if (this->trq_in_use_.compare_exchange_weak(mask, desired, std::memory_order::acquire)) { + auto i = __builtin_ctz(lsb); // count trailing zeroes + // Successfully claimed slot i - prepare the TransferRequest + auto *trq = &this->requests_[i]; + trq->transfer->context = trq; + trq->transfer->device_handle = this->device_handle_; + return trq; + } + // CAS failed - another thread modified the bitmask + // mask was already updated by compare_exchange_weak with the current value } - auto *trq = this->trq_pool_.front(); - this->trq_pool_.pop_front(); - trq->client = this; - trq->transfer->context = trq; - trq->transfer->device_handle = this->device_handle_; - return trq; } + void USBClient::disconnect() { this->on_disconnected(); auto err = usb_host_device_close(this->handle_, this->device_handle_); @@ -280,6 +378,8 @@ void USBClient::disconnect() { this->device_addr_ = -1; } +// THREAD CONTEXT: Called from main loop thread only +// - Used for device configuration and control operations bool USBClient::control_transfer(uint8_t type, uint8_t request, uint16_t value, uint16_t index, const transfer_cb_t &callback, const std::vector &data) { auto *trq = this->get_trq_(); @@ -315,6 +415,7 @@ bool USBClient::control_transfer(uint8_t type, uint8_t request, uint16_t value, return true; } +// CALLBACK CONTEXT: USB task (called from usb_host_client_handle_events in USB task) static void transfer_callback(usb_transfer_t *xfer) { auto *trq = static_cast(xfer->context); trq->status.error_code = xfer->status; @@ -322,12 +423,24 @@ static void transfer_callback(usb_transfer_t *xfer) { trq->status.endpoint = xfer->bEndpointAddress; trq->status.data = xfer->data_buffer; trq->status.data_len = xfer->actual_num_bytes; - if (trq->callback != nullptr) + + // Always execute callback in USB task context + // Callbacks should be fast and non-blocking (e.g., copy data to queue) + if (trq->callback != nullptr) { trq->callback(trq->status); + } + + // Release transfer slot AFTER callback completes to prevent slot exhaustion + // This is critical for high-throughput transfers (e.g., USB UART at 115200 baud) + // The callback has finished accessing xfer->data_buffer, so it's safe to release + // The release_trq() uses thread-safe atomic operations trq->client->release_trq(trq); } /** * Performs a transfer input operation. + * THREAD CONTEXT: Called from both USB task and main loop threads! + * - USB task: USB UART input callbacks call start_input() which calls this + * - Main loop: Initial setup and other components * * @param ep_address The endpoint address. * @param callback The callback function to be called when the transfer is complete. @@ -335,11 +448,11 @@ static void transfer_callback(usb_transfer_t *xfer) { * * @throws None. */ -void USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length) { +bool USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length) { auto *trq = this->get_trq_(); if (trq == nullptr) { ESP_LOGE(TAG, "Too many requests queued"); - return; + return false; } trq->callback = callback; trq->transfer->callback = transfer_callback; @@ -349,11 +462,16 @@ void USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, u if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to submit transfer, address=%x, length=%d, err=%x", ep_address, length, err); this->release_trq(trq); + return false; } + return true; } /** * Performs an output transfer operation. + * THREAD CONTEXT: Called from main loop thread only + * - USB UART output uses defer() to ensure main loop context + * - Modbus and other components call from loop() * * @param ep_address The endpoint address. * @param callback The callback function to be called when the transfer is complete. @@ -362,11 +480,11 @@ void USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, u * * @throws None. */ -void USBClient::transfer_out(uint8_t ep_address, const transfer_cb_t &callback, const uint8_t *data, uint16_t length) { +bool USBClient::transfer_out(uint8_t ep_address, const transfer_cb_t &callback, const uint8_t *data, uint16_t length) { auto *trq = this->get_trq_(); if (trq == nullptr) { ESP_LOGE(TAG, "Too many requests queued"); - return; + return false; } trq->callback = callback; trq->transfer->callback = transfer_callback; @@ -377,7 +495,9 @@ void USBClient::transfer_out(uint8_t ep_address, const transfer_cb_t &callback, if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to submit transfer, address=%x, length=%d, err=%x", ep_address, length, err); this->release_trq(trq); + return false; } + return true; } void USBClient::dump_config() { ESP_LOGCONFIG(TAG, @@ -386,7 +506,28 @@ void USBClient::dump_config() { " Product id %04X", this->vid_, this->pid_); } -void USBClient::release_trq(TransferRequest *trq) { this->trq_pool_.push_back(trq); } +// THREAD CONTEXT: Called from both USB task and main loop threads +// - USB task: Immediately after transfer callback completes +// - Main loop: When transfer submission fails +// +// THREAD SAFETY: Lock-free using atomic AND to clear bit +// Thread-safe atomic operation allows multithreaded deallocation +void USBClient::release_trq(TransferRequest *trq) { + if (trq == nullptr) + return; + + // Calculate index from pointer arithmetic + size_t index = trq - this->requests_; + if (index >= MAX_REQUESTS) { + ESP_LOGE(TAG, "Invalid TransferRequest pointer"); + return; + } + + // Atomically clear the bit to mark slot as available + // fetch_and with inverted bitmask clears the bit atomically + trq_bitmask_t mask = ~(static_cast(1) << index); + this->trq_in_use_.fetch_and(mask, std::memory_order_release); +} } // namespace usb_host } // namespace esphome diff --git a/esphome/components/usb_host/usb_host_component.cpp b/esphome/components/usb_host/usb_host_component.cpp index 682026a9c5..1e70c289df 100644 --- a/esphome/components/usb_host/usb_host_component.cpp +++ b/esphome/components/usb_host/usb_host_component.cpp @@ -1,5 +1,5 @@ // Should not be needed, but it's required to pass CI clang-tidy checks -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) #include "usb_host.h" #include #include "esphome/core/log.h" @@ -11,7 +11,7 @@ void USBHost::setup() { usb_host_config_t config{}; if (usb_host_install(&config) != ESP_OK) { - this->status_set_error("usb_host_install failed"); + this->status_set_error(LOG_STR("usb_host_install failed")); this->mark_failed(); return; } diff --git a/esphome/components/usb_uart/__init__.py b/esphome/components/usb_uart/__init__.py index 6999b1b955..a852e1f78b 100644 --- a/esphome/components/usb_uart/__init__.py +++ b/esphome/components/usb_uart/__init__.py @@ -24,7 +24,6 @@ usb_uart_ns = cg.esphome_ns.namespace("usb_uart") USBUartComponent = usb_uart_ns.class_("USBUartComponent", Component) USBUartChannel = usb_uart_ns.class_("USBUartChannel", UARTComponent) - UARTParityOptions = usb_uart_ns.enum("UARTParityOptions") UART_PARITY_OPTIONS = { "NONE": UARTParityOptions.UART_CONFIG_PARITY_NONE, diff --git a/esphome/components/usb_uart/ch34x.cpp b/esphome/components/usb_uart/ch34x.cpp index 74e7933824..889366b579 100644 --- a/esphome/components/usb_uart/ch34x.cpp +++ b/esphome/components/usb_uart/ch34x.cpp @@ -1,4 +1,4 @@ -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) #include "usb_uart.h" #include "usb/usb_host.h" #include "esphome/core/log.h" @@ -16,12 +16,12 @@ using namespace bytebuffer; void USBUartTypeCH34X::enable_channels() { // enable the channels for (auto channel : this->channels_) { - if (!channel->initialised_) + if (!channel->initialised_.load()) continue; usb_host::transfer_cb_t callback = [=](const usb_host::TransferStatus &status) { if (!status.success) { ESP_LOGE(TAG, "Control transfer failed, status=%s", esp_err_to_name(status.error_code)); - channel->initialised_ = false; + channel->initialised_.store(false); } }; @@ -48,7 +48,7 @@ void USBUartTypeCH34X::enable_channels() { auto factor = static_cast(clk / baud_rate); if (factor == 0 || factor == 0xFF) { ESP_LOGE(TAG, "Invalid baud rate %" PRIu32, baud_rate); - channel->initialised_ = false; + channel->initialised_.store(false); continue; } if ((clk / factor - baud_rate) > (baud_rate - clk / (factor + 1))) @@ -72,6 +72,7 @@ void USBUartTypeCH34X::enable_channels() { if (channel->index_ >= 2) cmd += 0xE; this->control_transfer(USB_VENDOR_DEV | usb_host::USB_DIR_OUT, cmd, value, (factor << 8) | divisor, callback); + this->control_transfer(USB_VENDOR_DEV | usb_host::USB_DIR_OUT, cmd + 3, 0x80, 0, callback); } USBUartTypeCdcAcm::enable_channels(); } diff --git a/esphome/components/usb_uart/cp210x.cpp b/esphome/components/usb_uart/cp210x.cpp index f7d60c307a..5fec0bed02 100644 --- a/esphome/components/usb_uart/cp210x.cpp +++ b/esphome/components/usb_uart/cp210x.cpp @@ -1,4 +1,4 @@ -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) #include "usb_uart.h" #include "usb/usb_host.h" #include "esphome/core/log.h" @@ -100,12 +100,12 @@ std::vector USBUartTypeCP210X::parse_descriptors(usb_device_handle_t dev void USBUartTypeCP210X::enable_channels() { // enable the channels for (auto channel : this->channels_) { - if (!channel->initialised_) + if (!channel->initialised_.load()) continue; usb_host::transfer_cb_t callback = [=](const usb_host::TransferStatus &status) { if (!status.success) { ESP_LOGE(TAG, "Control transfer failed, status=%s", esp_err_to_name(status.error_code)); - channel->initialised_ = false; + channel->initialised_.store(false); } }; this->control_transfer(USB_VENDOR_IFC | usb_host::USB_DIR_OUT, IFC_ENABLE, 1, channel->index_, callback); diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index 934306f480..6720c1e690 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -1,5 +1,5 @@ // Should not be needed, but it's required to pass CI clang-tidy checks -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) #include "usb_uart.h" #include "esphome/core/log.h" #include "esphome/components/uart/uart_debugger.h" @@ -130,7 +130,7 @@ size_t RingBuffer::pop(uint8_t *data, size_t len) { return len; } void USBUartChannel::write_array(const uint8_t *data, size_t len) { - if (!this->initialised_) { + if (!this->initialised_.load()) { ESP_LOGV(TAG, "Channel not initialised - write ignored"); return; } @@ -152,7 +152,7 @@ bool USBUartChannel::peek_byte(uint8_t *data) { return true; } bool USBUartChannel::read_array(uint8_t *data, size_t len) { - if (!this->initialised_) { + if (!this->initialised_.load()) { ESP_LOGV(TAG, "Channel not initialised - read ignored"); return false; } @@ -170,7 +170,34 @@ bool USBUartChannel::read_array(uint8_t *data, size_t len) { return status; } void USBUartComponent::setup() { USBClient::setup(); } -void USBUartComponent::loop() { USBClient::loop(); } +void USBUartComponent::loop() { + USBClient::loop(); + + // Process USB data from the lock-free queue + UsbDataChunk *chunk; + while ((chunk = this->usb_data_queue_.pop()) != nullptr) { + auto *channel = chunk->channel; + +#ifdef USE_UART_DEBUGGER + if (channel->debug_) { + uart::UARTDebug::log_hex(uart::UART_DIRECTION_RX, std::vector(chunk->data, chunk->data + chunk->length), + ','); // NOLINT() + } +#endif + + // Push data to ring buffer (now safe in main loop) + channel->input_buffer_.push(chunk->data, chunk->length); + + // Return chunk to pool for reuse + this->chunk_pool_.release(chunk); + } + + // Log dropped USB data periodically + uint16_t dropped = this->usb_data_queue_.get_and_reset_dropped_count(); + if (dropped > 0) { + ESP_LOGW(TAG, "Dropped %u USB data chunks due to buffer overflow", dropped); + } +} void USBUartComponent::dump_config() { USBClient::dump_config(); for (auto &channel : this->channels_) { @@ -187,49 +214,84 @@ void USBUartComponent::dump_config() { } } void USBUartComponent::start_input(USBUartChannel *channel) { - if (!channel->initialised_ || channel->input_started_ || - channel->input_buffer_.get_free_space() < channel->cdc_dev_.in_ep->wMaxPacketSize) + if (!channel->initialised_.load()) + return; + // THREAD CONTEXT: Called from both USB task and main loop threads + // - USB task: Immediate restart after successful transfer for continuous data flow + // - Main loop: Controlled restart after consuming data (backpressure mechanism) + // + // This dual-thread access is intentional for performance: + // - USB task restarts avoid context switch delays for high-speed data + // - Main loop restarts provide flow control when buffers are full + // + // The underlying transfer_in() uses lock-free atomic allocation from the + // TransferRequest pool, making this multi-threaded access safe + + // if already started, don't restart. A spurious failure in compare_exchange_weak + // is not a problem, as it will be retried on the next read_array() + auto started = false; + if (!channel->input_started_.compare_exchange_weak(started, true)) return; const auto *ep = channel->cdc_dev_.in_ep; + // CALLBACK CONTEXT: This lambda is executed in USB task via transfer_callback auto callback = [this, channel](const usb_host::TransferStatus &status) { ESP_LOGV(TAG, "Transfer result: length: %u; status %X", status.data_len, status.error_code); if (!status.success) { - ESP_LOGE(TAG, "Control transfer failed, status=%s", esp_err_to_name(status.error_code)); + ESP_LOGE(TAG, "Input transfer failed, status=%s", esp_err_to_name(status.error_code)); + // On failure, don't restart - let next read_array() trigger it + channel->input_started_.store(false); return; } -#ifdef USE_UART_DEBUGGER - if (channel->debug_) { - uart::UARTDebug::log_hex(uart::UART_DIRECTION_RX, - std::vector(status.data, status.data + status.data_len), ','); // NOLINT() - } -#endif - channel->input_started_ = false; - if (!channel->dummy_receiver_) { - for (size_t i = 0; i != status.data_len; i++) { - channel->input_buffer_.push(status.data[i]); + + if (!channel->dummy_receiver_ && status.data_len > 0) { + // Allocate a chunk from the pool + UsbDataChunk *chunk = this->chunk_pool_.allocate(); + if (chunk == nullptr) { + // No chunks available - queue is full or we're out of memory + this->usb_data_queue_.increment_dropped_count(); + // Mark input as not started so we can retry + channel->input_started_.store(false); + return; } + + // Copy data to chunk (this is fast, happens in USB task) + memcpy(chunk->data, status.data, status.data_len); + chunk->length = status.data_len; + chunk->channel = channel; + + // Push to lock-free queue for main loop processing + // Push always succeeds because pool size == queue size + this->usb_data_queue_.push(chunk); } - if (channel->input_buffer_.get_free_space() >= channel->cdc_dev_.in_ep->wMaxPacketSize) { - this->defer([this, channel] { this->start_input(channel); }); - } + + // On success, restart input immediately from USB task for performance + // The lock-free queue will handle backpressure + channel->input_started_.store(false); + this->start_input(channel); }; - channel->input_started_ = true; - this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize); + if (!this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize)) { + channel->input_started_.store(false); + } } void USBUartComponent::start_output(USBUartChannel *channel) { - if (channel->output_started_) + // IMPORTANT: This function must only be called from the main loop! + // The output_buffer_ is not thread-safe and can only be accessed from main loop. + // USB callbacks use defer() to ensure this function runs in the correct context. + if (channel->output_started_.load()) return; if (channel->output_buffer_.is_empty()) { return; } const auto *ep = channel->cdc_dev_.out_ep; + // CALLBACK CONTEXT: This lambda is executed in USB task via transfer_callback auto callback = [this, channel](const usb_host::TransferStatus &status) { ESP_LOGV(TAG, "Output Transfer result: length: %u; status %X", status.data_len, status.error_code); - channel->output_started_ = false; + channel->output_started_.store(false); + // Defer restart to main loop (defer is thread-safe) this->defer([this, channel] { this->start_output(channel); }); }; - channel->output_started_ = true; + channel->output_started_.store(true); uint8_t data[ep->wMaxPacketSize]; auto len = channel->output_buffer_.pop(data, ep->wMaxPacketSize); this->transfer_out(ep->bEndpointAddress, callback, data, len); @@ -249,7 +311,8 @@ static void fix_mps(const usb_ep_desc_t *ep) { if (ep != nullptr) { auto *ep_mutable = const_cast(ep); if (ep->wMaxPacketSize > 64) { - ESP_LOGW(TAG, "Corrected MPS of EP %u from %u to 64", ep->bEndpointAddress, ep->wMaxPacketSize); + ESP_LOGW(TAG, "Corrected MPS of EP 0x%02X from %u to 64", static_cast(ep->bEndpointAddress & 0xFF), + ep->wMaxPacketSize); ep_mutable->wMaxPacketSize = 64; } } @@ -257,7 +320,7 @@ static void fix_mps(const usb_ep_desc_t *ep) { void USBUartTypeCdcAcm::on_connected() { auto cdc_devs = this->parse_descriptors(this->device_handle_); if (cdc_devs.empty()) { - this->status_set_error("No CDC-ACM device found"); + this->status_set_error(LOG_STR("No CDC-ACM device found")); this->disconnect(); return; } @@ -272,13 +335,13 @@ void USBUartTypeCdcAcm::on_connected() { channel->cdc_dev_ = cdc_devs[i++]; fix_mps(channel->cdc_dev_.in_ep); fix_mps(channel->cdc_dev_.out_ep); - channel->initialised_ = true; + channel->initialised_.store(true); auto err = usb_host_interface_claim(this->handle_, this->device_handle_, channel->cdc_dev_.bulk_interface_number, 0); if (err != ESP_OK) { ESP_LOGE(TAG, "usb_host_interface_claim failed: %s, channel=%d, intf=%d", esp_err_to_name(err), channel->index_, channel->cdc_dev_.bulk_interface_number); - this->status_set_error("usb_host_interface_claim failed"); + this->status_set_error(LOG_STR("usb_host_interface_claim failed")); this->disconnect(); return; } @@ -301,21 +364,22 @@ void USBUartTypeCdcAcm::on_disconnected() { usb_host_endpoint_flush(this->device_handle_, channel->cdc_dev_.notify_ep->bEndpointAddress); } usb_host_interface_release(this->handle_, this->device_handle_, channel->cdc_dev_.bulk_interface_number); - channel->initialised_ = false; - channel->input_started_ = false; - channel->output_started_ = false; + // Reset the input and output started flags to their initial state to avoid the possibility of spurious restarts + channel->input_started_.store(true); + channel->output_started_.store(true); channel->input_buffer_.clear(); channel->output_buffer_.clear(); + channel->initialised_.store(false); } USBClient::on_disconnected(); } void USBUartTypeCdcAcm::enable_channels() { for (auto *channel : this->channels_) { - if (!channel->initialised_) + if (!channel->initialised_.load()) continue; - channel->input_started_ = false; - channel->output_started_ = false; + channel->input_started_.store(false); + channel->output_started_.store(false); this->start_input(channel); } } diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h index a103c51add..a5e7905ac5 100644 --- a/esphome/components/usb_uart/usb_uart.h +++ b/esphome/components/usb_uart/usb_uart.h @@ -1,15 +1,19 @@ #pragma once -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "esphome/components/uart/uart_component.h" #include "esphome/components/usb_host/usb_host.h" +#include "esphome/core/lock_free_queue.h" +#include "esphome/core/event_pool.h" +#include namespace esphome { namespace usb_uart { class USBUartTypeCdcAcm; class USBUartComponent; +class USBUartChannel; static const char *const TAG = "usb_uart"; @@ -68,6 +72,17 @@ class RingBuffer { uint8_t *buffer_; }; +// Structure for queuing received USB data chunks +struct UsbDataChunk { + static constexpr size_t MAX_CHUNK_SIZE = 64; // USB packet size + uint8_t data[MAX_CHUNK_SIZE]; + uint8_t length; // Max 64 bytes, so uint8_t is sufficient + USBUartChannel *channel; + + // Required for EventPool - no cleanup needed for POD types + void release() {} +}; + class USBUartChannel : public uart::UARTComponent, public Parented { friend class USBUartComponent; friend class USBUartTypeCdcAcm; @@ -90,16 +105,20 @@ class USBUartChannel : public uart::UARTComponent, public Parenteddummy_receiver_ = dummy_receiver; } protected: - const uint8_t index_; + // Larger structures first for better alignment RingBuffer input_buffer_; RingBuffer output_buffer_; - UARTParityOptions parity_{UART_CONFIG_PARITY_NONE}; - bool input_started_{true}; - bool output_started_{true}; CdcEps cdc_dev_{}; + // Enum (likely 4 bytes) + UARTParityOptions parity_{UART_CONFIG_PARITY_NONE}; + // Group atomics together (each 1 byte) + std::atomic input_started_{true}; + std::atomic output_started_{true}; + std::atomic initialised_{false}; + // Group regular bytes together to minimize padding + const uint8_t index_; bool debug_{}; bool dummy_receiver_{}; - bool initialised_{}; }; class USBUartComponent : public usb_host::USBClient { @@ -115,6 +134,11 @@ class USBUartComponent : public usb_host::USBClient { void start_input(USBUartChannel *channel); void start_output(USBUartChannel *channel); + // Lock-free data transfer from USB task to main loop + static constexpr int USB_DATA_QUEUE_SIZE = 32; + LockFreeQueue usb_data_queue_; + EventPool chunk_pool_; + protected: std::vector channels_{}; }; diff --git a/esphome/components/valve/__init__.py b/esphome/components/valve/__init__.py index 53254068af..73e907eb0f 100644 --- a/esphome/components/valve/__init__.py +++ b/esphome/components/valve/__init__.py @@ -21,7 +21,7 @@ from esphome.const import ( DEVICE_CLASS_GAS, DEVICE_CLASS_WATER, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass @@ -129,11 +129,6 @@ def valve_schema( return _VALVE_SCHEMA.extend(schema) -# Remove before 2025.11.0 -VALVE_SCHEMA = valve_schema() -VALVE_SCHEMA.add_extra(cv.deprecated_schema_constant("valve")) - - async def _setup_valve_core(var, config): await setup_entity(var, config, "valve") @@ -202,9 +197,9 @@ async def valve_stop_to_code(config, action_id, template_arg, args): @automation.register_action("valve.toggle", ToggleAction, VALVE_ACTION_SCHEMA) -def valve_toggle_to_code(config, action_id, template_arg, args): - paren = yield cg.get_variable(config[CONF_ID]) - yield cg.new_Pvariable(action_id, template_arg, paren) +async def valve_toggle_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) VALVE_CONTROL_ACTION_SCHEMA = cv.Schema( @@ -233,6 +228,6 @@ async def valve_control_to_code(config, action_id, template_arg, args): return var -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(valve_ns.using) diff --git a/esphome/components/valve/automation.h b/esphome/components/valve/automation.h index f2c06270c0..87e9cde088 100644 --- a/esphome/components/valve/automation.h +++ b/esphome/components/valve/automation.h @@ -11,7 +11,7 @@ template class OpenAction : public Action { public: explicit OpenAction(Valve *valve) : valve_(valve) {} - void play(Ts... x) override { this->valve_->make_call().set_command_open().perform(); } + void play(const Ts &...x) override { this->valve_->make_call().set_command_open().perform(); } protected: Valve *valve_; @@ -21,7 +21,7 @@ template class CloseAction : public Action { public: explicit CloseAction(Valve *valve) : valve_(valve) {} - void play(Ts... x) override { this->valve_->make_call().set_command_close().perform(); } + void play(const Ts &...x) override { this->valve_->make_call().set_command_close().perform(); } protected: Valve *valve_; @@ -31,7 +31,7 @@ template class StopAction : public Action { public: explicit StopAction(Valve *valve) : valve_(valve) {} - void play(Ts... x) override { this->valve_->make_call().set_command_stop().perform(); } + void play(const Ts &...x) override { this->valve_->make_call().set_command_stop().perform(); } protected: Valve *valve_; @@ -41,7 +41,7 @@ template class ToggleAction : public Action { public: explicit ToggleAction(Valve *valve) : valve_(valve) {} - void play(Ts... x) override { this->valve_->make_call().set_command_toggle().perform(); } + void play(const Ts &...x) override { this->valve_->make_call().set_command_toggle().perform(); } protected: Valve *valve_; @@ -54,7 +54,7 @@ template class ControlAction : public Action { TEMPLATABLE_VALUE(bool, stop) TEMPLATABLE_VALUE(float, position) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->valve_->make_call(); if (this->stop_.has_value()) call.set_stop(this->stop_.value(x...)); @@ -70,7 +70,7 @@ template class ControlAction : public Action { template class ValveIsOpenCondition : public Condition { public: ValveIsOpenCondition(Valve *valve) : valve_(valve) {} - bool check(Ts... x) override { return this->valve_->is_fully_open(); } + bool check(const Ts &...x) override { return this->valve_->is_fully_open(); } protected: Valve *valve_; @@ -79,7 +79,7 @@ template class ValveIsOpenCondition : public Condition { template class ValveIsClosedCondition : public Condition { public: ValveIsClosedCondition(Valve *valve) : valve_(valve) {} - bool check(Ts... x) override { return this->valve_->is_fully_closed(); } + bool check(const Ts &...x) override { return this->valve_->is_fully_closed(); } protected: Valve *valve_; diff --git a/esphome/components/valve/valve.cpp b/esphome/components/valve/valve.cpp index d1ec17945a..381d9061de 100644 --- a/esphome/components/valve/valve.cpp +++ b/esphome/components/valve/valve.cpp @@ -1,5 +1,8 @@ #include "valve.h" +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" +#include namespace esphome { namespace valve { @@ -146,6 +149,9 @@ void Valve::publish_state(bool save) { ESP_LOGD(TAG, " Current Operation: %s", valve_operation_to_str(this->current_operation)); this->state_callback_.call(); +#if defined(USE_VALVE) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_valve_update(this); +#endif if (save) { ValveRestoreState restore{}; @@ -155,7 +161,7 @@ void Valve::publish_state(bool save) { } } optional Valve::restore_state_() { - this->rtc_ = global_preferences->make_preference(this->get_object_id_hash()); + this->rtc_ = global_preferences->make_preference(this->get_preference_hash()); ValveRestoreState recovered{}; if (!this->rtc_.load(&recovered)) return {}; diff --git a/esphome/components/valve/valve.h b/esphome/components/valve/valve.h index 0e14a8d8f0..ab7ff5abe1 100644 --- a/esphome/components/valve/valve.h +++ b/esphome/components/valve/valve.h @@ -19,8 +19,8 @@ const extern float VALVE_CLOSED; if (traits_.get_is_assumed_state()) { \ ESP_LOGCONFIG(TAG, "%s Assumed State: YES", prefix); \ } \ - if (!(obj)->get_device_class().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class().c_str()); \ + if (!(obj)->get_device_class_ref().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class_ref().c_str()); \ } \ } diff --git a/esphome/components/veml3235/veml3235.cpp b/esphome/components/veml3235/veml3235.cpp index f3016fb171..1e02e3e802 100644 --- a/esphome/components/veml3235/veml3235.cpp +++ b/esphome/components/veml3235/veml3235.cpp @@ -14,14 +14,12 @@ void VEML3235Sensor::setup() { this->mark_failed(); return; } - if ((this->write(&ID_REG, 1, false) != i2c::ERROR_OK) || !this->read_bytes_raw(device_id, 2)) { + if ((this->read_register(ID_REG, device_id, sizeof device_id) != i2c::ERROR_OK)) { ESP_LOGE(TAG, "Unable to read ID"); this->mark_failed(); - return; } else if (device_id[0] != DEVICE_ID) { ESP_LOGE(TAG, "Incorrect device ID - expected 0x%.2x, read 0x%.2x", DEVICE_ID, device_id[0]); this->mark_failed(); - return; } } @@ -49,7 +47,7 @@ float VEML3235Sensor::read_lx_() { } uint8_t als_regs[] = {0, 0}; - if ((this->write(&ALS_REG, 1, false) != i2c::ERROR_OK) || !this->read_bytes_raw(als_regs, 2)) { + if ((this->read_register(ALS_REG, als_regs, sizeof als_regs) != i2c::ERROR_OK)) { this->status_set_warning(); return NAN; } diff --git a/esphome/components/veml7700/veml7700.cpp b/esphome/components/veml7700/veml7700.cpp index 2a4c246ac9..eb286ba21b 100644 --- a/esphome/components/veml7700/veml7700.cpp +++ b/esphome/components/veml7700/veml7700.cpp @@ -1,6 +1,7 @@ #include "veml7700.h" #include "esphome/core/application.h" #include "esphome/core/log.h" +#include namespace esphome { namespace veml7700 { @@ -12,30 +13,30 @@ static float reduce_to_zero(float a, float b) { return (a > b) ? (a - b) : 0; } template T get_next(const T (&array)[size], const T val) { size_t i = 0; - size_t idx = -1; - while (idx == -1 && i < size) { + size_t idx = std::numeric_limits::max(); + while (idx == std::numeric_limits::max() && i < size) { if (array[i] == val) { idx = i; break; } i++; } - if (idx == -1 || i + 1 >= size) + if (idx == std::numeric_limits::max() || i + 1 >= size) return val; return array[i + 1]; } template T get_prev(const T (&array)[size], const T val) { size_t i = size - 1; - size_t idx = -1; - while (idx == -1 && i > 0) { + size_t idx = std::numeric_limits::max(); + while (idx == std::numeric_limits::max() && i > 0) { if (array[i] == val) { idx = i; break; } i--; } - if (idx == -1 || i == 0) + if (idx == std::numeric_limits::max() || i == 0) return val; return array[i - 1]; } @@ -279,20 +280,18 @@ ErrorCode VEML7700Component::reconfigure_time_and_gain_(IntegrationTime time, Ga } ErrorCode VEML7700Component::read_sensor_output_(Readings &data) { - auto als_err = - this->read_register((uint8_t) CommandRegisters::ALS, (uint8_t *) &data.als_counts, VEML_REG_SIZE, false); + auto als_err = this->read_register((uint8_t) CommandRegisters::ALS, (uint8_t *) &data.als_counts, VEML_REG_SIZE); if (als_err != i2c::ERROR_OK) { ESP_LOGW(TAG, "Error reading ALS register, err = %d", als_err); } auto white_err = - this->read_register((uint8_t) CommandRegisters::WHITE, (uint8_t *) &data.white_counts, VEML_REG_SIZE, false); + this->read_register((uint8_t) CommandRegisters::WHITE, (uint8_t *) &data.white_counts, VEML_REG_SIZE); if (white_err != i2c::ERROR_OK) { ESP_LOGW(TAG, "Error reading WHITE register, err = %d", white_err); } ConfigurationRegister conf{0}; - auto err = - this->read_register((uint8_t) CommandRegisters::ALS_CONF_0, (uint8_t *) conf.raw_bytes, VEML_REG_SIZE, false); + auto err = this->read_register((uint8_t) CommandRegisters::ALS_CONF_0, (uint8_t *) conf.raw_bytes, VEML_REG_SIZE); if (err != i2c::ERROR_OK) { ESP_LOGW(TAG, "Error reading ALS_CONF_0 register, err = %d", white_err); } diff --git a/esphome/components/veml7700/veml7700.h b/esphome/components/veml7700/veml7700.h index b0d1451cf0..4b5edf733d 100644 --- a/esphome/components/veml7700/veml7700.h +++ b/esphome/components/veml7700/veml7700.h @@ -3,7 +3,6 @@ #include "esphome/components/i2c/i2c.h" #include "esphome/components/sensor/sensor.h" #include "esphome/core/component.h" -#include "esphome/core/optional.h" namespace esphome { namespace veml7700 { diff --git a/esphome/components/version/version_text_sensor.cpp b/esphome/components/version/version_text_sensor.cpp index ed093595cc..65dbfd27cf 100644 --- a/esphome/components/version/version_text_sensor.cpp +++ b/esphome/components/version/version_text_sensor.cpp @@ -2,6 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" #include "esphome/core/version.h" +#include "esphome/core/helpers.h" namespace esphome { namespace version { @@ -12,7 +13,7 @@ void VersionTextSensor::setup() { if (this->hide_timestamp_) { this->publish_state(ESPHOME_VERSION); } else { - this->publish_state(ESPHOME_VERSION " " + App.get_compilation_time()); + this->publish_state(str_sprintf(ESPHOME_VERSION " %s", App.get_compilation_time().c_str())); } } float VersionTextSensor::get_setup_priority() const { return setup_priority::DATA; } diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 743c90e700..551f0370f2 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -206,7 +206,7 @@ void VoiceAssistant::loop() { case State::START_MICROPHONE: { ESP_LOGD(TAG, "Starting Microphone"); if (!this->allocate_buffers_()) { - this->status_set_error("Failed to allocate buffers"); + this->status_set_error(LOG_STR("Failed to allocate buffers")); return; } if (this->status_has_error()) { @@ -242,7 +242,6 @@ void VoiceAssistant::loop() { msg.flags = flags; msg.audio_settings = audio_settings; msg.set_wake_word_phrase(StringRef(this->wake_word_)); - this->wake_word_ = ""; // Reset media player state tracking #ifdef USE_MEDIA_PLAYER @@ -430,8 +429,9 @@ void VoiceAssistant::client_subscription(api::APIConnection *client, bool subscr if (this->api_client_ != nullptr) { ESP_LOGE(TAG, "Multiple API Clients attempting to connect to Voice Assistant"); - ESP_LOGE(TAG, "Current client: %s", this->api_client_->get_client_combined_info().c_str()); - ESP_LOGE(TAG, "New client: %s", client->get_client_combined_info().c_str()); + ESP_LOGE(TAG, "Current client: %s (%s)", this->api_client_->get_name().c_str(), + this->api_client_->get_peername().c_str()); + ESP_LOGE(TAG, "New client: %s (%s)", client->get_name().c_str(), client->get_peername().c_str()); return; } @@ -657,7 +657,8 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { ESP_LOGW(TAG, "No text in STT_END event"); return; } else if (text.length() > 500) { - text = text.substr(0, 497) + "..."; + text.resize(497); + text += "..."; } ESP_LOGD(TAG, "Speech recognised as: \"%s\"", text.c_str()); this->defer([this, text]() { this->stt_end_trigger_->trigger(text); }); @@ -714,7 +715,8 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { return; } if (text.length() > 500) { - text = text.substr(0, 497) + "..."; + text.resize(497); + text += "..."; } ESP_LOGD(TAG, "Response: \"%s\"", text.c_str()); this->defer([this, text]() { diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index 95f77dbf09..8d3d3497ec 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -324,7 +324,7 @@ template class StartAction : public Action, public Parent TEMPLATABLE_VALUE(std::string, wake_word); public: - void play(Ts... x) override { + void play(const Ts &...x) override { this->parent_->set_wake_word(this->wake_word_.value(x...)); this->parent_->request_start(false, this->silence_detection_); } @@ -337,22 +337,22 @@ template class StartAction : public Action, public Parent template class StartContinuousAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->request_start(true, true); } + void play(const Ts &...x) override { this->parent_->request_start(true, true); } }; template class StopAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->request_stop(); } + void play(const Ts &...x) override { this->parent_->request_stop(); } }; template class IsRunningCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->is_running() || this->parent_->is_continuous(); } + bool check(const Ts &...x) override { return this->parent_->is_running() || this->parent_->is_continuous(); } }; template class ConnectedCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->get_api_connection() != nullptr; } + bool check(const Ts &...x) override { return this->parent_->get_api_connection() != nullptr; } }; extern VoiceAssistant *global_voice_assistant; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/wake_on_lan/wake_on_lan.cpp b/esphome/components/wake_on_lan/wake_on_lan.cpp index bed098755a..8c5bdac54b 100644 --- a/esphome/components/wake_on_lan/wake_on_lan.cpp +++ b/esphome/components/wake_on_lan/wake_on_lan.cpp @@ -67,19 +67,19 @@ void WakeOnLanButton::setup() { #if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) this->broadcast_socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); if (this->broadcast_socket_ == nullptr) { + this->status_set_error(LOG_STR("Could not create socket")); this->mark_failed(); - this->status_set_error("Could not create socket"); return; } int enable = 1; auto err = this->broadcast_socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)); if (err != 0) { - this->status_set_warning("Socket unable to set reuseaddr"); + this->status_set_warning(LOG_STR("Socket unable to set reuseaddr")); // we can still continue } err = this->broadcast_socket_->setsockopt(SOL_SOCKET, SO_BROADCAST, &enable, sizeof(int)); if (err != 0) { - this->status_set_warning("Socket unable to set broadcast"); + this->status_set_warning(LOG_STR("Socket unable to set broadcast")); } #endif } diff --git a/esphome/components/waveshare_epaper/waveshare_213v3.cpp b/esphome/components/waveshare_epaper/waveshare_213v3.cpp index 316cd80ccd..068cb91d31 100644 --- a/esphome/components/waveshare_epaper/waveshare_213v3.cpp +++ b/esphome/components/waveshare_epaper/waveshare_213v3.cpp @@ -181,7 +181,7 @@ void WaveshareEPaper2P13InV3::dump_config() { LOG_PIN(" Reset Pin: ", this->reset_pin_) LOG_PIN(" DC Pin: ", this->dc_pin_) LOG_PIN(" Busy Pin: ", this->busy_pin_) - LOG_UPDATE_INTERVAL(this) + LOG_UPDATE_INTERVAL(this); } void WaveshareEPaper2P13InV3::set_full_update_every(uint32_t full_update_every) { diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index 75c6b84b79..3510d157d6 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -2274,11 +2274,11 @@ void GDEW0154M09::clear_() { uint32_t pixsize = this->get_buffer_length_(); for (uint8_t j = 0; j < 2; j++) { this->command(CMD_DTM1_DATA_START_TRANS); - for (int count = 0; count < pixsize; count++) { + for (uint32_t count = 0; count < pixsize; count++) { this->data(0x00); } this->command(CMD_DTM2_DATA_START_TRANS2); - for (int count = 0; count < pixsize; count++) { + for (uint32_t count = 0; count < pixsize; count++) { this->data(0xff); } this->command(CMD_DISPLAY_REFRESH); @@ -2291,11 +2291,11 @@ void HOT GDEW0154M09::display() { this->init_internal_(); // "Mode 0 display" for now this->command(CMD_DTM1_DATA_START_TRANS); - for (int i = 0; i < this->get_buffer_length_(); i++) { + for (uint32_t i = 0; i < this->get_buffer_length_(); i++) { this->data(0xff); } this->command(CMD_DTM2_DATA_START_TRANS2); // write 'new' data to SRAM - for (int i = 0; i < this->get_buffer_length_(); i++) { + for (uint32_t i = 0; i < this->get_buffer_length_(); i++) { this->data(this->buffer_[i]); } this->command(CMD_DISPLAY_REFRESH); diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index 695757e137..17ad496f30 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -31,7 +31,7 @@ from esphome.const import ( PLATFORM_LN882X, PLATFORM_RTL87XX, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority import esphome.final_validate as fv from esphome.types import ConfigType @@ -52,9 +52,9 @@ def default_url(config: ConfigType) -> ConfigType: config = config.copy() if config[CONF_VERSION] == 1: if CONF_CSS_URL not in config: - config[CONF_CSS_URL] = "https://esphome.io/_static/webserver-v1.min.css" + config[CONF_CSS_URL] = "https://oi.esphome.io/v1/webserver-v1.min.css" if CONF_JS_URL not in config: - config[CONF_JS_URL] = "https://esphome.io/_static/webserver-v1.min.js" + config[CONF_JS_URL] = "https://oi.esphome.io/v1/webserver-v1.min.js" if config[CONF_VERSION] == 2: if CONF_CSS_URL not in config: config[CONF_CSS_URL] = "" @@ -136,6 +136,18 @@ def _final_validate_sorting(config: ConfigType) -> ConfigType: FINAL_VALIDATE_SCHEMA = _final_validate_sorting + +def _consume_web_server_sockets(config: ConfigType) -> ConfigType: + """Register socket needs for web_server component.""" + from esphome.components import socket + + # Web server needs 1 listening socket + typically 2 concurrent client connections + # (browser makes 2 connections for page + event stream) + sockets_needed = 3 + socket.consume_sockets(sockets_needed, "web_server")(config) + return config + + sorting_group = { cv.Required(CONF_ID): cv.declare_id(cg.int_), cv.Required(CONF_NAME): cv.string, @@ -205,6 +217,7 @@ CONFIG_SCHEMA = cv.All( validate_local, validate_sorting_groups, validate_ota, + _consume_web_server_sockets, ) @@ -269,13 +282,16 @@ def add_resource_as_progmem( cg.add_global(cg.RawExpression(size_t)) -@coroutine_with_priority(40.0) +@coroutine_with_priority(CoroPriority.WEB) async def to_code(config): paren = await cg.get_variable(config[CONF_WEB_SERVER_BASE_ID]) var = cg.new_Pvariable(config[CONF_ID], paren) await cg.register_component(var, config) + # Track controller registration for StaticVector sizing + CORE.register_controller() + version = config[CONF_VERSION] cg.add(paren.set_port(config[CONF_PORT])) diff --git a/esphome/components/web_server/list_entities.cpp b/esphome/components/web_server/list_entities.cpp index fb02821760..6b27545549 100644 --- a/esphome/components/web_server/list_entities.cpp +++ b/esphome/components/web_server/list_entities.cpp @@ -9,83 +9,64 @@ namespace esphome { namespace web_server { -#ifdef USE_ARDUINO +#ifdef USE_ESP32 +ListEntitiesIterator::ListEntitiesIterator(const WebServer *ws, AsyncEventSource *es) : web_server_(ws), events_(es) {} +#elif USE_ARDUINO ListEntitiesIterator::ListEntitiesIterator(const WebServer *ws, DeferredUpdateEventSource *es) : web_server_(ws), events_(es) {} #endif -#ifdef USE_ESP_IDF -ListEntitiesIterator::ListEntitiesIterator(const WebServer *ws, AsyncEventSource *es) : web_server_(ws), events_(es) {} -#endif ListEntitiesIterator::~ListEntitiesIterator() {} #ifdef USE_BINARY_SENSOR bool ListEntitiesIterator::on_binary_sensor(binary_sensor::BinarySensor *obj) { - if (this->events_->count() == 0) - return true; this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::binary_sensor_all_json_generator); return true; } #endif #ifdef USE_COVER bool ListEntitiesIterator::on_cover(cover::Cover *obj) { - if (this->events_->count() == 0) - return true; this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::cover_all_json_generator); return true; } #endif #ifdef USE_FAN bool ListEntitiesIterator::on_fan(fan::Fan *obj) { - if (this->events_->count() == 0) - return true; this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::fan_all_json_generator); return true; } #endif #ifdef USE_LIGHT bool ListEntitiesIterator::on_light(light::LightState *obj) { - if (this->events_->count() == 0) - return true; this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::light_all_json_generator); return true; } #endif #ifdef USE_SENSOR bool ListEntitiesIterator::on_sensor(sensor::Sensor *obj) { - if (this->events_->count() == 0) - return true; this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::sensor_all_json_generator); return true; } #endif #ifdef USE_SWITCH bool ListEntitiesIterator::on_switch(switch_::Switch *obj) { - if (this->events_->count() == 0) - return true; this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::switch_all_json_generator); return true; } #endif #ifdef USE_BUTTON bool ListEntitiesIterator::on_button(button::Button *obj) { - if (this->events_->count() == 0) - return true; this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::button_all_json_generator); return true; } #endif #ifdef USE_TEXT_SENSOR bool ListEntitiesIterator::on_text_sensor(text_sensor::TextSensor *obj) { - if (this->events_->count() == 0) - return true; this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::text_sensor_all_json_generator); return true; } #endif #ifdef USE_LOCK bool ListEntitiesIterator::on_lock(lock::Lock *obj) { - if (this->events_->count() == 0) - return true; this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::lock_all_json_generator); return true; } @@ -93,8 +74,6 @@ bool ListEntitiesIterator::on_lock(lock::Lock *obj) { #ifdef USE_VALVE bool ListEntitiesIterator::on_valve(valve::Valve *obj) { - if (this->events_->count() == 0) - return true; this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::valve_all_json_generator); return true; } @@ -102,8 +81,6 @@ bool ListEntitiesIterator::on_valve(valve::Valve *obj) { #ifdef USE_CLIMATE bool ListEntitiesIterator::on_climate(climate::Climate *obj) { - if (this->events_->count() == 0) - return true; this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::climate_all_json_generator); return true; } @@ -111,8 +88,6 @@ bool ListEntitiesIterator::on_climate(climate::Climate *obj) { #ifdef USE_NUMBER bool ListEntitiesIterator::on_number(number::Number *obj) { - if (this->events_->count() == 0) - return true; this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::number_all_json_generator); return true; } @@ -120,8 +95,6 @@ bool ListEntitiesIterator::on_number(number::Number *obj) { #ifdef USE_DATETIME_DATE bool ListEntitiesIterator::on_date(datetime::DateEntity *obj) { - if (this->events_->count() == 0) - return true; this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::date_all_json_generator); return true; } @@ -129,8 +102,6 @@ bool ListEntitiesIterator::on_date(datetime::DateEntity *obj) { #ifdef USE_DATETIME_TIME bool ListEntitiesIterator::on_time(datetime::TimeEntity *obj) { - if (this->events_->count() == 0) - return true; this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::time_all_json_generator); return true; } @@ -138,8 +109,6 @@ bool ListEntitiesIterator::on_time(datetime::TimeEntity *obj) { #ifdef USE_DATETIME_DATETIME bool ListEntitiesIterator::on_datetime(datetime::DateTimeEntity *obj) { - if (this->events_->count() == 0) - return true; this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::datetime_all_json_generator); return true; } @@ -147,8 +116,6 @@ bool ListEntitiesIterator::on_datetime(datetime::DateTimeEntity *obj) { #ifdef USE_TEXT bool ListEntitiesIterator::on_text(text::Text *obj) { - if (this->events_->count() == 0) - return true; this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::text_all_json_generator); return true; } @@ -156,8 +123,6 @@ bool ListEntitiesIterator::on_text(text::Text *obj) { #ifdef USE_SELECT bool ListEntitiesIterator::on_select(select::Select *obj) { - if (this->events_->count() == 0) - return true; this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::select_all_json_generator); return true; } @@ -165,8 +130,6 @@ bool ListEntitiesIterator::on_select(select::Select *obj) { #ifdef USE_ALARM_CONTROL_PANEL bool ListEntitiesIterator::on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *obj) { - if (this->events_->count() == 0) - return true; this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::alarm_control_panel_all_json_generator); return true; } @@ -174,8 +137,6 @@ bool ListEntitiesIterator::on_alarm_control_panel(alarm_control_panel::AlarmCont #ifdef USE_EVENT bool ListEntitiesIterator::on_event(event::Event *obj) { - if (this->events_->count() == 0) - return true; // Null event type, since we are just iterating over entities this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::event_all_json_generator); return true; @@ -184,8 +145,6 @@ bool ListEntitiesIterator::on_event(event::Event *obj) { #ifdef USE_UPDATE bool ListEntitiesIterator::on_update(update::UpdateEntity *obj) { - if (this->events_->count() == 0) - return true; this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::update_all_json_generator); return true; } diff --git a/esphome/components/web_server/list_entities.h b/esphome/components/web_server/list_entities.h index ba81c70c86..43e1cc2544 100644 --- a/esphome/components/web_server/list_entities.h +++ b/esphome/components/web_server/list_entities.h @@ -5,25 +5,24 @@ #include "esphome/core/component.h" #include "esphome/core/component_iterator.h" namespace esphome { -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 namespace web_server_idf { class AsyncEventSource; } #endif namespace web_server { -#ifdef USE_ARDUINO +#if !defined(USE_ESP32) && defined(USE_ARDUINO) class DeferredUpdateEventSource; #endif class WebServer; class ListEntitiesIterator : public ComponentIterator { public: -#ifdef USE_ARDUINO - ListEntitiesIterator(const WebServer *ws, DeferredUpdateEventSource *es); -#endif -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 ListEntitiesIterator(const WebServer *ws, esphome::web_server_idf::AsyncEventSource *es); +#elif defined(USE_ARDUINO) + ListEntitiesIterator(const WebServer *ws, DeferredUpdateEventSource *es); #endif virtual ~ListEntitiesIterator(); #ifdef USE_BINARY_SENSOR @@ -90,11 +89,10 @@ class ListEntitiesIterator : public ComponentIterator { protected: const WebServer *web_server_; -#ifdef USE_ARDUINO - DeferredUpdateEventSource *events_; -#endif -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 esphome::web_server_idf::AsyncEventSource *events_; +#elif USE_ARDUINO + DeferredUpdateEventSource *events_; #endif }; diff --git a/esphome/components/web_server/ota/__init__.py b/esphome/components/web_server/ota/__init__.py index 3af14fd453..260e6aea6d 100644 --- a/esphome/components/web_server/ota/__init__.py +++ b/esphome/components/web_server/ota/__init__.py @@ -1,9 +1,17 @@ +import logging + import esphome.codegen as cg from esphome.components.esp32 import add_idf_component from esphome.components.ota import BASE_OTA_SCHEMA, OTAComponent, ota_to_code +from esphome.config_helpers import merge_config import esphome.config_validation as cv -from esphome.const import CONF_ID +from esphome.const import CONF_ID, CONF_OTA, CONF_PLATFORM, CONF_WEB_SERVER from esphome.core import CORE, coroutine_with_priority +from esphome.coroutine import CoroPriority +import esphome.final_validate as fv +from esphome.types import ConfigType + +_LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@esphome/core"] DEPENDENCIES = ["network", "web_server_base"] @@ -11,6 +19,53 @@ DEPENDENCIES = ["network", "web_server_base"] web_server_ns = cg.esphome_ns.namespace("web_server") WebServerOTAComponent = web_server_ns.class_("WebServerOTAComponent", OTAComponent) + +def _web_server_ota_final_validate(config: ConfigType) -> None: + """Merge multiple web_server OTA instances into one. + + Multiple web_server OTA instances register duplicate HTTP handlers for /update, + causing undefined behavior. Merge them into a single instance. + """ + full_conf = fv.full_config.get() + ota_confs = full_conf.get(CONF_OTA, []) + + web_server_ota_configs: list[ConfigType] = [] + other_ota_configs: list[ConfigType] = [] + + for ota_conf in ota_confs: + if ota_conf.get(CONF_PLATFORM) == CONF_WEB_SERVER: + web_server_ota_configs.append(ota_conf) + else: + other_ota_configs.append(ota_conf) + + if len(web_server_ota_configs) <= 1: + return + + # Merge all web_server OTA configs into the first one + merged = web_server_ota_configs[0] + for ota_conf in web_server_ota_configs[1:]: + # Validate that IDs are consistent if manually specified + if ( + merged[CONF_ID].is_manual + and ota_conf[CONF_ID].is_manual + and merged[CONF_ID] != ota_conf[CONF_ID] + ): + raise cv.Invalid( + f"Found multiple web_server OTA configurations but {CONF_ID} is inconsistent" + ) + merged = merge_config(merged, ota_conf) + + _LOGGER.warning( + "Found and merged %d web_server OTA configurations into one instance", + len(web_server_ota_configs), + ) + + # Replace OTA configs with merged web_server + other OTA platforms + other_ota_configs.append(merged) + full_conf[CONF_OTA] = other_ota_configs + fv.full_config.set(full_conf) + + CONFIG_SCHEMA = ( cv.Schema( { @@ -21,12 +76,14 @@ CONFIG_SCHEMA = ( .extend(cv.COMPONENT_SCHEMA) ) +FINAL_VALIDATE_SCHEMA = _web_server_ota_final_validate -@coroutine_with_priority(52.0) + +@coroutine_with_priority(CoroPriority.WEB_SERVER_OTA) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await ota_to_code(var, config) await cg.register_component(var, config) cg.add_define("USE_WEBSERVER_OTA") - if CORE.using_esp_idf: + if CORE.is_esp32: add_idf_component(name="zorxx/multipart-parser", ref="1.0.1") diff --git a/esphome/components/web_server/ota/ota_web_server.cpp b/esphome/components/web_server/ota/ota_web_server.cpp index 7211f707e9..7929f3647f 100644 --- a/esphome/components/web_server/ota/ota_web_server.cpp +++ b/esphome/components/web_server/ota/ota_web_server.cpp @@ -17,6 +17,12 @@ #endif #endif // USE_ARDUINO +#if USE_ESP32 +using PlatformString = std::string; +#elif USE_ARDUINO +using PlatformString = String; +#endif + namespace esphome { namespace web_server { @@ -26,8 +32,8 @@ class OTARequestHandler : public AsyncWebHandler { public: OTARequestHandler(WebServerOTAComponent *parent) : parent_(parent) {} void handleRequest(AsyncWebServerRequest *request) override; - void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, - bool final) override; + void handleUpload(AsyncWebServerRequest *request, const PlatformString &filename, size_t index, uint8_t *data, + size_t len, bool final) override; bool canHandle(AsyncWebServerRequest *request) const override { // Check if this is an OTA update request bool is_ota_request = request->url() == "/update" && request->method() == HTTP_POST; @@ -100,7 +106,7 @@ void OTARequestHandler::ota_init_(const char *filename) { this->ota_success_ = false; } -void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, +void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const PlatformString &filename, size_t index, uint8_t *data, size_t len, bool final) { ota::OTAResponseTypes error_code = ota::OTA_RESPONSE_OK; @@ -198,9 +204,20 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Strin void OTARequestHandler::handleRequest(AsyncWebServerRequest *request) { AsyncWebServerResponse *response; // Use the ota_success_ flag to determine the actual result +#ifdef USE_ESP8266 + static const char UPDATE_SUCCESS[] PROGMEM = "Update Successful!"; + static const char UPDATE_FAILED[] PROGMEM = "Update Failed!"; + static const char TEXT_PLAIN[] PROGMEM = "text/plain"; + static const char CONNECTION_STR[] PROGMEM = "Connection"; + static const char CLOSE_STR[] PROGMEM = "close"; + const char *msg = this->ota_success_ ? UPDATE_SUCCESS : UPDATE_FAILED; + response = request->beginResponse_P(200, TEXT_PLAIN, msg); + response->addHeader(CONNECTION_STR, CLOSE_STR); +#else const char *msg = this->ota_success_ ? "Update Successful!" : "Update Failed!"; response = request->beginResponse(200, "text/plain", msg); response->addHeader("Connection", "close"); +#endif request->send(response); } diff --git a/esphome/components/web_server/server_index_v2.h b/esphome/components/web_server/server_index_v2.h index ec093d3186..e675d81552 100644 --- a/esphome/components/web_server/server_index_v2.h +++ b/esphome/components/web_server/server_index_v2.h @@ -494,155 +494,155 @@ const uint8_t INDEX_GZ[] PROGMEM = { 0x1c, 0x40, 0xc8, 0x12, 0x7c, 0xa6, 0xc1, 0x29, 0x21, 0xa4, 0xd5, 0x9f, 0x05, 0x5f, 0xe2, 0x9b, 0x98, 0xa6, 0xc1, 0xbc, 0xe8, 0x96, 0x04, 0xa0, 0x22, 0xa6, 0x6f, 0x45, 0x79, 0x6f, 0x9c, 0xa4, 0x8a, 0xea, 0xb5, 0x82, 0xb3, 0x59, 0x52, 0xcf, 0x96, 0x58, 0x9a, 0xe5, 0x93, 0x19, 0x25, 0xfc, 0xa6, 0x79, 0xeb, 0xf6, 0x36, 0xc7, 0xd7, 0x60, 0x76, - 0x65, 0x7c, 0x4d, 0x02, 0x5b, 0x3e, 0xbd, 0x0f, 0xc7, 0xe5, 0xef, 0x57, 0x34, 0xcf, 0xc3, 0xb1, 0xae, 0xb9, 0x3d, - 0x9e, 0x26, 0x41, 0xb4, 0x63, 0x69, 0x06, 0x08, 0x88, 0x89, 0x01, 0x46, 0xc0, 0xa7, 0xa1, 0x43, 0x64, 0x30, 0xf5, - 0x7a, 0x74, 0x4d, 0x0e, 0x5f, 0x2f, 0x12, 0xe1, 0xb8, 0x2a, 0x38, 0x99, 0x66, 0x54, 0x96, 0x2a, 0x34, 0x16, 0x27, - 0xfb, 0x50, 0xa0, 0x5e, 0x6f, 0x89, 0xa2, 0x19, 0x07, 0xca, 0xf6, 0x58, 0x9a, 0x63, 0xa2, 0x68, 0x76, 0xa2, 0x52, - 0x99, 0xa5, 0xb4, 0x1e, 0xbb, 0xf9, 0xbc, 0x3d, 0x84, 0x3f, 0x3a, 0x32, 0xf4, 0xf9, 0x68, 0x34, 0xba, 0x37, 0xaa, - 0xf6, 0x79, 0x34, 0xa2, 0x1d, 0x7a, 0xd4, 0x85, 0x24, 0x96, 0xa6, 0x8e, 0xc5, 0xb4, 0x0b, 0x89, 0xbb, 0xc5, 0xc3, - 0x2a, 0x43, 0xd8, 0x46, 0xc4, 0x8b, 0x87, 0x47, 0xd8, 0x8a, 0x69, 0x46, 0x17, 0x93, 0x30, 0x1b, 0xb3, 0x34, 0x68, - 0x15, 0xfe, 0x5c, 0x87, 0xa4, 0x3e, 0x3f, 0x3e, 0x3e, 0x2e, 0xfc, 0xc8, 0x3c, 0xb5, 0xa2, 0xa8, 0xf0, 0x87, 0x8b, - 0x72, 0x1a, 0xad, 0xd6, 0x68, 0x54, 0xf8, 0xcc, 0x14, 0x1c, 0x74, 0x86, 0xd1, 0x41, 0xa7, 0xf0, 0x6f, 0xac, 0x1a, - 0x85, 0x4f, 0xf5, 0x53, 0x46, 0xa3, 0x5a, 0x26, 0xcc, 0xe3, 0x56, 0xab, 0xf0, 0x15, 0xa1, 0x2d, 0xc0, 0x2c, 0x55, - 0x3f, 0x83, 0x70, 0x26, 0x38, 0x30, 0xf7, 0x6e, 0x22, 0xbc, 0xc1, 0xa5, 0xbe, 0x65, 0x44, 0x7d, 0x93, 0xa3, 0x40, - 0x17, 0xf8, 0x67, 0x3b, 0x78, 0x04, 0xc4, 0x2c, 0x83, 0x46, 0x89, 0x89, 0x2d, 0xd5, 0x5e, 0x03, 0x65, 0xc9, 0xd7, - 0x3f, 0x93, 0xa4, 0x8a, 0x29, 0x01, 0x27, 0x83, 0x9a, 0xea, 0x32, 0x3c, 0x4a, 0xb7, 0xc8, 0x0f, 0xf6, 0x69, 0xf9, - 0x71, 0xf7, 0x10, 0xf1, 0xc1, 0xfe, 0x70, 0xf1, 0x41, 0xa9, 0x25, 0x3e, 0x14, 0xf3, 0xb8, 0x13, 0xc4, 0x1d, 0xc6, - 0x74, 0xf8, 0xf1, 0x9a, 0xdf, 0x36, 0x61, 0x4b, 0x64, 0xae, 0x14, 0x2c, 0xbb, 0xbf, 0x35, 0x6b, 0xc6, 0x74, 0x66, - 0x7d, 0xd1, 0x43, 0xaa, 0x0f, 0x6f, 0x52, 0xe2, 0xbe, 0x31, 0xb6, 0xad, 0x2a, 0x19, 0x8d, 0x88, 0xfb, 0x66, 0x34, - 0x72, 0xcd, 0x59, 0xc9, 0x50, 0x50, 0x59, 0xeb, 0x75, 0xad, 0x44, 0xd6, 0xfa, 0xf2, 0x4b, 0xbb, 0xcc, 0x2e, 0xd0, - 0xa1, 0x27, 0x3b, 0xcc, 0xa4, 0xdf, 0x44, 0x2c, 0x87, 0xad, 0x06, 0x1f, 0x1a, 0xa9, 0xdf, 0xd5, 0x98, 0xd6, 0xae, - 0xd5, 0x2e, 0x01, 0xde, 0x70, 0x17, 0xf8, 0xea, 0x45, 0x01, 0x63, 0x6a, 0xf2, 0x16, 0x9f, 0xde, 0x7d, 0x15, 0x79, - 0x77, 0x02, 0x15, 0x2c, 0x7f, 0x93, 0xae, 0x1c, 0x02, 0x52, 0x30, 0x12, 0x62, 0x4f, 0xab, 0x10, 0x7c, 0x3c, 0x4e, - 0xe0, 0x5b, 0x2f, 0x8b, 0xda, 0xfd, 0xb1, 0xaa, 0x79, 0xbf, 0x36, 0xdf, 0xc0, 0x6e, 0xa8, 0x6f, 0x5b, 0x95, 0x9f, - 0x9e, 0x52, 0xc9, 0xe3, 0x73, 0xfd, 0x0d, 0x22, 0x69, 0x16, 0x2f, 0x34, 0x93, 0x5f, 0xa8, 0x94, 0x63, 0x01, 0xe9, - 0x36, 0xaa, 0xe3, 0xa8, 0x28, 0xf4, 0x61, 0x8d, 0x88, 0xe5, 0x53, 0xb8, 0xd7, 0x54, 0xb5, 0xa4, 0x9f, 0x62, 0xe1, - 0xf9, 0x8d, 0x15, 0xdf, 0xa9, 0x2d, 0x57, 0x61, 0x02, 0x3c, 0xca, 0x61, 0x7e, 0x27, 0x0a, 0x57, 0xfb, 0xdd, 0x0d, - 0x12, 0x5d, 0x47, 0xe1, 0x53, 0x45, 0x9e, 0xac, 0x19, 0x82, 0xf3, 0xbb, 0x5c, 0x10, 0xf3, 0xca, 0x14, 0x14, 0x76, - 0xfc, 0x52, 0xbe, 0x51, 0xd8, 0x92, 0xd1, 0x92, 0x7c, 0x1a, 0xa6, 0x8a, 0x8d, 0x12, 0x57, 0xf1, 0x83, 0xdd, 0x45, - 0xb5, 0xf2, 0x85, 0x6b, 0xc0, 0x56, 0xc4, 0xdb, 0x3b, 0xd9, 0x87, 0x06, 0x3d, 0xa7, 0x06, 0x7a, 0xba, 0x16, 0x64, - 0xf9, 0x44, 0xba, 0xc3, 0x95, 0x9f, 0xdf, 0x60, 0x3f, 0xbf, 0x71, 0xfe, 0xbc, 0x68, 0xde, 0xd0, 0xeb, 0x8f, 0x4c, - 0x34, 0x45, 0x38, 0x6d, 0x82, 0xe1, 0x23, 0x9d, 0xa3, 0x9a, 0x3d, 0xcb, 0x2c, 0x3f, 0x75, 0xd5, 0x41, 0x77, 0x96, - 0x43, 0x56, 0x84, 0x54, 0xdf, 0x83, 0x94, 0xa7, 0xb4, 0x5b, 0xcf, 0xe6, 0xb4, 0x83, 0xec, 0x06, 0x5b, 0x17, 0x0b, - 0x0e, 0x59, 0x14, 0xe2, 0x2e, 0x68, 0x69, 0xb6, 0xde, 0x32, 0x11, 0xf4, 0xd6, 0xc6, 0xfa, 0x81, 0x46, 0x6e, 0x43, - 0x4a, 0xaf, 0x6c, 0x3d, 0x93, 0x60, 0x5b, 0x26, 0xc0, 0xa7, 0x72, 0x1b, 0xc1, 0xa5, 0x6a, 0xfe, 0x5a, 0x49, 0xa1, - 0xab, 0xc5, 0x32, 0xb7, 0xf1, 0x21, 0x90, 0x05, 0xe1, 0x48, 0xd0, 0x0c, 0x3f, 0xa4, 0xe6, 0xb5, 0x3c, 0x86, 0xb4, - 0x00, 0x31, 0x13, 0xb4, 0x8f, 0xa7, 0xb7, 0x0f, 0xef, 0xfe, 0xfe, 0xe9, 0x17, 0x1a, 0x47, 0xe6, 0x5a, 0x1e, 0xd7, - 0xed, 0xc2, 0x46, 0x48, 0xc2, 0xbb, 0x80, 0xa5, 0x52, 0xe6, 0x5d, 0x83, 0x5f, 0xb4, 0x3b, 0xe5, 0x3a, 0x49, 0x37, - 0xa3, 0x89, 0xfc, 0x0a, 0x9f, 0x5e, 0x8a, 0x83, 0x47, 0xd3, 0x5b, 0xb3, 0x1a, 0xed, 0x95, 0xe4, 0xdb, 0x3f, 0x34, - 0xc7, 0x76, 0x7b, 0x52, 0x6f, 0x3d, 0x4f, 0xf4, 0x68, 0x7a, 0xdb, 0x55, 0x82, 0xb6, 0x99, 0x29, 0xa8, 0x5a, 0xd3, - 0x5b, 0x3b, 0xcb, 0xb8, 0xea, 0xc8, 0xf1, 0x0f, 0x72, 0x87, 0x86, 0x39, 0xed, 0xc2, 0xbd, 0xe3, 0x6c, 0x18, 0x26, - 0x5a, 0x98, 0x4f, 0x58, 0x14, 0x25, 0xb4, 0x6b, 0xe4, 0xb5, 0xd3, 0x7e, 0x04, 0x49, 0xba, 0xf6, 0x92, 0xd5, 0x57, - 0xc5, 0x42, 0x5e, 0x89, 0xa7, 0xf0, 0x3a, 0xe7, 0x09, 0x7c, 0xf4, 0x63, 0x23, 0x3a, 0x75, 0xf6, 0x6a, 0xab, 0x42, - 0x9e, 0xfc, 0x5d, 0x9f, 0xcb, 0x51, 0xeb, 0x4f, 0x5d, 0xb9, 0xe0, 0xad, 0xae, 0xe0, 0xd3, 0xa0, 0x79, 0x50, 0x9f, - 0x08, 0xbc, 0x2a, 0xa7, 0x80, 0x37, 0x4c, 0x0b, 0x83, 0xb4, 0x52, 0x7c, 0xda, 0xf1, 0xdb, 0xba, 0x4c, 0x76, 0x00, - 0x79, 0x61, 0x65, 0x51, 0x51, 0x9f, 0xcc, 0xbf, 0xcd, 0x6e, 0x79, 0xb2, 0x79, 0xb7, 0x3c, 0x31, 0xbb, 0xe5, 0x7e, - 0x8a, 0xfd, 0x7c, 0xd4, 0x86, 0x3f, 0xdd, 0x6a, 0x42, 0x41, 0xcb, 0x39, 0x98, 0xde, 0x3a, 0xa0, 0xa7, 0x35, 0x3b, - 0xd3, 0x5b, 0x95, 0x63, 0x0d, 0xb1, 0x9b, 0x16, 0x64, 0x1d, 0xe3, 0x96, 0x03, 0x85, 0xf0, 0xb7, 0x55, 0x7b, 0xd5, - 0x3e, 0x84, 0x77, 0xd0, 0xea, 0x68, 0xfd, 0x5d, 0xe7, 0xfe, 0x4d, 0x1b, 0xa4, 0x5c, 0x78, 0x81, 0xe1, 0xc6, 0xc8, - 0x17, 0xe1, 0xf5, 0x35, 0x8d, 0x82, 0x11, 0x1f, 0xce, 0xf2, 0x7f, 0xd2, 0xf0, 0x6b, 0x24, 0xde, 0xbb, 0xa5, 0x57, - 0xfa, 0x31, 0x4d, 0x55, 0xc6, 0xb7, 0xe9, 0x61, 0x51, 0xae, 0x53, 0x90, 0x0f, 0xc3, 0x84, 0x7a, 0x1d, 0xff, 0x70, - 0xc3, 0x26, 0xf8, 0x77, 0x59, 0x9b, 0x8d, 0x93, 0xf9, 0xbd, 0xc8, 0xb8, 0x17, 0x09, 0xbf, 0x0a, 0x07, 0xf6, 0x1a, - 0xb6, 0x8e, 0x37, 0x83, 0x3b, 0x30, 0x23, 0x5d, 0x18, 0xa1, 0xa0, 0xe5, 0x4e, 0x44, 0x47, 0xe1, 0x2c, 0x11, 0xf7, - 0xf7, 0xba, 0x8d, 0x32, 0xd6, 0x7a, 0xbd, 0x87, 0xa1, 0x57, 0x75, 0x1f, 0xc8, 0xa5, 0x3f, 0x7f, 0x72, 0x08, 0x7f, - 0x54, 0xfe, 0xd7, 0x5d, 0xa5, 0xab, 0x2b, 0xbb, 0x17, 0x74, 0xf5, 0xdd, 0x9a, 0x32, 0xae, 0x44, 0xb8, 0xd4, 0xc7, - 0x1f, 0x5a, 0x1b, 0xb4, 0xca, 0x07, 0x55, 0xd7, 0x5a, 0xd6, 0xaf, 0xaa, 0xfd, 0xeb, 0x3a, 0x7f, 0x60, 0xdd, 0xa1, - 0xd2, 0x5c, 0xeb, 0x75, 0xf5, 0x67, 0x08, 0xd7, 0x2a, 0x1b, 0x8c, 0xcb, 0xfa, 0xbb, 0xe4, 0xae, 0x34, 0x51, 0x54, - 0x34, 0x16, 0xac, 0x94, 0x5d, 0x65, 0xa5, 0xe4, 0x94, 0x5c, 0x9d, 0xf4, 0x6f, 0x27, 0x89, 0x33, 0x57, 0xc7, 0x25, - 0x89, 0xdb, 0xf6, 0x5b, 0xae, 0x23, 0xf3, 0x00, 0xe0, 0xd6, 0x76, 0x57, 0x7e, 0xde, 0xd6, 0xed, 0x83, 0xa6, 0x35, - 0x1f, 0x4b, 0xcd, 0xee, 0x65, 0x78, 0x47, 0xb3, 0xcb, 0x8e, 0xeb, 0x80, 0x9f, 0xa6, 0xa9, 0x52, 0x26, 0x64, 0x99, - 0xd3, 0x71, 0x9d, 0xdb, 0x49, 0x92, 0xe6, 0xc4, 0x8d, 0x85, 0x98, 0x06, 0xea, 0xfb, 0xb7, 0x37, 0x07, 0x3e, 0xcf, - 0xc6, 0xfb, 0x9d, 0x56, 0xab, 0x05, 0x17, 0xc0, 0xba, 0xce, 0x9c, 0xd1, 0x9b, 0xa7, 0xfc, 0x96, 0xb8, 0x2d, 0xa7, - 0xe5, 0xb4, 0x3b, 0xc7, 0x4e, 0xbb, 0x73, 0xe8, 0x3f, 0x3a, 0x76, 0x7b, 0x9f, 0x39, 0xce, 0x49, 0x44, 0x47, 0x39, - 0xfc, 0x70, 0x9c, 0x13, 0xa9, 0x78, 0xa9, 0xdf, 0x8e, 0xe3, 0x0f, 0x93, 0xbc, 0xd9, 0x76, 0x16, 0xfa, 0xd1, 0x71, - 0xe0, 0x50, 0x69, 0xe0, 0x7c, 0x3e, 0xea, 0x8c, 0x0e, 0x47, 0x4f, 0xba, 0xba, 0xb8, 0xf8, 0xac, 0x56, 0x1d, 0xab, - 0xff, 0x3b, 0x56, 0xb3, 0x5c, 0x64, 0xfc, 0x23, 0xd5, 0x39, 0x89, 0x0e, 0x88, 0x9e, 0x8d, 0x4d, 0x3b, 0xeb, 0x23, - 0xb5, 0x8f, 0xaf, 0x87, 0xa3, 0x4e, 0x55, 0x5d, 0xc2, 0xb8, 0x5f, 0x02, 0x79, 0xb2, 0x6f, 0x40, 0x3f, 0xb1, 0xd1, - 0xd4, 0x6e, 0x6e, 0x42, 0x54, 0xdb, 0xd5, 0x73, 0x1c, 0x9b, 0xf9, 0x9d, 0xc0, 0x19, 0x06, 0xa3, 0xab, 0x4a, 0x08, - 0x5c, 0x27, 0x22, 0xee, 0xab, 0x76, 0xe7, 0x18, 0xb7, 0xdb, 0x8f, 0xfc, 0x47, 0xc7, 0xc3, 0x16, 0x3e, 0xf4, 0x0f, - 0x9b, 0x07, 0xfe, 0x23, 0x7c, 0xdc, 0x3c, 0xc6, 0xc7, 0x2f, 0x8e, 0x87, 0xcd, 0x43, 0xff, 0x10, 0xb7, 0x9a, 0xc7, - 0x50, 0xd8, 0x3c, 0x6e, 0x1e, 0xcf, 0x9b, 0x87, 0xc7, 0xc3, 0x96, 0x2c, 0xed, 0xf8, 0x47, 0x47, 0xcd, 0x76, 0xcb, - 0x3f, 0x3a, 0xc2, 0x47, 0xfe, 0xa3, 0x47, 0xcd, 0xf6, 0x81, 0xff, 0xe8, 0xd1, 0xcb, 0xa3, 0x63, 0xff, 0x00, 0xde, - 0x1d, 0x1c, 0x0c, 0x0f, 0xfc, 0x76, 0xbb, 0x09, 0xff, 0xe0, 0x63, 0xbf, 0xa3, 0x7e, 0xb4, 0xdb, 0xfe, 0x41, 0x1b, - 0xb7, 0x92, 0xa3, 0x8e, 0xff, 0xe8, 0x09, 0x96, 0xff, 0xca, 0x6a, 0x58, 0xfe, 0x03, 0xdd, 0xe0, 0x27, 0x7e, 0xe7, - 0x91, 0xfa, 0x25, 0x3b, 0x9c, 0x1f, 0x1e, 0xff, 0xe0, 0xee, 0x6f, 0x9d, 0x43, 0x5b, 0xcd, 0xe1, 0xf8, 0xc8, 0x3f, - 0x38, 0xc0, 0x87, 0x6d, 0xff, 0xf8, 0x20, 0x6e, 0x1e, 0x76, 0xfc, 0x47, 0x8f, 0x87, 0xcd, 0xb6, 0xff, 0xf8, 0x31, - 0x6e, 0x35, 0x0f, 0xfc, 0x0e, 0x6e, 0xfb, 0x87, 0x07, 0xf2, 0xc7, 0x81, 0xdf, 0x99, 0x3f, 0x7e, 0xe2, 0x3f, 0x3a, - 0x8a, 0x1f, 0xf9, 0x87, 0xdf, 0x1e, 0x1e, 0xfb, 0x9d, 0x83, 0xf8, 0xe0, 0x91, 0xdf, 0x79, 0x3c, 0x7f, 0xe4, 0x1f, - 0xc6, 0xcd, 0xce, 0xa3, 0x7b, 0x5b, 0xb6, 0x3b, 0x3e, 0xe0, 0x48, 0xbe, 0x86, 0x17, 0x58, 0xbf, 0x80, 0xbf, 0xb1, - 0x6c, 0xfb, 0xef, 0xd8, 0x4d, 0xbe, 0xde, 0xf4, 0x89, 0x7f, 0xfc, 0x78, 0xa8, 0xaa, 0x43, 0x41, 0xd3, 0xd4, 0x80, - 0x26, 0xf3, 0xa6, 0x1a, 0x56, 0x76, 0xd7, 0x34, 0x1d, 0x99, 0xbf, 0x7a, 0xb0, 0x79, 0x13, 0x06, 0x56, 0xe3, 0xfe, - 0x87, 0xf6, 0x53, 0x2e, 0xf9, 0xc9, 0xfe, 0x58, 0x91, 0xfe, 0xb8, 0xf7, 0x99, 0xba, 0xdd, 0xf9, 0xb3, 0x2b, 0x9c, - 0x6e, 0x73, 0x7c, 0x64, 0x9f, 0x76, 0x7c, 0x70, 0xfa, 0x10, 0xcf, 0x47, 0xf6, 0x87, 0x7b, 0x3e, 0x52, 0xba, 0xe2, - 0x38, 0xbf, 0x16, 0x6b, 0x0e, 0x8e, 0x55, 0xab, 0xf8, 0xa9, 0xf0, 0x06, 0x39, 0x7c, 0x47, 0xac, 0xe8, 0x5e, 0x0b, - 0xc2, 0xa9, 0xed, 0x07, 0xe2, 0xc0, 0x62, 0xaf, 0x85, 0xe2, 0xb1, 0xc9, 0x36, 0x84, 0x84, 0x9f, 0x46, 0xc8, 0xb7, - 0x0f, 0xc1, 0x47, 0xf8, 0x87, 0xe3, 0x23, 0xb1, 0xf1, 0x51, 0xf3, 0xe5, 0x4b, 0x4f, 0x83, 0xf4, 0x14, 0x9c, 0xcb, - 0x67, 0x0f, 0x0e, 0x51, 0x35, 0xdc, 0x7d, 0x0a, 0x45, 0xb9, 0xab, 0x22, 0x5f, 0xef, 0x7e, 0x4d, 0xd8, 0x41, 0x9d, - 0x98, 0x24, 0xae, 0x76, 0xcb, 0x4c, 0xa5, 0xd4, 0xd1, 0x0f, 0xa5, 0x50, 0xea, 0xf8, 0x2d, 0xbf, 0x55, 0xba, 0x74, - 0xe0, 0x94, 0x2c, 0x59, 0x70, 0x11, 0xc2, 0x17, 0x6b, 0x13, 0x3e, 0x96, 0xdf, 0xb6, 0x85, 0xaf, 0x09, 0x40, 0xd2, - 0xcf, 0x50, 0x7d, 0xc8, 0x21, 0x70, 0x5d, 0x7d, 0xb7, 0x06, 0x9c, 0xc2, 0xfc, 0x06, 0x4e, 0xaa, 0x9a, 0xa8, 0xc4, - 0x04, 0xbc, 0x1d, 0xaf, 0x68, 0xc4, 0x42, 0xcf, 0xf5, 0xa6, 0x19, 0x1d, 0xd1, 0x2c, 0x6f, 0xd6, 0x8e, 0x6f, 0xca, - 0x93, 0x9b, 0xc8, 0x35, 0x9f, 0x46, 0xcd, 0xe0, 0x76, 0x6c, 0x32, 0xd0, 0xfe, 0x46, 0x57, 0x1b, 0x60, 0x6e, 0x81, - 0x4d, 0x49, 0x06, 0xb2, 0xb6, 0x52, 0xda, 0x5c, 0xa5, 0xb5, 0xb5, 0xfd, 0xce, 0x11, 0x72, 0x64, 0x31, 0xdc, 0x3b, - 0xfc, 0xbd, 0xd7, 0x3c, 0x68, 0xfd, 0x09, 0x59, 0xcd, 0xca, 0x8e, 0x2e, 0xb4, 0xbb, 0x2d, 0xad, 0xbe, 0x29, 0x5d, - 0x3f, 0x5b, 0xeb, 0x2a, 0x8a, 0xf8, 0x5c, 0xcd, 0xdd, 0x45, 0xdd, 0x54, 0x47, 0xb8, 0xd5, 0x0d, 0x11, 0x23, 0x36, - 0xf6, 0xec, 0x2f, 0x06, 0xab, 0x7b, 0x8d, 0xe5, 0x87, 0xc6, 0x51, 0x51, 0x55, 0x49, 0xd1, 0x42, 0xc6, 0x5b, 0x58, - 0xea, 0xa4, 0xcb, 0xa5, 0x97, 0x82, 0x8b, 0x9c, 0x58, 0x38, 0x85, 0x67, 0x54, 0x43, 0x72, 0x8a, 0x4b, 0x80, 0x24, - 0x82, 0x49, 0xaa, 0xfe, 0xaf, 0x8a, 0xcd, 0x0f, 0xed, 0xf8, 0xf2, 0x93, 0x30, 0x1d, 0x03, 0x15, 0x86, 0xe9, 0x78, - 0xcd, 0xad, 0xa6, 0x42, 0x46, 0x2b, 0xa5, 0x55, 0x57, 0x95, 0xfb, 0x2c, 0x7f, 0x7a, 0xf7, 0x5e, 0x5f, 0x80, 0xe6, - 0x82, 0x77, 0x5a, 0x46, 0x38, 0xaa, 0xcb, 0x9a, 0x1b, 0xe4, 0x8b, 0x93, 0x09, 0x15, 0xa1, 0xca, 0xd7, 0x04, 0x7d, - 0x02, 0x4e, 0xcd, 0x3a, 0xda, 0x1a, 0x25, 0xae, 0x94, 0xee, 0x24, 0xa2, 0x73, 0x36, 0xd4, 0xa2, 0x1e, 0x3b, 0xfa, - 0xe6, 0x80, 0xa6, 0x5c, 0x1a, 0xd2, 0xc6, 0xca, 0x1f, 0x33, 0x0c, 0x65, 0x46, 0x3e, 0x49, 0xb9, 0xdb, 0xfb, 0xa2, - 0xfc, 0xfa, 0xe9, 0xb6, 0x45, 0x48, 0x58, 0xfa, 0x71, 0x90, 0xd1, 0xe4, 0x9f, 0xc8, 0x17, 0x6c, 0xc8, 0xd3, 0x2f, - 0x2e, 0xe0, 0xab, 0xf4, 0x7e, 0x9c, 0xd1, 0x11, 0xf9, 0x02, 0x64, 0x7c, 0x20, 0xad, 0x0f, 0x60, 0x84, 0x8d, 0xdb, - 0x49, 0x82, 0xa5, 0xc6, 0xf4, 0x00, 0x85, 0x48, 0x81, 0xeb, 0x76, 0x8e, 0x5c, 0x47, 0xd9, 0xc4, 0xf2, 0x77, 0x4f, - 0x89, 0x53, 0xa9, 0x04, 0x38, 0xed, 0x8e, 0x7f, 0x14, 0x77, 0xfc, 0x27, 0xf3, 0xc7, 0xfe, 0x71, 0xdc, 0x7e, 0x3c, - 0x6f, 0xc2, 0xff, 0x1d, 0xff, 0x49, 0xd2, 0xec, 0xf8, 0x4f, 0xe0, 0xef, 0xb7, 0x87, 0xfe, 0x51, 0xdc, 0x6c, 0xfb, - 0xc7, 0xf3, 0x03, 0xff, 0xe0, 0x65, 0xbb, 0xe3, 0x1f, 0x38, 0x6d, 0x47, 0xb5, 0x03, 0x76, 0xad, 0xb8, 0xf3, 0x17, - 0x2b, 0x1b, 0x62, 0x43, 0x38, 0x4e, 0xe5, 0x9c, 0xba, 0xd8, 0x2b, 0xbf, 0xb1, 0xa8, 0xf7, 0xa7, 0x76, 0xd6, 0x3d, - 0x0b, 0x33, 0xf8, 0xd0, 0x4d, 0x7d, 0xef, 0xd6, 0xde, 0xe1, 0x1a, 0xbf, 0xd8, 0x30, 0x04, 0xec, 0x70, 0x17, 0xdb, - 0x47, 0xef, 0xe1, 0xdc, 0xba, 0xbc, 0x17, 0xdc, 0x5c, 0x8f, 0xb8, 0x9d, 0xb4, 0x55, 0x45, 0x73, 0x05, 0xa3, 0x64, - 0x16, 0x4c, 0x7e, 0x81, 0x41, 0x0e, 0xf2, 0x55, 0x54, 0xac, 0x8e, 0x0f, 0xa9, 0xaf, 0x19, 0xb7, 0x6e, 0x1f, 0xa0, - 0xd5, 0x81, 0x8d, 0x88, 0xc1, 0x7d, 0x11, 0x45, 0x61, 0x40, 0xaf, 0xb9, 0x69, 0x2b, 0x2c, 0x49, 0x7e, 0x41, 0xf3, - 0xbe, 0x0b, 0x45, 0x6e, 0xe0, 0x4a, 0x17, 0x9f, 0x5b, 0x7e, 0xec, 0xa7, 0x24, 0xec, 0xaa, 0x00, 0xcb, 0x43, 0x57, - 0xb0, 0x6b, 0x01, 0x3f, 0x2e, 0xda, 0xdb, 0xdb, 0xba, 0x5f, 0xa4, 0x02, 0x09, 0x73, 0xad, 0xbe, 0x11, 0x62, 0xb3, - 0x22, 0xd7, 0x46, 0x74, 0xd9, 0xaf, 0x44, 0x21, 0xd2, 0x78, 0xba, 0xa6, 0xa1, 0xf0, 0xc3, 0x54, 0x25, 0xd1, 0x58, - 0x0c, 0x0b, 0xb7, 0xe9, 0x01, 0x2a, 0xb8, 0x08, 0xad, 0xef, 0x00, 0xeb, 0x7d, 0xce, 0x45, 0x68, 0xce, 0xd2, 0x5a, - 0xd7, 0x06, 0x81, 0xa3, 0x37, 0xee, 0xf4, 0xde, 0xbc, 0x3f, 0x75, 0xd4, 0xf6, 0x3c, 0xd9, 0x8f, 0x3b, 0xbd, 0x13, - 0xe9, 0x33, 0x51, 0x27, 0xf1, 0x88, 0x3a, 0x89, 0xe7, 0xe8, 0x53, 0x99, 0x10, 0x49, 0x2b, 0xf6, 0xd5, 0xb4, 0xa5, - 0xcd, 0xa0, 0xbc, 0xbd, 0x93, 0x59, 0x22, 0x18, 0xdc, 0x71, 0xbd, 0x2f, 0x8f, 0xe1, 0xc1, 0x82, 0x95, 0x79, 0xd8, - 0x5a, 0x3b, 0xbc, 0x16, 0xa9, 0xf1, 0x0d, 0x8f, 0x58, 0x42, 0x4d, 0xe6, 0xb5, 0xee, 0xaa, 0x3c, 0x29, 0xb0, 0x5e, - 0x3b, 0x9f, 0x5d, 0x4f, 0x98, 0x70, 0xcd, 0x79, 0x86, 0x0f, 0xba, 0xc1, 0x89, 0x1c, 0xaa, 0x77, 0x55, 0x68, 0xe7, - 0xb5, 0xf9, 0x9a, 0x4f, 0x7d, 0x49, 0xf5, 0xec, 0xb5, 0x84, 0x80, 0x13, 0x72, 0xf1, 0x41, 0xaf, 0x74, 0x17, 0xdb, - 0xef, 0x8a, 0x93, 0xfd, 0xf8, 0xa0, 0x77, 0x15, 0x4c, 0x75, 0x7f, 0x2f, 0xf9, 0x78, 0x73, 0x5f, 0x09, 0x1f, 0xf7, - 0xe5, 0x51, 0x10, 0x75, 0x48, 0xd9, 0x28, 0xbf, 0x3c, 0x71, 0x7b, 0x27, 0x5a, 0x19, 0x70, 0x64, 0x60, 0xdd, 0x3d, - 0x6a, 0x99, 0xd3, 0x25, 0x09, 0x1f, 0xc3, 0x86, 0x54, 0x4d, 0xac, 0x41, 0x6a, 0x1e, 0xf7, 0xb8, 0xdd, 0x3b, 0x09, - 0x1d, 0xc9, 0x5b, 0x24, 0xf3, 0xc8, 0x83, 0x7d, 0x68, 0x1c, 0xf3, 0x09, 0xf5, 0x19, 0xdf, 0xbf, 0xa1, 0xd7, 0xcd, - 0x70, 0xca, 0x2a, 0xf7, 0x36, 0x28, 0x1d, 0xe5, 0x90, 0xdc, 0x78, 0xc4, 0xf5, 0xd9, 0xab, 0x4e, 0xe5, 0x6e, 0x3b, - 0x04, 0x9b, 0xc7, 0xb8, 0xe6, 0xa4, 0x4f, 0xce, 0x02, 0x8b, 0xf7, 0x4e, 0xf6, 0xc3, 0x15, 0x8c, 0x48, 0x7e, 0x5f, - 0x68, 0x47, 0x3b, 0x18, 0x36, 0x40, 0x6f, 0xae, 0xa3, 0xc4, 0x81, 0x71, 0xc8, 0x6b, 0x41, 0x5d, 0xb8, 0xbd, 0x7f, - 0xfd, 0x1f, 0xff, 0x4b, 0xfb, 0xd8, 0x4f, 0xf6, 0xe3, 0xb6, 0xe9, 0x6b, 0x65, 0x55, 0x8a, 0x13, 0x38, 0xee, 0x59, - 0x05, 0x85, 0xe9, 0x6d, 0x73, 0x9c, 0xb1, 0xa8, 0x19, 0x87, 0xc9, 0xc8, 0xed, 0x6d, 0xc7, 0xa6, 0x7d, 0x6c, 0x4b, - 0x43, 0x5d, 0x2f, 0x02, 0x7a, 0xfd, 0x4d, 0x07, 0x8f, 0xcc, 0xf9, 0x15, 0xb9, 0xb5, 0xed, 0x63, 0x48, 0xd5, 0xee, - 0xab, 0x1d, 0x45, 0x4a, 0xf5, 0x27, 0xc2, 0x34, 0x07, 0x4c, 0x6b, 0x27, 0x90, 0x0a, 0xd7, 0x29, 0x83, 0x5a, 0xff, - 0xf7, 0x7f, 0xfe, 0x97, 0xff, 0x66, 0x1e, 0x21, 0x56, 0xf5, 0xaf, 0xff, 0xfd, 0x3f, 0xff, 0x9f, 0xff, 0xfd, 0x5f, - 0xe1, 0xd4, 0x8a, 0x8e, 0x67, 0x49, 0xa6, 0xe2, 0x54, 0xc1, 0x2c, 0xc5, 0x5d, 0x1c, 0x48, 0xec, 0x9c, 0xb0, 0x5c, - 0xb0, 0x61, 0xfd, 0x4c, 0xd2, 0xb9, 0x1c, 0x50, 0xee, 0x4c, 0x0d, 0x9d, 0xdc, 0xe1, 0x45, 0x45, 0x50, 0x35, 0x94, - 0x4b, 0xc2, 0x2d, 0x4e, 0xf6, 0x01, 0xdf, 0x0f, 0x3b, 0xc6, 0xe9, 0x97, 0xcb, 0xb1, 0x30, 0x64, 0x02, 0x25, 0x45, - 0x55, 0xee, 0x40, 0x6c, 0x65, 0x01, 0x8f, 0x41, 0xc7, 0x2a, 0x96, 0xab, 0x57, 0x6b, 0xd3, 0xfd, 0x69, 0x96, 0x0b, - 0x36, 0x02, 0x94, 0x2b, 0x3f, 0xb1, 0x0c, 0x63, 0x37, 0x41, 0x57, 0x4c, 0xee, 0x0a, 0xd9, 0x8b, 0x22, 0xd0, 0xc3, - 0xe3, 0x3f, 0x15, 0x7f, 0x99, 0x80, 0x46, 0xe6, 0x78, 0x93, 0xf0, 0x56, 0x9b, 0xe7, 0x8f, 0x5a, 0xad, 0xe9, 0x2d, - 0x5a, 0x54, 0x23, 0xe0, 0x6d, 0x83, 0x49, 0x3a, 0xb6, 0x3b, 0x94, 0xf1, 0xef, 0xd2, 0x8d, 0xdd, 0x72, 0xc0, 0x17, - 0xee, 0xb4, 0x8a, 0xe2, 0xcf, 0x0b, 0xe9, 0x49, 0x65, 0xbf, 0x40, 0x9c, 0x5a, 0x3b, 0x9d, 0xaf, 0xb9, 0x3d, 0xb9, - 0x85, 0xd5, 0xaa, 0xa3, 0x5a, 0xc5, 0xed, 0xf5, 0xd3, 0x89, 0x76, 0x9c, 0xdd, 0x8e, 0x90, 0x1f, 0x42, 0xcc, 0x3b, - 0x6e, 0xe3, 0xb8, 0xb3, 0x28, 0xbb, 0x17, 0x82, 0x4f, 0xec, 0xc0, 0x3a, 0x0d, 0xe9, 0x90, 0x8e, 0x8c, 0xb3, 0x5e, - 0xbf, 0x57, 0x41, 0xf3, 0x22, 0x3e, 0xd8, 0x30, 0x96, 0x06, 0x49, 0x06, 0xd4, 0x9d, 0x56, 0xf1, 0x39, 0xec, 0xc0, - 0xc5, 0x28, 0xe1, 0xa1, 0x08, 0x24, 0xc1, 0x76, 0xed, 0xf0, 0x7c, 0x08, 0x3c, 0x89, 0x2f, 0x2c, 0x78, 0xba, 0xaa, - 0x2a, 0xb8, 0xcd, 0xeb, 0x67, 0x48, 0x0b, 0x5f, 0x36, 0xb7, 0xbb, 0x52, 0x5e, 0xb7, 0x6f, 0x75, 0xd4, 0xfb, 0x5d, - 0xcd, 0x5d, 0xa5, 0x05, 0x52, 0x07, 0x6d, 0x7e, 0xaf, 0xe4, 0xba, 0x7a, 0xfb, 0xb5, 0xf0, 0x5c, 0x09, 0xa6, 0xbb, - 0x5a, 0x4b, 0x16, 0x42, 0xad, 0x77, 0xe4, 0xdb, 0xd2, 0x64, 0x0a, 0xa7, 0x53, 0x59, 0x11, 0x75, 0x4f, 0xf6, 0x95, - 0xa6, 0x0b, 0xdc, 0x43, 0xa6, 0x74, 0xa8, 0x0c, 0x0a, 0x5d, 0x49, 0x6f, 0x05, 0xf5, 0x4b, 0xe7, 0x56, 0xc0, 0xa7, - 0xe3, 0x7a, 0xff, 0x0f, 0x82, 0x7a, 0x0b, 0xa7, 0xcf, 0x89, 0x00, 0x00}; + 0x65, 0x7c, 0xed, 0x25, 0x00, 0x5b, 0x3e, 0xbd, 0x0f, 0xc7, 0xe5, 0xef, 0x57, 0x34, 0xcf, 0xc3, 0xb1, 0xae, 0xb9, + 0x3d, 0x9e, 0x26, 0x41, 0xb4, 0x63, 0x69, 0x06, 0x08, 0x88, 0x89, 0x01, 0x46, 0xc0, 0xa7, 0xa1, 0x43, 0x64, 0x30, + 0xf5, 0x7a, 0x74, 0x4d, 0xe2, 0xaa, 0x5e, 0x24, 0xc2, 0x71, 0x55, 0x70, 0x32, 0xcd, 0xa8, 0x2c, 0x55, 0x68, 0x2c, + 0x4e, 0xf6, 0xa1, 0x40, 0xbd, 0xde, 0x12, 0x45, 0x33, 0x0e, 0x94, 0xed, 0xb1, 0x34, 0xc7, 0x44, 0xd1, 0xec, 0x44, + 0xa5, 0x32, 0x4b, 0x69, 0x3d, 0x76, 0xf3, 0x79, 0x7b, 0x08, 0x7f, 0x74, 0x64, 0xe8, 0xf3, 0xd1, 0x68, 0x74, 0x6f, + 0x54, 0xed, 0xf3, 0x68, 0x44, 0x3b, 0xf4, 0xa8, 0x0b, 0x49, 0x2c, 0x4d, 0x1d, 0x8b, 0x69, 0x17, 0x12, 0x77, 0x8b, + 0x87, 0x55, 0x86, 0xb0, 0x8d, 0x88, 0x17, 0x0f, 0x8f, 0xb0, 0x15, 0xd3, 0x8c, 0x2e, 0x26, 0x61, 0x36, 0x66, 0x69, + 0xd0, 0x2a, 0xfc, 0xb9, 0x0e, 0x49, 0x7d, 0x7e, 0x7c, 0x7c, 0x5c, 0xf8, 0x91, 0x79, 0x6a, 0x45, 0x51, 0xe1, 0x0f, + 0x17, 0xe5, 0x34, 0x5a, 0xad, 0xd1, 0xa8, 0xf0, 0x99, 0x29, 0x38, 0xe8, 0x0c, 0xa3, 0x83, 0x4e, 0xe1, 0xdf, 0x58, + 0x35, 0x0a, 0x9f, 0xea, 0xa7, 0x8c, 0x46, 0xb5, 0x4c, 0x98, 0xc7, 0xad, 0x56, 0xe1, 0x2b, 0x42, 0x5b, 0x80, 0x59, + 0xaa, 0x7e, 0x06, 0xe1, 0x4c, 0x70, 0x60, 0xee, 0xdd, 0x44, 0x78, 0x83, 0x4b, 0x7d, 0xcb, 0x88, 0xfa, 0x26, 0x47, + 0x81, 0x2e, 0xf0, 0xcf, 0x76, 0xf0, 0x08, 0x88, 0x59, 0x06, 0x8d, 0x12, 0x13, 0x5b, 0xaa, 0xbd, 0x06, 0xca, 0x92, + 0xaf, 0x7f, 0x26, 0x49, 0x15, 0x53, 0x02, 0x4e, 0x06, 0x35, 0xd5, 0x65, 0x78, 0x94, 0x6e, 0x91, 0x1f, 0xec, 0xd3, + 0xf2, 0xe3, 0xee, 0x21, 0xe2, 0x83, 0xfd, 0xe1, 0xe2, 0x83, 0x52, 0x4b, 0x7c, 0x28, 0xe6, 0x71, 0x27, 0x88, 0x3b, + 0x8c, 0xe9, 0xf0, 0xe3, 0x35, 0xbf, 0x6d, 0xc2, 0x96, 0xc8, 0x5c, 0x29, 0x58, 0x76, 0x7f, 0x6b, 0xd6, 0x8c, 0xe9, + 0xcc, 0xfa, 0xa2, 0x87, 0x54, 0x1f, 0xde, 0xa4, 0xc4, 0x7d, 0x63, 0x6c, 0x5b, 0x55, 0x32, 0x1a, 0x11, 0xf7, 0xcd, + 0x68, 0xe4, 0x9a, 0xb3, 0x92, 0xa1, 0xa0, 0xb2, 0xd6, 0xeb, 0x5a, 0x89, 0xac, 0xf5, 0xe5, 0x97, 0x76, 0x99, 0x5d, + 0xa0, 0x43, 0x4f, 0x76, 0x98, 0x49, 0xbf, 0x89, 0x58, 0x0e, 0x5b, 0x0d, 0x3e, 0x34, 0x52, 0xbf, 0xab, 0x31, 0xad, + 0x5d, 0xab, 0x5d, 0x02, 0xbc, 0xe1, 0x2e, 0xf0, 0xd5, 0x8b, 0x02, 0xc6, 0xd4, 0xe4, 0x2d, 0x3e, 0xbd, 0xfb, 0x2a, + 0xf2, 0xee, 0x04, 0x2a, 0x58, 0xfe, 0x26, 0x5d, 0x39, 0x04, 0xa4, 0x60, 0x24, 0xc4, 0x9e, 0x56, 0x21, 0xf8, 0x78, + 0x9c, 0xc0, 0xb7, 0x5e, 0x16, 0xb5, 0xfb, 0x63, 0x55, 0xf3, 0x7e, 0x6d, 0xbe, 0x81, 0xdd, 0x50, 0xdf, 0xb6, 0x2a, + 0x3f, 0x3d, 0xa5, 0x92, 0xc7, 0xe7, 0xfa, 0x1b, 0x44, 0xd2, 0x2c, 0x5e, 0x68, 0x26, 0xbf, 0x50, 0x29, 0xc7, 0x02, + 0xd2, 0x6d, 0x54, 0xc7, 0x51, 0x51, 0xe8, 0xc3, 0x1a, 0x11, 0xcb, 0xa7, 0x70, 0xaf, 0xa9, 0x6a, 0x49, 0x3f, 0xc5, + 0xc2, 0xf3, 0x1b, 0x2b, 0xbe, 0x53, 0x5b, 0xae, 0xc2, 0x04, 0x78, 0x94, 0xc3, 0xfc, 0x4e, 0x14, 0xae, 0xf6, 0xbb, + 0x1b, 0x24, 0xba, 0x8e, 0xc2, 0xa7, 0x8a, 0x3c, 0x59, 0x33, 0x04, 0xe7, 0x77, 0xb9, 0x20, 0xe6, 0x95, 0x29, 0x28, + 0xec, 0xf8, 0xa5, 0x7c, 0xa3, 0xb0, 0x25, 0xa3, 0x25, 0xf9, 0x34, 0x4c, 0x15, 0x1b, 0x25, 0xae, 0xe2, 0x07, 0xbb, + 0x8b, 0x6a, 0xe5, 0x0b, 0xd7, 0x80, 0xad, 0x88, 0xb7, 0x77, 0xb2, 0x0f, 0x0d, 0x7a, 0x4e, 0x0d, 0xf4, 0x74, 0x2d, + 0xc8, 0xf2, 0x89, 0x74, 0x87, 0x2b, 0x3f, 0xbf, 0xc1, 0x7e, 0x7e, 0xe3, 0xfc, 0x79, 0xd1, 0xbc, 0xa1, 0xd7, 0x1f, + 0x99, 0x68, 0x8a, 0x70, 0xda, 0x04, 0xc3, 0x47, 0x3a, 0x47, 0x35, 0x7b, 0x96, 0x59, 0x7e, 0xea, 0xaa, 0x83, 0xee, + 0x2c, 0x87, 0xac, 0x08, 0xa9, 0xbe, 0x07, 0x29, 0x4f, 0x69, 0xb7, 0x9e, 0xcd, 0x69, 0x07, 0xd9, 0x0d, 0xb6, 0x2e, + 0x16, 0x1c, 0xb2, 0x28, 0xc4, 0x5d, 0xd0, 0xd2, 0x6c, 0xbd, 0x65, 0x22, 0xe8, 0xad, 0x8d, 0xf5, 0x03, 0x8d, 0xdc, + 0x86, 0x94, 0x5e, 0xd9, 0x7a, 0x26, 0xc1, 0xb6, 0x4c, 0x80, 0x4f, 0xe5, 0x36, 0x82, 0x4b, 0xd5, 0xfc, 0xb5, 0x92, + 0x42, 0x57, 0x8b, 0x65, 0x6e, 0xe3, 0x43, 0x20, 0x0b, 0xc2, 0x91, 0xa0, 0x19, 0x7e, 0x48, 0xcd, 0x6b, 0x79, 0x0c, + 0x69, 0x01, 0x62, 0x26, 0x68, 0x1f, 0x4f, 0x6f, 0x1f, 0xde, 0xfd, 0xfd, 0xd3, 0x2f, 0x34, 0x8e, 0xcc, 0xb5, 0x3c, + 0xae, 0xdb, 0x85, 0x8d, 0x90, 0x84, 0x77, 0x01, 0x4b, 0xa5, 0xcc, 0xbb, 0x06, 0xbf, 0x68, 0x77, 0xca, 0x75, 0x92, + 0x6e, 0x46, 0x13, 0xf9, 0x15, 0x3e, 0xbd, 0x14, 0x07, 0x8f, 0xa6, 0xb7, 0x66, 0x35, 0xda, 0x2b, 0xc9, 0xb7, 0x7f, + 0x68, 0x8e, 0xed, 0xf6, 0xa4, 0xde, 0x7a, 0x9e, 0xe8, 0xd1, 0xf4, 0xb6, 0xab, 0x04, 0x6d, 0x33, 0x53, 0x50, 0xb5, + 0xa6, 0xb7, 0x76, 0x96, 0x71, 0xd5, 0x91, 0xe3, 0x1f, 0xe4, 0x0e, 0x0d, 0x73, 0xda, 0x85, 0x7b, 0xc7, 0xd9, 0x30, + 0x4c, 0xb4, 0x30, 0x9f, 0xb0, 0x28, 0x4a, 0x68, 0xd7, 0xc8, 0x6b, 0xa7, 0xfd, 0x08, 0x92, 0x74, 0xed, 0x25, 0xab, + 0xaf, 0x8a, 0x85, 0xbc, 0x12, 0x4f, 0xe1, 0x75, 0xce, 0x13, 0xf8, 0xe8, 0xc7, 0x46, 0x74, 0xea, 0xec, 0xd5, 0x56, + 0x85, 0x3c, 0xf9, 0xbb, 0x3e, 0x97, 0xa3, 0xd6, 0x9f, 0xba, 0x72, 0xc1, 0x5b, 0x5d, 0xc1, 0xa7, 0x41, 0xf3, 0xa0, + 0x3e, 0x11, 0x78, 0x55, 0x4e, 0x01, 0x6f, 0x98, 0x16, 0x06, 0x69, 0xa5, 0xf8, 0xb4, 0xe3, 0xb7, 0x75, 0x99, 0xec, + 0x00, 0xf2, 0xc2, 0xca, 0xa2, 0xa2, 0x3e, 0x99, 0x7f, 0x9b, 0xdd, 0xf2, 0x64, 0xf3, 0x6e, 0x79, 0x62, 0x76, 0xcb, + 0xfd, 0x14, 0xfb, 0xf9, 0xa8, 0x0d, 0x7f, 0xba, 0xd5, 0x84, 0x82, 0x96, 0x73, 0x30, 0xbd, 0x75, 0x40, 0x4f, 0x6b, + 0x76, 0xa6, 0xb7, 0x2a, 0xc7, 0x1a, 0x62, 0x37, 0x2d, 0xc8, 0x3a, 0xc6, 0x2d, 0x07, 0x0a, 0xe1, 0x6f, 0xab, 0xf6, + 0xaa, 0x7d, 0x08, 0xef, 0xa0, 0xd5, 0xd1, 0xfa, 0xbb, 0xce, 0xfd, 0x9b, 0x36, 0x48, 0xb9, 0xf0, 0x02, 0xc3, 0x8d, + 0x91, 0x2f, 0xc2, 0xeb, 0x6b, 0x1a, 0x05, 0x23, 0x3e, 0x9c, 0xe5, 0xff, 0xa4, 0xe1, 0xd7, 0x48, 0xbc, 0x77, 0x4b, + 0xaf, 0xf4, 0x63, 0x9a, 0xaa, 0x8c, 0x6f, 0xd3, 0xc3, 0xa2, 0x5c, 0xa7, 0x20, 0x1f, 0x86, 0x09, 0xf5, 0x3a, 0xfe, + 0xe1, 0x86, 0x4d, 0xf0, 0xef, 0xb2, 0x36, 0x1b, 0x27, 0xf3, 0x7b, 0x91, 0x71, 0x2f, 0x12, 0x7e, 0x15, 0x0e, 0xec, + 0x35, 0x6c, 0x1d, 0x6f, 0x06, 0x77, 0x60, 0x46, 0xba, 0x30, 0x42, 0x41, 0xcb, 0x9d, 0x88, 0x8e, 0xc2, 0x59, 0x22, + 0xee, 0xef, 0x75, 0x1b, 0x65, 0xac, 0xf5, 0x7a, 0x0f, 0x43, 0xaf, 0xea, 0x3e, 0x90, 0x4b, 0x7f, 0xfe, 0xe4, 0x10, + 0xfe, 0xa8, 0xfc, 0xaf, 0xbb, 0x4a, 0x57, 0x57, 0x76, 0x2f, 0xe8, 0xea, 0xbb, 0x35, 0x65, 0x5c, 0x89, 0x70, 0xa9, + 0x8f, 0x3f, 0xb4, 0x36, 0x68, 0x95, 0x0f, 0xaa, 0xae, 0xb5, 0xac, 0x5f, 0x55, 0xfb, 0xd7, 0x75, 0xfe, 0xc0, 0xba, + 0x43, 0xa5, 0xb9, 0xd6, 0xeb, 0xea, 0xcf, 0x10, 0xae, 0x55, 0x36, 0x18, 0x97, 0xf5, 0x77, 0xc9, 0x5d, 0x69, 0xa2, + 0xa8, 0x68, 0x2c, 0x58, 0x29, 0xbb, 0xca, 0x4a, 0xc9, 0x29, 0xb9, 0x3a, 0xe9, 0xdf, 0x4e, 0x12, 0x67, 0xae, 0x8e, + 0x4b, 0x12, 0xb7, 0xed, 0xb7, 0x5c, 0x47, 0xe6, 0x01, 0xc0, 0xad, 0xed, 0xae, 0xfc, 0xbc, 0xad, 0xdb, 0x07, 0x4d, + 0x6b, 0x3e, 0x96, 0x9a, 0xdd, 0xcb, 0xf0, 0x8e, 0x66, 0x97, 0x1d, 0xd7, 0x01, 0x3f, 0x4d, 0x53, 0xa5, 0x4c, 0xc8, + 0x32, 0xa7, 0xe3, 0x3a, 0xb7, 0x93, 0x24, 0xcd, 0x89, 0x1b, 0x0b, 0x31, 0x0d, 0xd4, 0xf7, 0x6f, 0x6f, 0x0e, 0x7c, + 0x9e, 0x8d, 0xf7, 0x3b, 0xad, 0x56, 0x0b, 0x2e, 0x80, 0x75, 0x9d, 0x39, 0xa3, 0x37, 0x4f, 0xf9, 0x2d, 0x71, 0x5b, + 0x4e, 0xcb, 0x69, 0x77, 0x8e, 0x9d, 0x76, 0xe7, 0xd0, 0x7f, 0x74, 0xec, 0xf6, 0x3e, 0x73, 0x9c, 0x93, 0x88, 0x8e, + 0x72, 0xf8, 0xe1, 0x38, 0x27, 0x52, 0xf1, 0x52, 0xbf, 0x1d, 0xc7, 0x1f, 0x26, 0x79, 0xb3, 0xed, 0x2c, 0xf4, 0xa3, + 0xe3, 0xc0, 0xa1, 0xd2, 0xc0, 0xf9, 0x7c, 0xd4, 0x19, 0x1d, 0x8e, 0x9e, 0x74, 0x75, 0x71, 0xf1, 0x59, 0xad, 0x3a, + 0x56, 0xff, 0x77, 0xac, 0x66, 0xb9, 0xc8, 0xf8, 0x47, 0xaa, 0x73, 0x12, 0x1d, 0x10, 0x3d, 0x1b, 0x9b, 0x76, 0xd6, + 0x47, 0x6a, 0x1f, 0x5f, 0x0f, 0x47, 0x9d, 0xaa, 0xba, 0x84, 0x71, 0xbf, 0x04, 0xf2, 0x64, 0xdf, 0x80, 0x7e, 0x62, + 0xa3, 0xa9, 0xdd, 0xdc, 0x84, 0xa8, 0xb6, 0xab, 0xe7, 0x38, 0x36, 0xf3, 0x3b, 0x81, 0x33, 0x0c, 0x46, 0x57, 0x95, + 0x10, 0xb8, 0x4e, 0x44, 0xdc, 0x57, 0xed, 0xce, 0x31, 0x6e, 0xb7, 0x1f, 0xf9, 0x8f, 0x8e, 0x87, 0x2d, 0x7c, 0xe8, + 0x1f, 0x36, 0x0f, 0xfc, 0x47, 0xf8, 0xb8, 0x79, 0x8c, 0x8f, 0x5f, 0x1c, 0x0f, 0x9b, 0x87, 0xfe, 0x21, 0x6e, 0x35, + 0x8f, 0xa1, 0xb0, 0x79, 0xdc, 0x3c, 0x9e, 0x37, 0x0f, 0x8f, 0x87, 0x2d, 0x59, 0xda, 0xf1, 0x8f, 0x8e, 0x9a, 0xed, + 0x96, 0x7f, 0x74, 0x84, 0x8f, 0xfc, 0x47, 0x8f, 0x9a, 0xed, 0x03, 0xff, 0xd1, 0xa3, 0x97, 0x47, 0xc7, 0xfe, 0x01, + 0xbc, 0x3b, 0x38, 0x18, 0x1e, 0xf8, 0xed, 0x76, 0x13, 0xfe, 0xc1, 0xc7, 0x7e, 0x47, 0xfd, 0x68, 0xb7, 0xfd, 0x83, + 0x36, 0x6e, 0x25, 0x47, 0x1d, 0xff, 0xd1, 0x13, 0x2c, 0xff, 0x95, 0xd5, 0xb0, 0xfc, 0x07, 0xba, 0xc1, 0x4f, 0xfc, + 0xce, 0x23, 0xf5, 0x4b, 0x76, 0x38, 0x3f, 0x3c, 0xfe, 0xc1, 0xdd, 0xdf, 0x3a, 0x87, 0xb6, 0x9a, 0xc3, 0xf1, 0x91, + 0x7f, 0x70, 0x80, 0x0f, 0xdb, 0xfe, 0xf1, 0x41, 0xdc, 0x3c, 0xec, 0xf8, 0x8f, 0x1e, 0x0f, 0x9b, 0x6d, 0xff, 0xf1, + 0x63, 0xdc, 0x6a, 0x1e, 0xf8, 0x1d, 0xdc, 0xf6, 0x0f, 0x0f, 0xe4, 0x8f, 0x03, 0xbf, 0x33, 0x7f, 0xfc, 0xc4, 0x7f, + 0x74, 0x14, 0x3f, 0xf2, 0x0f, 0xbf, 0x3d, 0x3c, 0xf6, 0x3b, 0x07, 0xf1, 0xc1, 0x23, 0xbf, 0xf3, 0x78, 0xfe, 0xc8, + 0x3f, 0x8c, 0x9b, 0x9d, 0x47, 0xf7, 0xb6, 0x6c, 0x77, 0x7c, 0xc0, 0x91, 0x7c, 0x0d, 0x2f, 0xb0, 0x7e, 0x01, 0x7f, + 0x63, 0xd9, 0xf6, 0xdf, 0xb1, 0x9b, 0x7c, 0xbd, 0xe9, 0x13, 0xff, 0xf8, 0xf1, 0x50, 0x55, 0x87, 0x82, 0xa6, 0xa9, + 0x01, 0x4d, 0xe6, 0x4d, 0x35, 0xac, 0xec, 0xae, 0x69, 0x3a, 0x32, 0x7f, 0xf5, 0x60, 0xf3, 0x26, 0x0c, 0xac, 0xc6, + 0xfd, 0x0f, 0xed, 0xa7, 0x5c, 0xf2, 0x93, 0xfd, 0xb1, 0x22, 0xfd, 0x71, 0xef, 0x33, 0x75, 0xbb, 0xf3, 0x67, 0x57, + 0x38, 0xdd, 0xe6, 0xf8, 0xc8, 0x3e, 0xed, 0xf8, 0xe0, 0xf4, 0x21, 0x9e, 0x8f, 0xec, 0x0f, 0xf7, 0x7c, 0xa4, 0x74, + 0xc5, 0x71, 0x7e, 0x2d, 0xd6, 0x1c, 0x1c, 0xab, 0x56, 0xf1, 0x53, 0xe1, 0x0d, 0x72, 0xf8, 0x8e, 0x58, 0xd1, 0xbd, + 0x16, 0x84, 0x53, 0xdb, 0x0f, 0xc4, 0x81, 0xc5, 0x5e, 0x0b, 0xc5, 0x63, 0x93, 0x6d, 0x08, 0x09, 0x3f, 0x8d, 0x90, + 0x6f, 0x1f, 0x82, 0x8f, 0xf0, 0x0f, 0xc7, 0x47, 0x62, 0xe3, 0xa3, 0xe6, 0xcb, 0x97, 0x9e, 0x06, 0xe9, 0x29, 0x38, + 0x97, 0xcf, 0x1e, 0x1c, 0xa2, 0x6a, 0xb8, 0xfb, 0x14, 0x8a, 0x72, 0x57, 0x45, 0xbe, 0xde, 0xfd, 0x9a, 0xb0, 0x83, + 0x3a, 0x31, 0x49, 0x5c, 0xed, 0x96, 0x99, 0x4a, 0xa9, 0xa3, 0x1f, 0x4a, 0xa1, 0xd4, 0xf1, 0x5b, 0x7e, 0xab, 0x74, + 0xe9, 0xc0, 0x29, 0x59, 0xb2, 0xe0, 0x22, 0x84, 0x2f, 0xd6, 0x26, 0x7c, 0x2c, 0xbf, 0x6d, 0x0b, 0x5f, 0x13, 0x80, + 0xa4, 0x9f, 0xa1, 0xfa, 0x90, 0x43, 0xe0, 0xba, 0xfa, 0x6e, 0x0d, 0x38, 0x85, 0xf9, 0x0d, 0x9c, 0x54, 0x35, 0x51, + 0x89, 0x09, 0x78, 0x3b, 0x5e, 0xd1, 0x88, 0x85, 0x9e, 0xeb, 0x4d, 0x33, 0x3a, 0xa2, 0x59, 0xde, 0xac, 0x1d, 0xdf, + 0x94, 0x27, 0x37, 0x91, 0x6b, 0x3e, 0x8d, 0x9a, 0xc1, 0xed, 0xd8, 0x64, 0xa0, 0xfd, 0x8d, 0xae, 0x36, 0xc0, 0xdc, + 0x02, 0x9b, 0x92, 0x0c, 0x64, 0x6d, 0xa5, 0xb4, 0xb9, 0x4a, 0x6b, 0x6b, 0xfb, 0x9d, 0x23, 0xe4, 0xc8, 0x62, 0xb8, + 0x77, 0xf8, 0x7b, 0xaf, 0x79, 0xd0, 0xfa, 0x13, 0xb2, 0x9a, 0x95, 0x1d, 0x5d, 0x68, 0x77, 0x5b, 0x5a, 0x7d, 0x53, + 0xba, 0x7e, 0xb6, 0xd6, 0x55, 0x14, 0xf1, 0xb9, 0x9a, 0xbb, 0x8b, 0xba, 0xa9, 0x8e, 0x70, 0xab, 0x1b, 0x22, 0x46, + 0x6c, 0xec, 0xd9, 0x5f, 0x0c, 0x56, 0xf7, 0x1a, 0xcb, 0x0f, 0x8d, 0xa3, 0xa2, 0xaa, 0x92, 0xa2, 0x85, 0x8c, 0xb7, + 0xb0, 0xd4, 0x49, 0x97, 0x4b, 0x2f, 0x05, 0x17, 0x39, 0xb1, 0x70, 0x0a, 0xcf, 0xa8, 0x86, 0xe4, 0x14, 0x97, 0x00, + 0x49, 0x04, 0x93, 0x54, 0xfd, 0x5f, 0x15, 0x9b, 0x1f, 0xda, 0xf1, 0xe5, 0x27, 0x61, 0x3a, 0x06, 0x2a, 0x0c, 0xd3, + 0xf1, 0x9a, 0x5b, 0x4d, 0x85, 0x8c, 0x56, 0x4a, 0xab, 0xae, 0x2a, 0xf7, 0x59, 0xfe, 0xf4, 0xee, 0xbd, 0xbe, 0x00, + 0xcd, 0x05, 0xef, 0xb4, 0x8c, 0x70, 0x54, 0x97, 0x35, 0x37, 0xc8, 0x17, 0x27, 0x13, 0x2a, 0x42, 0x95, 0xaf, 0x09, + 0xfa, 0x04, 0x9c, 0x9a, 0x75, 0xb4, 0x35, 0x4a, 0x5c, 0x29, 0xdd, 0x49, 0x44, 0xe7, 0x6c, 0xa8, 0x45, 0x3d, 0x76, + 0xf4, 0xcd, 0x01, 0x4d, 0xb9, 0x34, 0xa4, 0x8d, 0x95, 0x3f, 0x66, 0x18, 0xca, 0x8c, 0x7c, 0x92, 0x72, 0xb7, 0xf7, + 0x45, 0xf9, 0xf5, 0xd3, 0x6d, 0x8b, 0x90, 0xb0, 0xf4, 0xe3, 0x20, 0xa3, 0xc9, 0x3f, 0x91, 0x2f, 0xd8, 0x90, 0xa7, + 0x5f, 0x5c, 0xc0, 0x57, 0xe9, 0xfd, 0x38, 0xa3, 0x23, 0xf2, 0x05, 0xc8, 0xf8, 0x40, 0x5a, 0x1f, 0xc0, 0x08, 0x1b, + 0xb7, 0x93, 0x04, 0x4b, 0x8d, 0xe9, 0x01, 0x0a, 0x91, 0x02, 0xd7, 0xed, 0x1c, 0xb9, 0x8e, 0xb2, 0x89, 0xe5, 0xef, + 0x9e, 0x12, 0xa7, 0x52, 0x09, 0x70, 0xda, 0x1d, 0xff, 0x28, 0xee, 0xf8, 0x4f, 0xe6, 0x8f, 0xfd, 0xe3, 0xb8, 0xfd, + 0x78, 0xde, 0x84, 0xff, 0x3b, 0xfe, 0x93, 0xa4, 0xd9, 0xf1, 0x9f, 0xc0, 0xdf, 0x6f, 0x0f, 0xfd, 0xa3, 0xb8, 0xd9, + 0xf6, 0x8f, 0xe7, 0x07, 0xfe, 0xc1, 0xcb, 0x76, 0xc7, 0x3f, 0x70, 0xda, 0x8e, 0x6a, 0x07, 0xec, 0x5a, 0x71, 0xe7, + 0x2f, 0x56, 0x36, 0xc4, 0x86, 0x70, 0x9c, 0xca, 0x39, 0x75, 0xb1, 0x57, 0x7e, 0x63, 0x51, 0xef, 0x4f, 0xed, 0xac, + 0x7b, 0x16, 0x66, 0xf0, 0xa1, 0x9b, 0xfa, 0xde, 0xad, 0xbd, 0xc3, 0x35, 0x7e, 0xb1, 0x61, 0x08, 0xd8, 0xe1, 0x2e, + 0xb6, 0x8f, 0xde, 0xc3, 0xb9, 0x75, 0x79, 0x2f, 0xb8, 0xb9, 0x1e, 0x71, 0x3b, 0x69, 0xab, 0x8a, 0xe6, 0x0a, 0x46, + 0xc9, 0x2c, 0x98, 0xfc, 0x02, 0x83, 0x1c, 0xe4, 0xab, 0xa8, 0x58, 0x1d, 0x1f, 0x52, 0x5f, 0x33, 0x6e, 0xdd, 0x3e, + 0x40, 0xab, 0x03, 0x1b, 0x11, 0x83, 0xfb, 0x22, 0x8a, 0xc2, 0x80, 0x5e, 0x73, 0xd3, 0x56, 0x58, 0x92, 0xfc, 0x82, + 0xe6, 0x7d, 0x17, 0x8a, 0xdc, 0xc0, 0x95, 0x2e, 0x3e, 0xb7, 0xfc, 0xd8, 0x4f, 0x49, 0xd8, 0x55, 0x01, 0x96, 0x87, + 0xae, 0x60, 0xd7, 0x02, 0x7e, 0x5c, 0xb4, 0xb7, 0xb7, 0x75, 0xbf, 0x48, 0x05, 0x12, 0xe6, 0x5a, 0x7d, 0x23, 0xc4, + 0x66, 0x45, 0xae, 0x8d, 0xe8, 0xb2, 0x5f, 0x89, 0x42, 0xa4, 0xf1, 0x74, 0x4d, 0x43, 0xe1, 0x87, 0xa9, 0x4a, 0xa2, + 0xb1, 0x18, 0x16, 0x6e, 0xd3, 0x03, 0x54, 0x70, 0x11, 0x5a, 0xdf, 0x01, 0xd6, 0xfb, 0x9c, 0x8b, 0xd0, 0x9c, 0xa5, + 0xb5, 0xae, 0x0d, 0x02, 0x47, 0x6f, 0xdc, 0xe9, 0xbd, 0x79, 0x7f, 0xea, 0xa8, 0xed, 0x79, 0xb2, 0x1f, 0x77, 0x7a, + 0x27, 0xd2, 0x67, 0xa2, 0x4e, 0xe2, 0x11, 0x75, 0x12, 0xcf, 0xd1, 0xa7, 0x32, 0x21, 0x92, 0x56, 0xec, 0xab, 0x69, + 0x4b, 0x9b, 0x41, 0x79, 0x7b, 0x27, 0xb3, 0x44, 0x30, 0xb8, 0xe3, 0x7a, 0x5f, 0x1e, 0xc3, 0x83, 0x05, 0x2b, 0xf3, + 0xb0, 0xb5, 0x76, 0x78, 0x2d, 0x52, 0xe3, 0x1b, 0x1e, 0xb1, 0x84, 0x9a, 0xcc, 0x6b, 0xdd, 0x55, 0x79, 0x52, 0x60, + 0xbd, 0x76, 0x3e, 0xbb, 0x9e, 0x30, 0xe1, 0x9a, 0xf3, 0x0c, 0x1f, 0x74, 0x83, 0x13, 0x39, 0x54, 0xef, 0xaa, 0xd0, + 0xce, 0x6b, 0xf3, 0x35, 0x9f, 0xfa, 0x92, 0xea, 0xd9, 0x6b, 0x09, 0x01, 0x27, 0xe4, 0xe2, 0x83, 0x5e, 0xe9, 0x2e, + 0xb6, 0xdf, 0x15, 0x27, 0xfb, 0xf1, 0x41, 0xef, 0x2a, 0x98, 0xea, 0xfe, 0x5e, 0xf2, 0xf1, 0xe6, 0xbe, 0x12, 0x3e, + 0xee, 0xcb, 0xa3, 0x20, 0xea, 0x90, 0xb2, 0x51, 0x7e, 0x79, 0xe2, 0xf6, 0x4e, 0xb4, 0x32, 0xe0, 0xc8, 0xc0, 0xba, + 0x7b, 0xd4, 0x32, 0xa7, 0x4b, 0x12, 0x3e, 0x86, 0x0d, 0xa9, 0x9a, 0x58, 0x83, 0xd4, 0x3c, 0xee, 0x71, 0xbb, 0x77, + 0x12, 0x3a, 0x92, 0xb7, 0x48, 0xe6, 0x91, 0x07, 0xfb, 0xd0, 0x38, 0xe6, 0x13, 0xea, 0x33, 0xbe, 0x7f, 0x43, 0xaf, + 0x9b, 0xe1, 0x94, 0x55, 0xee, 0x6d, 0x50, 0x3a, 0xca, 0x21, 0xb9, 0xf1, 0x88, 0xeb, 0xb3, 0x57, 0x9d, 0xca, 0xdd, + 0x76, 0x08, 0x36, 0x8f, 0x71, 0xcd, 0x49, 0x9f, 0x9c, 0x05, 0x16, 0xef, 0x9d, 0xec, 0x87, 0x2b, 0x18, 0x91, 0xfc, + 0xbe, 0xd0, 0x8e, 0x76, 0x30, 0x6c, 0x80, 0xde, 0x5c, 0x47, 0x89, 0x03, 0xe3, 0x90, 0xd7, 0x82, 0xba, 0x70, 0x7b, + 0xff, 0xfa, 0x3f, 0xfe, 0x97, 0xf6, 0xb1, 0x9f, 0xec, 0xc7, 0x6d, 0xd3, 0xd7, 0xca, 0xaa, 0x14, 0x27, 0x70, 0xdc, + 0xb3, 0x0a, 0x0a, 0xd3, 0xdb, 0xe6, 0x38, 0x63, 0x51, 0x33, 0x0e, 0x93, 0x91, 0xdb, 0xdb, 0x8e, 0x4d, 0xfb, 0xd8, + 0x96, 0x86, 0xba, 0x5e, 0x04, 0xf4, 0xfa, 0x9b, 0x0e, 0x1e, 0x99, 0xf3, 0x2b, 0x72, 0x6b, 0xdb, 0xc7, 0x90, 0xaa, + 0xdd, 0x57, 0x3b, 0x8a, 0x94, 0xea, 0x4f, 0x84, 0x69, 0x0e, 0x98, 0xd6, 0x4e, 0x20, 0x15, 0xae, 0x53, 0x06, 0xb5, + 0xfe, 0xef, 0xff, 0xfc, 0x2f, 0xff, 0xcd, 0x3c, 0x42, 0xac, 0xea, 0x5f, 0xff, 0xfb, 0x7f, 0xfe, 0x3f, 0xff, 0xfb, + 0xbf, 0xc2, 0xa9, 0x15, 0x1d, 0xcf, 0x92, 0x4c, 0xc5, 0xa9, 0x82, 0x59, 0x8a, 0xbb, 0x38, 0x90, 0xd8, 0x39, 0x61, + 0xb9, 0x60, 0xc3, 0xfa, 0x99, 0xa4, 0x73, 0x39, 0xa0, 0xdc, 0x99, 0x1a, 0x3a, 0xb9, 0xc3, 0x8b, 0x8a, 0xa0, 0x6a, + 0x28, 0x97, 0x84, 0x5b, 0x9c, 0xec, 0x03, 0xbe, 0x1f, 0x76, 0x8c, 0xd3, 0x2f, 0x97, 0x63, 0x61, 0xc8, 0x04, 0x4a, + 0x8a, 0xaa, 0xdc, 0x81, 0xd8, 0xca, 0x02, 0x1e, 0x83, 0x8e, 0x55, 0x2c, 0x57, 0xaf, 0xd6, 0xa6, 0xfb, 0xd3, 0x2c, + 0x17, 0x6c, 0x04, 0x28, 0x57, 0x7e, 0x62, 0x19, 0xc6, 0x6e, 0x82, 0xae, 0x98, 0xdc, 0x15, 0xb2, 0x17, 0x45, 0xa0, + 0x87, 0xc7, 0x7f, 0x2a, 0xfe, 0x32, 0x01, 0x8d, 0xcc, 0xf1, 0x26, 0xe1, 0xad, 0x36, 0xcf, 0x1f, 0xb5, 0x5a, 0xd3, + 0x5b, 0xb4, 0xa8, 0x46, 0xc0, 0xdb, 0x06, 0x93, 0x74, 0x6c, 0x77, 0x28, 0xe3, 0xdf, 0xa5, 0x1b, 0xbb, 0xe5, 0x80, + 0x2f, 0xdc, 0x69, 0x15, 0xc5, 0x9f, 0x17, 0xd2, 0x93, 0xca, 0x7e, 0x81, 0x38, 0xb5, 0x76, 0x3a, 0x5f, 0x73, 0x7b, + 0x72, 0x0b, 0xab, 0x55, 0x47, 0xb5, 0x8a, 0xdb, 0xeb, 0xa7, 0x13, 0xed, 0x38, 0xbb, 0x1d, 0x21, 0x3f, 0x84, 0x98, + 0x77, 0xdc, 0xc6, 0x71, 0x67, 0x51, 0x76, 0x2f, 0x04, 0x9f, 0xd8, 0x81, 0x75, 0x1a, 0xd2, 0x21, 0x1d, 0x19, 0x67, + 0xbd, 0x7e, 0xaf, 0x82, 0xe6, 0x45, 0x7c, 0xb0, 0x61, 0x2c, 0x0d, 0x92, 0x0c, 0xa8, 0x3b, 0xad, 0xe2, 0x73, 0xd8, + 0x81, 0x8b, 0x51, 0xc2, 0x43, 0x11, 0x48, 0x82, 0xed, 0xda, 0xe1, 0xf9, 0x10, 0x78, 0x12, 0x5f, 0x58, 0xf0, 0x74, + 0x55, 0x55, 0x70, 0x9b, 0xd7, 0xcf, 0x90, 0x16, 0xbe, 0x6c, 0x6e, 0x77, 0xa5, 0xbc, 0x6e, 0xdf, 0xea, 0xa8, 0xf7, + 0xbb, 0x9a, 0xbb, 0x4a, 0x0b, 0xa4, 0x0e, 0xda, 0xfc, 0x5e, 0xc9, 0x75, 0xf5, 0xf6, 0x6b, 0xe1, 0xb9, 0x12, 0x4c, + 0x77, 0xb5, 0x96, 0x2c, 0x84, 0x5a, 0xef, 0xc8, 0xb7, 0xa5, 0xc9, 0x14, 0x4e, 0xa7, 0xb2, 0x22, 0xea, 0x9e, 0xec, + 0x2b, 0x4d, 0x17, 0xb8, 0x87, 0x4c, 0xe9, 0x50, 0x19, 0x14, 0xba, 0x92, 0xde, 0x0a, 0xea, 0x97, 0xce, 0xad, 0x80, + 0x4f, 0xc7, 0xf5, 0xfe, 0x1f, 0xe7, 0xe0, 0x1c, 0x12, 0xcf, 0x89, 0x00, 0x00}; } // namespace web_server } // namespace esphome diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index a8d94d80da..6bf6524fbc 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -3,12 +3,14 @@ #include "esphome/components/json/json_util.h" #include "esphome/components/network/util.h" #include "esphome/core/application.h" +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "esphome/core/util.h" -#ifdef USE_ARDUINO +#if !defined(USE_ESP32) && defined(USE_ARDUINO) #include "StreamString.h" #endif @@ -103,19 +105,18 @@ static UrlMatch match_url(const char *url_ptr, size_t url_len, bool only_domain) return match; } -#ifdef USE_ARDUINO +#if !defined(USE_ESP32) && defined(USE_ARDUINO) // helper for allowing only unique entries in the queue void DeferredUpdateEventSource::deq_push_back_with_dedup_(void *source, message_generator_t *message_generator) { DeferredEvent item(source, message_generator); - auto iter = std::find_if(this->deferred_queue_.begin(), this->deferred_queue_.end(), - [&item](const DeferredEvent &test) -> bool { return test == item; }); - - if (iter != this->deferred_queue_.end()) { - (*iter) = item; - } else { - this->deferred_queue_.push_back(item); + // Use range-based for loop instead of std::find_if to reduce template instantiation overhead and binary size + for (auto &event : this->deferred_queue_) { + if (event == item) { + return; // Already in queue, no need to update since items are equal + } } + this->deferred_queue_.push_back(item); } void DeferredUpdateEventSource::process_deferred_queue_() { @@ -127,6 +128,10 @@ void DeferredUpdateEventSource::process_deferred_queue_() { deferred_queue_.erase(deferred_queue_.begin()); this->consecutive_send_failures_ = 0; // Reset failure count on successful send } else { + // NOTE: Similar logic exists in web_server_idf/web_server_idf.cpp in AsyncEventSourceResponse::process_buffer_() + // The implementations differ due to platform-specific APIs (DISCARDED vs HTTPD_SOCK_ERR_TIMEOUT, close() vs + // fd_.store(0)), but the failure counting and timeout logic should be kept in sync. If you change this logic, + // also update the ESP-IDF implementation. this->consecutive_send_failures_++; if (this->consecutive_send_failures_ >= MAX_CONSECUTIVE_SEND_FAILURES) { // Too many failures, connection is likely dead @@ -148,6 +153,10 @@ void DeferredUpdateEventSource::loop() { void DeferredUpdateEventSource::deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator) { + // Skip if no connected clients to avoid unnecessary deferred queue processing + if (this->count() == 0) + return; + // allow all json "details_all" to go through before publishing bare state events, this avoids unnamed entries showing // up in the web GUI and reduces event load during initial connect if (!entities_iterator_.completed() && 0 != strcmp(event_type, "state_detail_all")) @@ -193,6 +202,9 @@ void DeferredUpdateEventSourceList::loop() { void DeferredUpdateEventSourceList::deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator) { + // Skip if no event sources (no connected clients) to avoid unnecessary iteration + if (this->empty()) + return; for (DeferredUpdateEventSource *dues : *this) { dues->deferrable_send_state(source, event_type, message_generator); } @@ -209,49 +221,51 @@ void DeferredUpdateEventSourceList::add_new_client(WebServer *ws, AsyncWebServer DeferredUpdateEventSource *es = new DeferredUpdateEventSource(ws, "/events"); this->push_back(es); - es->onConnect([this, ws, es](AsyncEventSourceClient *client) { - ws->defer([this, ws, es]() { this->on_client_connect_(ws, es); }); - }); + es->onConnect([this, es](AsyncEventSourceClient *client) { this->on_client_connect_(es); }); - es->onDisconnect([this, ws, es](AsyncEventSourceClient *client) { - ws->defer([this, es]() { this->on_client_disconnect_((DeferredUpdateEventSource *) es); }); - }); + es->onDisconnect([this, es](AsyncEventSourceClient *client) { this->on_client_disconnect_(es); }); es->handleRequest(request); } -void DeferredUpdateEventSourceList::on_client_connect_(WebServer *ws, DeferredUpdateEventSource *source) { - // Configure reconnect timeout and send config - // this should always go through since the AsyncEventSourceClient event queue is empty on connect - std::string message = ws->get_config_json(); - source->try_send_nodefer(message.c_str(), "ping", millis(), 30000); +void DeferredUpdateEventSourceList::on_client_connect_(DeferredUpdateEventSource *source) { + WebServer *ws = source->web_server_; + ws->defer([ws, source]() { + // Configure reconnect timeout and send config + // this should always go through since the AsyncEventSourceClient event queue is empty on connect + std::string message = ws->get_config_json(); + source->try_send_nodefer(message.c_str(), "ping", millis(), 30000); #ifdef USE_WEBSERVER_SORTING - for (auto &group : ws->sorting_groups_) { - message = json::build_json([group](JsonObject root) { + for (auto &group : ws->sorting_groups_) { + json::JsonBuilder builder; + JsonObject root = builder.root(); root["name"] = group.second.name; root["sorting_weight"] = group.second.weight; - }); + message = builder.serialize(); - // up to 31 groups should be able to be queued initially without defer - source->try_send_nodefer(message.c_str(), "sorting_group"); - } + // up to 31 groups should be able to be queued initially without defer + source->try_send_nodefer(message.c_str(), "sorting_group"); + } #endif - source->entities_iterator_.begin(ws->include_internal_); + source->entities_iterator_.begin(ws->include_internal_); - // just dump them all up-front and take advantage of the deferred queue - // on second thought that takes too long, but leaving the commented code here for debug purposes - // while(!source->entities_iterator_.completed()) { - // source->entities_iterator_.advance(); - //} + // just dump them all up-front and take advantage of the deferred queue + // on second thought that takes too long, but leaving the commented code here for debug purposes + // while(!source->entities_iterator_.completed()) { + // source->entities_iterator_.advance(); + //} + }); } void DeferredUpdateEventSourceList::on_client_disconnect_(DeferredUpdateEventSource *source) { - // This method was called via WebServer->defer() and is no longer executing in the - // context of the network callback. The object is now dead and can be safely deleted. - this->remove(source); - delete source; // NOLINT + source->web_server_->defer([this, source]() { + // This method was called via WebServer->defer() and is no longer executing in the + // context of the network callback. The object is now dead and can be safely deleted. + this->remove(source); + delete source; // NOLINT + }); } #endif @@ -265,21 +279,24 @@ void WebServer::set_js_include(const char *js_include) { this->js_include_ = js_ #endif std::string WebServer::get_config_json() { - return json::build_json([this](JsonObject root) { - root["title"] = App.get_friendly_name().empty() ? App.get_name() : App.get_friendly_name(); - root["comment"] = App.get_comment(); + json::JsonBuilder builder; + JsonObject root = builder.root(); + + root["title"] = App.get_friendly_name().empty() ? App.get_name() : App.get_friendly_name(); + root["comment"] = App.get_comment(); #if defined(USE_WEBSERVER_OTA_DISABLED) || !defined(USE_WEBSERVER_OTA) - root["ota"] = false; // Note: USE_WEBSERVER_OTA_DISABLED only affects web_server, not captive_portal + root["ota"] = false; // Note: USE_WEBSERVER_OTA_DISABLED only affects web_server, not captive_portal #else - root["ota"] = true; + root["ota"] = true; #endif - root["log"] = this->expose_log_; - root["lang"] = "en"; - }); + root["log"] = this->expose_log_; + root["lang"] = "en"; + + return builder.serialize(); } void WebServer::setup() { - this->setup_controller(this->include_internal_); + ControllerRegistry::register_controller(this); this->base_->init(); #ifdef USE_LOGGER @@ -293,7 +310,7 @@ void WebServer::setup() { } #endif -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 this->base_->add_handler(&this->events_); #endif this->base_->add_handler(this); @@ -309,7 +326,7 @@ void WebServer::dump_config() { ESP_LOGCONFIG(TAG, "Web Server:\n" " Address: %s:%u", - network::get_use_address().c_str(), this->base_->get_port()); + network::get_use_address(), this->base_->get_port()); } float WebServer::get_setup_priority() const { return setup_priority::WIFI - 1.0f; } @@ -342,8 +359,8 @@ void WebServer::handle_pna_cors_request(AsyncWebServerRequest *request) { AsyncWebServerResponse *response = request->beginResponse(200, ""); response->addHeader(HEADER_CORS_ALLOW_PNA, "true"); response->addHeader(HEADER_PNA_NAME, App.get_name().c_str()); - std::string mac = get_mac_address_pretty(); - response->addHeader(HEADER_PNA_ID, mac.c_str()); + char mac_s[18]; + response->addHeader(HEADER_PNA_ID, get_mac_address_pretty_into_buffer(mac_s)); request->send(response); } #endif @@ -377,11 +394,14 @@ void WebServer::handle_js_request(AsyncWebServerRequest *request) { #endif // Helper functions to reduce code size by avoiding macro expansion -static void set_json_id(JsonObject &root, EntityBase *obj, const std::string &id, JsonDetail start_config) { - root["id"] = id; +static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, JsonDetail start_config) { + char id_buf[160]; // object_id can be up to 128 chars + prefix + dash + null + const auto &object_id = obj->get_object_id(); + snprintf(id_buf, sizeof(id_buf), "%s-%s", prefix, object_id.c_str()); + root["id"] = id_buf; if (start_config == DETAIL_ALL) { root["name"] = obj->get_name(); - root["icon"] = obj->get_icon(); + root["icon"] = obj->get_icon_ref(); root["entity_category"] = obj->get_entity_category(); bool is_disabled = obj->is_disabled_by_default(); if (is_disabled) @@ -389,17 +409,19 @@ static void set_json_id(JsonObject &root, EntityBase *obj, const std::string &id } } +// Keep as separate function even though only used once: reduces code size by ~48 bytes +// by allowing compiler to share code between template instantiations (bool, float, etc.) template -static void set_json_value(JsonObject &root, EntityBase *obj, const std::string &id, const T &value, +static void set_json_value(JsonObject &root, EntityBase *obj, const char *prefix, const T &value, JsonDetail start_config) { - set_json_id(root, obj, id, start_config); + set_json_id(root, obj, prefix, start_config); root["value"] = value; } template -static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const std::string &id, - const std::string &state, const T &value, JsonDetail start_config) { - set_json_value(root, obj, id, value, start_config); +static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const char *prefix, const std::string &state, + const T &value, JsonDetail start_config) { + set_json_value(root, obj, prefix, value, start_config); root["state"] = state; } @@ -410,16 +432,17 @@ static JsonDetail get_request_detail(AsyncWebServerRequest *request) { } #ifdef USE_SENSOR -void WebServer::on_sensor_update(sensor::Sensor *obj, float state) { - if (this->events_.empty()) +void WebServer::on_sensor_update(sensor::Sensor *obj) { + if (!this->include_internal_ && obj->is_internal()) return; this->events_.deferrable_send_state(obj, "state", sensor_state_json_generator); } void WebServer::handle_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (sensor::Sensor *obj : App.get_sensors()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + // Note: request->method() is always HTTP_GET here (canHandle ensures this) + if (match.method_empty()) { auto detail = get_request_detail(request); std::string data = this->sensor_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); @@ -435,36 +458,36 @@ std::string WebServer::sensor_all_json_generator(WebServer *web_server, void *so return web_server->sensor_json((sensor::Sensor *) (source), ((sensor::Sensor *) (source))->state, DETAIL_ALL); } std::string WebServer::sensor_json(sensor::Sensor *obj, float value, JsonDetail start_config) { - return json::build_json([this, obj, value, start_config](JsonObject root) { - std::string state; - if (std::isnan(value)) { - state = "NA"; - } else { - state = value_accuracy_to_string(value, obj->get_accuracy_decimals()); - if (!obj->get_unit_of_measurement().empty()) - state += " " + obj->get_unit_of_measurement(); - } - set_json_icon_state_value(root, obj, "sensor-" + obj->get_object_id(), state, value, start_config); - if (start_config == DETAIL_ALL) { - this->add_sorting_info_(root, obj); - if (!obj->get_unit_of_measurement().empty()) - root["uom"] = obj->get_unit_of_measurement(); - } - }); + json::JsonBuilder builder; + JsonObject root = builder.root(); + + const auto uom_ref = obj->get_unit_of_measurement_ref(); + + std::string state = + std::isnan(value) ? "NA" : value_accuracy_with_uom_to_string(value, obj->get_accuracy_decimals(), uom_ref); + set_json_icon_state_value(root, obj, "sensor", state, value, start_config); + if (start_config == DETAIL_ALL) { + this->add_sorting_info_(root, obj); + if (!uom_ref.empty()) + root["uom"] = uom_ref; + } + + return builder.serialize(); } #endif #ifdef USE_TEXT_SENSOR -void WebServer::on_text_sensor_update(text_sensor::TextSensor *obj, const std::string &state) { - if (this->events_.empty()) +void WebServer::on_text_sensor_update(text_sensor::TextSensor *obj) { + if (!this->include_internal_ && obj->is_internal()) return; this->events_.deferrable_send_state(obj, "state", text_sensor_state_json_generator); } void WebServer::handle_text_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (text_sensor::TextSensor *obj : App.get_text_sensors()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + // Note: request->method() is always HTTP_GET here (canHandle ensures this) + if (match.method_empty()) { auto detail = get_request_detail(request); std::string data = this->text_sensor_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); @@ -483,38 +506,64 @@ std::string WebServer::text_sensor_all_json_generator(WebServer *web_server, voi } std::string WebServer::text_sensor_json(text_sensor::TextSensor *obj, const std::string &value, JsonDetail start_config) { - return json::build_json([this, obj, value, start_config](JsonObject root) { - set_json_icon_state_value(root, obj, "text_sensor-" + obj->get_object_id(), value, value, start_config); - if (start_config == DETAIL_ALL) { - this->add_sorting_info_(root, obj); - } - }); + json::JsonBuilder builder; + JsonObject root = builder.root(); + + set_json_icon_state_value(root, obj, "text_sensor", value, value, start_config); + if (start_config == DETAIL_ALL) { + this->add_sorting_info_(root, obj); + } + + return builder.serialize(); } #endif #ifdef USE_SWITCH -void WebServer::on_switch_update(switch_::Switch *obj, bool state) { - if (this->events_.empty()) +void WebServer::on_switch_update(switch_::Switch *obj) { + if (!this->include_internal_ && obj->is_internal()) return; this->events_.deferrable_send_state(obj, "state", switch_state_json_generator); } void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (switch_::Switch *obj : App.get_switches()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); std::string data = this->switch_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); - } else if (match.method_equals("toggle")) { - this->defer([obj]() { obj->toggle(); }); - request->send(200); + return; + } + + // Handle action methods with single defer and response + enum SwitchAction { NONE, TOGGLE, TURN_ON, TURN_OFF }; + SwitchAction action = NONE; + + if (match.method_equals("toggle")) { + action = TOGGLE; } else if (match.method_equals("turn_on")) { - this->defer([obj]() { obj->turn_on(); }); - request->send(200); + action = TURN_ON; } else if (match.method_equals("turn_off")) { - this->defer([obj]() { obj->turn_off(); }); + 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; + } + }); request->send(200); } else { request->send(404); @@ -530,20 +579,23 @@ std::string WebServer::switch_all_json_generator(WebServer *web_server, void *so return web_server->switch_json((switch_::Switch *) (source), ((switch_::Switch *) (source))->state, DETAIL_ALL); } std::string WebServer::switch_json(switch_::Switch *obj, bool value, JsonDetail start_config) { - return json::build_json([this, obj, value, start_config](JsonObject root) { - set_json_icon_state_value(root, obj, "switch-" + obj->get_object_id(), value ? "ON" : "OFF", value, start_config); - if (start_config == DETAIL_ALL) { - root["assumed_state"] = obj->assumed_state(); - this->add_sorting_info_(root, obj); - } - }); + json::JsonBuilder builder; + JsonObject root = builder.root(); + + set_json_icon_state_value(root, obj, "switch", value ? "ON" : "OFF", value, start_config); + if (start_config == DETAIL_ALL) { + root["assumed_state"] = obj->assumed_state(); + this->add_sorting_info_(root, obj); + } + + return builder.serialize(); } #endif #ifdef USE_BUTTON void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (button::Button *obj : App.get_buttons()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); @@ -567,26 +619,30 @@ std::string WebServer::button_all_json_generator(WebServer *web_server, void *so return web_server->button_json((button::Button *) (source), DETAIL_ALL); } std::string WebServer::button_json(button::Button *obj, JsonDetail start_config) { - return json::build_json([this, obj, start_config](JsonObject root) { - set_json_id(root, obj, "button-" + obj->get_object_id(), start_config); - if (start_config == DETAIL_ALL) { - this->add_sorting_info_(root, obj); - } - }); + json::JsonBuilder builder; + JsonObject root = builder.root(); + + set_json_id(root, obj, "button", start_config); + if (start_config == DETAIL_ALL) { + this->add_sorting_info_(root, obj); + } + + return builder.serialize(); } #endif #ifdef USE_BINARY_SENSOR void WebServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj) { - if (this->events_.empty()) + if (!this->include_internal_ && obj->is_internal()) return; this->events_.deferrable_send_state(obj, "state", binary_sensor_state_json_generator); } void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (binary_sensor::BinarySensor *obj : App.get_binary_sensors()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + // Note: request->method() is always HTTP_GET here (canHandle ensures this) + if (match.method_empty()) { auto detail = get_request_detail(request); std::string data = this->binary_sensor_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); @@ -604,25 +660,27 @@ std::string WebServer::binary_sensor_all_json_generator(WebServer *web_server, v ((binary_sensor::BinarySensor *) (source))->state, DETAIL_ALL); } std::string WebServer::binary_sensor_json(binary_sensor::BinarySensor *obj, bool value, JsonDetail start_config) { - return json::build_json([this, obj, value, start_config](JsonObject root) { - set_json_icon_state_value(root, obj, "binary_sensor-" + obj->get_object_id(), value ? "ON" : "OFF", value, - start_config); - if (start_config == DETAIL_ALL) { - this->add_sorting_info_(root, obj); - } - }); + json::JsonBuilder builder; + JsonObject root = builder.root(); + + set_json_icon_state_value(root, obj, "binary_sensor", value ? "ON" : "OFF", value, start_config); + if (start_config == DETAIL_ALL) { + this->add_sorting_info_(root, obj); + } + + return builder.serialize(); } #endif #ifdef USE_FAN void WebServer::on_fan_update(fan::Fan *obj) { - if (this->events_.empty()) + if (!this->include_internal_ && obj->is_internal()) return; this->events_.deferrable_send_state(obj, "state", fan_state_json_generator); } void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (fan::Fan *obj : App.get_fans()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; if (request->method() == HTTP_GET && match.method_empty()) { @@ -632,18 +690,17 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc } else if (match.method_equals("toggle")) { this->defer([obj]() { obj->toggle().perform(); }); request->send(200); - } else if (match.method_equals("turn_on") || match.method_equals("turn_off")) { - auto call = match.method_equals("turn_on") ? obj->turn_on() : obj->turn_off(); - - if (request->hasParam("speed_level")) { - auto speed_level = request->getParam("speed_level")->value(); - auto val = parse_number(speed_level.c_str()); - if (!val.has_value()) { - ESP_LOGW(TAG, "Can't convert '%s' to number!", speed_level.c_str()); - return; - } - call.set_speed(*val); + } else { + bool is_on = match.method_equals("turn_on"); + bool is_off = match.method_equals("turn_off"); + if (!is_on && !is_off) { + request->send(404); + return; } + auto call = is_on ? obj->turn_on() : obj->turn_off(); + + parse_int_param_(request, "speed_level", call, &decltype(call)::set_speed); + if (request->hasParam("oscillation")) { auto speed = request->getParam("oscillation")->value(); auto val = parse_on_off(speed.c_str()); @@ -664,8 +721,6 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc } this->defer([call]() mutable { call.perform(); }); request->send(200); - } else { - request->send(404); } return; } @@ -678,32 +733,34 @@ std::string WebServer::fan_all_json_generator(WebServer *web_server, void *sourc return web_server->fan_json((fan::Fan *) (source), DETAIL_ALL); } std::string WebServer::fan_json(fan::Fan *obj, JsonDetail start_config) { - return json::build_json([this, obj, start_config](JsonObject root) { - set_json_icon_state_value(root, obj, "fan-" + obj->get_object_id(), obj->state ? "ON" : "OFF", obj->state, - start_config); - const auto traits = obj->get_traits(); - if (traits.supports_speed()) { - root["speed_level"] = obj->speed; - root["speed_count"] = traits.supported_speed_count(); - } - if (obj->get_traits().supports_oscillation()) - root["oscillation"] = obj->oscillating; - if (start_config == DETAIL_ALL) { - this->add_sorting_info_(root, obj); - } - }); + json::JsonBuilder builder; + JsonObject root = builder.root(); + + set_json_icon_state_value(root, obj, "fan", obj->state ? "ON" : "OFF", obj->state, start_config); + const auto traits = obj->get_traits(); + if (traits.supports_speed()) { + root["speed_level"] = obj->speed; + root["speed_count"] = traits.supported_speed_count(); + } + if (obj->get_traits().supports_oscillation()) + root["oscillation"] = obj->oscillating; + if (start_config == DETAIL_ALL) { + this->add_sorting_info_(root, obj); + } + + return builder.serialize(); } #endif #ifdef USE_LIGHT void WebServer::on_light_update(light::LightState *obj) { - if (this->events_.empty()) + if (!this->include_internal_ && obj->is_internal()) return; this->events_.deferrable_send_state(obj, "state", light_state_json_generator); } void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (light::LightState *obj : App.get_lights()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; if (request->method() == HTTP_GET && match.method_empty()) { @@ -713,75 +770,35 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa } else if (match.method_equals("toggle")) { this->defer([obj]() { obj->toggle().perform(); }); request->send(200); - } else if (match.method_equals("turn_on")) { - auto call = obj->turn_on(); - if (request->hasParam("brightness")) { - auto brightness = parse_number(request->getParam("brightness")->value().c_str()); - if (brightness.has_value()) { - call.set_brightness(*brightness / 255.0f); - } + } else { + bool is_on = match.method_equals("turn_on"); + bool is_off = match.method_equals("turn_off"); + if (!is_on && !is_off) { + request->send(404); + return; } - if (request->hasParam("r")) { - auto r = parse_number(request->getParam("r")->value().c_str()); - if (r.has_value()) { - call.set_red(*r / 255.0f); - } + auto call = is_on ? obj->turn_on() : obj->turn_off(); + + if (is_on) { + // Parse color parameters + parse_light_param_(request, "brightness", call, &decltype(call)::set_brightness, 255.0f); + parse_light_param_(request, "r", call, &decltype(call)::set_red, 255.0f); + parse_light_param_(request, "g", call, &decltype(call)::set_green, 255.0f); + parse_light_param_(request, "b", call, &decltype(call)::set_blue, 255.0f); + parse_light_param_(request, "white_value", call, &decltype(call)::set_white, 255.0f); + parse_light_param_(request, "color_temp", call, &decltype(call)::set_color_temperature); + + // Parse timing parameters + parse_light_param_uint_(request, "flash", call, &decltype(call)::set_flash_length, 1000); } - if (request->hasParam("g")) { - auto g = parse_number(request->getParam("g")->value().c_str()); - if (g.has_value()) { - call.set_green(*g / 255.0f); - } - } - if (request->hasParam("b")) { - auto b = parse_number(request->getParam("b")->value().c_str()); - if (b.has_value()) { - call.set_blue(*b / 255.0f); - } - } - if (request->hasParam("white_value")) { - auto white_value = parse_number(request->getParam("white_value")->value().c_str()); - if (white_value.has_value()) { - call.set_white(*white_value / 255.0f); - } - } - if (request->hasParam("color_temp")) { - auto color_temp = parse_number(request->getParam("color_temp")->value().c_str()); - if (color_temp.has_value()) { - call.set_color_temperature(*color_temp); - } - } - if (request->hasParam("flash")) { - auto flash = parse_number(request->getParam("flash")->value().c_str()); - if (flash.has_value()) { - call.set_flash_length(*flash * 1000); - } - } - if (request->hasParam("transition")) { - auto transition = parse_number(request->getParam("transition")->value().c_str()); - if (transition.has_value()) { - call.set_transition_length(*transition * 1000); - } - } - if (request->hasParam("effect")) { - const char *effect = request->getParam("effect")->value().c_str(); - call.set_effect(effect); + parse_light_param_uint_(request, "transition", call, &decltype(call)::set_transition_length, 1000); + + if (is_on) { + parse_string_param_(request, "effect", call, &decltype(call)::set_effect); } this->defer([call]() mutable { call.perform(); }); request->send(200); - } else if (match.method_equals("turn_off")) { - auto call = obj->turn_off(); - if (request->hasParam("transition")) { - auto transition = parse_number(request->getParam("transition")->value().c_str()); - if (transition.has_value()) { - call.set_transition_length(*transition * 1000); - } - } - this->defer([call]() mutable { call.perform(); }); - request->send(200); - } else { - request->send(404); } return; } @@ -794,32 +811,34 @@ std::string WebServer::light_all_json_generator(WebServer *web_server, void *sou return web_server->light_json((light::LightState *) (source), DETAIL_ALL); } std::string WebServer::light_json(light::LightState *obj, JsonDetail start_config) { - return json::build_json([this, obj, start_config](JsonObject root) { - set_json_id(root, obj, "light-" + obj->get_object_id(), start_config); - root["state"] = obj->remote_values.is_on() ? "ON" : "OFF"; + json::JsonBuilder builder; + JsonObject root = builder.root(); - light::LightJSONSchema::dump_json(*obj, root); - if (start_config == DETAIL_ALL) { - JsonArray opt = root["effects"].to(); - opt.add("None"); - for (auto const &option : obj->get_effects()) { - opt.add(option->get_name()); - } - this->add_sorting_info_(root, obj); + set_json_value(root, obj, "light", obj->remote_values.is_on() ? "ON" : "OFF", start_config); + + light::LightJSONSchema::dump_json(*obj, root); + if (start_config == DETAIL_ALL) { + JsonArray opt = root["effects"].to(); + opt.add("None"); + for (auto const &option : obj->get_effects()) { + opt.add(option->get_name()); } - }); + this->add_sorting_info_(root, obj); + } + + return builder.serialize(); } #endif #ifdef USE_COVER void WebServer::on_cover_update(cover::Cover *obj) { - if (this->events_.empty()) + if (!this->include_internal_ && obj->is_internal()) return; this->events_.deferrable_send_state(obj, "state", cover_state_json_generator); } void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (cover::Cover *obj : App.get_covers()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; if (request->method() == HTTP_GET && match.method_empty()) { @@ -830,15 +849,28 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa } auto call = obj->make_call(); - if (match.method_equals("open")) { - call.set_command_open(); - } else if (match.method_equals("close")) { - call.set_command_close(); - } else if (match.method_equals("stop")) { - call.set_command_stop(); - } else if (match.method_equals("toggle")) { - call.set_command_toggle(); - } else if (!match.method_equals("set")) { + + // Lookup table for cover methods + static const struct { + const char *name; + cover::CoverCall &(cover::CoverCall::*action)(); + } METHODS[] = { + {"open", &cover::CoverCall::set_command_open}, + {"close", &cover::CoverCall::set_command_close}, + {"stop", &cover::CoverCall::set_command_stop}, + {"toggle", &cover::CoverCall::set_command_toggle}, + }; + + bool found = false; + for (const auto &method : METHODS) { + if (match.method_equals(method.name)) { + (call.*method.action)(); + found = true; + break; + } + } + + if (!found && !match.method_equals("set")) { request->send(404); return; } @@ -850,18 +882,8 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa return; } - if (request->hasParam("position")) { - auto position = parse_number(request->getParam("position")->value().c_str()); - if (position.has_value()) { - call.set_position(*position); - } - } - if (request->hasParam("tilt")) { - auto tilt = parse_number(request->getParam("tilt")->value().c_str()); - if (tilt.has_value()) { - call.set_tilt(*tilt); - } - } + parse_float_param_(request, "position", call, &decltype(call)::set_position); + parse_float_param_(request, "tilt", call, &decltype(call)::set_tilt); this->defer([call]() mutable { call.perform(); }); request->send(200); @@ -873,34 +895,37 @@ std::string WebServer::cover_state_json_generator(WebServer *web_server, void *s return web_server->cover_json((cover::Cover *) (source), DETAIL_STATE); } std::string WebServer::cover_all_json_generator(WebServer *web_server, void *source) { - return web_server->cover_json((cover::Cover *) (source), DETAIL_STATE); + return web_server->cover_json((cover::Cover *) (source), DETAIL_ALL); } std::string WebServer::cover_json(cover::Cover *obj, JsonDetail start_config) { - return json::build_json([this, obj, start_config](JsonObject root) { - set_json_icon_state_value(root, obj, "cover-" + obj->get_object_id(), obj->is_fully_closed() ? "CLOSED" : "OPEN", - obj->position, start_config); - root["current_operation"] = cover::cover_operation_to_str(obj->current_operation); + json::JsonBuilder builder; + JsonObject root = builder.root(); - if (obj->get_traits().get_supports_position()) - root["position"] = obj->position; - if (obj->get_traits().get_supports_tilt()) - root["tilt"] = obj->tilt; - if (start_config == DETAIL_ALL) { - this->add_sorting_info_(root, obj); - } - }); + set_json_icon_state_value(root, obj, "cover", obj->is_fully_closed() ? "CLOSED" : "OPEN", obj->position, + start_config); + root["current_operation"] = cover::cover_operation_to_str(obj->current_operation); + + if (obj->get_traits().get_supports_position()) + root["position"] = obj->position; + if (obj->get_traits().get_supports_tilt()) + root["tilt"] = obj->tilt; + if (start_config == DETAIL_ALL) { + this->add_sorting_info_(root, obj); + } + + return builder.serialize(); } #endif #ifdef USE_NUMBER -void WebServer::on_number_update(number::Number *obj, float state) { - if (this->events_.empty()) +void WebServer::on_number_update(number::Number *obj) { + if (!this->include_internal_ && obj->is_internal()) return; this->events_.deferrable_send_state(obj, "state", number_state_json_generator); } void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_numbers()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; if (request->method() == HTTP_GET && match.method_empty()) { @@ -915,11 +940,7 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM } auto call = obj->make_call(); - if (request->hasParam("value")) { - auto value = parse_number(request->getParam("value")->value().c_str()); - if (value.has_value()) - call.set_value(*value); - } + parse_float_param_(request, "value", call, &decltype(call)::set_value); this->defer([call]() mutable { call.perform(); }); request->send(200); @@ -935,43 +956,43 @@ std::string WebServer::number_all_json_generator(WebServer *web_server, void *so return web_server->number_json((number::Number *) (source), ((number::Number *) (source))->state, DETAIL_ALL); } std::string WebServer::number_json(number::Number *obj, float value, JsonDetail start_config) { - return json::build_json([this, obj, value, start_config](JsonObject root) { - set_json_id(root, obj, "number-" + obj->get_object_id(), start_config); - if (start_config == DETAIL_ALL) { - root["min_value"] = - value_accuracy_to_string(obj->traits.get_min_value(), step_to_accuracy_decimals(obj->traits.get_step())); - root["max_value"] = - value_accuracy_to_string(obj->traits.get_max_value(), step_to_accuracy_decimals(obj->traits.get_step())); - root["step"] = - value_accuracy_to_string(obj->traits.get_step(), step_to_accuracy_decimals(obj->traits.get_step())); - root["mode"] = (int) obj->traits.get_mode(); - if (!obj->traits.get_unit_of_measurement().empty()) - root["uom"] = obj->traits.get_unit_of_measurement(); - this->add_sorting_info_(root, obj); - } - if (std::isnan(value)) { - root["value"] = "\"NaN\""; - root["state"] = "NA"; - } else { - root["value"] = value_accuracy_to_string(value, step_to_accuracy_decimals(obj->traits.get_step())); - std::string state = value_accuracy_to_string(value, step_to_accuracy_decimals(obj->traits.get_step())); - if (!obj->traits.get_unit_of_measurement().empty()) - state += " " + obj->traits.get_unit_of_measurement(); - root["state"] = state; - } - }); + json::JsonBuilder builder; + JsonObject root = builder.root(); + + const auto uom_ref = obj->traits.get_unit_of_measurement_ref(); + + std::string val_str = std::isnan(value) + ? "\"NaN\"" + : value_accuracy_to_string(value, step_to_accuracy_decimals(obj->traits.get_step())); + std::string state_str = std::isnan(value) ? "NA" + : value_accuracy_with_uom_to_string( + value, step_to_accuracy_decimals(obj->traits.get_step()), uom_ref); + set_json_icon_state_value(root, obj, "number", state_str, val_str, start_config); + if (start_config == DETAIL_ALL) { + root["min_value"] = + value_accuracy_to_string(obj->traits.get_min_value(), step_to_accuracy_decimals(obj->traits.get_step())); + root["max_value"] = + value_accuracy_to_string(obj->traits.get_max_value(), step_to_accuracy_decimals(obj->traits.get_step())); + root["step"] = value_accuracy_to_string(obj->traits.get_step(), step_to_accuracy_decimals(obj->traits.get_step())); + root["mode"] = (int) obj->traits.get_mode(); + if (!uom_ref.empty()) + root["uom"] = uom_ref; + this->add_sorting_info_(root, obj); + } + + return builder.serialize(); } #endif #ifdef USE_DATETIME_DATE void WebServer::on_date_update(datetime::DateEntity *obj) { - if (this->events_.empty()) + if (!this->include_internal_ && obj->is_internal()) return; this->events_.deferrable_send_state(obj, "state", date_state_json_generator); } void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_dates()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); @@ -991,10 +1012,7 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat return; } - if (request->hasParam("value")) { - std::string value = request->getParam("value")->value().c_str(); // NOLINT - call.set_date(value); - } + parse_string_param_(request, "value", call, &decltype(call)::set_date); this->defer([call]() mutable { call.perform(); }); request->send(200); @@ -1010,27 +1028,28 @@ std::string WebServer::date_all_json_generator(WebServer *web_server, void *sour return web_server->date_json((datetime::DateEntity *) (source), DETAIL_ALL); } std::string WebServer::date_json(datetime::DateEntity *obj, JsonDetail start_config) { - return json::build_json([this, obj, start_config](JsonObject root) { - set_json_id(root, obj, "date-" + obj->get_object_id(), start_config); - std::string value = str_sprintf("%d-%02d-%02d", obj->year, obj->month, obj->day); - root["value"] = value; - root["state"] = value; - if (start_config == DETAIL_ALL) { - this->add_sorting_info_(root, obj); - } - }); + json::JsonBuilder builder; + JsonObject root = builder.root(); + + std::string value = str_sprintf("%d-%02d-%02d", obj->year, obj->month, obj->day); + set_json_icon_state_value(root, obj, "date", value, value, start_config); + if (start_config == DETAIL_ALL) { + this->add_sorting_info_(root, obj); + } + + return builder.serialize(); } #endif // USE_DATETIME_DATE #ifdef USE_DATETIME_TIME void WebServer::on_time_update(datetime::TimeEntity *obj) { - if (this->events_.empty()) + if (!this->include_internal_ && obj->is_internal()) return; this->events_.deferrable_send_state(obj, "state", time_state_json_generator); } void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_times()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); @@ -1050,10 +1069,7 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat return; } - if (request->hasParam("value")) { - std::string value = request->getParam("value")->value().c_str(); // NOLINT - call.set_time(value); - } + parse_string_param_(request, "value", call, &decltype(call)::set_time); this->defer([call]() mutable { call.perform(); }); request->send(200); @@ -1068,27 +1084,28 @@ std::string WebServer::time_all_json_generator(WebServer *web_server, void *sour return web_server->time_json((datetime::TimeEntity *) (source), DETAIL_ALL); } std::string WebServer::time_json(datetime::TimeEntity *obj, JsonDetail start_config) { - return json::build_json([this, obj, start_config](JsonObject root) { - set_json_id(root, obj, "time-" + obj->get_object_id(), start_config); - std::string value = str_sprintf("%02d:%02d:%02d", obj->hour, obj->minute, obj->second); - root["value"] = value; - root["state"] = value; - if (start_config == DETAIL_ALL) { - this->add_sorting_info_(root, obj); - } - }); + json::JsonBuilder builder; + JsonObject root = builder.root(); + + std::string value = str_sprintf("%02d:%02d:%02d", obj->hour, obj->minute, obj->second); + set_json_icon_state_value(root, obj, "time", value, value, start_config); + if (start_config == DETAIL_ALL) { + this->add_sorting_info_(root, obj); + } + + return builder.serialize(); } #endif // USE_DATETIME_TIME #ifdef USE_DATETIME_DATETIME void WebServer::on_datetime_update(datetime::DateTimeEntity *obj) { - if (this->events_.empty()) + if (!this->include_internal_ && obj->is_internal()) return; this->events_.deferrable_send_state(obj, "state", datetime_state_json_generator); } void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_datetimes()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); @@ -1108,10 +1125,7 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur return; } - if (request->hasParam("value")) { - std::string value = request->getParam("value")->value().c_str(); // NOLINT - call.set_datetime(value); - } + parse_string_param_(request, "value", call, &decltype(call)::set_datetime); this->defer([call]() mutable { call.perform(); }); request->send(200); @@ -1126,28 +1140,29 @@ std::string WebServer::datetime_all_json_generator(WebServer *web_server, void * return web_server->datetime_json((datetime::DateTimeEntity *) (source), DETAIL_ALL); } std::string WebServer::datetime_json(datetime::DateTimeEntity *obj, JsonDetail start_config) { - return json::build_json([this, obj, start_config](JsonObject root) { - set_json_id(root, obj, "datetime-" + obj->get_object_id(), start_config); - std::string value = str_sprintf("%d-%02d-%02d %02d:%02d:%02d", obj->year, obj->month, obj->day, obj->hour, - obj->minute, obj->second); - root["value"] = value; - root["state"] = value; - if (start_config == DETAIL_ALL) { - this->add_sorting_info_(root, obj); - } - }); + json::JsonBuilder builder; + JsonObject root = builder.root(); + + std::string value = + str_sprintf("%d-%02d-%02d %02d:%02d:%02d", obj->year, obj->month, obj->day, obj->hour, obj->minute, obj->second); + set_json_icon_state_value(root, obj, "datetime", value, value, start_config); + if (start_config == DETAIL_ALL) { + this->add_sorting_info_(root, obj); + } + + return builder.serialize(); } #endif // USE_DATETIME_DATETIME #ifdef USE_TEXT -void WebServer::on_text_update(text::Text *obj, const std::string &state) { - if (this->events_.empty()) +void WebServer::on_text_update(text::Text *obj) { + if (!this->include_internal_ && obj->is_internal()) return; this->events_.deferrable_send_state(obj, "state", text_state_json_generator); } void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_texts()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; if (request->method() == HTTP_GET && match.method_empty()) { @@ -1162,10 +1177,7 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat } auto call = obj->make_call(); - if (request->hasParam("value")) { - String value = request->getParam("value")->value(); - call.set_value(value.c_str()); // NOLINT - } + parse_string_param_(request, "value", call, &decltype(call)::set_value); this->defer([call]() mutable { call.perform(); }); request->send(200); @@ -1181,39 +1193,37 @@ std::string WebServer::text_all_json_generator(WebServer *web_server, void *sour return web_server->text_json((text::Text *) (source), ((text::Text *) (source))->state, DETAIL_ALL); } std::string WebServer::text_json(text::Text *obj, const std::string &value, JsonDetail start_config) { - return json::build_json([this, obj, value, start_config](JsonObject root) { - set_json_id(root, obj, "text-" + obj->get_object_id(), start_config); - root["min_length"] = obj->traits.get_min_length(); - root["max_length"] = obj->traits.get_max_length(); - root["pattern"] = obj->traits.get_pattern(); - if (obj->traits.get_mode() == text::TextMode::TEXT_MODE_PASSWORD) { - root["state"] = "********"; - } else { - root["state"] = value; - } - root["value"] = value; - if (start_config == DETAIL_ALL) { - root["mode"] = (int) obj->traits.get_mode(); - this->add_sorting_info_(root, obj); - } - }); + json::JsonBuilder builder; + JsonObject root = builder.root(); + + std::string state = obj->traits.get_mode() == text::TextMode::TEXT_MODE_PASSWORD ? "********" : value; + set_json_icon_state_value(root, obj, "text", state, value, start_config); + root["min_length"] = obj->traits.get_min_length(); + root["max_length"] = obj->traits.get_max_length(); + root["pattern"] = obj->traits.get_pattern(); + if (start_config == DETAIL_ALL) { + root["mode"] = (int) obj->traits.get_mode(); + this->add_sorting_info_(root, obj); + } + + return builder.serialize(); } #endif #ifdef USE_SELECT -void WebServer::on_select_update(select::Select *obj, const std::string &state, size_t index) { - if (this->events_.empty()) +void WebServer::on_select_update(select::Select *obj) { + if (!this->include_internal_ && obj->is_internal()) return; this->events_.deferrable_send_state(obj, "state", select_state_json_generator); } void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_selects()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); - std::string data = this->select_json(obj, obj->state, detail); + std::string data = this->select_json(obj, obj->has_state() ? obj->current_option() : "", detail); request->send(200, "application/json", data.c_str()); return; } @@ -1224,11 +1234,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM } auto call = obj->make_call(); - - if (request->hasParam("option")) { - auto option = request->getParam("option")->value(); - call.set_option(option.c_str()); // NOLINT - } + parse_string_param_(request, "option", call, &decltype(call)::set_option); this->defer([call]() mutable { call.perform(); }); request->send(200); @@ -1237,37 +1243,42 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM request->send(404); } std::string WebServer::select_state_json_generator(WebServer *web_server, void *source) { - return web_server->select_json((select::Select *) (source), ((select::Select *) (source))->state, DETAIL_STATE); + auto *obj = (select::Select *) (source); + return web_server->select_json(obj, obj->has_state() ? obj->current_option() : "", DETAIL_STATE); } std::string WebServer::select_all_json_generator(WebServer *web_server, void *source) { - return web_server->select_json((select::Select *) (source), ((select::Select *) (source))->state, DETAIL_ALL); + auto *obj = (select::Select *) (source); + return web_server->select_json(obj, obj->has_state() ? obj->current_option() : "", DETAIL_ALL); } -std::string WebServer::select_json(select::Select *obj, const std::string &value, JsonDetail start_config) { - return json::build_json([this, obj, value, start_config](JsonObject root) { - set_json_icon_state_value(root, obj, "select-" + obj->get_object_id(), value, value, start_config); - if (start_config == DETAIL_ALL) { - JsonArray opt = root["option"].to(); - for (auto &option : obj->traits.get_options()) { - opt.add(option); - } - this->add_sorting_info_(root, obj); +std::string WebServer::select_json(select::Select *obj, const char *value, JsonDetail start_config) { + json::JsonBuilder builder; + JsonObject root = builder.root(); + + set_json_icon_state_value(root, obj, "select", value, value, start_config); + if (start_config == DETAIL_ALL) { + JsonArray opt = root["option"].to(); + for (auto &option : obj->traits.get_options()) { + opt.add(option); } - }); + this->add_sorting_info_(root, obj); + } + + return builder.serialize(); } #endif // Longest: HORIZONTAL -#define PSTR_LOCAL(mode_s) strncpy_P(buf, (PGM_P) ((mode_s)), 15) +#define PSTR_LOCAL(mode_s) ESPHOME_strncpy_P(buf, (ESPHOME_PGM_P) ((mode_s)), 15) #ifdef USE_CLIMATE void WebServer::on_climate_update(climate::Climate *obj) { - if (this->events_.empty()) + if (!this->include_internal_ && obj->is_internal()) return; this->events_.deferrable_send_state(obj, "state", climate_state_json_generator); } void WebServer::handle_climate_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_climates()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; if (request->method() == HTTP_GET && match.method_empty()) { @@ -1284,38 +1295,15 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url auto call = obj->make_call(); - if (request->hasParam("mode")) { - auto mode = request->getParam("mode")->value(); - call.set_mode(mode.c_str()); // NOLINT - } + // Parse string mode parameters + parse_string_param_(request, "mode", call, &decltype(call)::set_mode); + parse_string_param_(request, "fan_mode", call, &decltype(call)::set_fan_mode); + parse_string_param_(request, "swing_mode", call, &decltype(call)::set_swing_mode); - if (request->hasParam("fan_mode")) { - auto mode = request->getParam("fan_mode")->value(); - call.set_fan_mode(mode.c_str()); // NOLINT - } - - if (request->hasParam("swing_mode")) { - auto mode = request->getParam("swing_mode")->value(); - call.set_swing_mode(mode.c_str()); // NOLINT - } - - if (request->hasParam("target_temperature_high")) { - auto target_temperature_high = parse_number(request->getParam("target_temperature_high")->value().c_str()); - if (target_temperature_high.has_value()) - call.set_target_temperature_high(*target_temperature_high); - } - - if (request->hasParam("target_temperature_low")) { - auto target_temperature_low = parse_number(request->getParam("target_temperature_low")->value().c_str()); - if (target_temperature_low.has_value()) - call.set_target_temperature_low(*target_temperature_low); - } - - if (request->hasParam("target_temperature")) { - auto target_temperature = parse_number(request->getParam("target_temperature")->value().c_str()); - if (target_temperature.has_value()) - call.set_target_temperature(*target_temperature); - } + // Parse temperature parameters + parse_float_param_(request, "target_temperature_high", call, &decltype(call)::set_target_temperature_high); + parse_float_param_(request, "target_temperature_low", call, &decltype(call)::set_target_temperature_low); + parse_float_param_(request, "target_temperature", call, &decltype(call)::set_target_temperature); this->defer([call]() mutable { call.perform(); }); request->send(200); @@ -1324,125 +1312,153 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url request->send(404); } std::string WebServer::climate_state_json_generator(WebServer *web_server, void *source) { + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson return web_server->climate_json((climate::Climate *) (source), DETAIL_STATE); } std::string WebServer::climate_all_json_generator(WebServer *web_server, void *source) { + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson return web_server->climate_json((climate::Climate *) (source), DETAIL_ALL); } std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_config) { // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - return json::build_json([this, obj, start_config](JsonObject root) { - set_json_id(root, obj, "climate-" + obj->get_object_id(), start_config); - const auto traits = obj->get_traits(); - int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals(); - int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals(); - char buf[16]; + json::JsonBuilder builder; + JsonObject root = builder.root(); + set_json_id(root, obj, "climate", start_config); + const auto traits = obj->get_traits(); + int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals(); + int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals(); + char buf[16]; - if (start_config == DETAIL_ALL) { - JsonArray opt = root["modes"].to(); - for (climate::ClimateMode m : traits.get_supported_modes()) - opt.add(PSTR_LOCAL(climate::climate_mode_to_string(m))); - if (!traits.get_supported_custom_fan_modes().empty()) { - JsonArray opt = root["fan_modes"].to(); - for (climate::ClimateFanMode m : traits.get_supported_fan_modes()) - opt.add(PSTR_LOCAL(climate::climate_fan_mode_to_string(m))); - } - - if (!traits.get_supported_custom_fan_modes().empty()) { - JsonArray opt = root["custom_fan_modes"].to(); - for (auto const &custom_fan_mode : traits.get_supported_custom_fan_modes()) - opt.add(custom_fan_mode); - } - if (traits.get_supports_swing_modes()) { - JsonArray opt = root["swing_modes"].to(); - for (auto swing_mode : traits.get_supported_swing_modes()) - opt.add(PSTR_LOCAL(climate::climate_swing_mode_to_string(swing_mode))); - } - if (traits.get_supports_presets() && obj->preset.has_value()) { - JsonArray opt = root["presets"].to(); - for (climate::ClimatePreset m : traits.get_supported_presets()) - opt.add(PSTR_LOCAL(climate::climate_preset_to_string(m))); - } - if (!traits.get_supported_custom_presets().empty() && obj->custom_preset.has_value()) { - JsonArray opt = root["custom_presets"].to(); - for (auto const &custom_preset : traits.get_supported_custom_presets()) - opt.add(custom_preset); - } - this->add_sorting_info_(root, obj); + if (start_config == DETAIL_ALL) { + JsonArray opt = root["modes"].to(); + for (climate::ClimateMode m : traits.get_supported_modes()) + opt.add(PSTR_LOCAL(climate::climate_mode_to_string(m))); + if (!traits.get_supported_custom_fan_modes().empty()) { + JsonArray opt = root["fan_modes"].to(); + for (climate::ClimateFanMode m : traits.get_supported_fan_modes()) + opt.add(PSTR_LOCAL(climate::climate_fan_mode_to_string(m))); } - bool has_state = false; - root["mode"] = PSTR_LOCAL(climate_mode_to_string(obj->mode)); - root["max_temp"] = value_accuracy_to_string(traits.get_visual_max_temperature(), target_accuracy); - root["min_temp"] = value_accuracy_to_string(traits.get_visual_min_temperature(), target_accuracy); - root["step"] = traits.get_visual_target_temperature_step(); - if (traits.get_supports_action()) { - root["action"] = PSTR_LOCAL(climate_action_to_string(obj->action)); - root["state"] = root["action"]; - has_state = true; - } - if (traits.get_supports_fan_modes() && obj->fan_mode.has_value()) { - root["fan_mode"] = PSTR_LOCAL(climate_fan_mode_to_string(obj->fan_mode.value())); - } - if (!traits.get_supported_custom_fan_modes().empty() && obj->custom_fan_mode.has_value()) { - root["custom_fan_mode"] = obj->custom_fan_mode.value().c_str(); - } - if (traits.get_supports_presets() && obj->preset.has_value()) { - root["preset"] = PSTR_LOCAL(climate_preset_to_string(obj->preset.value())); - } - if (!traits.get_supported_custom_presets().empty() && obj->custom_preset.has_value()) { - root["custom_preset"] = obj->custom_preset.value().c_str(); + if (!traits.get_supported_custom_fan_modes().empty()) { + JsonArray opt = root["custom_fan_modes"].to(); + for (auto const &custom_fan_mode : traits.get_supported_custom_fan_modes()) + opt.add(custom_fan_mode); } if (traits.get_supports_swing_modes()) { - root["swing_mode"] = PSTR_LOCAL(climate_swing_mode_to_string(obj->swing_mode)); + JsonArray opt = root["swing_modes"].to(); + for (auto swing_mode : traits.get_supported_swing_modes()) + opt.add(PSTR_LOCAL(climate::climate_swing_mode_to_string(swing_mode))); } - if (traits.get_supports_current_temperature()) { - if (!std::isnan(obj->current_temperature)) { - root["current_temperature"] = value_accuracy_to_string(obj->current_temperature, current_accuracy); - } else { - root["current_temperature"] = "NA"; - } + if (traits.get_supports_presets() && obj->preset.has_value()) { + JsonArray opt = root["presets"].to(); + for (climate::ClimatePreset m : traits.get_supported_presets()) + opt.add(PSTR_LOCAL(climate::climate_preset_to_string(m))); } - if (traits.get_supports_two_point_target_temperature()) { - root["target_temperature_low"] = value_accuracy_to_string(obj->target_temperature_low, target_accuracy); - root["target_temperature_high"] = value_accuracy_to_string(obj->target_temperature_high, target_accuracy); - if (!has_state) { - root["state"] = value_accuracy_to_string((obj->target_temperature_high + obj->target_temperature_low) / 2.0f, - target_accuracy); - } + if (!traits.get_supported_custom_presets().empty() && obj->has_custom_preset()) { + JsonArray opt = root["custom_presets"].to(); + for (auto const &custom_preset : traits.get_supported_custom_presets()) + opt.add(custom_preset); + } + this->add_sorting_info_(root, obj); + } + + bool has_state = false; + root["mode"] = PSTR_LOCAL(climate_mode_to_string(obj->mode)); + root["max_temp"] = value_accuracy_to_string(traits.get_visual_max_temperature(), target_accuracy); + root["min_temp"] = value_accuracy_to_string(traits.get_visual_min_temperature(), target_accuracy); + root["step"] = traits.get_visual_target_temperature_step(); + if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) { + root["action"] = PSTR_LOCAL(climate_action_to_string(obj->action)); + root["state"] = root["action"]; + has_state = true; + } + if (traits.get_supports_fan_modes() && obj->fan_mode.has_value()) { + root["fan_mode"] = PSTR_LOCAL(climate_fan_mode_to_string(obj->fan_mode.value())); + } + if (!traits.get_supported_custom_fan_modes().empty() && obj->has_custom_fan_mode()) { + root["custom_fan_mode"] = obj->get_custom_fan_mode(); + } + if (traits.get_supports_presets() && obj->preset.has_value()) { + root["preset"] = PSTR_LOCAL(climate_preset_to_string(obj->preset.value())); + } + if (!traits.get_supported_custom_presets().empty() && obj->has_custom_preset()) { + root["custom_preset"] = obj->get_custom_preset(); + } + if (traits.get_supports_swing_modes()) { + root["swing_mode"] = PSTR_LOCAL(climate_swing_mode_to_string(obj->swing_mode)); + } + if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) { + if (!std::isnan(obj->current_temperature)) { + root["current_temperature"] = value_accuracy_to_string(obj->current_temperature, current_accuracy); } else { - root["target_temperature"] = value_accuracy_to_string(obj->target_temperature, target_accuracy); - if (!has_state) - root["state"] = root["target_temperature"]; + root["current_temperature"] = "NA"; } - }); + } + if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | + climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) { + root["target_temperature_low"] = value_accuracy_to_string(obj->target_temperature_low, target_accuracy); + root["target_temperature_high"] = value_accuracy_to_string(obj->target_temperature_high, target_accuracy); + if (!has_state) { + root["state"] = value_accuracy_to_string((obj->target_temperature_high + obj->target_temperature_low) / 2.0f, + target_accuracy); + } + } else { + root["target_temperature"] = value_accuracy_to_string(obj->target_temperature, target_accuracy); + if (!has_state) + root["state"] = root["target_temperature"]; + } + + return builder.serialize(); // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) } #endif #ifdef USE_LOCK void WebServer::on_lock_update(lock::Lock *obj) { - if (this->events_.empty()) + if (!this->include_internal_ && obj->is_internal()) return; this->events_.deferrable_send_state(obj, "state", lock_state_json_generator); } void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (lock::Lock *obj : App.get_locks()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); std::string data = this->lock_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); - } else if (match.method_equals("lock")) { - this->defer([obj]() { obj->lock(); }); - request->send(200); + return; + } + + // Handle action methods with single defer and response + enum LockAction { NONE, LOCK, UNLOCK, OPEN }; + LockAction action = NONE; + + if (match.method_equals("lock")) { + action = LOCK; } else if (match.method_equals("unlock")) { - this->defer([obj]() { obj->unlock(); }); - request->send(200); + action = UNLOCK; } else if (match.method_equals("open")) { - this->defer([obj]() { obj->open(); }); + 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; + } + }); request->send(200); } else { request->send(404); @@ -1458,25 +1474,27 @@ std::string WebServer::lock_all_json_generator(WebServer *web_server, void *sour return web_server->lock_json((lock::Lock *) (source), ((lock::Lock *) (source))->state, DETAIL_ALL); } std::string WebServer::lock_json(lock::Lock *obj, lock::LockState value, JsonDetail start_config) { - return json::build_json([this, obj, value, start_config](JsonObject root) { - set_json_icon_state_value(root, obj, "lock-" + obj->get_object_id(), lock::lock_state_to_string(value), value, - start_config); - if (start_config == DETAIL_ALL) { - this->add_sorting_info_(root, obj); - } - }); + json::JsonBuilder builder; + JsonObject root = builder.root(); + + set_json_icon_state_value(root, obj, "lock", lock::lock_state_to_string(value), value, start_config); + if (start_config == DETAIL_ALL) { + this->add_sorting_info_(root, obj); + } + + return builder.serialize(); } #endif #ifdef USE_VALVE void WebServer::on_valve_update(valve::Valve *obj) { - if (this->events_.empty()) + if (!this->include_internal_ && obj->is_internal()) return; this->events_.deferrable_send_state(obj, "state", valve_state_json_generator); } void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (valve::Valve *obj : App.get_valves()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; if (request->method() == HTTP_GET && match.method_empty()) { @@ -1487,15 +1505,28 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa } auto call = obj->make_call(); - if (match.method_equals("open")) { - call.set_command_open(); - } else if (match.method_equals("close")) { - call.set_command_close(); - } else if (match.method_equals("stop")) { - call.set_command_stop(); - } else if (match.method_equals("toggle")) { - call.set_command_toggle(); - } else if (!match.method_equals("set")) { + + // Lookup table for valve methods + static const struct { + const char *name; + valve::ValveCall &(valve::ValveCall::*action)(); + } METHODS[] = { + {"open", &valve::ValveCall::set_command_open}, + {"close", &valve::ValveCall::set_command_close}, + {"stop", &valve::ValveCall::set_command_stop}, + {"toggle", &valve::ValveCall::set_command_toggle}, + }; + + bool found = false; + for (const auto &method : METHODS) { + if (match.method_equals(method.name)) { + (call.*method.action)(); + found = true; + break; + } + } + + if (!found && !match.method_equals("set")) { request->send(404); return; } @@ -1506,12 +1537,7 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa return; } - if (request->hasParam("position")) { - auto position = parse_number(request->getParam("position")->value().c_str()); - if (position.has_value()) { - call.set_position(*position); - } - } + parse_float_param_(request, "position", call, &decltype(call)::set_position); this->defer([call]() mutable { call.perform(); }); request->send(200); @@ -1526,29 +1552,32 @@ std::string WebServer::valve_all_json_generator(WebServer *web_server, void *sou return web_server->valve_json((valve::Valve *) (source), DETAIL_ALL); } std::string WebServer::valve_json(valve::Valve *obj, JsonDetail start_config) { - return json::build_json([this, obj, start_config](JsonObject root) { - set_json_icon_state_value(root, obj, "valve-" + obj->get_object_id(), obj->is_fully_closed() ? "CLOSED" : "OPEN", - obj->position, start_config); - root["current_operation"] = valve::valve_operation_to_str(obj->current_operation); + json::JsonBuilder builder; + JsonObject root = builder.root(); - if (obj->get_traits().get_supports_position()) - root["position"] = obj->position; - if (start_config == DETAIL_ALL) { - this->add_sorting_info_(root, obj); - } - }); + set_json_icon_state_value(root, obj, "valve", obj->is_fully_closed() ? "CLOSED" : "OPEN", obj->position, + start_config); + root["current_operation"] = valve::valve_operation_to_str(obj->current_operation); + + if (obj->get_traits().get_supports_position()) + root["position"] = obj->position; + if (start_config == DETAIL_ALL) { + this->add_sorting_info_(root, obj); + } + + return builder.serialize(); } #endif #ifdef USE_ALARM_CONTROL_PANEL void WebServer::on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) { - if (this->events_.empty()) + if (!this->include_internal_ && obj->is_internal()) return; this->events_.deferrable_send_state(obj, "state", alarm_control_panel_state_json_generator); } void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (alarm_control_panel::AlarmControlPanel *obj : App.get_alarm_control_panels()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; if (request->method() == HTTP_GET && match.method_empty()) { @@ -1559,21 +1588,30 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques } auto call = obj->make_call(); - if (request->hasParam("code")) { - call.set_code(request->getParam("code")->value().c_str()); // NOLINT + parse_string_param_(request, "code", call, &decltype(call)::set_code); + + // Lookup table for alarm control panel methods + static const struct { + const char *name; + alarm_control_panel::AlarmControlPanelCall &(alarm_control_panel::AlarmControlPanelCall::*action)(); + } METHODS[] = { + {"disarm", &alarm_control_panel::AlarmControlPanelCall::disarm}, + {"arm_away", &alarm_control_panel::AlarmControlPanelCall::arm_away}, + {"arm_home", &alarm_control_panel::AlarmControlPanelCall::arm_home}, + {"arm_night", &alarm_control_panel::AlarmControlPanelCall::arm_night}, + {"arm_vacation", &alarm_control_panel::AlarmControlPanelCall::arm_vacation}, + }; + + bool found = false; + for (const auto &method : METHODS) { + if (match.method_equals(method.name)) { + (call.*method.action)(); + found = true; + break; + } } - if (match.method_equals("disarm")) { - call.disarm(); - } else if (match.method_equals("arm_away")) { - call.arm_away(); - } else if (match.method_equals("arm_home")) { - call.arm_home(); - } else if (match.method_equals("arm_night")) { - call.arm_night(); - } else if (match.method_equals("arm_vacation")) { - call.arm_vacation(); - } else { + if (!found) { request->send(404); return; } @@ -1597,28 +1635,34 @@ std::string WebServer::alarm_control_panel_all_json_generator(WebServer *web_ser std::string WebServer::alarm_control_panel_json(alarm_control_panel::AlarmControlPanel *obj, alarm_control_panel::AlarmControlPanelState value, JsonDetail start_config) { - return json::build_json([this, obj, value, start_config](JsonObject root) { - char buf[16]; - set_json_icon_state_value(root, obj, "alarm-control-panel-" + obj->get_object_id(), - PSTR_LOCAL(alarm_control_panel_state_to_string(value)), value, start_config); - if (start_config == DETAIL_ALL) { - this->add_sorting_info_(root, obj); - } - }); + json::JsonBuilder builder; + JsonObject root = builder.root(); + + char buf[16]; + set_json_icon_state_value(root, obj, "alarm-control-panel", PSTR_LOCAL(alarm_control_panel_state_to_string(value)), + value, start_config); + if (start_config == DETAIL_ALL) { + this->add_sorting_info_(root, obj); + } + + return builder.serialize(); } #endif #ifdef USE_EVENT -void WebServer::on_event(event::Event *obj, const std::string &event_type) { +void WebServer::on_event(event::Event *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", event_state_json_generator); } void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (event::Event *obj : App.get_events()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + // Note: request->method() is always HTTP_GET here (canHandle ensures this) + if (match.method_empty()) { auto detail = get_request_detail(request); std::string data = this->event_json(obj, "", detail); request->send(200, "application/json", data.c_str()); @@ -1629,7 +1673,8 @@ void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMa } static std::string get_event_type(event::Event *event) { - return (event && event->last_event_type) ? *event->last_event_type : ""; + const char *last_type = event ? event->get_last_event_type() : nullptr; + return last_type ? last_type : ""; } std::string WebServer::event_state_json_generator(WebServer *web_server, void *source) { @@ -1641,32 +1686,46 @@ std::string WebServer::event_all_json_generator(WebServer *web_server, void *sou return web_server->event_json(event, get_event_type(event), DETAIL_ALL); } std::string WebServer::event_json(event::Event *obj, const std::string &event_type, JsonDetail start_config) { - return json::build_json([this, obj, event_type, start_config](JsonObject root) { - set_json_id(root, obj, "event-" + obj->get_object_id(), start_config); - if (!event_type.empty()) { - root["event_type"] = event_type; + json::JsonBuilder builder; + JsonObject root = builder.root(); + + set_json_id(root, obj, "event", start_config); + if (!event_type.empty()) { + root["event_type"] = event_type; + } + if (start_config == DETAIL_ALL) { + JsonArray event_types = root["event_types"].to(); + for (const char *event_type : obj->get_event_types()) { + event_types.add(event_type); } - if (start_config == DETAIL_ALL) { - JsonArray event_types = root["event_types"].to(); - for (auto const &event_type : obj->get_event_types()) { - event_types.add(event_type); - } - root["device_class"] = obj->get_device_class(); - this->add_sorting_info_(root, obj); - } - }); + root["device_class"] = obj->get_device_class_ref(); + this->add_sorting_info_(root, obj); + } + + return builder.serialize(); } #endif #ifdef USE_UPDATE +static const char *update_state_to_string(update::UpdateState state) { + switch (state) { + case update::UPDATE_STATE_NO_UPDATE: + return "NO UPDATE"; + case update::UPDATE_STATE_AVAILABLE: + return "UPDATE AVAILABLE"; + case update::UPDATE_STATE_INSTALLING: + return "INSTALLING"; + default: + return "UNKNOWN"; + } +} + void WebServer::on_update(update::UpdateEntity *obj) { - if (this->events_.empty()) - return; this->events_.deferrable_send_state(obj, "state", update_state_json_generator); } void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (update::UpdateEntity *obj : App.get_updates()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; if (request->method() == HTTP_GET && match.method_empty()) { @@ -1688,38 +1747,29 @@ void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlM request->send(404); } std::string WebServer::update_state_json_generator(WebServer *web_server, void *source) { + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson return web_server->update_json((update::UpdateEntity *) (source), DETAIL_STATE); } std::string WebServer::update_all_json_generator(WebServer *web_server, void *source) { + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson return web_server->update_json((update::UpdateEntity *) (source), DETAIL_STATE); } std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_config) { // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - return json::build_json([this, obj, start_config](JsonObject root) { - set_json_id(root, obj, "update-" + obj->get_object_id(), start_config); - root["value"] = obj->update_info.latest_version; - switch (obj->state) { - case update::UPDATE_STATE_NO_UPDATE: - root["state"] = "NO UPDATE"; - break; - case update::UPDATE_STATE_AVAILABLE: - root["state"] = "UPDATE AVAILABLE"; - break; - case update::UPDATE_STATE_INSTALLING: - root["state"] = "INSTALLING"; - break; - default: - root["state"] = "UNKNOWN"; - break; - } - if (start_config == DETAIL_ALL) { - root["current_version"] = obj->update_info.current_version; - root["title"] = obj->update_info.title; - root["summary"] = obj->update_info.summary; - root["release_url"] = obj->update_info.release_url; - this->add_sorting_info_(root, obj); - } - }); + json::JsonBuilder builder; + JsonObject root = builder.root(); + + set_json_icon_state_value(root, obj, "update", update_state_to_string(obj->state), obj->update_info.latest_version, + start_config); + if (start_config == DETAIL_ALL) { + root["current_version"] = obj->update_info.current_version; + root["title"] = obj->update_info.title; + root["summary"] = obj->update_info.summary; + root["release_url"] = obj->update_info.release_url; + this->add_sorting_info_(root, obj); + } + + return builder.serialize(); // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) } #endif @@ -1728,24 +1778,24 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) const { const auto &url = request->url(); const auto method = request->method(); - // Simple URL checks - if (url == "/") - return true; - -#ifdef USE_ARDUINO - if (url == "/events") - return true; + // Static URL checks + static const char *const STATIC_URLS[] = { + "/", +#if !defined(USE_ESP32) && defined(USE_ARDUINO) + "/events", #endif - #ifdef USE_WEBSERVER_CSS_INCLUDE - if (url == "/0.css") - return true; + "/0.css", #endif - #ifdef USE_WEBSERVER_JS_INCLUDE - if (url == "/0.js") - return true; + "/0.js", #endif + }; + + for (const auto &static_url : STATIC_URLS) { + if (url == static_url) + return true; + } #ifdef USE_WEBSERVER_PRIVATE_NETWORK_ACCESS if (method == HTTP_OPTIONS && request->hasHeader(HEADER_CORS_REQ_PNA)) @@ -1765,92 +1815,87 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) const { if (!is_get_or_post) return false; - // GET-only components - if (is_get) { + // Use lookup tables for domain checks + static const char *const GET_ONLY_DOMAINS[] = { #ifdef USE_SENSOR - if (match.domain_equals("sensor")) - return true; + "sensor", #endif #ifdef USE_BINARY_SENSOR - if (match.domain_equals("binary_sensor")) - return true; + "binary_sensor", #endif #ifdef USE_TEXT_SENSOR - if (match.domain_equals("text_sensor")) - return true; + "text_sensor", #endif #ifdef USE_EVENT - if (match.domain_equals("event")) - return true; + "event", #endif - } + }; - // GET+POST components - if (is_get_or_post) { + static const char *const GET_POST_DOMAINS[] = { #ifdef USE_SWITCH - if (match.domain_equals("switch")) - return true; + "switch", #endif #ifdef USE_BUTTON - if (match.domain_equals("button")) - return true; + "button", #endif #ifdef USE_FAN - if (match.domain_equals("fan")) - return true; + "fan", #endif #ifdef USE_LIGHT - if (match.domain_equals("light")) - return true; + "light", #endif #ifdef USE_COVER - if (match.domain_equals("cover")) - return true; + "cover", #endif #ifdef USE_NUMBER - if (match.domain_equals("number")) - return true; + "number", #endif #ifdef USE_DATETIME_DATE - if (match.domain_equals("date")) - return true; + "date", #endif #ifdef USE_DATETIME_TIME - if (match.domain_equals("time")) - return true; + "time", #endif #ifdef USE_DATETIME_DATETIME - if (match.domain_equals("datetime")) - return true; + "datetime", #endif #ifdef USE_TEXT - if (match.domain_equals("text")) - return true; + "text", #endif #ifdef USE_SELECT - if (match.domain_equals("select")) - return true; + "select", #endif #ifdef USE_CLIMATE - if (match.domain_equals("climate")) - return true; + "climate", #endif #ifdef USE_LOCK - if (match.domain_equals("lock")) - return true; + "lock", #endif #ifdef USE_VALVE - if (match.domain_equals("valve")) - return true; + "valve", #endif #ifdef USE_ALARM_CONTROL_PANEL - if (match.domain_equals("alarm_control_panel")) - return true; + "alarm_control_panel", #endif #ifdef USE_UPDATE - if (match.domain_equals("update")) - return true; + "update", #endif + }; + + // Check GET-only domains + if (is_get) { + for (const auto &domain : GET_ONLY_DOMAINS) { + if (match.domain_equals(domain)) + return true; + } + } + + // Check GET+POST domains + if (is_get_or_post) { + for (const auto &domain : GET_POST_DOMAINS) { + if (match.domain_equals(domain)) + return true; + } } return false; @@ -1864,7 +1909,7 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { return; } -#ifdef USE_ARDUINO +#if !defined(USE_ESP32) && defined(USE_ARDUINO) if (url == "/events") { this->events_.add_new_client(this, request); return; diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index ef1b03a73b..7e1af88645 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -48,8 +48,15 @@ struct UrlMatch { return domain && domain_len == strlen(str) && memcmp(domain, str, domain_len) == 0; } - bool id_equals(const std::string &str) const { - return id && id_len == str.length() && memcmp(id, str.c_str(), id_len) == 0; + bool id_equals_entity(EntityBase *entity) const { + // Zero-copy comparison using StringRef + StringRef static_ref = entity->get_object_id_ref_for_api_(); + if (!static_ref.empty()) { + return id && id_len == static_ref.size() && memcmp(id, static_ref.c_str(), id_len) == 0; + } + // Fallback to allocation (rare) + const auto &obj_id = entity->get_object_id(); + return id && id_len == obj_id.length() && memcmp(id, obj_id.c_str(), id_len) == 0; } bool method_equals(const char *str) const { @@ -81,7 +88,7 @@ enum JsonDetail { DETAIL_ALL, DETAIL_STATE }; implemented in a more straightforward way for ESP-IDF. Arduino platform will eventually go away and this workaround can be forgotten. */ -#ifdef USE_ARDUINO +#if !defined(USE_ESP32) && defined(USE_ARDUINO) using message_generator_t = std::string(WebServer *, void *); class DeferredUpdateEventSourceList; @@ -141,7 +148,7 @@ class DeferredUpdateEventSource : public AsyncEventSource { class DeferredUpdateEventSourceList : public std::list { protected: - void on_client_connect_(WebServer *ws, DeferredUpdateEventSource *source); + void on_client_connect_(DeferredUpdateEventSource *source); void on_client_disconnect_(DeferredUpdateEventSource *source); public: @@ -164,7 +171,7 @@ class DeferredUpdateEventSourceList : public std::list that's sent to each client. Defaults to - * https://esphome.io/_static/webserver-v1.min.css + * https://oi.esphome.io/v1/webserver-v1.min.css * * @param css_url The url to the web server stylesheet. */ void set_css_url(const char *css_url); /** Set the URL to the script that's embedded in the index page. Defaults to - * https://esphome.io/_static/webserver-v1.min.js + * https://oi.esphome.io/v1/webserver-v1.min.js * * @param js_url The url to the web server script. */ @@ -248,7 +255,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { #endif #ifdef USE_SENSOR - void on_sensor_update(sensor::Sensor *obj, float state) override; + void on_sensor_update(sensor::Sensor *obj) override; /// Handle a sensor request under '/sensor/'. void handle_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -259,7 +266,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { #endif #ifdef USE_SWITCH - void on_switch_update(switch_::Switch *obj, bool state) override; + void on_switch_update(switch_::Switch *obj) override; /// Handle a switch request under '/switch//'. void handle_switch_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -317,7 +324,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { #endif #ifdef USE_TEXT_SENSOR - void on_text_sensor_update(text_sensor::TextSensor *obj, const std::string &state) override; + void on_text_sensor_update(text_sensor::TextSensor *obj) override; /// Handle a text sensor request under '/text_sensor/'. void handle_text_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -341,7 +348,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { #endif #ifdef USE_NUMBER - void on_number_update(number::Number *obj, float state) override; + void on_number_update(number::Number *obj) override; /// Handle a number request under '/number/'. void handle_number_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -385,7 +392,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { #endif #ifdef USE_TEXT - void on_text_update(text::Text *obj, const std::string &state) override; + void on_text_update(text::Text *obj) override; /// Handle a text input request under '/text/'. void handle_text_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -396,14 +403,14 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { #endif #ifdef USE_SELECT - void on_select_update(select::Select *obj, const std::string &state, size_t index) override; + void on_select_update(select::Select *obj) override; /// Handle a select request under '/select/'. void handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match); static std::string select_state_json_generator(WebServer *web_server, void *source); static std::string select_all_json_generator(WebServer *web_server, void *source); /// Dump the select state with its value as a JSON string. - std::string select_json(select::Select *obj, const std::string &value, JsonDetail start_config); + std::string select_json(select::Select *obj, const char *value, JsonDetail start_config); #endif #ifdef USE_CLIMATE @@ -455,7 +462,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { #endif #ifdef USE_EVENT - void on_event(event::Event *obj, const std::string &event_type) override; + void on_event(event::Event *obj) override; static std::string event_state_json_generator(WebServer *web_server, void *source); static std::string event_all_json_generator(WebServer *web_server, void *source); @@ -498,12 +505,71 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { protected: void add_sorting_info_(JsonObject &root, EntityBase *entity); - web_server_base::WebServerBase *base_; -#ifdef USE_ARDUINO - DeferredUpdateEventSourceList events_; + +#ifdef USE_LIGHT + // Helper to parse and apply a float parameter with optional scaling + template + void parse_light_param_(AsyncWebServerRequest *request, const char *param_name, T &call, Ret (T::*setter)(float), + float scale = 1.0f) { + if (request->hasParam(param_name)) { + auto value = parse_number(request->getParam(param_name)->value().c_str()); + if (value.has_value()) { + (call.*setter)(*value / scale); + } + } + } + + // Helper to parse and apply a uint32_t parameter with optional scaling + template + void parse_light_param_uint_(AsyncWebServerRequest *request, const char *param_name, T &call, + Ret (T::*setter)(uint32_t), uint32_t scale = 1) { + if (request->hasParam(param_name)) { + auto value = parse_number(request->getParam(param_name)->value().c_str()); + if (value.has_value()) { + (call.*setter)(*value * scale); + } + } + } #endif -#ifdef USE_ESP_IDF + + // Generic helper to parse and apply a float parameter + template + void parse_float_param_(AsyncWebServerRequest *request, const char *param_name, T &call, Ret (T::*setter)(float)) { + if (request->hasParam(param_name)) { + auto value = parse_number(request->getParam(param_name)->value().c_str()); + if (value.has_value()) { + (call.*setter)(*value); + } + } + } + + // Generic helper to parse and apply an int parameter + template + void parse_int_param_(AsyncWebServerRequest *request, const char *param_name, T &call, Ret (T::*setter)(int)) { + if (request->hasParam(param_name)) { + auto value = parse_number(request->getParam(param_name)->value().c_str()); + if (value.has_value()) { + (call.*setter)(*value); + } + } + } + + // Generic helper to parse and apply a string parameter + template + void parse_string_param_(AsyncWebServerRequest *request, const char *param_name, T &call, + Ret (T::*setter)(const std::string &)) { + if (request->hasParam(param_name)) { + // .c_str() is required for Arduino framework where value() returns Arduino String instead of std::string + std::string value = request->getParam(param_name)->value().c_str(); // NOLINT(readability-redundant-string-cstr) + (call.*setter)(value); + } + } + + web_server_base::WebServerBase *base_; +#ifdef USE_ESP32 AsyncEventSource events_{"/events", this}; +#elif USE_ARDUINO + DeferredUpdateEventSourceList events_; #endif #if USE_WEBSERVER_VERSION == 1 diff --git a/esphome/components/web_server/web_server_v1.cpp b/esphome/components/web_server/web_server_v1.cpp index 0f558f6d81..870a338620 100644 --- a/esphome/components/web_server/web_server_v1.cpp +++ b/esphome/components/web_server/web_server_v1.cpp @@ -34,23 +34,23 @@ void WebServer::set_js_url(const char *js_url) { this->js_url_ = js_url; } void WebServer::handle_index_request(AsyncWebServerRequest *request) { AsyncResponseStream *stream = request->beginResponseStream("text/html"); const std::string &title = App.get_name(); - stream->print(F("")); + stream->print(ESPHOME_F("<!DOCTYPE html><html lang=\"en\"><head><meta charset=UTF-8><meta " + "name=viewport content=\"width=device-width, initial-scale=1,user-scalable=no\"><title>")); stream->print(title.c_str()); - stream->print(F("")); + stream->print(ESPHOME_F("")); #ifdef USE_WEBSERVER_CSS_INCLUDE - stream->print(F("")); + stream->print(ESPHOME_F("")); #endif if (strlen(this->css_url_) > 0) { - stream->print(F(R"(print(ESPHOME_F(R"(print(this->css_url_); - stream->print(F("\">")); + stream->print(ESPHOME_F("\">")); } - stream->print(F("")); - stream->print(F("

")); + stream->print(ESPHOME_F("")); + stream->print(ESPHOME_F("

")); stream->print(title.c_str()); - stream->print(F("

")); - stream->print(F("

States

")); + stream->print(ESPHOME_F("")); + stream->print(ESPHOME_F("

States

NameStateActions
")); #ifdef USE_SENSOR for (auto *obj : App.get_sensors()) { @@ -190,26 +190,28 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) { } #endif - stream->print(F("
NameStateActions

See ESPHome Web API for " - "REST API documentation.

")); + stream->print( + ESPHOME_F("

See ESPHome Web API for " + "REST API documentation.

")); #if defined(USE_WEBSERVER_OTA) && !defined(USE_WEBSERVER_OTA_DISABLED) // Show OTA form only if web_server OTA is not explicitly disabled // Note: USE_WEBSERVER_OTA_DISABLED only affects web_server, not captive_portal - stream->print(F("

OTA Update

")); + stream->print( + ESPHOME_F("

OTA Update

")); #endif - stream->print(F("

Debug Log

"));
+  stream->print(ESPHOME_F("

Debug Log

"));
 #ifdef USE_WEBSERVER_JS_INCLUDE
   if (this->js_include_ != nullptr) {
-    stream->print(F(""));
+    stream->print(ESPHOME_F(""));
   }
 #endif
   if (strlen(this->js_url_) > 0) {
-    stream->print(F(""));
+    stream->print(ESPHOME_F("\">"));
   }
-  stream->print(F("
")); + stream->print(ESPHOME_F("

")); request->send(stream); } diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py index 9f3371c233..4cf76eba0e 100644 --- a/esphome/components/web_server_base/__init__.py +++ b/esphome/components/web_server_base/__init__.py @@ -2,16 +2,17 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_ID from esphome.core import CORE, coroutine_with_priority +from esphome.coroutine import CoroPriority -CODEOWNERS = ["@OttoWinter"] +CODEOWNERS = ["@esphome/core"] DEPENDENCIES = ["network"] def AUTO_LOAD(): + if CORE.is_esp32: + return ["web_server_idf"] if CORE.using_arduino: return ["async_tcp"] - if CORE.using_esp_idf: - return ["web_server_idf"] return [] @@ -26,12 +27,15 @@ CONFIG_SCHEMA = cv.Schema( ) -@coroutine_with_priority(65.0) +@coroutine_with_priority(CoroPriority.WEB_SERVER_BASE) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) cg.add(cg.RawExpression(f"{web_server_base_ns}::global_web_server_base = {var}")) + if CORE.is_esp32: + return + if CORE.using_arduino: if CORE.is_esp32: cg.add_library("WiFi", None) @@ -39,5 +43,7 @@ async def to_code(config): cg.add_library("Update", None) if CORE.is_esp8266: cg.add_library("ESP8266WiFi", None) + if CORE.is_libretiny: + CORE.add_platformio_option("lib_ignore", ["ESPAsyncTCP", "RPAsyncTCP"]) # https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json cg.add_library("ESP32Async/ESPAsyncWebServer", "3.7.10") diff --git a/esphome/components/web_server_base/web_server_base.h b/esphome/components/web_server_base/web_server_base.h index cfca776ee1..fbf0d00c06 100644 --- a/esphome/components/web_server_base/web_server_base.h +++ b/esphome/components/web_server_base/web_server_base.h @@ -7,11 +7,31 @@ #include "esphome/core/component.h" -#ifdef USE_ARDUINO -#include -#elif USE_ESP_IDF +// Platform-agnostic macros for web server components +// On ESP32 (both Arduino and IDF): Use plain strings (no PROGMEM) +// On ESP8266: Use Arduino's F() macro for PROGMEM strings +#ifdef USE_ESP32 +#define ESPHOME_F(string_literal) (string_literal) +#define ESPHOME_PGM_P const char * +#define ESPHOME_strncpy_P strncpy +#else +// ESP8266 uses Arduino macros +#define ESPHOME_F(string_literal) F(string_literal) +#define ESPHOME_PGM_P PGM_P +#define ESPHOME_strncpy_P strncpy_P +#endif + +#if USE_ESP32 #include "esphome/core/hal.h" #include "esphome/components/web_server_idf/web_server_idf.h" +#else +#include +#endif + +#if USE_ESP32 +using PlatformString = std::string; +#elif USE_ARDUINO +using PlatformString = String; #endif namespace esphome { @@ -28,8 +48,8 @@ class MiddlewareHandler : public AsyncWebHandler { bool canHandle(AsyncWebServerRequest *request) const override { return next_->canHandle(request); } void handleRequest(AsyncWebServerRequest *request) override { next_->handleRequest(request); } - void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, - bool final) override { + void handleUpload(AsyncWebServerRequest *request, const PlatformString &filename, size_t index, uint8_t *data, + size_t len, bool final) override { next_->handleUpload(request, filename, index, data, len, final); } void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) override { @@ -65,8 +85,8 @@ class AuthMiddlewareHandler : public MiddlewareHandler { return; MiddlewareHandler::handleRequest(request); } - void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, - bool final) override { + void handleUpload(AsyncWebServerRequest *request, const PlatformString &filename, size_t index, uint8_t *data, + size_t len, bool final) override { if (!check_auth(request)) return; MiddlewareHandler::handleUpload(request, filename, index, data, len, final); @@ -91,7 +111,7 @@ class WebServerBase : public Component { this->initialized_++; return; } - this->server_ = std::make_shared(this->port_); + this->server_ = std::make_unique(this->port_); // All content is controlled and created by user - so allowing all origins is fine here. DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*"); this->server_->begin(); @@ -107,7 +127,7 @@ class WebServerBase : public Component { this->server_ = nullptr; } } - std::shared_ptr get_server() const { return server_; } + AsyncWebServer *get_server() const { return this->server_.get(); } float get_setup_priority() const override; #ifdef USE_WEBSERVER_AUTH @@ -123,7 +143,7 @@ class WebServerBase : public Component { protected: int initialized_{0}; uint16_t port_{80}; - std::shared_ptr server_{nullptr}; + std::unique_ptr server_{nullptr}; std::vector handlers_; #ifdef USE_WEBSERVER_AUTH internal::Credentials credentials_; diff --git a/esphome/components/web_server_idf/__init__.py b/esphome/components/web_server_idf/__init__.py index 506e1c5c13..74a9d657a6 100644 --- a/esphome/components/web_server_idf/__init__.py +++ b/esphome/components/web_server_idf/__init__.py @@ -5,7 +5,7 @@ CODEOWNERS = ["@dentra"] CONFIG_SCHEMA = cv.All( cv.Schema({}), - cv.only_with_esp_idf, + cv.only_on_esp32, ) diff --git a/esphome/components/web_server_idf/multipart.cpp b/esphome/components/web_server_idf/multipart.cpp index 8655226ab9..2092a41a8e 100644 --- a/esphome/components/web_server_idf/multipart.cpp +++ b/esphome/components/web_server_idf/multipart.cpp @@ -1,5 +1,5 @@ #include "esphome/core/defines.h" -#if defined(USE_ESP_IDF) && defined(USE_WEBSERVER_OTA) +#if defined(USE_ESP32) && defined(USE_WEBSERVER_OTA) #include "multipart.h" #include "utils.h" #include "esphome/core/log.h" @@ -251,4 +251,4 @@ std::string str_trim(const std::string &str) { } // namespace web_server_idf } // namespace esphome -#endif // defined(USE_ESP_IDF) && defined(USE_WEBSERVER_OTA) +#endif // defined(USE_ESP32) && defined(USE_WEBSERVER_OTA) diff --git a/esphome/components/web_server_idf/multipart.h b/esphome/components/web_server_idf/multipart.h index 967c72ffa5..8fbe90c4a0 100644 --- a/esphome/components/web_server_idf/multipart.h +++ b/esphome/components/web_server_idf/multipart.h @@ -1,6 +1,6 @@ #pragma once #include "esphome/core/defines.h" -#if defined(USE_ESP_IDF) && defined(USE_WEBSERVER_OTA) +#if defined(USE_ESP32) && defined(USE_WEBSERVER_OTA) #include #include @@ -83,4 +83,4 @@ std::string str_trim(const std::string &str); } // namespace web_server_idf } // namespace esphome -#endif // defined(USE_ESP_IDF) && defined(USE_WEBSERVER_OTA) +#endif // defined(USE_ESP32) && defined(USE_WEBSERVER_OTA) diff --git a/esphome/components/web_server_idf/utils.cpp b/esphome/components/web_server_idf/utils.cpp index ac5df90bb8..d5d34b520b 100644 --- a/esphome/components/web_server_idf/utils.cpp +++ b/esphome/components/web_server_idf/utils.cpp @@ -1,4 +1,4 @@ -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include #include #include @@ -122,4 +122,4 @@ const char *stristr(const char *haystack, const char *needle) { } // namespace web_server_idf } // namespace esphome -#endif // USE_ESP_IDF +#endif // USE_ESP32 diff --git a/esphome/components/web_server_idf/utils.h b/esphome/components/web_server_idf/utils.h index 988b962d72..f70a5f0760 100644 --- a/esphome/components/web_server_idf/utils.h +++ b/esphome/components/web_server_idf/utils.h @@ -1,5 +1,5 @@ #pragma once -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include #include @@ -24,4 +24,4 @@ const char *stristr(const char *haystack, const char *needle); } // namespace web_server_idf } // namespace esphome -#endif // USE_ESP_IDF +#endif // USE_ESP32 diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 40fb015b99..c910ed06c5 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -1,9 +1,10 @@ -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include #include #include #include +#include #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -25,6 +26,10 @@ #include "esphome/components/web_server/list_entities.h" #endif // USE_WEBSERVER +// Include socket headers after Arduino headers to avoid IPADDR_NONE/INADDR_NONE macro conflicts +#include +#include + namespace esphome { namespace web_server_idf { @@ -46,6 +51,65 @@ DefaultHeaders default_headers_instance; DefaultHeaders &DefaultHeaders::Instance() { return default_headers_instance; } +namespace { +// Non-blocking send function to prevent watchdog timeouts when TCP buffers are full +/** + * Sends data on a socket in non-blocking mode. + * + * @param hd HTTP server handle (unused). + * @param sockfd Socket file descriptor. + * @param buf Buffer to send. + * @param buf_len Length of buffer. + * @param flags Flags for send(). + * @return + * - Number of bytes sent on success. + * - HTTPD_SOCK_ERR_INVALID if buf is nullptr. + * - HTTPD_SOCK_ERR_TIMEOUT if the send buffer is full (EAGAIN/EWOULDBLOCK). + * - HTTPD_SOCK_ERR_FAIL for other errors. + */ +int nonblocking_send(httpd_handle_t hd, int sockfd, const char *buf, size_t buf_len, int flags) { + if (buf == nullptr) { + return HTTPD_SOCK_ERR_INVALID; + } + + // Use MSG_DONTWAIT to prevent blocking when TCP send buffer is full + int ret = send(sockfd, buf, buf_len, flags | MSG_DONTWAIT); + if (ret < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + // Buffer full - retry later + return HTTPD_SOCK_ERR_TIMEOUT; + } + // Real error + ESP_LOGD(TAG, "send error: errno %d", errno); + return HTTPD_SOCK_ERR_FAIL; + } + return ret; +} +} // namespace + +void AsyncWebServer::safe_close_with_shutdown(httpd_handle_t hd, int sockfd) { + // CRITICAL: Shut down receive BEFORE closing to prevent lwIP race conditions + // + // The race condition occurs because close() initiates lwIP teardown while + // the TCP/IP thread can still receive packets, causing assertions when + // recv_tcp() sees partially-torn-down state. + // + // By shutting down receive first, we tell lwIP to stop accepting new data BEFORE + // the teardown begins, eliminating the race window. We only shutdown RD (not RDWR) + // to allow the FIN packet to be sent cleanly during close(). + // + // Note: This function may be called with an already-closed socket if the network + // stack closed it. In that case, shutdown() will fail but close() is safe to call. + // + // See: https://github.com/esphome/esphome-webserver/issues/163 + + // Attempt shutdown - ignore errors as socket may already be closed + shutdown(sockfd, SHUT_RD); + + // Always close - safe even if socket is already closed by network stack + close(sockfd); +} + void AsyncWebServer::end() { if (this->server_) { httpd_stop(this->server_); @@ -53,6 +117,18 @@ void AsyncWebServer::end() { } } +void AsyncWebServer::set_lru_purge_enable(bool enable) { + if (this->lru_purge_enable_ == enable) { + return; // No change needed + } + this->lru_purge_enable_ = enable; + // If server is already running, restart it with new config + if (this->server_) { + this->end(); + this->begin(); + } +} + void AsyncWebServer::begin() { if (this->server_) { this->end(); @@ -60,6 +136,10 @@ void AsyncWebServer::begin() { httpd_config_t config = HTTPD_DEFAULT_CONFIG(); config.server_port = this->port_; config.uri_match_fn = [](const char * /*unused*/, const char * /*unused*/, size_t /*unused*/) { return true; }; + // Enable LRU purging if requested (e.g., by captive portal to handle probe bursts) + config.lru_purge_enable = this->lru_purge_enable_; + // Use custom close function that shuts down before closing to prevent lwIP race conditions + config.close_fn = AsyncWebServer::safe_close_with_shutdown; if (httpd_start(&this->server_, &config) == ESP_OK) { const httpd_uri_t handler_get = { .uri = "", @@ -116,7 +196,7 @@ esp_err_t AsyncWebServer::request_post_handler(httpd_req_t *r) { } // Handle regular form data - if (r->content_len > HTTPD_MAX_REQ_HDR_LEN) { + if (r->content_len > CONFIG_HTTPD_MAX_REQ_HDR_LEN) { ESP_LOGW(TAG, "Request size is to big: %zu", r->content_len); httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr); return ESP_FAIL; @@ -164,8 +244,8 @@ esp_err_t AsyncWebServer::request_handler_(AsyncWebServerRequest *request) const AsyncWebServerRequest::~AsyncWebServerRequest() { delete this->rsp_; - for (const auto &pair : this->params_) { - delete pair.second; // NOLINT(cppcoreguidelines-owning-memory) + for (auto *param : this->params_) { + delete param; // NOLINT(cppcoreguidelines-owning-memory) } } @@ -201,14 +281,28 @@ void AsyncWebServerRequest::send(int code, const char *content_type, const char void AsyncWebServerRequest::redirect(const std::string &url) { httpd_resp_set_status(*this, "302 Found"); httpd_resp_set_hdr(*this, "Location", url.c_str()); + httpd_resp_set_hdr(*this, "Connection", "close"); httpd_resp_send(*this, nullptr, 0); } void AsyncWebServerRequest::init_response_(AsyncWebServerResponse *rsp, int code, const char *content_type) { - httpd_resp_set_status(*this, code == 200 ? HTTPD_200 - : code == 404 ? HTTPD_404 - : code == 409 ? HTTPD_409 - : to_string(code).c_str()); + // Set status code - use constants for common codes, default to 500 for unknown codes + const char *status; + switch (code) { + case 200: + status = HTTPD_200; + break; + case 404: + status = HTTPD_404; + break; + case 409: + status = HTTPD_409; + break; + default: + status = HTTPD_500; + break; + } + httpd_resp_set_status(*this, status); if (content_type && *content_type) { httpd_resp_set_type(*this, content_type); @@ -253,7 +347,7 @@ bool AsyncWebServerRequest::authenticate(const char *username, const char *passw esp_crypto_base64_encode(reinterpret_cast(digest.get()), n, &out, reinterpret_cast(user_info.c_str()), user_info.size()); - return strncmp(digest.get(), auth_str + auth_prefix_len, auth.value().size() - auth_prefix_len) == 0; + return strcmp(digest.get(), auth_str + auth_prefix_len) == 0; } void AsyncWebServerRequest::requestAuthentication(const char *realm) const { @@ -265,11 +359,14 @@ void AsyncWebServerRequest::requestAuthentication(const char *realm) const { #endif AsyncWebParameter *AsyncWebServerRequest::getParam(const std::string &name) { - auto find = this->params_.find(name); - if (find != this->params_.end()) { - return find->second; + // Check cache first - only successful lookups are cached + for (auto *param : this->params_) { + if (param->name() == name) { + return param; + } } + // Look up value from query strings optional val = query_key_value(this->post_query_, name); if (!val.has_value()) { auto url_query = request_get_url_query(*this); @@ -278,11 +375,14 @@ AsyncWebParameter *AsyncWebServerRequest::getParam(const std::string &name) { } } - AsyncWebParameter *param = nullptr; - if (val.has_value()) { - param = new AsyncWebParameter(val.value()); // NOLINT(cppcoreguidelines-owning-memory) + // Don't cache misses to avoid wasting memory when handlers check for + // optional parameters that don't exist in the request + if (!val.has_value()) { + return nullptr; } - this->params_.insert({name, param}); + + auto *param = new AsyncWebParameter(name, val.value()); // NOLINT(cppcoreguidelines-owning-memory) + this->params_.push_back(param); return param; } @@ -290,7 +390,13 @@ void AsyncWebServerResponse::addHeader(const char *name, const char *value) { httpd_resp_set_hdr(*this->req_, name, value); } -void AsyncResponseStream::print(float value) { this->print(to_string(value)); } +void AsyncResponseStream::print(float value) { + // Use stack buffer to avoid temporary string allocation + // Size: sign (1) + digits (10) + decimal (1) + precision (6) + exponent (5) + null (1) = 24, use 32 for safety + char buf[32]; + int len = snprintf(buf, sizeof(buf), "%f", value); + this->content_.append(buf, len); +} void AsyncResponseStream::printf(const char *fmt, ...) { va_list args; @@ -317,29 +423,30 @@ AsyncEventSource::~AsyncEventSource() { } void AsyncEventSource::handleRequest(AsyncWebServerRequest *request) { - auto *rsp = // NOLINT(cppcoreguidelines-owning-memory) - new AsyncEventSourceResponse(request, this, this->web_server_); + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory,clang-analyzer-cplusplus.NewDeleteLeaks) + auto *rsp = new AsyncEventSourceResponse(request, this, this->web_server_); if (this->on_connect_) { this->on_connect_(rsp); } - this->sessions_.insert(rsp); + this->sessions_.push_back(rsp); } void AsyncEventSource::loop() { // Clean up dead sessions safely // This follows the ESP-IDF pattern where free_ctx marks resources as dead // and the main loop handles the actual cleanup to avoid race conditions - auto it = this->sessions_.begin(); - while (it != this->sessions_.end()) { - auto *ses = *it; + for (size_t i = 0; i < this->sessions_.size();) { + auto *ses = this->sessions_[i]; // If the session has a dead socket (marked by destroy callback) if (ses->fd_.load() == 0) { ESP_LOGD(TAG, "Removing dead event source session"); - it = this->sessions_.erase(it); delete ses; // NOLINT(cppcoreguidelines-owning-memory) + // Remove by swapping with last element (O(1) removal, order doesn't matter for sessions) + this->sessions_[i] = this->sessions_.back(); + this->sessions_.pop_back(); } else { ses->loop(); - ++it; + ++i; } } } @@ -354,6 +461,9 @@ void AsyncEventSource::try_send_nodefer(const char *message, const char *event, void AsyncEventSource::deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator) { + // Skip if no connected clients to avoid unnecessary processing + if (this->empty()) + return; for (auto *ses : this->sessions_) { if (ses->fd_.load() != 0) { // Skip dead sessions ses->deferrable_send_state(source, event_type, message_generator); @@ -384,6 +494,9 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest * this->hd_ = req->handle; this->fd_.store(httpd_req_to_sockfd(req)); + // Use non-blocking send to prevent watchdog timeouts when TCP buffers are full + httpd_sess_set_send_override(this->hd_, this->fd_.load(), nonblocking_send); + // Configure reconnect timeout and send config // this should always go through since the tcp send buffer is empty on connect std::string message = ws->get_config_json(); @@ -392,10 +505,11 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest * #ifdef USE_WEBSERVER_SORTING for (auto &group : ws->sorting_groups_) { // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - message = json::build_json([group](JsonObject root) { - root["name"] = group.second.name; - root["sorting_weight"] = group.second.weight; - }); + json::JsonBuilder builder; + JsonObject root = builder.root(); + root["name"] = group.second.name; + root["sorting_weight"] = group.second.weight; + message = builder.serialize(); // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) // a (very) large number of these should be able to be queued initially without defer @@ -415,10 +529,12 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest * void AsyncEventSourceResponse::destroy(void *ptr) { auto *rsp = static_cast(ptr); - ESP_LOGD(TAG, "Event source connection closed (fd: %d)", rsp->fd_.load()); - // Mark as dead by setting fd to 0 - will be cleaned up in the main loop - rsp->fd_.store(0); + int fd = rsp->fd_.exchange(0); // Atomically get and clear fd + ESP_LOGD(TAG, "Event source connection closed (fd: %d)", fd); + // Mark as dead - will be cleaned up in the main loop // Note: We don't delete or remove from set here to avoid race conditions + // httpd will call our custom close_fn (safe_close_with_shutdown) which handles + // shutdown() before close() to prevent lwIP race conditions } // helper for allowing only unique entries in the queue @@ -428,8 +544,7 @@ void AsyncEventSourceResponse::deq_push_back_with_dedup_(void *source, message_g // Use range-based for loop instead of std::find_if to reduce template instantiation overhead and binary size for (auto &event : this->deferred_queue_) { if (event == item) { - event = item; - return; + return; // Already in queue, no need to update since items are equal } } this->deferred_queue_.push_back(item); @@ -458,15 +573,45 @@ void AsyncEventSourceResponse::process_buffer_() { return; } - int bytes_sent = httpd_socket_send(this->hd_, this->fd_.load(), event_buffer_.c_str() + event_bytes_sent_, - event_buffer_.size() - event_bytes_sent_, 0); - if (bytes_sent == HTTPD_SOCK_ERR_TIMEOUT || bytes_sent == HTTPD_SOCK_ERR_FAIL) { - // Socket error - just return, the connection will be closed by httpd - // and our destroy callback will be called + size_t remaining = event_buffer_.size() - event_bytes_sent_; + int bytes_sent = + httpd_socket_send(this->hd_, this->fd_.load(), event_buffer_.c_str() + event_bytes_sent_, remaining, 0); + if (bytes_sent == HTTPD_SOCK_ERR_TIMEOUT) { + // EAGAIN/EWOULDBLOCK - socket buffer full, try again later + // NOTE: Similar logic exists in web_server/web_server.cpp in DeferredUpdateEventSource::process_deferred_queue_() + // The implementations differ due to platform-specific APIs (HTTPD_SOCK_ERR_TIMEOUT vs DISCARDED, fd_.store(0) vs + // close()), but the failure counting and timeout logic should be kept in sync. If you change this logic, also + // update the Arduino implementation. + this->consecutive_send_failures_++; + if (this->consecutive_send_failures_ >= MAX_CONSECUTIVE_SEND_FAILURES) { + // Too many failures, connection is likely dead + ESP_LOGW(TAG, "Closing stuck EventSource connection after %" PRIu16 " failed sends", + this->consecutive_send_failures_); + this->fd_.store(0); // Mark for cleanup + this->deferred_queue_.clear(); + } return; } + if (bytes_sent == HTTPD_SOCK_ERR_FAIL) { + // Real socket error - connection will be closed by httpd and destroy callback will be called + return; + } + if (bytes_sent <= 0) { + // Unexpected error or zero bytes sent + ESP_LOGW(TAG, "Unexpected send result: %d", bytes_sent); + return; + } + + // Successful send - reset failure counter + this->consecutive_send_failures_ = 0; event_bytes_sent_ += bytes_sent; + // Log partial sends for debugging + if (event_bytes_sent_ < event_buffer_.size()) { + ESP_LOGV(TAG, "Partial send: %d/%zu bytes (total: %zu/%zu)", bytes_sent, remaining, event_bytes_sent_, + event_buffer_.size()); + } + if (event_bytes_sent_ == event_buffer_.size()) { event_buffer_.resize(0); event_bytes_sent_ = 0; @@ -498,16 +643,19 @@ bool AsyncEventSourceResponse::try_send_nodefer(const char *message, const char event_buffer_.append(chunk_len_header); + // Use stack buffer for formatting numeric fields to avoid temporary string allocations + // Size: "retry: " (7) + max uint32 (10 digits) + CRLF (2) + null (1) = 20 bytes, use 32 for safety + constexpr size_t num_buf_size = 32; + char num_buf[num_buf_size]; + if (reconnect) { - event_buffer_.append("retry: ", sizeof("retry: ") - 1); - event_buffer_.append(to_string(reconnect)); - event_buffer_.append(CRLF_STR, CRLF_LEN); + int len = snprintf(num_buf, num_buf_size, "retry: %" PRIu32 CRLF_STR, reconnect); + event_buffer_.append(num_buf, len); } if (id) { - event_buffer_.append("id: ", sizeof("id: ") - 1); - event_buffer_.append(to_string(id)); - event_buffer_.append(CRLF_STR, CRLF_LEN); + int len = snprintf(num_buf, num_buf_size, "id: %" PRIu32 CRLF_STR, id); + event_buffer_.append(num_buf, len); } if (event && *event) { @@ -669,4 +817,4 @@ esp_err_t AsyncWebServer::handle_multipart_upload_(httpd_req_t *r, const char *c } // namespace web_server_idf } // namespace esphome -#endif // !defined(USE_ESP_IDF) +#endif // !defined(USE_ESP32) diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index 76540ef232..a139e9e4df 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -1,5 +1,5 @@ #pragma once -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include "esphome/core/defines.h" #include @@ -8,7 +8,6 @@ #include #include #include -#include #include #include #include @@ -22,18 +21,14 @@ class ListEntitiesIterator; #endif namespace web_server_idf { -#define F(string_literal) (string_literal) -#define PGM_P const char * -#define strncpy_P strncpy - -using String = std::string; - class AsyncWebParameter { public: - AsyncWebParameter(std::string value) : value_(std::move(value)) {} + AsyncWebParameter(std::string name, std::string value) : name_(std::move(name)), value_(std::move(value)) {} + const std::string &name() const { return this->name_; } const std::string &value() const { return this->value_; } protected: + std::string name_; std::string value_; }; @@ -174,7 +169,11 @@ class AsyncWebServerRequest { protected: httpd_req_t *req_; AsyncWebServerResponse *rsp_{}; - std::map params_; + // Use vector instead of map/unordered_map: most requests have 0-3 params, so linear search + // is faster than tree/hash overhead. AsyncWebParameter stores both name and value to avoid + // duplicate storage. Only successful lookups are cached to prevent cache pollution when + // handlers check for optional parameters that don't exist. + std::vector params_; std::string post_query_; AsyncWebServerRequest(httpd_req_t *req) : req_(req) {} AsyncWebServerRequest(httpd_req_t *req, std::string post_query) : req_(req), post_query_(std::move(post_query)) {} @@ -200,12 +199,17 @@ class AsyncWebServer { return *handler; } + void set_lru_purge_enable(bool enable); + httpd_handle_t get_server() { return this->server_; } + protected: uint16_t port_{}; httpd_handle_t server_{}; + bool lru_purge_enable_{false}; static esp_err_t request_handler(httpd_req_t *r); static esp_err_t request_post_handler(httpd_req_t *r); esp_err_t request_handler_(AsyncWebServerRequest *request) const; + static void safe_close_with_shutdown(httpd_handle_t hd, int sockfd); #ifdef USE_WEBSERVER_OTA esp_err_t handle_multipart_upload_(httpd_req_t *r, const char *content_type); #endif @@ -283,6 +287,8 @@ class AsyncEventSourceResponse { std::unique_ptr entities_iterator_; std::string event_buffer_{""}; size_t event_bytes_sent_; + uint16_t consecutive_send_failures_{0}; + static constexpr uint16_t MAX_CONSECUTIVE_SEND_FAILURES = 2500; // ~20 seconds at 125Hz loop rate }; using AsyncEventSourceClient = AsyncEventSourceResponse; @@ -313,7 +319,10 @@ class AsyncEventSource : public AsyncWebHandler { protected: std::string url_; - std::set sessions_; + // Use vector instead of set: SSE sessions are typically 1-5 connections (browsers, dashboards). + // Linear search is faster than red-black tree overhead for this small dataset. + // Only operations needed: add session, remove session, iterate sessions - no need for sorted order. + std::vector sessions_; connect_handler_t on_connect_{}; esphome::web_server::WebServer *web_server_; }; @@ -341,4 +350,4 @@ class DefaultHeaders { using namespace esphome::web_server_idf; // NOLINT(google-global-names-in-headers) -#endif // !defined(USE_ESP_IDF) +#endif // !defined(USE_ESP32) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index b74237ad2e..31d9ca0f70 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -1,12 +1,17 @@ +import logging + from esphome import automation from esphome.automation import Condition import esphome.codegen as cg from esphome.components.const import CONF_USE_PSRAM from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant -from esphome.components.network import IPAddress +from esphome.components.network import ( + has_high_performance_networking, + ip_address_literal, +) +from esphome.components.psram import is_guaranteed as psram_is_guaranteed from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv -from esphome.config_validation import only_with_esp_idf from esphome.const import ( CONF_AP, CONF_BSSID, @@ -42,17 +47,33 @@ from esphome.const import ( CONF_TTLS_PHASE_2, CONF_USE_ADDRESS, CONF_USERNAME, + Platform, PlatformFramework, ) -from esphome.core import CORE, HexInt, coroutine_with_priority +from esphome.core import CORE, CoroPriority, HexInt, coroutine_with_priority import esphome.final_validate as fv from . import wpa2_eap +_LOGGER = logging.getLogger(__name__) + AUTO_LOAD = ["network"] -NO_WIFI_VARIANTS = [const.VARIANT_ESP32H2] +_LOGGER = logging.getLogger(__name__) + +NO_WIFI_VARIANTS = [const.VARIANT_ESP32H2, const.VARIANT_ESP32P4] CONF_SAVE = "save" +CONF_MIN_AUTH_MODE = "min_auth_mode" + +# Maximum number of WiFi networks that can be configured +# Limited to 127 because selected_sta_index_ is int8_t in C++ +MAX_WIFI_NETWORKS = 127 + +# Default AP timeout - allows sufficient time to try all BSSIDs during initial connection +# After AP starts, WiFi scanning is skipped to avoid disrupting the AP, so we only +# get best-effort connection attempts. Longer timeout ensures we exhaust all options +# before falling back to AP mode. Aligned with improv wifi_timeout default. +DEFAULT_AP_TIMEOUT = "90s" wifi_ns = cg.esphome_ns.namespace("wifi") EAPAuth = wifi_ns.struct("EAPAuth") @@ -66,8 +87,17 @@ WIFI_POWER_SAVE_MODES = { "LIGHT": WiFiPowerSaveMode.WIFI_POWER_SAVE_LIGHT, "HIGH": WiFiPowerSaveMode.WIFI_POWER_SAVE_HIGH, } + +WifiMinAuthMode = wifi_ns.enum("WifiMinAuthMode") +WIFI_MIN_AUTH_MODES = { + "WPA": WifiMinAuthMode.WIFI_MIN_AUTH_MODE_WPA, + "WPA2": WifiMinAuthMode.WIFI_MIN_AUTH_MODE_WPA2, + "WPA3": WifiMinAuthMode.WIFI_MIN_AUTH_MODE_WPA3, +} +VALIDATE_WIFI_MIN_AUTH_MODE = cv.enum(WIFI_MIN_AUTH_MODES, upper=True) WiFiConnectedCondition = wifi_ns.class_("WiFiConnectedCondition", Condition) WiFiEnabledCondition = wifi_ns.class_("WiFiEnabledCondition", Condition) +WiFiAPActiveCondition = wifi_ns.class_("WiFiAPActiveCondition", Condition) WiFiEnableAction = wifi_ns.class_("WiFiEnableAction", automation.Action) WiFiDisableAction = wifi_ns.class_("WiFiDisableAction", automation.Action) WiFiConfigureAction = wifi_ns.class_( @@ -125,8 +155,8 @@ EAP_AUTH_SCHEMA = cv.All( cv.Optional(CONF_USERNAME): cv.string_strict, cv.Optional(CONF_PASSWORD): cv.string_strict, cv.Optional(CONF_CERTIFICATE_AUTHORITY): wpa2_eap.validate_certificate, - cv.SplitDefault(CONF_TTLS_PHASE_2, esp32_idf="mschapv2"): cv.All( - cv.enum(TTLS_PHASE_2), cv.only_with_esp_idf + cv.SplitDefault(CONF_TTLS_PHASE_2, esp32="mschapv2"): cv.All( + cv.enum(TTLS_PHASE_2), cv.only_on_esp32 ), cv.Inclusive( CONF_CERTIFICATE, "certificate_and_key" @@ -154,7 +184,7 @@ CONF_AP_TIMEOUT = "ap_timeout" WIFI_NETWORK_AP = WIFI_NETWORK_BASE.extend( { cv.Optional( - CONF_AP_TIMEOUT, default="1min" + CONF_AP_TIMEOUT, default=DEFAULT_AP_TIMEOUT ): cv.positive_time_period_milliseconds, } ) @@ -170,7 +200,7 @@ WIFI_NETWORK_STA = WIFI_NETWORK_BASE.extend( { cv.Optional(CONF_BSSID): cv.mac_address, cv.Optional(CONF_HIDDEN): cv.boolean, - cv.Optional(CONF_PRIORITY, default=0.0): cv.float_, + cv.Optional(CONF_PRIORITY, default=0): cv.int_range(min=-128, max=127), cv.Optional(CONF_EAP): EAP_AUTH_SCHEMA, } ) @@ -179,8 +209,29 @@ WIFI_NETWORK_STA = WIFI_NETWORK_BASE.extend( def validate_variant(_): if CORE.is_esp32: variant = get_esp32_variant() - if variant in NO_WIFI_VARIANTS: - raise cv.Invalid(f"{variant} does not support WiFi") + if variant in NO_WIFI_VARIANTS and "esp32_hosted" not in fv.full_config.get(): + raise cv.Invalid(f"WiFi requires component esp32_hosted on {variant}") + + +def _apply_min_auth_mode_default(config): + """Apply platform-specific default for min_auth_mode and warn ESP8266 users.""" + # Only apply defaults for platforms that support min_auth_mode + if CONF_MIN_AUTH_MODE not in config and (CORE.is_esp8266 or CORE.is_esp32): + if CORE.is_esp8266: + _LOGGER.warning( + "The minimum WiFi authentication mode (wifi -> min_auth_mode) is not set. " + "This controls the weakest encryption your device will accept when connecting to WiFi. " + "Currently defaults to WPA (less secure), but will change to WPA2 (more secure) in 2026.6.0. " + "WPA uses TKIP encryption which has known security vulnerabilities and should be avoided. " + "WPA2 uses AES encryption which is significantly more secure. " + "To silence this warning, explicitly set min_auth_mode under 'wifi:'. " + "If your router supports WPA2 or WPA3, set 'min_auth_mode: WPA2'. " + "If your router only supports WPA, set 'min_auth_mode: WPA'." + ) + config[CONF_MIN_AUTH_MODE] = VALIDATE_WIFI_MIN_AUTH_MODE("WPA") + elif CORE.is_esp32: + config[CONF_MIN_AUTH_MODE] = VALIDATE_WIFI_MIN_AUTH_MODE("WPA2") + return config def final_validate(config): @@ -194,45 +245,7 @@ def final_validate(config): ) -def final_validate_power_esp32_ble(value): - if not CORE.is_esp32: - return - if value != "NONE": - # WiFi should be in modem sleep (!=NONE) with BLE coexistence - # https://docs.espressif.com/projects/esp-idf/en/v3.3.5/api-guides/wifi.html#station-sleep - return - for conflicting in [ - "esp32_ble", - "esp32_ble_beacon", - "esp32_ble_server", - "esp32_ble_tracker", - ]: - if conflicting not in fv.full_config.get(): - continue - - try: - # Only arduino 1.0.5+ and esp-idf impacted - cv.require_framework_version( - esp32_arduino=cv.Version(1, 0, 5), - esp_idf=cv.Version(4, 0, 0), - )(None) - except cv.Invalid: - pass - else: - raise cv.Invalid( - f"power_save_mode NONE is incompatible with {conflicting}. " - f"Please remove the power save mode. See also " - f"https://github.com/esphome/issues/issues/2141#issuecomment-865688582" - ) - - FINAL_VALIDATE_SCHEMA = cv.All( - cv.Schema( - { - cv.Optional(CONF_POWER_SAVE_MODE): final_validate_power_esp32_ble, - }, - extra=cv.ALLOW_EXTRA, - ), final_validate, validate_variant, ) @@ -251,11 +264,15 @@ def _validate(config): if CONF_EAP in config: network[CONF_EAP] = config.pop(CONF_EAP) if CONF_NETWORKS in config: - raise cv.Invalid( - "You cannot use the 'ssid:' option together with 'networks:'. Please " - "copy your network into the 'networks:' key" - ) - config[CONF_NETWORKS] = cv.ensure_list(WIFI_NETWORK_STA)(network) + # In testing mode, merged component tests may have both ssid and networks + # Just use the networks list and ignore the single ssid + if not CORE.testing_mode: + raise cv.Invalid( + "You cannot use the 'ssid:' option together with 'networks:'. Please " + "copy your network into the 'networks:' key" + ) + else: + config[CONF_NETWORKS] = cv.ensure_list(WIFI_NETWORK_STA)(network) if (CONF_NETWORKS not in config) and (CONF_AP not in config): config = config.copy() @@ -294,7 +311,9 @@ CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(WiFiComponent), - cv.Optional(CONF_NETWORKS): cv.ensure_list(WIFI_NETWORK_STA), + cv.Optional(CONF_NETWORKS): cv.All( + cv.ensure_list(WIFI_NETWORK_STA), cv.Length(max=MAX_WIFI_NETWORKS) + ), cv.Optional(CONF_SSID): cv.ssid, cv.Optional(CONF_PASSWORD): validate_password, cv.Optional(CONF_MANUAL_IP): STA_MANUAL_IP_SCHEMA, @@ -315,14 +334,18 @@ CONFIG_SCHEMA = cv.All( ): cv.enum(WIFI_POWER_SAVE_MODES, upper=True), cv.Optional(CONF_FAST_CONNECT, default=False): cv.boolean, cv.Optional(CONF_USE_ADDRESS): cv.string_strict, + cv.Optional(CONF_MIN_AUTH_MODE): cv.All( + VALIDATE_WIFI_MIN_AUTH_MODE, + cv.only_on([Platform.ESP32, Platform.ESP8266]), + ), cv.SplitDefault(CONF_OUTPUT_POWER, esp8266=20.0): cv.All( cv.decibel, cv.float_range(min=8.5, max=20.5) ), - cv.SplitDefault(CONF_ENABLE_BTM, esp32_idf=False): cv.All( - cv.boolean, cv.only_with_esp_idf + cv.SplitDefault(CONF_ENABLE_BTM, esp32=False): cv.All( + cv.boolean, cv.only_on_esp32 ), - cv.SplitDefault(CONF_ENABLE_RRM, esp32_idf=False): cv.All( - cv.boolean, cv.only_with_esp_idf + cv.SplitDefault(CONF_ENABLE_RRM, esp32=False): cv.All( + cv.boolean, cv.only_on_esp32 ), cv.Optional(CONF_PASSIVE_SCAN, default=False): cv.boolean, cv.Optional("enable_mdns"): cv.invalid( @@ -335,10 +358,11 @@ CONFIG_SCHEMA = cv.All( single=True ), cv.Optional(CONF_USE_PSRAM): cv.All( - only_with_esp_idf, cv.requires_component("psram"), cv.boolean + cv.only_on_esp32, cv.requires_component("psram"), cv.boolean ), } ), + _apply_min_auth_mode_default, _validate, ) @@ -368,9 +392,7 @@ def eap_auth(config): def safe_ip(ip): - if ip is None: - return IPAddress(0, 0, 0, 0) - return IPAddress(str(ip)) + return ip_address_literal(ip) def manual_ip(config): @@ -408,21 +430,37 @@ def wifi_network(config, ap, static_ip): return ap -@coroutine_with_priority(60.0) +@coroutine_with_priority(CoroPriority.COMMUNICATION) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) cg.add(var.set_use_address(config[CONF_USE_ADDRESS])) - def add_sta(ap, network): - ip_config = network.get(CONF_MANUAL_IP, config.get(CONF_MANUAL_IP)) - cg.add(var.add_sta(wifi_network(network, ap, ip_config))) + # Track if any network uses Enterprise authentication + has_eap = False + # Track if any network uses manual IP + has_manual_ip = False - for network in config.get(CONF_NETWORKS, []): - cg.with_local_variable(network[CONF_ID], WiFiAP(), add_sta, network) + # Initialize FixedVector with the count of networks + networks = config.get(CONF_NETWORKS, []) + if networks: + cg.add(var.init_sta(len(networks))) + + def add_sta(ap: cg.MockObj, network: dict) -> None: + ip_config = network.get(CONF_MANUAL_IP, config.get(CONF_MANUAL_IP)) + cg.add(var.add_sta(wifi_network(network, ap, ip_config))) + + for network in networks: + if CONF_EAP in network: + has_eap = True + if network.get(CONF_MANUAL_IP) or config.get(CONF_MANUAL_IP): + has_manual_ip = True + cg.with_local_variable(network[CONF_ID], WiFiAP(), add_sta, network) if CONF_AP in config: conf = config[CONF_AP] ip_config = conf.get(CONF_MANUAL_IP) + if ip_config: + has_manual_ip = True cg.with_local_variable( conf[CONF_ID], WiFiAP(), @@ -434,21 +472,35 @@ async def to_code(config): add_idf_sdkconfig_option("CONFIG_ESP_WIFI_SOFTAP_SUPPORT", False) add_idf_sdkconfig_option("CONFIG_LWIP_DHCPS", False) + # Disable Enterprise WiFi support if no EAP is configured + if CORE.is_esp32: + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT", has_eap) + + # Only define USE_WIFI_MANUAL_IP if any AP uses manual IP + if has_manual_ip: + cg.add_define("USE_WIFI_MANUAL_IP") + cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) cg.add(var.set_power_save_mode(config[CONF_POWER_SAVE_MODE])) - cg.add(var.set_fast_connect(config[CONF_FAST_CONNECT])) - cg.add(var.set_passive_scan(config[CONF_PASSIVE_SCAN])) + if CONF_MIN_AUTH_MODE in config: + cg.add(var.set_min_auth_mode(config[CONF_MIN_AUTH_MODE])) + if config[CONF_FAST_CONNECT]: + cg.add_define("USE_WIFI_FAST_CONNECT") + # passive_scan defaults to false in C++ - only set if true + if config[CONF_PASSIVE_SCAN]: + cg.add(var.set_passive_scan(True)) if CONF_OUTPUT_POWER in config: cg.add(var.set_output_power(config[CONF_OUTPUT_POWER])) - - cg.add(var.set_enable_on_boot(config[CONF_ENABLE_ON_BOOT])) + # enable_on_boot defaults to true in C++ - only set if false + if not config[CONF_ENABLE_ON_BOOT]: + cg.add(var.set_enable_on_boot(False)) if CORE.is_esp8266: cg.add_library("ESP8266WiFi", None) - elif (CORE.is_esp32 and CORE.using_arduino) or CORE.is_rp2040: + elif CORE.is_rp2040: cg.add_library("WiFi", None) - if CORE.is_esp32 and CORE.using_esp_idf: + if CORE.is_esp32: if config[CONF_ENABLE_BTM] or config[CONF_ENABLE_RRM]: add_idf_sdkconfig_option("CONFIG_WPA_11KV_SUPPORT", True) cg.add_define("USE_WIFI_11KV_SUPPORT") @@ -459,6 +511,56 @@ async def to_code(config): if config.get(CONF_USE_PSRAM): add_idf_sdkconfig_option("CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP", True) + + # Apply high performance WiFi settings if high performance networking is enabled + if CORE.is_esp32 and CORE.using_esp_idf and has_high_performance_networking(): + # Check if PSRAM is guaranteed (set by psram component during final validation) + psram_guaranteed = psram_is_guaranteed() + + # Always allocate WiFi buffers in PSRAM if available + add_idf_sdkconfig_option("CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP", True) + + if psram_guaranteed: + _LOGGER.info( + "Applying high-performance WiFi settings (PSRAM guaranteed): 512 RX buffers, 32 TX buffers" + ) + # PSRAM is guaranteed - use aggressive settings + # Higher maximum values are allowed because CONFIG_LWIP_WND_SCALE is set to true in networking component + # Based on https://github.com/espressif/esp-adf/issues/297#issuecomment-783811702 + + # Large dynamic RX buffers (requires PSRAM) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM", 16) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM", 512) + + # Static TX buffers for better performance + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_STATIC_TX_BUFFER", True) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_TX_BUFFER_TYPE", 0) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_CACHE_TX_BUFFER_NUM", 32) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_STATIC_TX_BUFFER_NUM", 8) + + # AMPDU settings optimized for PSRAM + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_AMPDU_TX_ENABLED", True) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_TX_BA_WIN", 16) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_AMPDU_RX_ENABLED", True) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_RX_BA_WIN", 32) + else: + _LOGGER.info( + "Applying optimized WiFi settings: 64 RX buffers, 64 TX buffers" + ) + # PSRAM not guaranteed - use more conservative, but still optimized settings + # Based on https://github.com/espressif/esp-idf/blob/release/v5.4/examples/wifi/iperf/sdkconfig.defaults.esp32 + + # Standard buffer counts + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM", 16) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM", 64) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM", 64) + + # Standard AMPDU settings + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_AMPDU_TX_ENABLED", True) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_TX_BA_WIN", 32) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_AMPDU_RX_ENABLED", True) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_RX_BA_WIN", 32) + cg.add_define("USE_WIFI") # must register before OTA safe mode check @@ -476,6 +578,8 @@ async def to_code(config): var.get_disconnect_trigger(), [], on_disconnect_config ) + CORE.add_job(final_step) + @automation.register_condition("wifi.connected", WiFiConnectedCondition, cv.Schema({})) async def wifi_connected_to_code(config, condition_id, template_arg, args): @@ -487,6 +591,11 @@ async def wifi_enabled_to_code(config, condition_id, template_arg, args): return cg.new_Pvariable(condition_id, template_arg) +@automation.register_condition("wifi.ap_active", WiFiAPActiveCondition, cv.Schema({})) +async def wifi_ap_active_to_code(config, condition_id, template_arg, args): + return cg.new_Pvariable(condition_id, template_arg) + + @automation.register_action("wifi.enable", WiFiEnableAction, cv.Schema({})) async def wifi_enable_to_code(config, action_id, template_arg, args): return cg.new_Pvariable(action_id, template_arg) @@ -497,6 +606,58 @@ async def wifi_disable_to_code(config, action_id, template_arg, args): return cg.new_Pvariable(action_id, template_arg) +KEEP_SCAN_RESULTS_KEY = "wifi_keep_scan_results" +RUNTIME_POWER_SAVE_KEY = "wifi_runtime_power_save" +WIFI_CALLBACKS_KEY = "wifi_callbacks" + + +def request_wifi_scan_results(): + """Request that WiFi scan results be kept in memory after connection. + + Components that need access to scan results after WiFi is connected should + call this function during their code generation. This prevents the WiFi component from + freeing scan result memory after successful connection. + """ + CORE.data[KEEP_SCAN_RESULTS_KEY] = True + + +def enable_runtime_power_save_control(): + """Enable runtime WiFi power save control. + + Components that need to dynamically switch WiFi power saving on/off for latency + performance (e.g., audio streaming, large data transfers) should call this + function during their code generation. This enables the request_high_performance() + and release_high_performance() APIs. + + Only supported on ESP32. + """ + CORE.data[RUNTIME_POWER_SAVE_KEY] = True + + +def request_wifi_callbacks() -> None: + """Request that WiFi callbacks be compiled in. + + Components that need to be notified about WiFi state changes (IP address changes, + scan results, connection state) should call this function during their code generation. + This enables the add_on_ip_state_callback(), add_on_wifi_scan_state_callback(), + and add_on_wifi_connect_state_callback() APIs. + """ + CORE.data[WIFI_CALLBACKS_KEY] = True + + +@coroutine_with_priority(CoroPriority.FINAL) +async def final_step(): + """Final code generation step to configure optional WiFi features.""" + if CORE.data.get(KEEP_SCAN_RESULTS_KEY, False): + cg.add( + cg.RawExpression("wifi::global_wifi_component->set_keep_scan_results(true)") + ) + if CORE.data.get(RUNTIME_POWER_SAVE_KEY, False): + cg.add_define("USE_WIFI_RUNTIME_POWER_SAVE") + if CORE.data.get(WIFI_CALLBACKS_KEY, False): + cg.add_define("USE_WIFI_CALLBACKS") + + @automation.register_action( "wifi.configure", WiFiConfigureAction, @@ -535,8 +696,10 @@ async def wifi_set_sta_to_code(config, action_id, template_arg, args): FILTER_SOURCE_FILES = filter_source_files_from_platform( { - "wifi_component_esp32_arduino.cpp": {PlatformFramework.ESP32_ARDUINO}, - "wifi_component_esp_idf.cpp": {PlatformFramework.ESP32_IDF}, + "wifi_component_esp_idf.cpp": { + PlatformFramework.ESP32_IDF, + PlatformFramework.ESP32_ARDUINO, + }, "wifi_component_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, "wifi_component_libretiny.cpp": { PlatformFramework.BK72XX_ARDUINO, diff --git a/esphome/components/wifi/automation.h b/esphome/components/wifi/automation.h new file mode 100644 index 0000000000..7997baff65 --- /dev/null +++ b/esphome/components/wifi/automation.h @@ -0,0 +1,116 @@ +#pragma once + +#include "esphome/core/defines.h" +#ifdef USE_WIFI +#include "wifi_component.h" + +namespace esphome::wifi { + +template class WiFiConnectedCondition : public Condition { + public: + bool check(const Ts &...x) override { return global_wifi_component->is_connected(); } +}; + +template class WiFiEnabledCondition : public Condition { + public: + bool check(const Ts &...x) override { return !global_wifi_component->is_disabled(); } +}; + +template class WiFiAPActiveCondition : public Condition { + public: + bool check(const Ts &...x) override { return global_wifi_component->is_ap_active(); } +}; + +template class WiFiEnableAction : public Action { + public: + void play(const Ts &...x) override { global_wifi_component->enable(); } +}; + +template class WiFiDisableAction : public Action { + public: + void play(const Ts &...x) override { global_wifi_component->disable(); } +}; + +template class WiFiConfigureAction : public Action, public Component { + public: + TEMPLATABLE_VALUE(std::string, ssid) + TEMPLATABLE_VALUE(std::string, password) + TEMPLATABLE_VALUE(bool, save) + TEMPLATABLE_VALUE(uint32_t, connection_timeout) + + void play(const Ts &...x) override { + auto ssid = this->ssid_.value(x...); + auto password = this->password_.value(x...); + // Avoid multiple calls + if (this->connecting_) + return; + // If already connected to the same AP, do nothing + if (global_wifi_component->wifi_ssid() == ssid) { + // Callback to notify the user that the connection was successful + this->connect_trigger_->trigger(); + return; + } + // Create a new WiFiAP object with the new SSID and password + this->new_sta_.set_ssid(ssid); + this->new_sta_.set_password(password); + // Save the current STA + this->old_sta_ = global_wifi_component->get_sta(); + // Disable WiFi + global_wifi_component->disable(); + // Set the state to connecting + this->connecting_ = true; + // Store the new STA so once the WiFi is enabled, it will connect to it + // This is necessary because the WiFiComponent will raise an error and fallback to the saved STA + // if trying to connect to a new STA while already connected to another one + if (this->save_.value(x...)) { + global_wifi_component->save_wifi_sta(new_sta_.get_ssid(), new_sta_.get_password()); + } else { + global_wifi_component->set_sta(new_sta_); + } + // Enable WiFi + global_wifi_component->enable(); + // Set timeout for the connection + this->set_timeout("wifi-connect-timeout", this->connection_timeout_.value(x...), [this, x...]() { + // If the timeout is reached, stop connecting and revert to the old AP + global_wifi_component->disable(); + global_wifi_component->save_wifi_sta(old_sta_.get_ssid(), old_sta_.get_password()); + global_wifi_component->enable(); + // Start a timeout for the fallback if the connection to the old AP fails + this->set_timeout("wifi-fallback-timeout", this->connection_timeout_.value(x...), [this]() { + this->connecting_ = false; + this->error_trigger_->trigger(); + }); + }); + } + + Trigger<> *get_connect_trigger() const { return this->connect_trigger_; } + Trigger<> *get_error_trigger() const { return this->error_trigger_; } + + void loop() override { + if (!this->connecting_) + return; + if (global_wifi_component->is_connected()) { + // The WiFi is connected, stop the timeout and reset the connecting flag + this->cancel_timeout("wifi-connect-timeout"); + this->cancel_timeout("wifi-fallback-timeout"); + this->connecting_ = false; + if (global_wifi_component->wifi_ssid() == this->new_sta_.get_ssid()) { + // Callback to notify the user that the connection was successful + this->connect_trigger_->trigger(); + } else { + // Callback to notify the user that the connection failed + this->error_trigger_->trigger(); + } + } + } + + protected: + bool connecting_{false}; + WiFiAP new_sta_; + WiFiAP old_sta_; + Trigger<> *connect_trigger_{new Trigger<>()}; + Trigger<> *error_trigger_{new Trigger<>()}; +}; + +} // namespace esphome::wifi +#endif diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index f815ab73c2..d53de83bd3 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1,9 +1,9 @@ #include "wifi_component.h" #ifdef USE_WIFI +#include #include -#include -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #if (ESP_IDF_VERSION_MAJOR >= 5 && ESP_IDF_VERSION_MINOR >= 1) #include #else @@ -11,7 +11,7 @@ #endif #endif -#if defined(USE_ESP32) || defined(USE_ESP_IDF) +#if defined(USE_ESP32) #include #endif #ifdef USE_ESP8266 @@ -37,15 +37,311 @@ #include "esphome/components/esp32_improv/esp32_improv_component.h" #endif -namespace esphome { -namespace wifi { +namespace esphome::wifi { static const char *const TAG = "wifi"; +/// WiFi Retry Logic - Priority-Based BSSID Selection +/// +/// The WiFi component uses a state machine with priority degradation to handle connection failures +/// and automatically cycle through different BSSIDs in mesh networks or multiple configured networks. +/// +/// Connection Flow: +/// ┌──────────────────────────────────────────────────────────────────────┐ +/// │ Fast Connect Path (Optional) │ +/// ├──────────────────────────────────────────────────────────────────────┤ +/// │ Entered if: configuration has 'fast_connect: true' │ +/// │ Optimization to skip scanning when possible: │ +/// │ │ +/// │ 1. INITIAL_CONNECT → Try one of: │ +/// │ a) Saved BSSID+channel (from previous boot) │ +/// │ b) First configured non-hidden network (any BSSID) │ +/// │ ↓ │ +/// │ [FAILED] → Check if more configured networks available │ +/// │ ↓ │ +/// │ 2. FAST_CONNECT_CYCLING_APS → Try remaining configured networks │ +/// │ (1 attempt each, any BSSID) │ +/// │ ↓ │ +/// │ [All Failed] → Fall through to explicit hidden or scanning │ +/// │ │ +/// │ Note: Fast connect data saved from previous successful connection │ +/// └──────────────────────────────────────────────────────────────────────┘ +/// ↓ +/// ┌──────────────────────────────────────────────────────────────────────┐ +/// │ Explicit Hidden Networks Path (Optional) │ +/// ├──────────────────────────────────────────────────────────────────────┤ +/// │ Entered if: first configured network has 'hidden: true' │ +/// │ │ +/// │ 1. EXPLICIT_HIDDEN → Try consecutive hidden networks (1 attempt) │ +/// │ Stop when visible network reached │ +/// │ ↓ │ +/// │ Example: Hidden1, Hidden2, Visible1, Hidden3, Visible2 │ +/// │ Try: Hidden1, Hidden2 (stop at Visible1) │ +/// │ ↓ │ +/// │ [All Failed] → Fall back to scan-based connection │ +/// │ │ +/// │ Note: Fast connect saves BSSID after first successful connection, │ +/// │ so subsequent boots use fast path instead of hidden mode │ +/// └──────────────────────────────────────────────────────────────────────┘ +/// ↓ +/// ┌──────────────────────────────────────────────────────────────────────┐ +/// │ Scan-Based Connection Path │ +/// ├──────────────────────────────────────────────────────────────────────┤ +/// │ │ +/// │ 1. SCAN → Sort by priority (highest first), then RSSI │ +/// │ ┌─────────────────────────────────────────────────┐ │ +/// │ │ scan_result_[0] = Best BSSID (highest priority) │ │ +/// │ │ scan_result_[1] = Second best │ │ +/// │ │ scan_result_[2] = Third best │ │ +/// │ └─────────────────────────────────────────────────┘ │ +/// │ ↓ │ +/// │ 2. SCAN_CONNECTING → Try scan_result_[0] (2 attempts) │ +/// │ (Visible1, Visible2 from example above) │ +/// │ ↓ │ +/// │ 3. FAILED → Decrease priority: 0.0 → -1.0 → -2.0 │ +/// │ (stored in persistent sta_priorities_) │ +/// │ ↓ │ +/// │ 4. Check for hidden networks: │ +/// │ - If found → RETRY_HIDDEN (try SSIDs not in scan, 1 attempt) │ +/// │ Skip hidden networks before first visible one │ +/// │ (Skip Hidden1/Hidden2, try Hidden3 from example) │ +/// │ - If none → Skip RETRY_HIDDEN, go to step 5 │ +/// │ ↓ │ +/// │ 5. FAILED → RESTARTING_ADAPTER (skipped if AP/improv active) │ +/// │ ↓ │ +/// │ 6. Loop back to start: │ +/// │ - If first network is hidden → EXPLICIT_HIDDEN (retry cycle) │ +/// │ - Otherwise → SCAN_CONNECTING (rescan) │ +/// │ ↓ │ +/// │ 7. RESCAN → Apply stored priorities, sort again │ +/// │ ┌─────────────────────────────────────────────────┐ │ +/// │ │ scan_result_[0] = BSSID B (priority 0.0) ← NEW │ │ +/// │ │ scan_result_[1] = BSSID C (priority 0.0) │ │ +/// │ │ scan_result_[2] = BSSID A (priority -2.0) ← OLD │ │ +/// │ └─────────────────────────────────────────────────┘ │ +/// │ ↓ │ +/// │ 8. SCAN_CONNECTING → Try scan_result_[0] (next best) │ +/// │ │ +/// │ Key: Priority system cycles through BSSIDs ACROSS scan cycles │ +/// │ Full retry cycle: EXPLICIT_HIDDEN → SCAN → RETRY_HIDDEN │ +/// │ Always try best available BSSID (scan_result_[0]) │ +/// └──────────────────────────────────────────────────────────────────────┘ +/// +/// Retry Phases: +/// - INITIAL_CONNECT: Try saved BSSID+channel (fast_connect), or fall back to normal flow +/// - FAST_CONNECT_CYCLING_APS: Cycle through remaining configured networks (1 attempt each, fast_connect only) +/// - EXPLICIT_HIDDEN: Try consecutive networks marked hidden:true before scanning (1 attempt per SSID) +/// - SCAN_CONNECTING: Connect using scan results (2 attempts per BSSID) +/// - RETRY_HIDDEN: Try networks not found in scan (1 attempt per SSID, skipped if none found) +/// - RESTARTING_ADAPTER: Restart WiFi adapter to clear stuck state +/// +/// Hidden Network Handling: +/// - Networks marked 'hidden: true' before first non-hidden → Tried in EXPLICIT_HIDDEN phase +/// - Networks marked 'hidden: true' after first non-hidden → Tried in RETRY_HIDDEN phase +/// - After successful connection, fast_connect saves BSSID → subsequent boots use fast path +/// - Networks not in scan results → Tried in RETRY_HIDDEN phase +/// - Networks visible in scan + not marked hidden → Skipped in RETRY_HIDDEN phase +/// - Networks marked 'hidden: true' always use hidden mode, even if broadcasting SSID + +static const LogString *retry_phase_to_log_string(WiFiRetryPhase phase) { + switch (phase) { + case WiFiRetryPhase::INITIAL_CONNECT: + return LOG_STR("INITIAL_CONNECT"); +#ifdef USE_WIFI_FAST_CONNECT + case WiFiRetryPhase::FAST_CONNECT_CYCLING_APS: + return LOG_STR("FAST_CONNECT_CYCLING"); +#endif + case WiFiRetryPhase::EXPLICIT_HIDDEN: + return LOG_STR("EXPLICIT_HIDDEN"); + case WiFiRetryPhase::SCAN_CONNECTING: + return LOG_STR("SCAN_CONNECTING"); + case WiFiRetryPhase::RETRY_HIDDEN: + return LOG_STR("RETRY_HIDDEN"); + case WiFiRetryPhase::RESTARTING_ADAPTER: + return LOG_STR("RESTARTING"); + default: + return LOG_STR("UNKNOWN"); + } +} + +bool WiFiComponent::went_through_explicit_hidden_phase_() const { + // If first configured network is marked hidden, we went through EXPLICIT_HIDDEN phase + // This means those networks were already tried and should be skipped in RETRY_HIDDEN + return !this->sta_.empty() && this->sta_[0].get_hidden(); +} + +int8_t WiFiComponent::find_first_non_hidden_index_() const { + // Find the first network that is NOT marked hidden:true + // This is where EXPLICIT_HIDDEN phase would have stopped + for (size_t i = 0; i < this->sta_.size(); i++) { + if (!this->sta_[i].get_hidden()) { + return static_cast(i); + } + } + return -1; // All networks are hidden +} + +// 2 attempts per BSSID in SCAN_CONNECTING phase +// Rationale: This is the ONLY phase where we decrease BSSID priority, so we must be very sure. +// Auth failures are common immediately after scan due to WiFi stack state transitions. +// Trying twice filters out false positives and prevents unnecessarily marking a good BSSID as bad. +// After 2 genuine failures, priority degradation ensures we skip this BSSID on subsequent scans. +static constexpr uint8_t WIFI_RETRY_COUNT_PER_BSSID = 2; + +// 1 attempt per SSID in RETRY_HIDDEN phase +// Rationale: Try hidden mode once, then rescan to get next best BSSID via priority system +static constexpr uint8_t WIFI_RETRY_COUNT_PER_SSID = 1; + +// 1 attempt per AP in fast_connect mode (INITIAL_CONNECT and FAST_CONNECT_CYCLING_APS) +// Rationale: Fast connect prioritizes speed - try each AP once to find a working one quickly +static constexpr uint8_t WIFI_RETRY_COUNT_PER_AP = 1; + +/// Cooldown duration in milliseconds after adapter restart or repeated failures +/// Allows WiFi hardware to stabilize before next connection attempt +static constexpr uint32_t WIFI_COOLDOWN_DURATION_MS = 500; + +/// Cooldown duration when fallback AP is active and captive portal may be running +/// Longer interval gives users time to configure WiFi without constant connection attempts +/// While connecting, WiFi can't beacon the AP properly, so needs longer cooldown +static constexpr uint32_t WIFI_COOLDOWN_WITH_AP_ACTIVE_MS = 30000; + +static constexpr uint8_t get_max_retries_for_phase(WiFiRetryPhase phase) { + switch (phase) { + case WiFiRetryPhase::INITIAL_CONNECT: +#ifdef USE_WIFI_FAST_CONNECT + case WiFiRetryPhase::FAST_CONNECT_CYCLING_APS: +#endif + // INITIAL_CONNECT and FAST_CONNECT_CYCLING_APS both use 1 attempt per AP (fast_connect mode) + return WIFI_RETRY_COUNT_PER_AP; + case WiFiRetryPhase::EXPLICIT_HIDDEN: + // Explicitly hidden network: 1 attempt (user marked as hidden, try once then scan) + return WIFI_RETRY_COUNT_PER_SSID; + case WiFiRetryPhase::SCAN_CONNECTING: + // Scan-based phase: 2 attempts per BSSID (handles transient auth failures after scan) + return WIFI_RETRY_COUNT_PER_BSSID; + case WiFiRetryPhase::RETRY_HIDDEN: + // Hidden network mode: 1 attempt per SSID + return WIFI_RETRY_COUNT_PER_SSID; + default: + return WIFI_RETRY_COUNT_PER_BSSID; + } +} + +static void apply_scan_result_to_params(WiFiAP ¶ms, const WiFiScanResult &scan) { + params.set_hidden(false); + params.set_ssid(scan.get_ssid()); + params.set_bssid(scan.get_bssid()); + params.set_channel(scan.get_channel()); +} + +bool WiFiComponent::needs_scan_results_() const { + // Only SCAN_CONNECTING phase needs scan results + if (this->retry_phase_ != WiFiRetryPhase::SCAN_CONNECTING) { + return false; + } + // Need scan if we have no results or no matching networks + return this->scan_result_.empty() || !this->scan_result_[0].get_matches(); +} + +bool WiFiComponent::ssid_was_seen_in_scan_(const std::string &ssid) const { + // Check if this SSID is configured as hidden + // If explicitly marked hidden, we should always try hidden mode regardless of scan results + for (const auto &conf : this->sta_) { + if (conf.get_ssid() == ssid && conf.get_hidden()) { + return false; // Treat as not seen - force hidden mode attempt + } + } + + // Otherwise, check if we saw it in scan results + for (const auto &scan : this->scan_result_) { + if (scan.get_ssid() == ssid) { + return true; + } + } + return false; +} + +int8_t WiFiComponent::find_next_hidden_sta_(int8_t start_index) { + // Find next SSID that wasn't in scan results (might be hidden) + bool include_explicit_hidden = !this->went_through_explicit_hidden_phase_(); + // Start searching from start_index + 1 + for (size_t i = start_index + 1; i < this->sta_.size(); i++) { + const auto &sta = this->sta_[i]; + + // Skip networks that were already tried in EXPLICIT_HIDDEN phase + // Those are: networks marked hidden:true that appear before the first non-hidden network + // If all networks are hidden (first_non_hidden_idx == -1), skip all of them + if (!include_explicit_hidden && sta.get_hidden()) { + int8_t first_non_hidden_idx = this->find_first_non_hidden_index_(); + if (first_non_hidden_idx < 0 || static_cast(i) < first_non_hidden_idx) { + ESP_LOGD(TAG, "Skipping " LOG_SECRET("'%s'") " (explicit hidden, already tried)", sta.get_ssid().c_str()); + continue; + } + } + + // If we didn't scan this cycle, treat all networks as potentially hidden + // Otherwise, only retry networks that weren't seen in the scan + if (!this->did_scan_this_cycle_ || !this->ssid_was_seen_in_scan_(sta.get_ssid())) { + ESP_LOGD(TAG, "Hidden candidate " LOG_SECRET("'%s'") " at index %d", sta.get_ssid().c_str(), static_cast(i)); + return static_cast(i); + } + ESP_LOGD(TAG, "Skipping hidden retry for visible network " LOG_SECRET("'%s'"), sta.get_ssid().c_str()); + } + // No hidden SSIDs found + return -1; +} + +void WiFiComponent::start_initial_connection_() { + // If first network (highest priority) is explicitly marked hidden, try it first before scanning + // This respects user's priority order when they explicitly configure hidden networks + if (!this->sta_.empty() && this->sta_[0].get_hidden()) { + ESP_LOGI(TAG, "Starting with explicit hidden network (highest priority)"); + this->selected_sta_index_ = 0; + this->retry_phase_ = WiFiRetryPhase::EXPLICIT_HIDDEN; + WiFiAP params = this->build_params_for_current_phase_(); + this->start_connecting(params); + } else { + ESP_LOGI(TAG, "Starting scan"); + this->start_scanning(); + } +} + +#if defined(USE_ESP32) && defined(USE_WIFI_WPA2_EAP) && ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE +static const char *eap_phase2_to_str(esp_eap_ttls_phase2_types type) { + switch (type) { + case ESP_EAP_TTLS_PHASE2_PAP: + return "pap"; + case ESP_EAP_TTLS_PHASE2_CHAP: + return "chap"; + case ESP_EAP_TTLS_PHASE2_MSCHAP: + return "mschap"; + case ESP_EAP_TTLS_PHASE2_MSCHAPV2: + return "mschapv2"; + case ESP_EAP_TTLS_PHASE2_EAP: + return "eap"; + default: + return "unknown"; + } +} +#endif + float WiFiComponent::get_setup_priority() const { return setup_priority::WIFI; } void WiFiComponent::setup() { this->wifi_pre_setup_(); + +#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) + // Create semaphore for high-performance mode requests + // Start at 0, increment on request, decrement on release + this->high_performance_semaphore_ = xSemaphoreCreateCounting(UINT32_MAX, 0); + if (this->high_performance_semaphore_ == nullptr) { + ESP_LOGE(TAG, "Failed semaphore"); + } + + // Store the configured power save mode as baseline + this->configured_power_save_ = this->power_save_; +#endif + if (this->enable_on_boot_) { this->start(); } else { @@ -57,18 +353,19 @@ void WiFiComponent::setup() { } void WiFiComponent::start() { + char mac_s[18]; ESP_LOGCONFIG(TAG, "Starting\n" " Local MAC: %s", - get_mac_address_pretty().c_str()); + get_mac_address_pretty_into_buffer(mac_s)); this->last_connected_ = millis(); uint32_t hash = this->has_sta() ? fnv1_hash(App.get_compilation_time()) : 88491487UL; this->pref_ = global_preferences->make_preference(hash, true); - if (this->fast_connect_) { - this->fast_connect_pref_ = global_preferences->make_preference(hash + 1, false); - } +#ifdef USE_WIFI_FAST_CONNECT + this->fast_connect_pref_ = global_preferences->make_preference(hash + 1, false); +#endif SavedWifiSettings save{}; if (this->pref_.load(&save)) { @@ -86,20 +383,46 @@ void WiFiComponent::start() { ESP_LOGV(TAG, "Setting Output Power Option failed"); } +#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) + // Synchronize power_save_ with semaphore state before applying + if (this->high_performance_semaphore_ != nullptr) { + UBaseType_t semaphore_count = uxSemaphoreGetCount(this->high_performance_semaphore_); + if (semaphore_count > 0) { + this->power_save_ = WIFI_POWER_SAVE_NONE; + this->is_high_performance_mode_ = true; + } else { + this->power_save_ = this->configured_power_save_; + this->is_high_performance_mode_ = false; + } + } +#endif if (!this->wifi_apply_power_save_()) { ESP_LOGV(TAG, "Setting Power Save Option failed"); } - if (this->fast_connect_) { - this->trying_loaded_ap_ = this->load_fast_connect_settings_(); - if (!this->trying_loaded_ap_) { - this->ap_index_ = 0; - this->selected_ap_ = this->sta_[this->ap_index_]; - } - this->start_connecting(this->selected_ap_, false); + this->transition_to_phase_(WiFiRetryPhase::INITIAL_CONNECT); +#ifdef USE_WIFI_FAST_CONNECT + WiFiAP params; + bool loaded_fast_connect = this->load_fast_connect_settings_(params); + // Fast connect optimization: only use when we have saved BSSID+channel data + // Without saved data, try first configured network or use normal flow + if (loaded_fast_connect) { + ESP_LOGI(TAG, "Starting fast_connect (saved) " LOG_SECRET("'%s'"), params.get_ssid().c_str()); + this->start_connecting(params); + } else if (!this->sta_.empty() && !this->sta_[0].get_hidden()) { + // No saved data, but have configured networks - try first non-hidden network + ESP_LOGI(TAG, "Starting fast_connect (config) " LOG_SECRET("'%s'"), this->sta_[0].get_ssid().c_str()); + this->selected_sta_index_ = 0; + params = this->build_params_for_current_phase_(); + this->start_connecting(params); } else { - this->start_scanning(); + // No saved data and (no networks OR first is hidden) - use normal flow + this->start_initial_connection_(); } +#else + // Without fast_connect: go straight to scanning (or hidden mode if all networks are hidden) + this->start_initial_connection_(); +#endif #ifdef USE_WIFI_AP } else if (this->has_ap()) { this->setup_ap_config_(); @@ -127,9 +450,7 @@ void WiFiComponent::start() { void WiFiComponent::restart_adapter() { ESP_LOGW(TAG, "Restarting adapter"); this->wifi_mode_(false, {}); - delay(100); // NOLINT - this->num_retried_ = 0; - this->retry_hidden_ = false; + this->error_from_callback_ = false; } void WiFiComponent::loop() { @@ -148,24 +469,32 @@ void WiFiComponent::loop() { switch (this->state_) { case WIFI_COMPONENT_STATE_COOLDOWN: { - this->status_set_warning("waiting to reconnect"); - if (millis() - this->action_started_ > 5000) { - if (this->fast_connect_ || this->retry_hidden_) { - this->start_connecting(this->selected_ap_, false); - } else { - this->start_scanning(); - } + this->status_set_warning(LOG_STR("waiting to reconnect")); + // Skip cooldown if new credentials were provided while connecting + if (this->skip_cooldown_next_cycle_) { + this->skip_cooldown_next_cycle_ = false; + this->check_connecting_finished(); + break; + } + // Use longer cooldown when captive portal/improv is active to avoid disrupting user config + bool portal_active = this->is_captive_portal_active_() || this->is_esp32_improv_active_(); + uint32_t cooldown_duration = portal_active ? WIFI_COOLDOWN_WITH_AP_ACTIVE_MS : WIFI_COOLDOWN_DURATION_MS; + if (now - this->action_started_ > cooldown_duration) { + // After cooldown we either restarted the adapter because of + // a failure, or something tried to connect over and over + // so we entered cooldown. In both cases we call + // check_connecting_finished to continue the state machine. + this->check_connecting_finished(); } break; } case WIFI_COMPONENT_STATE_STA_SCANNING: { - this->status_set_warning("scanning for networks"); + this->status_set_warning(LOG_STR("scanning for networks")); this->check_scanning_finished(); break; } - case WIFI_COMPONENT_STATE_STA_CONNECTING: - case WIFI_COMPONENT_STATE_STA_CONNECTING_2: { - this->status_set_warning("associating to network"); + case WIFI_COMPONENT_STATE_STA_CONNECTING: { + this->status_set_warning(LOG_STR("associating to network")); this->check_connecting_finished(); break; } @@ -174,6 +503,8 @@ void WiFiComponent::loop() { if (!this->is_connected()) { ESP_LOGW(TAG, "Connection lost; reconnecting"); this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING; + // Clear error flag before reconnecting so first attempt is not seen as immediate failure + this->error_from_callback_ = false; this->retry_connect(); } else { this->status_clear_warning(); @@ -202,7 +533,8 @@ void WiFiComponent::loop() { #endif // USE_WIFI_AP #ifdef USE_IMPROV - if (esp32_improv::global_improv_component != nullptr && !esp32_improv::global_improv_component->is_active()) { + if (esp32_improv::global_improv_component != nullptr && !esp32_improv::global_improv_component->is_active() && + !esp32_improv::global_improv_component->should_start()) { if (now - this->last_connected_ > esp32_improv::global_improv_component->get_wifi_timeout()) { if (this->wifi_mode_(true, {})) esp32_improv::global_improv_component->start(); @@ -218,13 +550,38 @@ void WiFiComponent::loop() { } } } + +#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) + // Check if power save mode needs to be updated based on high-performance requests + if (this->high_performance_semaphore_ != nullptr) { + // Semaphore count directly represents active requests (starts at 0, increments on request) + UBaseType_t semaphore_count = uxSemaphoreGetCount(this->high_performance_semaphore_); + + if (semaphore_count > 0 && !this->is_high_performance_mode_) { + // Transition to high-performance mode (no power save) + ESP_LOGV(TAG, "Switching to high-performance mode (%" PRIu32 " active %s)", (uint32_t) semaphore_count, + semaphore_count == 1 ? "request" : "requests"); + this->power_save_ = WIFI_POWER_SAVE_NONE; + if (this->wifi_apply_power_save_()) { + this->is_high_performance_mode_ = true; + } + } else if (semaphore_count == 0 && this->is_high_performance_mode_) { + // Restore to configured power save mode + ESP_LOGV(TAG, "Restoring power save mode to configured setting"); + this->power_save_ = this->configured_power_save_; + if (this->wifi_apply_power_save_()) { + this->is_high_performance_mode_ = false; + } + } + } +#endif } WiFiComponent::WiFiComponent() { global_wifi_component = this; } bool WiFiComponent::has_ap() const { return this->has_ap_; } +bool WiFiComponent::is_ap_active() const { return this->state_ == WIFI_COMPONENT_STATE_AP; } bool WiFiComponent::has_sta() const { return !this->sta_.empty(); } -void WiFiComponent::set_fast_connect(bool fast_connect) { this->fast_connect_ = fast_connect; } #ifdef USE_WIFI_11KV_SUPPORT void WiFiComponent::set_btm(bool btm) { this->btm_ = btm; } void WiFiComponent::set_rrm(bool rrm) { this->rrm_ = rrm; } @@ -245,13 +602,10 @@ network::IPAddress WiFiComponent::get_dns_address(int num) { return this->wifi_dns_ip_(num); return {}; } -std::string WiFiComponent::get_use_address() const { - if (this->use_address_.empty()) { - return App.get_name() + ".local"; - } - return this->use_address_; -} -void WiFiComponent::set_use_address(const std::string &use_address) { this->use_address_ = use_address; } +// set_use_address() is guaranteed to be called during component setup by Python code generation, +// so use_address_ will always be valid when get_use_address() is called - no fallback needed. +const char *WiFiComponent::get_use_address() const { return this->use_address_; } +void WiFiComponent::set_use_address(const char *use_address) { this->use_address_ = use_address; } #ifdef USE_WIFI_AP void WiFiComponent::setup_ap_config_() { @@ -264,29 +618,35 @@ void WiFiComponent::setup_ap_config_() { std::string name = App.get_name(); if (name.length() > 32) { if (App.is_name_add_mac_suffix_enabled()) { - name.erase(name.begin() + 25, name.end() - 7); // Remove characters between 25 and the mac address + // Keep first 25 chars and last 7 chars (MAC suffix), remove middle + name.erase(25, name.length() - 32); } else { - name = name.substr(0, 32); + name.resize(32); } } this->ap_.set_ssid(name); } + this->ap_setup_ = this->wifi_start_ap_(this->ap_); + + auto ip_address = this->wifi_soft_ap_ip().str(); ESP_LOGCONFIG(TAG, "Setting up AP:\n" " AP SSID: '%s'\n" - " AP Password: '%s'", - this->ap_.get_ssid().c_str(), this->ap_.get_password().c_str()); - if (this->ap_.get_manual_ip().has_value()) { - auto manual = *this->ap_.get_manual_ip(); + " AP Password: '%s'\n" + " IP Address: %s", + this->ap_.get_ssid().c_str(), this->ap_.get_password().c_str(), ip_address.c_str()); + +#ifdef USE_WIFI_MANUAL_IP + auto manual_ip = this->ap_.get_manual_ip(); + if (manual_ip.has_value()) { ESP_LOGCONFIG(TAG, " AP Static IP: '%s'\n" " AP Gateway: '%s'\n" " AP Subnet: '%s'", - manual.static_ip.str().c_str(), manual.gateway.str().c_str(), manual.subnet.str().c_str()); + manual_ip->static_ip.str().c_str(), manual_ip->gateway.str().c_str(), + manual_ip->subnet.str().c_str()); } - - this->ap_setup_ = this->wifi_start_ap_(this->ap_); - ESP_LOGCONFIG(TAG, " IP Address: %s", this->wifi_soft_ap_ip().str().c_str()); +#endif if (!this->has_sta()) { this->state_ = WIFI_COMPONENT_STATE_AP; @@ -303,16 +663,68 @@ float WiFiComponent::get_loop_priority() const { return 10.0f; // before other loop components } +void WiFiComponent::init_sta(size_t count) { this->sta_.init(count); } void WiFiComponent::add_sta(const WiFiAP &ap) { this->sta_.push_back(ap); } void WiFiComponent::set_sta(const WiFiAP &ap) { this->clear_sta(); + this->init_sta(1); this->add_sta(ap); + this->selected_sta_index_ = 0; + // When new credentials are set (e.g., from improv), skip cooldown to retry immediately + this->skip_cooldown_next_cycle_ = true; +} + +WiFiAP WiFiComponent::build_params_for_current_phase_() { + const WiFiAP *config = this->get_selected_sta_(); + if (config == nullptr) { + ESP_LOGE(TAG, "No valid network config (selected_sta_index_=%d, sta_.size()=%zu)", + static_cast(this->selected_sta_index_), this->sta_.size()); + // Return empty params - caller should handle this gracefully + return WiFiAP(); + } + + WiFiAP params = *config; + + switch (this->retry_phase_) { + case WiFiRetryPhase::INITIAL_CONNECT: +#ifdef USE_WIFI_FAST_CONNECT + case WiFiRetryPhase::FAST_CONNECT_CYCLING_APS: +#endif + // Fast connect phases: use config-only (no scan results) + // BSSID/channel from config if user specified them, otherwise empty + break; + + case WiFiRetryPhase::EXPLICIT_HIDDEN: + case WiFiRetryPhase::RETRY_HIDDEN: + // Hidden network mode: clear BSSID/channel to trigger probe request + // (both explicit hidden and retry hidden use same behavior) + params.set_bssid(optional{}); + params.set_channel(optional{}); + break; + + case WiFiRetryPhase::SCAN_CONNECTING: + // Scan-based phase: always use best scan result (index 0 - highest priority after sorting) + if (!this->scan_result_.empty()) { + apply_scan_result_to_params(params, this->scan_result_[0]); + } + break; + + case WiFiRetryPhase::RESTARTING_ADAPTER: + // Should not be building params during restart + break; + } + + return params; +} + +WiFiAP WiFiComponent::get_sta() const { + const WiFiAP *config = this->get_selected_sta_(); + return config ? *config : WiFiAP{}; } -void WiFiComponent::clear_sta() { this->sta_.clear(); } void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &password) { - SavedWifiSettings save{}; - snprintf(save.ssid, sizeof(save.ssid), "%s", ssid.c_str()); - snprintf(save.password, sizeof(save.password), "%s", password.c_str()); + SavedWifiSettings save{}; // zero-initialized - all bytes set to \0, guaranteeing null termination + strncpy(save.ssid, ssid.c_str(), sizeof(save.ssid) - 1); // max 32 chars, byte 32 remains \0 + strncpy(save.password, password.c_str(), sizeof(save.password) - 1); // max 64 chars, byte 64 remains \0 this->pref_.save(&save); // ensure it's written immediately global_preferences->sync(); @@ -321,16 +733,40 @@ void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &pa sta.set_ssid(ssid); sta.set_password(password); this->set_sta(sta); + + // Trigger connection attempt (exits cooldown if needed, no-op if already connecting/connected) + this->connect_soon_(); } -void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) { - ESP_LOGI(TAG, "Connecting to '%s'", ap.get_ssid().c_str()); +void WiFiComponent::connect_soon_() { + // Only trigger retry if we're in cooldown - if already connecting/connected, do nothing + if (this->state_ == WIFI_COMPONENT_STATE_COOLDOWN) { + ESP_LOGD(TAG, "Exiting cooldown early due to new WiFi credentials"); + this->retry_connect(); + } +} + +void WiFiComponent::start_connecting(const WiFiAP &ap) { + // Log connection attempt at INFO level with priority + char bssid_s[18]; + int8_t priority = 0; + + if (ap.get_bssid().has_value()) { + format_mac_addr_upper(ap.get_bssid().value().data(), bssid_s); + priority = this->get_sta_priority(ap.get_bssid().value()); + } + + ESP_LOGI(TAG, + "Connecting to " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") " (priority %d, attempt %u/%u in phase %s)...", + ap.get_ssid().c_str(), ap.get_bssid().has_value() ? bssid_s : LOG_STR_LITERAL("any"), priority, + this->num_retried_ + 1, get_max_retries_for_phase(this->retry_phase_), + LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_))); + #ifdef ESPHOME_LOG_HAS_VERBOSE ESP_LOGV(TAG, "Connection Params:"); ESP_LOGV(TAG, " SSID: '%s'", ap.get_ssid().c_str()); if (ap.get_bssid().has_value()) { - bssid_t b = *ap.get_bssid(); - ESP_LOGV(TAG, " BSSID: %02X:%02X:%02X:%02X:%02X:%02X", b[0], b[1], b[2], b[3], b[4], b[5]); + ESP_LOGV(TAG, " BSSID: %s", bssid_s); } else { ESP_LOGV(TAG, " BSSID: Not Set"); } @@ -342,15 +778,8 @@ void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) { ESP_LOGV(TAG, " Identity: " LOG_SECRET("'%s'"), eap_config.identity.c_str()); ESP_LOGV(TAG, " Username: " LOG_SECRET("'%s'"), eap_config.username.c_str()); ESP_LOGV(TAG, " Password: " LOG_SECRET("'%s'"), eap_config.password.c_str()); -#ifdef USE_ESP_IDF -#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE - std::map phase2types = {{ESP_EAP_TTLS_PHASE2_PAP, "pap"}, - {ESP_EAP_TTLS_PHASE2_CHAP, "chap"}, - {ESP_EAP_TTLS_PHASE2_MSCHAP, "mschap"}, - {ESP_EAP_TTLS_PHASE2_MSCHAPV2, "mschapv2"}, - {ESP_EAP_TTLS_PHASE2_EAP, "eap"}}; - ESP_LOGV(TAG, " TTLS Phase 2: " LOG_SECRET("'%s'"), phase2types[eap_config.ttls_phase_2].c_str()); -#endif +#if defined(USE_ESP32) && defined(USE_WIFI_WPA2_EAP) && ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + ESP_LOGV(TAG, " TTLS Phase 2: " LOG_SECRET("'%s'"), eap_phase2_to_str(eap_config.ttls_phase_2)); #endif bool ca_cert_present = eap_config.ca_cert != nullptr && strlen(eap_config.ca_cert); bool client_cert_present = eap_config.client_cert != nullptr && strlen(eap_config.client_cert); @@ -369,11 +798,14 @@ void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) { } else { ESP_LOGV(TAG, " Channel not set"); } +#ifdef USE_WIFI_MANUAL_IP if (ap.get_manual_ip().has_value()) { ManualIP m = *ap.get_manual_ip(); ESP_LOGV(TAG, " Manual IP: Static IP=%s Gateway=%s Subnet=%s DNS1=%s DNS2=%s", m.static_ip.str().c_str(), m.gateway.str().c_str(), m.subnet.str().c_str(), m.dns1.str().c_str(), m.dns2.str().c_str()); - } else { + } else +#endif + { ESP_LOGV(TAG, " Using DHCP IP"); } ESP_LOGV(TAG, " Hidden: %s", YESNO(ap.get_hidden())); @@ -381,19 +813,24 @@ void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) { if (!this->wifi_sta_connect_(ap)) { ESP_LOGE(TAG, "wifi_sta_connect_ failed"); - this->retry_connect(); - return; - } - - if (!two) { - this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING; + // Enter cooldown to allow WiFi hardware to stabilize + // (immediate failure suggests hardware not ready, different from connection timeout) + this->state_ = WIFI_COMPONENT_STATE_COOLDOWN; } else { - this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING_2; + this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING; } this->action_started_ = millis(); } const LogString *get_signal_bars(int8_t rssi) { + // Check for disconnected sentinel value first + if (rssi == WIFI_RSSI_DISCONNECTED) { + // MULTIPLICATION SIGN + // Unicode: U+00D7, UTF-8: C3 97 + return LOG_STR("\033[0;31m" // red + "\xc3\x97\xc3\x97\xc3\x97\xc3\x97" + "\033[0m"); + } // LOWER ONE QUARTER BLOCK // Unicode: U+2582, UTF-8: E2 96 82 // LOWER HALF BLOCK @@ -438,13 +875,15 @@ const LogString *get_signal_bars(int8_t rssi) { void WiFiComponent::print_connect_params_() { bssid_t bssid = wifi_bssid(); + char bssid_s[18]; + format_mac_addr_upper(bssid.data(), bssid_s); - ESP_LOGCONFIG(TAG, " Local MAC: %s", get_mac_address_pretty().c_str()); + char mac_s[18]; + ESP_LOGCONFIG(TAG, " Local MAC: %s", get_mac_address_pretty_into_buffer(mac_s)); if (this->is_disabled()) { ESP_LOGCONFIG(TAG, " Disabled"); return; } - ESP_LOGCONFIG(TAG, " SSID: " LOG_SECRET("'%s'"), wifi_ssid().c_str()); for (auto &ip : wifi_sta_ip_addresses()) { if (ip.is_set()) { ESP_LOGCONFIG(TAG, " IP Address: %s", ip.str().c_str()); @@ -452,22 +891,23 @@ void WiFiComponent::print_connect_params_() { } int8_t rssi = wifi_rssi(); ESP_LOGCONFIG(TAG, - " BSSID: " LOG_SECRET("%02X:%02X:%02X:%02X:%02X:%02X") "\n" - " Hostname: '%s'\n" - " Signal strength: %d dB %s", - bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5], App.get_name().c_str(), rssi, - LOG_STR_ARG(get_signal_bars(rssi))); - if (this->selected_ap_.get_bssid().has_value()) { - ESP_LOGV(TAG, " Priority: %.1f", this->get_sta_priority(*this->selected_ap_.get_bssid())); - } - ESP_LOGCONFIG(TAG, - " Channel: %" PRId32 "\n" - " Subnet: %s\n" - " Gateway: %s\n" - " DNS1: %s\n" - " DNS2: %s", + " SSID: " LOG_SECRET("'%s'") "\n" + " BSSID: " LOG_SECRET("%s") "\n" + " Hostname: '%s'\n" + " Signal strength: %d dB %s\n" + " Channel: %" PRId32 "\n" + " Subnet: %s\n" + " Gateway: %s\n" + " DNS1: %s\n" + " DNS2: %s", + wifi_ssid().c_str(), bssid_s, App.get_name().c_str(), rssi, LOG_STR_ARG(get_signal_bars(rssi)), get_wifi_channel(), wifi_subnet_mask_().str().c_str(), wifi_gateway_ip_().str().c_str(), wifi_dns_ip_(0).str().c_str(), wifi_dns_ip_(1).str().c_str()); +#ifdef ESPHOME_LOG_HAS_VERBOSE + if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_bssid().has_value()) { + ESP_LOGV(TAG, " Priority: %d", this->get_sta_priority(*config->get_bssid())); + } +#endif #ifdef USE_WIFI_11KV_SUPPORT ESP_LOGCONFIG(TAG, " BTM: %s\n" @@ -505,8 +945,39 @@ void WiFiComponent::start_scanning() { this->state_ = WIFI_COMPONENT_STATE_STA_SCANNING; } -// Helper function for WiFi scan result comparison -// Returns true if 'a' should be placed before 'b' in the sorted order +/// Comparator for WiFi scan result sorting - determines which network should be tried first +/// Returns true if 'a' should be placed before 'b' in the sorted order (a is "better" than b) +/// +/// Sorting logic (in priority order): +/// 1. Matching networks always ranked before non-matching networks +/// 2. For matching networks: Priority first (CRITICAL - tracks failure history) +/// 3. RSSI as tiebreaker for equal priority or non-matching networks +/// +/// WHY PRIORITY MUST BE CHECKED FIRST: +/// The priority field tracks connection failure history via priority degradation: +/// - Initial priority: 0.0 (from config or default) +/// - Each connection failure: priority -= 1.0 (becomes -1.0, -2.0, -3.0, etc.) +/// - Failed BSSIDs sorted lower → naturally try different BSSID on next scan +/// +/// This enables automatic BSSID cycling for various real-world failure scenarios: +/// - Crashed/hung AP (visible but not responding) +/// - Misconfigured mesh node (accepts auth but no DHCP/routing) +/// - Capacity limits (AP refuses new clients) +/// - Rogue AP (same SSID, wrong password or malicious) +/// - Intermittent hardware issues (flaky radio, overheating) +/// +/// Example mesh network: 3 APs with same SSID "home", all at priority 0.0 initially +/// - Try strongest BSSID A (sorted by RSSI) → fails → priority A becomes -1.0 +/// - Next scan: BSSID B and C (priority 0.0) sorted BEFORE A (priority -1.0) +/// - Try next strongest BSSID B → succeeds or fails and gets deprioritized +/// - System naturally cycles through all BSSIDs via priority degradation +/// - Eventually finds working AP or tries all options before restarting adapter +/// +/// If we checked RSSI first (Bug in PR #9963): +/// - Same failed BSSID would keep being selected if it has strongest signal +/// - Device stuck connecting to crashed AP with -30dBm while working AP at -50dBm ignored +/// - Priority degradation would be useless +/// - Mesh networks would never recover from single AP failure [[nodiscard]] inline static bool wifi_scan_result_is_better(const WiFiScanResult &a, const WiFiScanResult &b) { // Matching networks always come before non-matching if (a.get_matches() && !b.get_matches()) @@ -514,21 +985,13 @@ void WiFiComponent::start_scanning() { if (!a.get_matches() && b.get_matches()) return false; - if (a.get_matches() && b.get_matches()) { - // For APs with the same SSID, always prefer stronger signal - // This helps with mesh networks and multiple APs - if (a.get_ssid() == b.get_ssid()) { - return a.get_rssi() > b.get_rssi(); - } - - // For different SSIDs, check priority first - if (a.get_priority() != b.get_priority()) - return a.get_priority() > b.get_priority(); - // If priorities are equal, prefer stronger signal - return a.get_rssi() > b.get_rssi(); + // Both matching: check priority first (tracks connection failures via priority degradation) + // Priority is decreased when a BSSID fails to connect, so lower priority = previously failed + if (a.get_matches() && b.get_matches() && a.get_priority() != b.get_priority()) { + return a.get_priority() > b.get_priority(); } - // Both don't match - sort by signal strength + // Use RSSI as tiebreaker (for equal-priority matching networks or all non-matching networks) return a.get_rssi() > b.get_rssi(); } @@ -536,7 +999,7 @@ void WiFiComponent::start_scanning() { // Using insertion sort instead of std::stable_sort saves flash memory // by avoiding template instantiations (std::rotate, std::stable_sort, lambdas) // IMPORTANT: This sort is stable (preserves relative order of equal elements) -static void insertion_sort_scan_results(std::vector &results) { +template static void insertion_sort_scan_results(VectorType &results) { const size_t size = results.size(); for (size_t i = 1; i < size; i++) { // Make a copy to avoid issues with move semantics during comparison @@ -553,6 +1016,23 @@ static void insertion_sort_scan_results(std::vector &results) { } } +// Helper function to log scan results - marked noinline to prevent re-inlining into loop +__attribute__((noinline)) static void log_scan_result(const WiFiScanResult &res) { + char bssid_s[18]; + auto bssid = res.get_bssid(); + format_mac_addr_upper(bssid.data(), bssid_s); + + if (res.get_matches()) { + ESP_LOGI(TAG, "- '%s' %s" LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), + res.get_is_hidden() ? LOG_STR_LITERAL("(HIDDEN) ") : LOG_STR_LITERAL(""), bssid_s, + LOG_STR_ARG(get_signal_bars(res.get_rssi()))); + ESP_LOGD(TAG, " Channel: %2u, RSSI: %3d dB, Priority: %4d", res.get_channel(), res.get_rssi(), res.get_priority()); + } else { + ESP_LOGD(TAG, "- " LOG_SECRET("'%s'") " " LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), bssid_s, + LOG_STR_ARG(get_signal_bars(res.get_rssi()))); + } +} + void WiFiComponent::check_scanning_finished() { if (!this->scan_done_) { if (millis() - this->action_started_ > 30000) { @@ -562,6 +1042,7 @@ void WiFiComponent::check_scanning_finished() { return; } this->scan_done_ = false; + this->did_scan_this_cycle_ = true; if (this->scan_result_.empty()) { ESP_LOGW(TAG, "No networks found"); @@ -574,10 +1055,12 @@ void WiFiComponent::check_scanning_finished() { for (auto &ap : this->sta_) { if (res.matches(ap)) { res.set_matches(true); - if (!this->has_sta_priority(res.get_bssid())) { - this->set_sta_priority(res.get_bssid(), ap.get_priority()); + // Cache priority lookup - do single search instead of 2 separate searches + const bssid_t &bssid = res.get_bssid(); + if (!this->has_sta_priority(bssid)) { + this->set_sta_priority(bssid, ap.get_priority()); } - res.set_priority(this->get_sta_priority(res.get_bssid())); + res.set_priority(this->get_sta_priority(bssid)); break; } } @@ -587,72 +1070,54 @@ void WiFiComponent::check_scanning_finished() { insertion_sort_scan_results(this->scan_result_); for (auto &res : this->scan_result_) { - char bssid_s[18]; - auto bssid = res.get_bssid(); - sprintf(bssid_s, "%02X:%02X:%02X:%02X:%02X:%02X", bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]); + log_scan_result(res); + } - if (res.get_matches()) { - ESP_LOGI(TAG, "- '%s' %s" LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), - res.get_is_hidden() ? "(HIDDEN) " : "", bssid_s, LOG_STR_ARG(get_signal_bars(res.get_rssi()))); - ESP_LOGD(TAG, " Channel: %u", res.get_channel()); - ESP_LOGD(TAG, " RSSI: %d dB", res.get_rssi()); - } else { - ESP_LOGD(TAG, "- " LOG_SECRET("'%s'") " " LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), bssid_s, - LOG_STR_ARG(get_signal_bars(res.get_rssi()))); + // SYNCHRONIZATION POINT: Establish link between scan_result_[0] and selected_sta_index_ + // After sorting, scan_result_[0] contains the best network. Now find which sta_[i] config + // matches that network and record it in selected_sta_index_. This keeps the two indices + // synchronized so build_params_for_current_phase_() can safely use both to build connection parameters. + const WiFiScanResult &scan_res = this->scan_result_[0]; + bool found_match = false; + if (scan_res.get_matches()) { + for (size_t i = 0; i < this->sta_.size(); i++) { + if (scan_res.matches(this->sta_[i])) { + // Safe cast: sta_.size() limited to MAX_WIFI_NETWORKS (127) in __init__.py validation + // No overflow check needed - YAML validation prevents >127 networks + this->selected_sta_index_ = static_cast(i); // Links scan_result_[0] with sta_[i] + found_match = true; + break; + } } } - if (!this->scan_result_[0].get_matches()) { + if (!found_match) { ESP_LOGW(TAG, "No matching network found"); - this->retry_connect(); - return; - } - - WiFiAP connect_params; - WiFiScanResult scan_res = this->scan_result_[0]; - for (auto &config : this->sta_) { - // search for matching STA config, at least one will match (from checks before) - if (!scan_res.matches(config)) { - continue; + // No scan results matched our configured networks - transition directly to hidden mode + // Don't call retry_connect() since we never attempted a connection (no BSSID to penalize) + this->transition_to_phase_(WiFiRetryPhase::RETRY_HIDDEN); + // If no hidden networks to try, skip connection attempt (will be handled on next loop) + if (this->selected_sta_index_ == -1) { + return; } - - if (config.get_hidden()) { - // selected network is hidden, we use the data from the config - connect_params.set_hidden(true); - connect_params.set_ssid(config.get_ssid()); - // don't set BSSID and channel, there might be multiple hidden networks - // but we can't know which one is the correct one. Rely on probe-req with just SSID. - } else { - // selected network is visible, we use the data from the scan - // limit the connect params to only connect to exactly this network - // (network selection is done during scan phase). - connect_params.set_hidden(false); - connect_params.set_ssid(scan_res.get_ssid()); - connect_params.set_channel(scan_res.get_channel()); - connect_params.set_bssid(scan_res.get_bssid()); - } - // copy manual IP (if set) - connect_params.set_manual_ip(config.get_manual_ip()); - -#ifdef USE_WIFI_WPA2_EAP - // copy EAP parameters (if set) - connect_params.set_eap(config.get_eap()); -#endif - - // copy password (if set) - connect_params.set_password(config.get_password()); - - break; + // Now start connection attempt in hidden mode + } else if (this->transition_to_phase_(WiFiRetryPhase::SCAN_CONNECTING)) { + return; // scan started, wait for next loop iteration } yield(); - this->selected_ap_ = connect_params; - this->start_connecting(connect_params, false); + WiFiAP params = this->build_params_for_current_phase_(); + // Ensure we're in SCAN_CONNECTING phase when connecting with scan results + // (needed when scan was started directly without transition_to_phase_, e.g., initial scan) + this->start_connecting(params); } void WiFiComponent::dump_config() { - ESP_LOGCONFIG(TAG, "WiFi:"); + ESP_LOGCONFIG(TAG, + "WiFi:\n" + " Connected: %s", + YESNO(this->is_connected())); this->print_connect_params_(); } @@ -666,10 +1131,22 @@ void WiFiComponent::check_connecting_finished() { return; } - // We won't retry hidden networks unless a reconnect fails more than three times again - this->retry_hidden_ = false; - ESP_LOGI(TAG, "Connected"); + // Warn if we had to retry with hidden network mode for a network that's not marked hidden + // Only warn if we actually connected without scan data (SSID only), not if scan succeeded on retry + if (const WiFiAP *config = this->get_selected_sta_(); this->retry_phase_ == WiFiRetryPhase::RETRY_HIDDEN && + config && !config->get_hidden() && + this->scan_result_.empty()) { + ESP_LOGW(TAG, LOG_SECRET("'%s'") " should be marked hidden", config->get_ssid().c_str()); + } + // Reset to initial phase on successful connection (don't log transition, just reset state) + this->retry_phase_ = WiFiRetryPhase::INITIAL_CONNECT; + this->num_retried_ = 0; + // Ensure next connection attempt does not inherit error state + // so when WiFi disconnects later we start fresh and don't see + // the first connection as a failure. + this->error_from_callback_ = false; + this->print_connect_params_(); if (this->has_ap()) { @@ -690,8 +1167,17 @@ void WiFiComponent::check_connecting_finished() { this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTED; this->num_retried_ = 0; - if (this->fast_connect_) { - this->save_fast_connect_settings_(); + // Clear priority tracking if all priorities are at minimum + this->clear_priorities_if_all_min_(); + +#ifdef USE_WIFI_FAST_CONNECT + this->save_fast_connect_settings_(); +#endif + + // Free scan results memory unless a component needs them + if (!this->keep_scan_results_) { + this->scan_result_.clear(); + this->scan_result_.shrink_to_fit(); } return; @@ -705,7 +1191,7 @@ void WiFiComponent::check_connecting_finished() { } if (this->error_from_callback_) { - ESP_LOGW(TAG, "Connecting to network failed"); + ESP_LOGW(TAG, "Connecting to network failed (callback)"); this->retry_connect(); return; } @@ -730,68 +1216,413 @@ void WiFiComponent::check_connecting_finished() { this->retry_connect(); } -void WiFiComponent::retry_connect() { - if (this->selected_ap_.get_bssid()) { - auto bssid = *this->selected_ap_.get_bssid(); - float priority = this->get_sta_priority(bssid); - this->set_sta_priority(bssid, priority - 1.0f); +/// Determine the next retry phase based on current state and failure conditions +/// This function examines the current retry phase, number of retries, and failure reasons +/// to decide what phase to move to next. It does not modify any state - it only returns +/// the recommended next phase. +/// +/// @return The next WiFiRetryPhase to transition to (may be same as current phase if should retry) +WiFiRetryPhase WiFiComponent::determine_next_phase_() { + switch (this->retry_phase_) { + case WiFiRetryPhase::INITIAL_CONNECT: +#ifdef USE_WIFI_FAST_CONNECT + case WiFiRetryPhase::FAST_CONNECT_CYCLING_APS: + // INITIAL_CONNECT and FAST_CONNECT_CYCLING_APS: no retries, try next AP or fall back to scan + if (this->selected_sta_index_ < static_cast(this->sta_.size()) - 1) { + return WiFiRetryPhase::FAST_CONNECT_CYCLING_APS; // Move to next AP + } +#endif + // Check if we should try explicit hidden networks before scanning + // This handles reconnection after connection loss where first network is hidden + if (!this->sta_.empty() && this->sta_[0].get_hidden()) { + return WiFiRetryPhase::EXPLICIT_HIDDEN; + } + // No more APs to try, fall back to scan + return WiFiRetryPhase::SCAN_CONNECTING; + + case WiFiRetryPhase::EXPLICIT_HIDDEN: { + // Try all explicitly hidden networks before scanning + if (this->num_retried_ + 1 < WIFI_RETRY_COUNT_PER_SSID) { + return WiFiRetryPhase::EXPLICIT_HIDDEN; // Keep retrying same SSID + } + + // Exhausted retries on current SSID - check for more explicitly hidden networks + // Stop when we reach a visible network (proceed to scanning) + size_t next_index = this->selected_sta_index_ + 1; + if (next_index < this->sta_.size() && this->sta_[next_index].get_hidden()) { + // Found another explicitly hidden network + return WiFiRetryPhase::EXPLICIT_HIDDEN; + } + + // No more consecutive explicitly hidden networks + // If ALL networks are hidden, skip scanning and go directly to restart + if (this->find_first_non_hidden_index_() < 0) { + return WiFiRetryPhase::RESTARTING_ADAPTER; + } + // Otherwise proceed to scanning for non-hidden networks + return WiFiRetryPhase::SCAN_CONNECTING; + } + + case WiFiRetryPhase::SCAN_CONNECTING: + // If scan found no matching networks, skip to hidden network mode + if (!this->scan_result_.empty() && !this->scan_result_[0].get_matches()) { + return WiFiRetryPhase::RETRY_HIDDEN; + } + + if (this->num_retried_ + 1 < WIFI_RETRY_COUNT_PER_BSSID) { + return WiFiRetryPhase::SCAN_CONNECTING; // Keep retrying same BSSID + } + + // Exhausted retries on current BSSID (scan_result_[0]) + // Its priority has been decreased, so on next scan it will be sorted lower + // and we'll try the next best BSSID. + // Check if there are any potentially hidden networks to try + if (this->find_next_hidden_sta_(-1) >= 0) { + return WiFiRetryPhase::RETRY_HIDDEN; // Found hidden networks to try + } + // No hidden networks - always go through RESTARTING_ADAPTER phase + // This ensures num_retried_ gets reset and a fresh scan is triggered + // The actual adapter restart will be skipped if captive portal/improv is active + return WiFiRetryPhase::RESTARTING_ADAPTER; + + case WiFiRetryPhase::RETRY_HIDDEN: + // If no hidden SSIDs to try (selected_sta_index_ == -1), skip directly to rescan + if (this->selected_sta_index_ >= 0) { + if (this->num_retried_ + 1 < WIFI_RETRY_COUNT_PER_SSID) { + return WiFiRetryPhase::RETRY_HIDDEN; // Keep retrying same SSID + } + + // Exhausted retries on current SSID - check if there are more potentially hidden SSIDs to try + if (this->selected_sta_index_ < static_cast(this->sta_.size()) - 1) { + // Check if find_next_hidden_sta_() would actually find another hidden SSID + // as it might have been seen in the scan results and we want to skip those + // otherwise we will get stuck in RETRY_HIDDEN phase + if (this->find_next_hidden_sta_(this->selected_sta_index_) != -1) { + // More hidden SSIDs available - stay in RETRY_HIDDEN, advance will happen in retry_connect() + return WiFiRetryPhase::RETRY_HIDDEN; + } + } + } + // Exhausted all potentially hidden SSIDs - always go through RESTARTING_ADAPTER + // This ensures num_retried_ gets reset and a fresh scan is triggered + // The actual adapter restart will be skipped if captive portal/improv is active + return WiFiRetryPhase::RESTARTING_ADAPTER; + + case WiFiRetryPhase::RESTARTING_ADAPTER: + // After restart, go back to explicit hidden if we went through it initially + if (this->went_through_explicit_hidden_phase_()) { + return WiFiRetryPhase::EXPLICIT_HIDDEN; + } + // Skip scanning when captive portal/improv is active to avoid disrupting AP + // Even passive scans can cause brief AP disconnections on ESP32 + if (this->is_captive_portal_active_() || this->is_esp32_improv_active_()) { + return WiFiRetryPhase::RETRY_HIDDEN; + } + return WiFiRetryPhase::SCAN_CONNECTING; } - delay(10); - if (!this->is_captive_portal_active_() && !this->is_esp32_improv_active_() && - (this->num_retried_ > 3 || this->error_from_callback_)) { - if (this->fast_connect_) { - if (this->trying_loaded_ap_) { - this->trying_loaded_ap_ = false; - this->ap_index_ = 0; // Retry from the first configured AP - } else if (this->ap_index_ >= this->sta_.size() - 1) { - ESP_LOGW(TAG, "No more APs to try"); - this->ap_index_ = 0; - this->restart_adapter(); - } else { - // Try next AP - this->ap_index_++; - } - this->num_retried_ = 0; - this->selected_ap_ = this->sta_[this->ap_index_]; - } else { - if (this->num_retried_ > 5) { - // If retry failed for more than 5 times, let's restart STA - this->restart_adapter(); - } else { - // Try hidden networks after 3 failed retries - ESP_LOGD(TAG, "Retrying with hidden networks"); - this->retry_hidden_ = true; - this->num_retried_++; - } - } - } else { - this->num_retried_++; + // Should never reach here + return WiFiRetryPhase::SCAN_CONNECTING; +} + +/// Transition from current retry phase to a new phase with logging and phase-specific setup +/// This function handles the actual state change, including: +/// - Logging the phase transition +/// - Resetting the retry counter +/// - Performing phase-specific initialization (e.g., advancing AP index, starting scans) +/// +/// @param new_phase The phase we're transitioning TO +/// @return true if connection attempt should be skipped (scan started or no networks to try) +/// false if caller can proceed with connection attempt +bool WiFiComponent::transition_to_phase_(WiFiRetryPhase new_phase) { + WiFiRetryPhase old_phase = this->retry_phase_; + + // No-op if staying in same phase + if (old_phase == new_phase) { + return false; } - this->error_from_callback_ = false; - if (this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTING) { - yield(); - this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING_2; - this->start_connecting(this->selected_ap_, true); + + ESP_LOGD(TAG, "Retry phase: %s → %s", LOG_STR_ARG(retry_phase_to_log_string(old_phase)), + LOG_STR_ARG(retry_phase_to_log_string(new_phase))); + + this->retry_phase_ = new_phase; + this->num_retried_ = 0; // Reset retry counter on phase change + + // Phase-specific setup + switch (new_phase) { +#ifdef USE_WIFI_FAST_CONNECT + case WiFiRetryPhase::FAST_CONNECT_CYCLING_APS: + // Move to next configured AP - clear old scan data so new AP is tried with config only + this->selected_sta_index_++; + this->scan_result_.clear(); + break; +#endif + + case WiFiRetryPhase::EXPLICIT_HIDDEN: + // Starting explicit hidden phase - reset to first network + this->selected_sta_index_ = 0; + break; + + case WiFiRetryPhase::SCAN_CONNECTING: + // Transitioning to scan-based connection +#ifdef USE_WIFI_FAST_CONNECT + if (old_phase == WiFiRetryPhase::FAST_CONNECT_CYCLING_APS) { + ESP_LOGI(TAG, "Fast connect exhausted, falling back to scan"); + } +#endif + // Trigger scan if we don't have scan results OR if transitioning from phases that need fresh scan + if (this->scan_result_.empty() || old_phase == WiFiRetryPhase::EXPLICIT_HIDDEN || + old_phase == WiFiRetryPhase::RETRY_HIDDEN || old_phase == WiFiRetryPhase::RESTARTING_ADAPTER) { + this->selected_sta_index_ = -1; // Will be set after scan completes + this->start_scanning(); + return true; // Started scan, wait for completion + } + // Already have scan results - selected_sta_index_ should already be synchronized + // (set in check_scanning_finished() when scan completed) + // No need to reset it here + break; + + case WiFiRetryPhase::RETRY_HIDDEN: + // Starting hidden mode - find first SSID that wasn't in scan results + if (old_phase == WiFiRetryPhase::SCAN_CONNECTING) { + // Keep scan results so we can skip SSIDs that were visible in the scan + // Don't clear scan_result_ - we need it to know which SSIDs are NOT hidden + + // If first network is marked hidden, we went through EXPLICIT_HIDDEN phase + // In that case, skip networks marked hidden:true (already tried) + // Otherwise, include them (they haven't been tried yet) + this->selected_sta_index_ = this->find_next_hidden_sta_(-1); + + if (this->selected_sta_index_ == -1) { + ESP_LOGD(TAG, "All SSIDs visible or already tried, skipping hidden mode"); + } + } + break; + + case WiFiRetryPhase::RESTARTING_ADAPTER: + // Skip actual adapter restart if captive portal/improv is active + // This allows state machine to reset num_retried_ and trigger fresh scan + // without disrupting the captive portal/improv connection + if (!this->is_captive_portal_active_() && !this->is_esp32_improv_active_()) { + this->restart_adapter(); + } + // Clear scan flag - we're starting a new retry cycle + this->did_scan_this_cycle_ = false; + // Always enter cooldown after restart (or skip-restart) to allow stabilization + // Use extended cooldown when AP is active to avoid constant scanning that blocks DNS + this->state_ = WIFI_COMPONENT_STATE_COOLDOWN; + this->action_started_ = millis(); + // Return true to indicate we should wait (go to COOLDOWN) instead of immediately connecting + return true; + + default: + break; + } + + return false; // Did not start scan, can proceed with connection +} + +/// Clear BSSID priority tracking if all priorities are at minimum (saves memory) +/// At minimum priority, all BSSIDs are equally bad, so priority tracking is useless +/// Called after successful connection or after failed connection attempts +void WiFiComponent::clear_priorities_if_all_min_() { + if (this->sta_priorities_.empty()) { return; } - this->state_ = WIFI_COMPONENT_STATE_COOLDOWN; - this->action_started_ = millis(); + int8_t first_priority = this->sta_priorities_[0].priority; + + // Only clear if all priorities have been decremented to the minimum value + // At this point, all BSSIDs have been equally penalized and priority info is useless + if (first_priority != std::numeric_limits::min()) { + return; + } + + for (const auto &pri : this->sta_priorities_) { + if (pri.priority != first_priority) { + return; // Not all same, nothing to do + } + } + + // All priorities are at minimum - clear the vector to save memory and reset + ESP_LOGD(TAG, "Clearing BSSID priorities (all at minimum)"); + this->sta_priorities_.clear(); + this->sta_priorities_.shrink_to_fit(); } -bool WiFiComponent::can_proceed() { - if (!this->has_sta() || this->state_ == WIFI_COMPONENT_STATE_DISABLED || this->ap_setup_) { - return true; +/// Log failed connection attempt and decrease BSSID priority to avoid repeated failures +/// This function identifies which BSSID was attempted (from scan results or config), +/// decreases its priority by 1.0 to discourage future attempts, and logs the change. +/// +/// The priority degradation system ensures that failed BSSIDs are automatically sorted +/// lower in subsequent scans, naturally cycling through different APs without explicit +/// BSSID tracking within a scan cycle. +/// +/// Priority sources: +/// - SCAN_CONNECTING phase: Uses BSSID from scan_result_[0] (best match after sorting) +/// - Other phases: Uses BSSID from config if explicitly specified by user or fast_connect +/// +/// If no BSSID is available (SSID-only connection), priority adjustment is skipped. +/// +/// IMPORTANT: Priority is only decreased on the LAST attempt for a BSSID in SCAN_CONNECTING phase. +/// This prevents false positives from transient WiFi stack state issues after scanning. +/// Single failures don't necessarily mean the AP is bad - two genuine failures provide +/// higher confidence before degrading priority and skipping the BSSID in future scans. +void WiFiComponent::log_and_adjust_priority_for_failed_connect_() { + // Determine which BSSID we tried to connect to + optional failed_bssid; + + if (this->retry_phase_ == WiFiRetryPhase::SCAN_CONNECTING && !this->scan_result_.empty()) { + // Scan-based phase: always use best result (index 0) + failed_bssid = this->scan_result_[0].get_bssid(); + } else if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_bssid()) { + // Config has specific BSSID (fast_connect or user-specified) + failed_bssid = *config->get_bssid(); } - return this->is_connected(); + + if (!failed_bssid.has_value()) { + return; // No BSSID to penalize + } + + // Get SSID for logging + std::string ssid; + if (this->retry_phase_ == WiFiRetryPhase::SCAN_CONNECTING && !this->scan_result_.empty()) { + ssid = this->scan_result_[0].get_ssid(); + } else if (const WiFiAP *config = this->get_selected_sta_()) { + ssid = config->get_ssid(); + } + + // Only decrease priority on the last attempt for this phase + // This prevents false positives from transient WiFi stack issues + uint8_t max_retries = get_max_retries_for_phase(this->retry_phase_); + bool is_last_attempt = (this->num_retried_ + 1 >= max_retries); + + // Decrease priority only on last attempt to avoid false positives from transient failures + int8_t old_priority = this->get_sta_priority(failed_bssid.value()); + int8_t new_priority = old_priority; + + if (is_last_attempt) { + // Decrease priority, but clamp to int8_t::min to prevent overflow + new_priority = + (old_priority > std::numeric_limits::min()) ? (old_priority - 1) : std::numeric_limits::min(); + this->set_sta_priority(failed_bssid.value(), new_priority); + } + char bssid_s[18]; + format_mac_addr_upper(failed_bssid.value().data(), bssid_s); + ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d", ssid.c_str(), bssid_s, + old_priority, new_priority); + + // After adjusting priority, check if all priorities are now at minimum + // If so, clear the vector to save memory and reset for fresh start + this->clear_priorities_if_all_min_(); } + +/// Handle target advancement or retry counter increment when staying in the same phase +/// This function is called when a connection attempt fails and determine_next_phase_() indicates +/// we should stay in the current phase. It decides whether to: +/// - Advance to the next target (AP in fast_connect, SSID in hidden mode) +/// - Or increment the retry counter to try the same target again +/// +/// Phase-specific behavior: +/// - FAST_CONNECT_CYCLING_APS: Always advance to next AP (no retries per AP) +/// - RETRY_HIDDEN: Advance to next SSID after exhausting retries on current SSID +/// - Other phases: Increment retry counter (will retry same target) +void WiFiComponent::advance_to_next_target_or_increment_retry_() { + WiFiRetryPhase current_phase = this->retry_phase_; + + // Check if we need to advance to next AP/SSID within the same phase +#ifdef USE_WIFI_FAST_CONNECT + if (current_phase == WiFiRetryPhase::FAST_CONNECT_CYCLING_APS) { + // Fast connect: always advance to next AP (no retries per AP) + this->selected_sta_index_++; + this->num_retried_ = 0; + ESP_LOGD(TAG, "Next AP in %s", LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_))); + return; + } +#endif + + if (current_phase == WiFiRetryPhase::EXPLICIT_HIDDEN && this->num_retried_ + 1 >= WIFI_RETRY_COUNT_PER_SSID) { + // Explicit hidden: exhausted retries on current SSID, find next explicitly hidden network + // Stop when we reach a visible network (proceed to scanning) + size_t next_index = this->selected_sta_index_ + 1; + if (next_index < this->sta_.size() && this->sta_[next_index].get_hidden()) { + this->selected_sta_index_ = static_cast(next_index); + this->num_retried_ = 0; + ESP_LOGD(TAG, "Next explicit hidden network at index %d", static_cast(next_index)); + return; + } + // No more consecutive explicit hidden networks found - fall through to trigger phase change + } + + if (current_phase == WiFiRetryPhase::RETRY_HIDDEN && this->num_retried_ + 1 >= WIFI_RETRY_COUNT_PER_SSID) { + // Hidden mode: exhausted retries on current SSID, find next potentially hidden SSID + // If first network is marked hidden, we went through EXPLICIT_HIDDEN phase + // In that case, skip networks marked hidden:true (already tried) + // Otherwise, include them (they haven't been tried yet) + int8_t next_index = this->find_next_hidden_sta_(this->selected_sta_index_); + if (next_index != -1) { + // Found another potentially hidden SSID + this->selected_sta_index_ = next_index; + this->num_retried_ = 0; + return; + } + // No more potentially hidden SSIDs - set selected_sta_index_ to -1 to trigger phase change + // This ensures determine_next_phase_() will skip the RETRY_HIDDEN logic and transition out + this->selected_sta_index_ = -1; + // Return early - phase change will happen on next wifi_loop() iteration + return; + } + + // Don't increment retry counter if we're in a scan phase with no valid targets + if (this->needs_scan_results_()) { + return; + } + + // Increment retry counter to try the same target again + this->num_retried_++; + ESP_LOGD(TAG, "Retry attempt %u/%u in phase %s", this->num_retried_ + 1, + get_max_retries_for_phase(this->retry_phase_), LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_))); +} + +void WiFiComponent::retry_connect() { + this->log_and_adjust_priority_for_failed_connect_(); + + // Determine next retry phase based on current state + WiFiRetryPhase current_phase = this->retry_phase_; + WiFiRetryPhase next_phase = this->determine_next_phase_(); + + // Handle phase transitions (transition_to_phase_ handles same-phase no-op internally) + if (this->transition_to_phase_(next_phase)) { + return; // Scan started or adapter restarted (which sets its own state) + } + + if (next_phase == current_phase) { + this->advance_to_next_target_or_increment_retry_(); + } + + this->error_from_callback_ = false; + + yield(); + // Check if we have a valid target before building params + // After exhausting all networks in a phase, selected_sta_index_ may be -1 + // In that case, skip connection and let next wifi_loop() handle phase transition + if (this->selected_sta_index_ >= 0) { + WiFiAP params = this->build_params_for_current_phase_(); + this->start_connecting(params); + } +} + void WiFiComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; } bool WiFiComponent::is_connected() { return this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTED && this->wifi_sta_connect_status_() == WiFiSTAConnectStatus::CONNECTED && !this->error_from_callback_; } -void WiFiComponent::set_power_save_mode(WiFiPowerSaveMode power_save) { this->power_save_ = power_save; } +void WiFiComponent::set_power_save_mode(WiFiPowerSaveMode power_save) { + this->power_save_ = power_save; +#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) + this->configured_power_save_ = power_save; +#endif +} void WiFiComponent::set_passive_scan(bool passive) { this->passive_scan_ = passive; } @@ -810,16 +1641,62 @@ bool WiFiComponent::is_esp32_improv_active_() { #endif } -bool WiFiComponent::load_fast_connect_settings_() { +#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) +bool WiFiComponent::request_high_performance() { + // Already configured for high performance - request satisfied + if (this->configured_power_save_ == WIFI_POWER_SAVE_NONE) { + return true; + } + + // Semaphore initialization failed + if (this->high_performance_semaphore_ == nullptr) { + return false; + } + + // Give the semaphore (non-blocking). This increments the count. + return xSemaphoreGive(this->high_performance_semaphore_) == pdTRUE; +} + +bool WiFiComponent::release_high_performance() { + // Already configured for high performance - nothing to release + if (this->configured_power_save_ == WIFI_POWER_SAVE_NONE) { + return true; + } + + // Semaphore initialization failed + if (this->high_performance_semaphore_ == nullptr) { + return false; + } + + // Take the semaphore (non-blocking). This decrements the count. + return xSemaphoreTake(this->high_performance_semaphore_, 0) == pdTRUE; +} +#endif // USE_ESP32 && USE_WIFI_RUNTIME_POWER_SAVE + +#ifdef USE_WIFI_FAST_CONNECT +bool WiFiComponent::load_fast_connect_settings_(WiFiAP ¶ms) { SavedWifiFastConnectSettings fast_connect_save{}; if (this->fast_connect_pref_.load(&fast_connect_save)) { + // Validate saved AP index + if (fast_connect_save.ap_index < 0 || static_cast(fast_connect_save.ap_index) >= this->sta_.size()) { + ESP_LOGW(TAG, "AP index out of bounds"); + return false; + } + + // Set selected index for future operations (save, retry, etc) + this->selected_sta_index_ = fast_connect_save.ap_index; + + // Copy entire config, then override with fast connect data + params = this->sta_[fast_connect_save.ap_index]; + + // Override with saved BSSID/channel from fast connect (SSID/password/etc already copied from config) bssid_t bssid{}; std::copy(fast_connect_save.bssid, fast_connect_save.bssid + 6, bssid.begin()); - this->ap_index_ = fast_connect_save.ap_index; - this->selected_ap_ = this->sta_[this->ap_index_]; - this->selected_ap_.set_bssid(bssid); - this->selected_ap_.set_channel(fast_connect_save.channel); + params.set_bssid(bssid); + params.set_channel(fast_connect_save.channel); + // Fast connect uses specific BSSID+channel, not hidden network probe (even if config has hidden: true) + params.set_hidden(false); ESP_LOGD(TAG, "Loaded fast_connect settings"); return true; @@ -831,19 +1708,27 @@ bool WiFiComponent::load_fast_connect_settings_() { void WiFiComponent::save_fast_connect_settings_() { bssid_t bssid = wifi_bssid(); uint8_t channel = get_wifi_channel(); + // selected_sta_index_ is always valid here (called only after successful connection) + // Fallback to 0 is defensive programming for robustness + int8_t ap_index = this->selected_sta_index_ >= 0 ? this->selected_sta_index_ : 0; - if (bssid != this->selected_ap_.get_bssid() || channel != this->selected_ap_.get_channel()) { - SavedWifiFastConnectSettings fast_connect_save{}; - - memcpy(fast_connect_save.bssid, bssid.data(), 6); - fast_connect_save.channel = channel; - fast_connect_save.ap_index = this->ap_index_; - - this->fast_connect_pref_.save(&fast_connect_save); - - ESP_LOGD(TAG, "Saved fast_connect settings"); + // Skip save if settings haven't changed (compare with previously saved settings to reduce flash wear) + SavedWifiFastConnectSettings previous_save{}; + if (this->fast_connect_pref_.load(&previous_save) && memcmp(previous_save.bssid, bssid.data(), 6) == 0 && + previous_save.channel == channel && previous_save.ap_index == ap_index) { + return; // No change, nothing to save } + + SavedWifiFastConnectSettings fast_connect_save{}; + memcpy(fast_connect_save.bssid, bssid.data(), 6); + fast_connect_save.channel = channel; + fast_connect_save.ap_index = ap_index; + + this->fast_connect_pref_.save(&fast_connect_save); + + ESP_LOGD(TAG, "Saved fast_connect settings"); } +#endif void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = ssid; } void WiFiAP::set_bssid(bssid_t bssid) { this->bssid_ = bssid; } @@ -853,7 +1738,9 @@ void WiFiAP::set_password(const std::string &password) { this->password_ = passw void WiFiAP::set_eap(optional eap_auth) { this->eap_ = std::move(eap_auth); } #endif void WiFiAP::set_channel(optional channel) { this->channel_ = channel; } +#ifdef USE_WIFI_MANUAL_IP void WiFiAP::set_manual_ip(optional manual_ip) { this->manual_ip_ = manual_ip; } +#endif void WiFiAP::set_hidden(bool hidden) { this->hidden_ = hidden; } const std::string &WiFiAP::get_ssid() const { return this->ssid_; } const optional &WiFiAP::get_bssid() const { return this->bssid_; } @@ -862,18 +1749,20 @@ const std::string &WiFiAP::get_password() const { return this->password_; } const optional &WiFiAP::get_eap() const { return this->eap_; } #endif const optional &WiFiAP::get_channel() const { return this->channel_; } +#ifdef USE_WIFI_MANUAL_IP const optional &WiFiAP::get_manual_ip() const { return this->manual_ip_; } +#endif bool WiFiAP::get_hidden() const { return this->hidden_; } WiFiScanResult::WiFiScanResult(const bssid_t &bssid, std::string ssid, uint8_t channel, int8_t rssi, bool with_auth, bool is_hidden) : bssid_(bssid), - ssid_(std::move(ssid)), channel_(channel), rssi_(rssi), + ssid_(std::move(ssid)), with_auth_(with_auth), is_hidden_(is_hidden) {} -bool WiFiScanResult::matches(const WiFiAP &config) { +bool WiFiScanResult::matches(const WiFiAP &config) const { if (config.get_hidden()) { // User configured a hidden network, only match actually hidden networks // don't match SSID @@ -923,6 +1812,5 @@ bool WiFiScanResult::operator==(const WiFiScanResult &rhs) const { return this-> WiFiComponent *global_wifi_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -} // namespace wifi -} // namespace esphome +} // namespace esphome::wifi #endif diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index bbe1bbb874..b6b956a12d 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -20,7 +20,7 @@ #include #endif -#if defined(USE_ESP_IDF) && defined(USE_WIFI_WPA2_EAP) +#if defined(USE_ESP32) && defined(USE_WIFI_WPA2_EAP) #if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1) #include #else @@ -49,8 +49,15 @@ extern "C" { #include #endif -namespace esphome { -namespace wifi { +#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) +#include +#include +#endif + +namespace esphome::wifi { + +/// Sentinel value for RSSI when WiFi is not connected +static constexpr int8_t WIFI_RSSI_DISCONNECTED = -127; struct SavedWifiSettings { char ssid[33]; @@ -74,12 +81,6 @@ enum WiFiComponentState : uint8_t { WIFI_COMPONENT_STATE_STA_SCANNING, /** WiFi is in STA(+AP) mode and currently connecting to an AP. */ WIFI_COMPONENT_STATE_STA_CONNECTING, - /** WiFi is in STA(+AP) mode and currently connecting to an AP a second time. - * - * This is required because for some reason ESPs don't like to connect to WiFi APs directly after - * a scan. - * */ - WIFI_COMPONENT_STATE_STA_CONNECTING_2, /** WiFi is in STA(+AP) mode and successfully connected. */ WIFI_COMPONENT_STATE_STA_CONNECTED, /** WiFi is in AP-only mode and internal AP is already enabled. */ @@ -94,6 +95,24 @@ enum class WiFiSTAConnectStatus : int { ERROR_CONNECT_FAILED, }; +/// Tracks the current retry strategy/phase for WiFi connection attempts +enum class WiFiRetryPhase : uint8_t { + /// Initial connection attempt (varies based on fast_connect setting) + INITIAL_CONNECT, +#ifdef USE_WIFI_FAST_CONNECT + /// Fast connect mode: cycling through configured APs (config-only, no scan) + FAST_CONNECT_CYCLING_APS, +#endif + /// Explicitly hidden networks (user marked as hidden, try before scanning) + EXPLICIT_HIDDEN, + /// Scan-based: connecting to best AP from scan results + SCAN_CONNECTING, + /// Retry networks not found in scan (might be hidden) + RETRY_HIDDEN, + /// Restarting WiFi adapter to clear stuck state + RESTARTING_ADAPTER, +}; + /// Struct for setting static IPs in WiFiComponent. struct ManualIP { network::IPAddress static_ip; @@ -113,7 +132,7 @@ struct EAPAuth { const char *client_cert; const char *client_key; // used for EAP-TTLS -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 esp_eap_ttls_phase2_types ttls_phase_2; #endif }; @@ -121,6 +140,14 @@ struct EAPAuth { using bssid_t = std::array; +// Use std::vector for RP2040 since scan count is unknown (callback-based) +// Use FixedVector for other platforms where count is queried first +#ifdef USE_RP2040 +template using wifi_scan_vector_t = std::vector; +#else +template using wifi_scan_vector_t = FixedVector; +#endif + class WiFiAP { public: void set_ssid(const std::string &ssid); @@ -131,8 +158,10 @@ class WiFiAP { void set_eap(optional eap_auth); #endif // USE_WIFI_WPA2_EAP void set_channel(optional channel); - void set_priority(float priority) { priority_ = priority; } + void set_priority(int8_t priority) { priority_ = priority; } +#ifdef USE_WIFI_MANUAL_IP void set_manual_ip(optional manual_ip); +#endif void set_hidden(bool hidden); const std::string &get_ssid() const; const optional &get_bssid() const; @@ -141,8 +170,10 @@ class WiFiAP { const optional &get_eap() const; #endif // USE_WIFI_WPA2_EAP const optional &get_channel() const; - float get_priority() const { return priority_; } + int8_t get_priority() const { return priority_; } +#ifdef USE_WIFI_MANUAL_IP const optional &get_manual_ip() const; +#endif bool get_hidden() const; protected: @@ -152,9 +183,11 @@ class WiFiAP { #ifdef USE_WIFI_WPA2_EAP optional eap_; #endif // USE_WIFI_WPA2_EAP +#ifdef USE_WIFI_MANUAL_IP optional manual_ip_; - float priority_{0}; +#endif optional channel_; + int8_t priority_{0}; bool hidden_{false}; }; @@ -162,7 +195,7 @@ class WiFiScanResult { public: WiFiScanResult(const bssid_t &bssid, std::string ssid, uint8_t channel, int8_t rssi, bool with_auth, bool is_hidden); - bool matches(const WiFiAP &config); + bool matches(const WiFiAP &config) const; bool get_matches() const; void set_matches(bool matches); @@ -172,17 +205,17 @@ class WiFiScanResult { int8_t get_rssi() const; bool get_with_auth() const; bool get_is_hidden() const; - float get_priority() const { return priority_; } - void set_priority(float priority) { priority_ = priority; } + int8_t get_priority() const { return priority_; } + void set_priority(int8_t priority) { priority_ = priority; } bool operator==(const WiFiScanResult &rhs) const; protected: bssid_t bssid_; - std::string ssid_; - float priority_{0.0f}; uint8_t channel_; int8_t rssi_; + std::string ssid_; + int8_t priority_{0}; bool matches_{false}; bool with_auth_; bool is_hidden_; @@ -190,7 +223,7 @@ class WiFiScanResult { struct WiFiSTAPriority { bssid_t bssid; - float priority; + int8_t priority; }; enum WiFiPowerSaveMode : uint8_t { @@ -199,7 +232,13 @@ enum WiFiPowerSaveMode : uint8_t { WIFI_POWER_SAVE_HIGH, }; -#ifdef USE_ESP_IDF +enum WifiMinAuthMode : uint8_t { + WIFI_MIN_AUTH_MODE_WPA = 0, + WIFI_MIN_AUTH_MODE_WPA2, + WIFI_MIN_AUTH_MODE_WPA3, +}; + +#ifdef USE_ESP32 struct IDFWiFiEvent; #endif @@ -210,9 +249,14 @@ class WiFiComponent : public Component { WiFiComponent(); void set_sta(const WiFiAP &ap); - WiFiAP get_sta() { return this->selected_ap_; } + // Returns a copy of the currently selected AP configuration + WiFiAP get_sta() const; + void init_sta(size_t count); void add_sta(const WiFiAP &ap); - void clear_sta(); + void clear_sta() { + this->sta_.clear(); + this->selected_sta_index_ = -1; + } #ifdef USE_WIFI_AP /** Setup an Access Point that should be created if no connection to a station can be made. @@ -224,6 +268,7 @@ class WiFiComponent : public Component { */ void set_ap(const WiFiAP &ap); WiFiAP get_ap() { return this->ap_; } + void set_ap_timeout(uint32_t ap_timeout) { ap_timeout_ = ap_timeout; } #endif // USE_WIFI_AP void enable(); @@ -231,26 +276,26 @@ class WiFiComponent : public Component { bool is_disabled(); void start_scanning(); void check_scanning_finished(); - void start_connecting(const WiFiAP &ap, bool two); - void set_fast_connect(bool fast_connect); - void set_ap_timeout(uint32_t ap_timeout) { ap_timeout_ = ap_timeout; } + void start_connecting(const WiFiAP &ap); + // Backward compatibility overload - ignores 'two' parameter + void start_connecting(const WiFiAP &ap, bool /* two */) { this->start_connecting(ap); } void check_connecting_finished(); void retry_connect(); - bool can_proceed() override; - void set_reboot_timeout(uint32_t reboot_timeout); bool is_connected(); void set_power_save_mode(WiFiPowerSaveMode power_save); + void set_min_auth_mode(WifiMinAuthMode min_auth_mode) { min_auth_mode_ = min_auth_mode; } void set_output_power(float output_power) { output_power_ = output_power; } void set_passive_scan(bool passive); void save_wifi_sta(const std::string &ssid, const std::string &password); + // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) /// Setup WiFi interface. @@ -267,6 +312,7 @@ class WiFiComponent : public Component { bool has_sta() const; bool has_ap() const; + bool is_ap_active() const; #ifdef USE_WIFI_11KV_SUPPORT void set_btm(bool btm); @@ -275,10 +321,10 @@ class WiFiComponent : public Component { network::IPAddress get_dns_address(int num); network::IPAddresses get_ip_addresses(); - std::string get_use_address() const; - void set_use_address(const std::string &use_address); + const char *get_use_address() const; + void set_use_address(const char *use_address); - const std::vector &get_scan_result() const { return scan_result_; } + const wifi_scan_vector_t &get_scan_result() const { return scan_result_; } network::IPAddress wifi_soft_ap_ip(); @@ -289,14 +335,14 @@ class WiFiComponent : public Component { } return false; } - float get_sta_priority(const bssid_t bssid) { + int8_t get_sta_priority(const bssid_t bssid) { for (auto &it : this->sta_priorities_) { if (it.bssid == bssid) return it.priority; } - return 0.0f; + return 0; } - void set_sta_priority(const bssid_t bssid, float priority) { + void set_sta_priority(const bssid_t bssid, int8_t priority) { for (auto &it : this->sta_priorities_) { if (it.bssid == bssid) { it.priority = priority; @@ -316,25 +362,134 @@ class WiFiComponent : public Component { int8_t wifi_rssi(); void set_enable_on_boot(bool enable_on_boot) { this->enable_on_boot_ = enable_on_boot; } + void set_keep_scan_results(bool keep_scan_results) { this->keep_scan_results_ = keep_scan_results; } Trigger<> *get_connect_trigger() const { return this->connect_trigger_; }; Trigger<> *get_disconnect_trigger() const { return this->disconnect_trigger_; }; int32_t get_wifi_channel(); +#ifdef USE_WIFI_CALLBACKS + /// Add a callback that will be called on configuration changes (IP change, SSID change, etc.) + /// @param callback The callback to be called; template arguments are: + /// - IP addresses + /// - DNS address 1 + /// - DNS address 2 + void add_on_ip_state_callback( + std::function &&callback) { + this->ip_state_callback_.add(std::move(callback)); + } + /// - Wi-Fi scan results + void add_on_wifi_scan_state_callback(std::function &)> &&callback) { + this->wifi_scan_state_callback_.add(std::move(callback)); + } + /// - Wi-Fi SSID + /// - Wi-Fi BSSID + void add_on_wifi_connect_state_callback(std::function &&callback) { + this->wifi_connect_state_callback_.add(std::move(callback)); + } +#endif // USE_WIFI_CALLBACKS + +#ifdef USE_WIFI_RUNTIME_POWER_SAVE + /** Request high-performance mode (no power saving) for improved WiFi latency. + * + * Components that need maximum WiFi performance (e.g., audio streaming, large data transfers) + * can call this method to temporarily disable WiFi power saving. Multiple components can + * request high performance simultaneously using a counting semaphore. + * + * Power saving will be restored to the YAML-configured mode when all components have + * called release_high_performance(). + * + * Note: Only supported on ESP32. + * + * @return true if request was satisfied (high-performance mode active or already configured), + * false if operation failed (semaphore error) + */ + bool request_high_performance(); + + /** Release a high-performance mode request. + * + * Should be called when a component no longer needs maximum WiFi latency. + * When all requests are released (semaphore count reaches zero), WiFi power saving + * is restored to the YAML-configured mode. + * + * Note: Only supported on ESP32. + * + * @return true if release was successful (or already in high-performance config), + * false if operation failed (semaphore error) + */ + bool release_high_performance(); +#endif // USE_WIFI_RUNTIME_POWER_SAVE + protected: #ifdef USE_WIFI_AP void setup_ap_config_(); #endif // USE_WIFI_AP void print_connect_params_(); + WiFiAP build_params_for_current_phase_(); + + /// Determine next retry phase based on current state and failure conditions + WiFiRetryPhase determine_next_phase_(); + /// Transition to a new retry phase with logging + /// Returns true if a scan was started (caller should wait), false otherwise + bool transition_to_phase_(WiFiRetryPhase new_phase); + /// Check if we need valid scan results for the current phase but don't have any + /// Returns true if the phase requires scan results but they're missing or don't match + bool needs_scan_results_() const; + /// Check if we went through EXPLICIT_HIDDEN phase (first network is marked hidden) + /// Used in RETRY_HIDDEN to determine whether to skip explicitly hidden networks + bool went_through_explicit_hidden_phase_() const; + /// Find the index of the first non-hidden network + /// Returns where EXPLICIT_HIDDEN phase would have stopped, or -1 if all networks are hidden + int8_t find_first_non_hidden_index_() const; + /// Check if an SSID was seen in the most recent scan results + /// Used to skip hidden mode for SSIDs we know are visible + bool ssid_was_seen_in_scan_(const std::string &ssid) const; + /// Find next SSID that wasn't in scan results (might be hidden) + /// Returns index of next potentially hidden SSID, or -1 if none found + /// @param start_index Start searching from index after this (-1 to start from beginning) + int8_t find_next_hidden_sta_(int8_t start_index); + /// Log failed connection and decrease BSSID priority to avoid repeated attempts + void log_and_adjust_priority_for_failed_connect_(); + /// Clear BSSID priority tracking if all priorities are at minimum (saves memory) + void clear_priorities_if_all_min_(); + /// Advance to next target (AP/SSID) within current phase, or increment retry counter + /// Called when staying in the same phase after a failed connection attempt + void advance_to_next_target_or_increment_retry_(); + /// Start initial connection - either scan or connect directly to hidden networks + void start_initial_connection_(); + const WiFiAP *get_selected_sta_() const { + if (this->selected_sta_index_ >= 0 && static_cast(this->selected_sta_index_) < this->sta_.size()) { + return &this->sta_[this->selected_sta_index_]; + } + return nullptr; + } + + void reset_selected_ap_to_first_if_invalid_() { + if (this->selected_sta_index_ < 0 || static_cast(this->selected_sta_index_) >= this->sta_.size()) { + this->selected_sta_index_ = this->sta_.empty() ? -1 : 0; + } + } + + bool all_networks_hidden_() const { + if (this->sta_.empty()) + return false; + for (const auto &ap : this->sta_) { + if (!ap.get_hidden()) + return false; + } + return true; + } + + void connect_soon_(); void wifi_loop_(); bool wifi_mode_(optional sta, optional ap); bool wifi_sta_pre_setup_(); bool wifi_apply_output_power_(float output_power); bool wifi_apply_power_save_(); - bool wifi_sta_ip_config_(optional manual_ip); + bool wifi_sta_ip_config_(const optional &manual_ip); bool wifi_apply_hostname_(); bool wifi_sta_connect_(const WiFiAP &ap); void wifi_pre_setup_(); @@ -342,7 +497,7 @@ class WiFiComponent : public Component { bool wifi_scan_start_(bool passive); #ifdef USE_WIFI_AP - bool wifi_ap_ip_config_(optional manual_ip); + bool wifi_ap_ip_config_(const optional &manual_ip); bool wifi_start_ap_(const WiFiAP &ap); #endif // USE_WIFI_AP @@ -355,8 +510,10 @@ class WiFiComponent : public Component { bool is_captive_portal_active_(); bool is_esp32_improv_active_(); - bool load_fast_connect_settings_(); +#ifdef USE_WIFI_FAST_CONNECT + bool load_fast_connect_settings_(WiFiAP ¶ms); void save_fast_connect_settings_(); +#endif #ifdef USE_ESP8266 static void wifi_event_callback(System_Event_t *event); @@ -368,7 +525,7 @@ class WiFiComponent : public Component { void wifi_event_callback_(arduino_event_id_t event, arduino_event_info_t info); void wifi_scan_done_callback_(); #endif -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 void wifi_process_event_(IDFWiFiEvent *data); #endif @@ -382,35 +539,47 @@ class WiFiComponent : public Component { void wifi_scan_done_callback_(); #endif - std::string use_address_; - std::vector sta_; + FixedVector sta_; std::vector sta_priorities_; - std::vector scan_result_; - WiFiAP selected_ap_; + wifi_scan_vector_t scan_result_; +#ifdef USE_WIFI_AP WiFiAP ap_; +#endif optional output_power_; +#ifdef USE_WIFI_CALLBACKS + CallbackManager ip_state_callback_; + CallbackManager &)> wifi_scan_state_callback_; + CallbackManager wifi_connect_state_callback_; +#endif // USE_WIFI_CALLBACKS ESPPreferenceObject pref_; +#ifdef USE_WIFI_FAST_CONNECT ESPPreferenceObject fast_connect_pref_; +#endif // Group all 32-bit integers together uint32_t action_started_; uint32_t last_connected_{0}; uint32_t reboot_timeout_{}; +#ifdef USE_WIFI_AP uint32_t ap_timeout_{}; +#endif // Group all 8-bit values together WiFiComponentState state_{WIFI_COMPONENT_STATE_OFF}; WiFiPowerSaveMode power_save_{WIFI_POWER_SAVE_NONE}; + WifiMinAuthMode min_auth_mode_{WIFI_MIN_AUTH_MODE_WPA2}; + WiFiRetryPhase retry_phase_{WiFiRetryPhase::INITIAL_CONNECT}; uint8_t num_retried_{0}; - uint8_t ap_index_{0}; + // Index into sta_ array for the currently selected AP configuration (-1 = none selected) + // Used to access password, manual_ip, priority, EAP settings, and hidden flag + // int8_t limits to 127 APs (enforced in __init__.py via MAX_WIFI_NETWORKS) + int8_t selected_sta_index_{-1}; + #if USE_NETWORK_IPV6 uint8_t num_ipv6_addresses_{0}; #endif /* USE_NETWORK_IPV6 */ // Group all boolean values together - bool fast_connect_{false}; - bool trying_loaded_ap_{false}; - bool retry_hidden_{false}; bool has_ap_{false}; bool handled_connected_state_{false}; bool error_from_callback_{false}; @@ -422,117 +591,29 @@ class WiFiComponent : public Component { bool btm_{false}; bool rrm_{false}; #endif - bool enable_on_boot_; + bool enable_on_boot_{true}; bool got_ipv4_address_{false}; + bool keep_scan_results_{false}; + bool did_scan_this_cycle_{false}; + bool skip_cooldown_next_cycle_{false}; +#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) + WiFiPowerSaveMode configured_power_save_{WIFI_POWER_SAVE_NONE}; + bool is_high_performance_mode_{false}; + + SemaphoreHandle_t high_performance_semaphore_{nullptr}; +#endif // Pointers at the end (naturally aligned) Trigger<> *connect_trigger_{new Trigger<>()}; Trigger<> *disconnect_trigger_{new Trigger<>()}; + + private: + // Stores a pointer to a string literal (static storage duration). + // ONLY set from Python-generated code with string literals - never dynamic strings. + const char *use_address_{""}; }; extern WiFiComponent *global_wifi_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -template class WiFiConnectedCondition : public Condition { - public: - bool check(Ts... x) override { return global_wifi_component->is_connected(); } -}; - -template class WiFiEnabledCondition : public Condition { - public: - bool check(Ts... x) override { return !global_wifi_component->is_disabled(); } -}; - -template class WiFiEnableAction : public Action { - public: - void play(Ts... x) override { global_wifi_component->enable(); } -}; - -template class WiFiDisableAction : public Action { - public: - void play(Ts... x) override { global_wifi_component->disable(); } -}; - -template class WiFiConfigureAction : public Action, public Component { - public: - TEMPLATABLE_VALUE(std::string, ssid) - TEMPLATABLE_VALUE(std::string, password) - TEMPLATABLE_VALUE(bool, save) - TEMPLATABLE_VALUE(uint32_t, connection_timeout) - - void play(Ts... x) override { - auto ssid = this->ssid_.value(x...); - auto password = this->password_.value(x...); - // Avoid multiple calls - if (this->connecting_) - return; - // If already connected to the same AP, do nothing - if (global_wifi_component->wifi_ssid() == ssid) { - // Callback to notify the user that the connection was successful - this->connect_trigger_->trigger(); - return; - } - // Create a new WiFiAP object with the new SSID and password - this->new_sta_.set_ssid(ssid); - this->new_sta_.set_password(password); - // Save the current STA - this->old_sta_ = global_wifi_component->get_sta(); - // Disable WiFi - global_wifi_component->disable(); - // Set the state to connecting - this->connecting_ = true; - // Store the new STA so once the WiFi is enabled, it will connect to it - // This is necessary because the WiFiComponent will raise an error and fallback to the saved STA - // if trying to connect to a new STA while already connected to another one - if (this->save_.value(x...)) { - global_wifi_component->save_wifi_sta(new_sta_.get_ssid(), new_sta_.get_password()); - } else { - global_wifi_component->set_sta(new_sta_); - } - // Enable WiFi - global_wifi_component->enable(); - // Set timeout for the connection - this->set_timeout("wifi-connect-timeout", this->connection_timeout_.value(x...), [this, x...]() { - // If the timeout is reached, stop connecting and revert to the old AP - global_wifi_component->disable(); - global_wifi_component->save_wifi_sta(old_sta_.get_ssid(), old_sta_.get_password()); - global_wifi_component->enable(); - // Start a timeout for the fallback if the connection to the old AP fails - this->set_timeout("wifi-fallback-timeout", this->connection_timeout_.value(x...), [this]() { - this->connecting_ = false; - this->error_trigger_->trigger(); - }); - }); - } - - Trigger<> *get_connect_trigger() const { return this->connect_trigger_; } - Trigger<> *get_error_trigger() const { return this->error_trigger_; } - - void loop() override { - if (!this->connecting_) - return; - if (global_wifi_component->is_connected()) { - // The WiFi is connected, stop the timeout and reset the connecting flag - this->cancel_timeout("wifi-connect-timeout"); - this->cancel_timeout("wifi-fallback-timeout"); - this->connecting_ = false; - if (global_wifi_component->wifi_ssid() == this->new_sta_.get_ssid()) { - // Callback to notify the user that the connection was successful - this->connect_trigger_->trigger(); - } else { - // Callback to notify the user that the connection failed - this->error_trigger_->trigger(); - } - } - } - - protected: - bool connecting_{false}; - WiFiAP new_sta_; - WiFiAP old_sta_; - Trigger<> *connect_trigger_{new Trigger<>()}; - Trigger<> *error_trigger_{new Trigger<>()}; -}; - -} // namespace wifi -} // namespace esphome +} // namespace esphome::wifi #endif diff --git a/esphome/components/wifi/wifi_component_esp32_arduino.cpp b/esphome/components/wifi/wifi_component_esp32_arduino.cpp deleted file mode 100644 index 67b1f565ff..0000000000 --- a/esphome/components/wifi/wifi_component_esp32_arduino.cpp +++ /dev/null @@ -1,866 +0,0 @@ -#include "wifi_component.h" - -#ifdef USE_WIFI -#ifdef USE_ESP32_FRAMEWORK_ARDUINO - -#include -#include - -#include -#include -#ifdef USE_WIFI_WPA2_EAP -#include -#endif - -#ifdef USE_WIFI_AP -#include "dhcpserver/dhcpserver.h" -#endif // USE_WIFI_AP - -#include "lwip/apps/sntp.h" -#include "lwip/dns.h" -#include "lwip/err.h" - -#include "esphome/core/application.h" -#include "esphome/core/hal.h" -#include "esphome/core/helpers.h" -#include "esphome/core/log.h" -#include "esphome/core/util.h" - -namespace esphome { -namespace wifi { - -static const char *const TAG = "wifi_esp32"; - -static esp_netif_t *s_sta_netif = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -#ifdef USE_WIFI_AP -static esp_netif_t *s_ap_netif = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -#endif // USE_WIFI_AP - -static bool s_sta_connecting = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - -void WiFiComponent::wifi_pre_setup_() { - uint8_t mac[6]; - if (has_custom_mac_address()) { - get_mac_address_raw(mac); - set_mac_address(mac); - } - auto f = std::bind(&WiFiComponent::wifi_event_callback_, this, std::placeholders::_1, std::placeholders::_2); - WiFi.onEvent(f); - WiFi.persistent(false); - // Make sure WiFi is in clean state before anything starts - this->wifi_mode_(false, false); -} - -bool WiFiComponent::wifi_mode_(optional sta, optional ap) { - wifi_mode_t current_mode = WiFiClass::getMode(); - bool current_sta = current_mode == WIFI_MODE_STA || current_mode == WIFI_MODE_APSTA; - bool current_ap = current_mode == WIFI_MODE_AP || current_mode == WIFI_MODE_APSTA; - - bool set_sta = sta.value_or(current_sta); - bool set_ap = ap.value_or(current_ap); - - wifi_mode_t set_mode; - if (set_sta && set_ap) { - set_mode = WIFI_MODE_APSTA; - } else if (set_sta && !set_ap) { - set_mode = WIFI_MODE_STA; - } else if (!set_sta && set_ap) { - set_mode = WIFI_MODE_AP; - } else { - set_mode = WIFI_MODE_NULL; - } - - if (current_mode == set_mode) - return true; - - if (set_sta && !current_sta) { - ESP_LOGV(TAG, "Enabling STA"); - } else if (!set_sta && current_sta) { - ESP_LOGV(TAG, "Disabling STA"); - } - if (set_ap && !current_ap) { - ESP_LOGV(TAG, "Enabling AP"); - } else if (!set_ap && current_ap) { - ESP_LOGV(TAG, "Disabling AP"); - } - - bool ret = WiFiClass::mode(set_mode); - - if (!ret) { - ESP_LOGW(TAG, "Setting mode failed"); - return false; - } - - // WiFiClass::mode above calls esp_netif_create_default_wifi_sta() and - // esp_netif_create_default_wifi_ap(), which creates the interfaces. - // s_sta_netif handle is set during ESPHOME_EVENT_ID_WIFI_STA_START event - -#ifdef USE_WIFI_AP - if (set_ap) - s_ap_netif = esp_netif_get_handle_from_ifkey("WIFI_AP_DEF"); -#endif - - return ret; -} - -bool WiFiComponent::wifi_sta_pre_setup_() { - if (!this->wifi_mode_(true, {})) - return false; - - WiFi.setAutoReconnect(false); - delay(10); - return true; -} - -bool WiFiComponent::wifi_apply_output_power_(float output_power) { - int8_t val = static_cast(output_power * 4); - return esp_wifi_set_max_tx_power(val) == ESP_OK; -} - -bool WiFiComponent::wifi_apply_power_save_() { - wifi_ps_type_t power_save; - switch (this->power_save_) { - case WIFI_POWER_SAVE_LIGHT: - power_save = WIFI_PS_MIN_MODEM; - break; - case WIFI_POWER_SAVE_HIGH: - power_save = WIFI_PS_MAX_MODEM; - break; - case WIFI_POWER_SAVE_NONE: - default: - power_save = WIFI_PS_NONE; - break; - } - return esp_wifi_set_ps(power_save) == ESP_OK; -} - -bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { - // enable STA - if (!this->wifi_mode_(true, {})) - return false; - - // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html#_CPPv417wifi_sta_config_t - wifi_config_t conf; - memset(&conf, 0, sizeof(conf)); - if (ap.get_ssid().size() > sizeof(conf.sta.ssid)) { - ESP_LOGE(TAG, "SSID too long"); - return false; - } - if (ap.get_password().size() > sizeof(conf.sta.password)) { - ESP_LOGE(TAG, "Password too long"); - return false; - } - memcpy(reinterpret_cast(conf.sta.ssid), ap.get_ssid().c_str(), ap.get_ssid().size()); - memcpy(reinterpret_cast(conf.sta.password), ap.get_password().c_str(), ap.get_password().size()); - - // The weakest authmode to accept in the fast scan mode - if (ap.get_password().empty()) { - conf.sta.threshold.authmode = WIFI_AUTH_OPEN; - } else { - conf.sta.threshold.authmode = WIFI_AUTH_WPA_WPA2_PSK; - } - -#ifdef USE_WIFI_WPA2_EAP - if (ap.get_eap().has_value()) { - conf.sta.threshold.authmode = WIFI_AUTH_WPA2_ENTERPRISE; - } -#endif - - if (ap.get_bssid().has_value()) { - conf.sta.bssid_set = true; - memcpy(conf.sta.bssid, ap.get_bssid()->data(), 6); - } else { - conf.sta.bssid_set = false; - } - if (ap.get_channel().has_value()) { - conf.sta.channel = *ap.get_channel(); - conf.sta.scan_method = WIFI_FAST_SCAN; - } else { - conf.sta.scan_method = WIFI_ALL_CHANNEL_SCAN; - } - // Listen interval for ESP32 station to receive beacon when WIFI_PS_MAX_MODEM is set. - // Units: AP beacon intervals. Defaults to 3 if set to 0. - conf.sta.listen_interval = 0; - - // Protected Management Frame - // Device will prefer to connect in PMF mode if other device also advertises PMF capability. - conf.sta.pmf_cfg.capable = true; - conf.sta.pmf_cfg.required = false; - - // note, we do our own filtering - // The minimum rssi to accept in the fast scan mode - conf.sta.threshold.rssi = -127; - - conf.sta.threshold.authmode = WIFI_AUTH_OPEN; - - wifi_config_t current_conf; - esp_err_t err; - err = esp_wifi_get_config(WIFI_IF_STA, ¤t_conf); - if (err != ERR_OK) { - ESP_LOGW(TAG, "esp_wifi_get_config failed: %s", esp_err_to_name(err)); - // can continue - } - - if (memcmp(¤t_conf, &conf, sizeof(wifi_config_t)) != 0) { // NOLINT - err = esp_wifi_disconnect(); - if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_wifi_disconnect failed: %s", esp_err_to_name(err)); - return false; - } - } - - err = esp_wifi_set_config(WIFI_IF_STA, &conf); - if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_wifi_set_config failed: %s", esp_err_to_name(err)); - return false; - } - - if (!this->wifi_sta_ip_config_(ap.get_manual_ip())) { - return false; - } - - // setup enterprise authentication if required -#ifdef USE_WIFI_WPA2_EAP - if (ap.get_eap().has_value()) { - // note: all certificates and keys have to be null terminated. Lengths are appended by +1 to include \0. - EAPAuth eap = ap.get_eap().value(); - err = esp_eap_client_set_identity((uint8_t *) eap.identity.c_str(), eap.identity.length()); - if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_eap_client_set_identity failed: %d", err); - } - int ca_cert_len = strlen(eap.ca_cert); - int client_cert_len = strlen(eap.client_cert); - int client_key_len = strlen(eap.client_key); - if (ca_cert_len) { - err = esp_eap_client_set_ca_cert((uint8_t *) eap.ca_cert, ca_cert_len + 1); - if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_eap_client_set_ca_cert failed: %d", err); - } - } - // workout what type of EAP this is - // validation is not required as the config tool has already validated it - if (client_cert_len && client_key_len) { - // if we have certs, this must be EAP-TLS - err = esp_eap_client_set_certificate_and_key((uint8_t *) eap.client_cert, client_cert_len + 1, - (uint8_t *) eap.client_key, client_key_len + 1, - (uint8_t *) eap.password.c_str(), strlen(eap.password.c_str())); - if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_eap_client_set_certificate_and_key failed: %d", err); - } - } else { - // in the absence of certs, assume this is username/password based - err = esp_eap_client_set_username((uint8_t *) eap.username.c_str(), eap.username.length()); - if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_eap_client_set_username failed: %d", err); - } - err = esp_eap_client_set_password((uint8_t *) eap.password.c_str(), eap.password.length()); - if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_eap_client_set_password failed: %d", err); - } - } - err = esp_wifi_sta_enterprise_enable(); - if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_wifi_sta_enterprise_enable failed: %d", err); - } - } -#endif // USE_WIFI_WPA2_EAP - - this->wifi_apply_hostname_(); - - s_sta_connecting = true; - - err = esp_wifi_connect(); - if (err != ESP_OK) { - ESP_LOGW(TAG, "esp_wifi_connect failed: %s", esp_err_to_name(err)); - return false; - } - - return true; -} - -bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { - // enable STA - if (!this->wifi_mode_(true, {})) - return false; - - // Check if the STA interface is initialized before using it - if (s_sta_netif == nullptr) { - ESP_LOGW(TAG, "STA interface not initialized"); - return false; - } - - esp_netif_dhcp_status_t dhcp_status; - esp_err_t err = esp_netif_dhcpc_get_status(s_sta_netif, &dhcp_status); - if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_netif_dhcpc_get_status failed: %s", esp_err_to_name(err)); - return false; - } - - if (!manual_ip.has_value()) { - // sntp_servermode_dhcp lwip/sntp.c (Required to lock TCPIP core functionality!) - // https://github.com/esphome/issues/issues/6591 - // https://github.com/espressif/arduino-esp32/issues/10526 - { - LwIPLock lock; - // lwIP starts the SNTP client if it gets an SNTP server from DHCP. We don't need the time, and more importantly, - // the built-in SNTP client has a memory leak in certain situations. Disable this feature. - // https://github.com/esphome/issues/issues/2299 - sntp_servermode_dhcp(false); - } - - // No manual IP is set; use DHCP client - if (dhcp_status != ESP_NETIF_DHCP_STARTED) { - err = esp_netif_dhcpc_start(s_sta_netif); - if (err != ESP_OK) { - ESP_LOGV(TAG, "Starting DHCP client failed: %d", err); - } - return err == ESP_OK; - } - return true; - } - - esp_netif_ip_info_t info; // struct of ip4_addr_t with ip, netmask, gw - info.ip = manual_ip->static_ip; - info.gw = manual_ip->gateway; - info.netmask = manual_ip->subnet; - err = esp_netif_dhcpc_stop(s_sta_netif); - if (err != ESP_OK && err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STOPPED) { - ESP_LOGV(TAG, "Stopping DHCP client failed: %s", esp_err_to_name(err)); - } - - err = esp_netif_set_ip_info(s_sta_netif, &info); - if (err != ESP_OK) { - ESP_LOGV(TAG, "Setting manual IP info failed: %s", esp_err_to_name(err)); - } - - esp_netif_dns_info_t dns; - if (manual_ip->dns1.is_set()) { - dns.ip = manual_ip->dns1; - esp_netif_set_dns_info(s_sta_netif, ESP_NETIF_DNS_MAIN, &dns); - } - if (manual_ip->dns2.is_set()) { - dns.ip = manual_ip->dns2; - esp_netif_set_dns_info(s_sta_netif, ESP_NETIF_DNS_BACKUP, &dns); - } - - return true; -} - -network::IPAddresses WiFiComponent::wifi_sta_ip_addresses() { - if (!this->has_sta()) - return {}; - network::IPAddresses addresses; - esp_netif_ip_info_t ip; - esp_err_t err = esp_netif_get_ip_info(s_sta_netif, &ip); - if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_netif_get_ip_info failed: %s", esp_err_to_name(err)); - // TODO: do something smarter - // return false; - } else { - addresses[0] = network::IPAddress(&ip.ip); - } -#if USE_NETWORK_IPV6 - struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES]; - uint8_t count = 0; - count = esp_netif_get_all_ip6(s_sta_netif, if_ip6s); - assert(count <= CONFIG_LWIP_IPV6_NUM_ADDRESSES); - for (int i = 0; i < count; i++) { - addresses[i + 1] = network::IPAddress(&if_ip6s[i]); - } -#endif /* USE_NETWORK_IPV6 */ - return addresses; -} - -bool WiFiComponent::wifi_apply_hostname_() { - // setting is done in SYSTEM_EVENT_STA_START callback - return true; -} -const char *get_auth_mode_str(uint8_t mode) { - switch (mode) { - case WIFI_AUTH_OPEN: - return "OPEN"; - case WIFI_AUTH_WEP: - return "WEP"; - case WIFI_AUTH_WPA_PSK: - return "WPA PSK"; - case WIFI_AUTH_WPA2_PSK: - return "WPA2 PSK"; - case WIFI_AUTH_WPA_WPA2_PSK: - return "WPA/WPA2 PSK"; - case WIFI_AUTH_WPA2_ENTERPRISE: - return "WPA2 Enterprise"; - case WIFI_AUTH_WPA3_PSK: - return "WPA3 PSK"; - case WIFI_AUTH_WPA2_WPA3_PSK: - return "WPA2/WPA3 PSK"; - case WIFI_AUTH_WAPI_PSK: - return "WAPI PSK"; - default: - return "UNKNOWN"; - } -} - -using esphome_ip4_addr_t = esp_ip4_addr_t; - -std::string format_ip4_addr(const esphome_ip4_addr_t &ip) { - char buf[20]; - sprintf(buf, "%u.%u.%u.%u", uint8_t(ip.addr >> 0), uint8_t(ip.addr >> 8), uint8_t(ip.addr >> 16), - uint8_t(ip.addr >> 24)); - return buf; -} -const char *get_op_mode_str(uint8_t mode) { - switch (mode) { - case WIFI_OFF: - return "OFF"; - case WIFI_STA: - return "STA"; - case WIFI_AP: - return "AP"; - case WIFI_AP_STA: - return "AP+STA"; - default: - return "UNKNOWN"; - } -} -const char *get_disconnect_reason_str(uint8_t reason) { - switch (reason) { - case WIFI_REASON_AUTH_EXPIRE: - return "Auth Expired"; - case WIFI_REASON_AUTH_LEAVE: - return "Auth Leave"; - case WIFI_REASON_ASSOC_EXPIRE: - return "Association Expired"; - case WIFI_REASON_ASSOC_TOOMANY: - return "Too Many Associations"; - case WIFI_REASON_NOT_AUTHED: - return "Not Authenticated"; - case WIFI_REASON_NOT_ASSOCED: - return "Not Associated"; - case WIFI_REASON_ASSOC_LEAVE: - return "Association Leave"; - case WIFI_REASON_ASSOC_NOT_AUTHED: - return "Association not Authenticated"; - case WIFI_REASON_DISASSOC_PWRCAP_BAD: - return "Disassociate Power Cap Bad"; - case WIFI_REASON_DISASSOC_SUPCHAN_BAD: - return "Disassociate Supported Channel Bad"; - case WIFI_REASON_IE_INVALID: - return "IE Invalid"; - case WIFI_REASON_MIC_FAILURE: - return "Mic Failure"; - case WIFI_REASON_4WAY_HANDSHAKE_TIMEOUT: - return "4-Way Handshake Timeout"; - case WIFI_REASON_GROUP_KEY_UPDATE_TIMEOUT: - return "Group Key Update Timeout"; - case WIFI_REASON_IE_IN_4WAY_DIFFERS: - return "IE In 4-Way Handshake Differs"; - case WIFI_REASON_GROUP_CIPHER_INVALID: - return "Group Cipher Invalid"; - case WIFI_REASON_PAIRWISE_CIPHER_INVALID: - return "Pairwise Cipher Invalid"; - case WIFI_REASON_AKMP_INVALID: - return "AKMP Invalid"; - case WIFI_REASON_UNSUPP_RSN_IE_VERSION: - return "Unsupported RSN IE version"; - case WIFI_REASON_INVALID_RSN_IE_CAP: - return "Invalid RSN IE Cap"; - case WIFI_REASON_802_1X_AUTH_FAILED: - return "802.1x Authentication Failed"; - case WIFI_REASON_CIPHER_SUITE_REJECTED: - return "Cipher Suite Rejected"; - case WIFI_REASON_BEACON_TIMEOUT: - return "Beacon Timeout"; - case WIFI_REASON_NO_AP_FOUND: - return "AP Not Found"; - case WIFI_REASON_AUTH_FAIL: - return "Authentication Failed"; - case WIFI_REASON_ASSOC_FAIL: - return "Association Failed"; - case WIFI_REASON_HANDSHAKE_TIMEOUT: - return "Handshake Failed"; - case WIFI_REASON_CONNECTION_FAIL: - return "Connection Failed"; - case WIFI_REASON_AP_TSF_RESET: - return "AP TSF reset"; - case WIFI_REASON_ROAMING: - return "Station Roaming"; - case WIFI_REASON_ASSOC_COMEBACK_TIME_TOO_LONG: - return "Association comeback time too long"; - case WIFI_REASON_SA_QUERY_TIMEOUT: - return "SA query timeout"; - case WIFI_REASON_NO_AP_FOUND_W_COMPATIBLE_SECURITY: - return "No AP found with compatible security"; - case WIFI_REASON_NO_AP_FOUND_IN_AUTHMODE_THRESHOLD: - return "No AP found in auth mode threshold"; - case WIFI_REASON_NO_AP_FOUND_IN_RSSI_THRESHOLD: - return "No AP found in RSSI threshold"; - case WIFI_REASON_UNSPECIFIED: - default: - return "Unspecified"; - } -} - -void WiFiComponent::wifi_loop_() {} - -#define ESPHOME_EVENT_ID_WIFI_READY ARDUINO_EVENT_WIFI_READY -#define ESPHOME_EVENT_ID_WIFI_SCAN_DONE ARDUINO_EVENT_WIFI_SCAN_DONE -#define ESPHOME_EVENT_ID_WIFI_STA_START ARDUINO_EVENT_WIFI_STA_START -#define ESPHOME_EVENT_ID_WIFI_STA_STOP ARDUINO_EVENT_WIFI_STA_STOP -#define ESPHOME_EVENT_ID_WIFI_STA_CONNECTED ARDUINO_EVENT_WIFI_STA_CONNECTED -#define ESPHOME_EVENT_ID_WIFI_STA_DISCONNECTED ARDUINO_EVENT_WIFI_STA_DISCONNECTED -#define ESPHOME_EVENT_ID_WIFI_STA_AUTHMODE_CHANGE ARDUINO_EVENT_WIFI_STA_AUTHMODE_CHANGE -#define ESPHOME_EVENT_ID_WIFI_STA_GOT_IP ARDUINO_EVENT_WIFI_STA_GOT_IP -#define ESPHOME_EVENT_ID_WIFI_STA_GOT_IP6 ARDUINO_EVENT_WIFI_STA_GOT_IP6 -#define ESPHOME_EVENT_ID_WIFI_STA_LOST_IP ARDUINO_EVENT_WIFI_STA_LOST_IP -#define ESPHOME_EVENT_ID_WIFI_AP_START ARDUINO_EVENT_WIFI_AP_START -#define ESPHOME_EVENT_ID_WIFI_AP_STOP ARDUINO_EVENT_WIFI_AP_STOP -#define ESPHOME_EVENT_ID_WIFI_AP_STACONNECTED ARDUINO_EVENT_WIFI_AP_STACONNECTED -#define ESPHOME_EVENT_ID_WIFI_AP_STADISCONNECTED ARDUINO_EVENT_WIFI_AP_STADISCONNECTED -#define ESPHOME_EVENT_ID_WIFI_AP_STAIPASSIGNED ARDUINO_EVENT_WIFI_AP_STAIPASSIGNED -#define ESPHOME_EVENT_ID_WIFI_AP_PROBEREQRECVED ARDUINO_EVENT_WIFI_AP_PROBEREQRECVED -#define ESPHOME_EVENT_ID_WIFI_AP_GOT_IP6 ARDUINO_EVENT_WIFI_AP_GOT_IP6 -using esphome_wifi_event_id_t = arduino_event_id_t; -using esphome_wifi_event_info_t = arduino_event_info_t; - -void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_wifi_event_info_t info) { - switch (event) { - case ESPHOME_EVENT_ID_WIFI_READY: { - ESP_LOGV(TAG, "Ready"); - break; - } - case ESPHOME_EVENT_ID_WIFI_SCAN_DONE: { - auto it = info.wifi_scan_done; - ESP_LOGV(TAG, "Scan done: status=%u number=%u scan_id=%u", it.status, it.number, it.scan_id); - - this->wifi_scan_done_callback_(); - break; - } - case ESPHOME_EVENT_ID_WIFI_STA_START: { - ESP_LOGV(TAG, "STA start"); - // apply hostname - s_sta_netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF"); - esp_err_t err = esp_netif_set_hostname(s_sta_netif, App.get_name().c_str()); - if (err != ERR_OK) { - ESP_LOGW(TAG, "esp_netif_set_hostname failed: %s", esp_err_to_name(err)); - } - break; - } - case ESPHOME_EVENT_ID_WIFI_STA_STOP: { - ESP_LOGV(TAG, "STA stop"); - // Clear the STA interface handle to prevent use-after-free - s_sta_netif = nullptr; - break; - } - case ESPHOME_EVENT_ID_WIFI_STA_CONNECTED: { - auto it = info.wifi_sta_connected; - char buf[33]; - memcpy(buf, it.ssid, it.ssid_len); - buf[it.ssid_len] = '\0'; - ESP_LOGV(TAG, "Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf, - format_mac_address_pretty(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode)); -#if USE_NETWORK_IPV6 - this->set_timeout(100, [] { WiFi.enableIPv6(); }); -#endif /* USE_NETWORK_IPV6 */ - - break; - } - case ESPHOME_EVENT_ID_WIFI_STA_DISCONNECTED: { - auto it = info.wifi_sta_disconnected; - char buf[33]; - memcpy(buf, it.ssid, it.ssid_len); - buf[it.ssid_len] = '\0'; - if (it.reason == WIFI_REASON_NO_AP_FOUND) { - ESP_LOGW(TAG, "Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf); - } else { - ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, - format_mac_address_pretty(it.bssid).c_str(), get_disconnect_reason_str(it.reason)); - } - - uint8_t reason = it.reason; - if (reason == WIFI_REASON_AUTH_EXPIRE || reason == WIFI_REASON_BEACON_TIMEOUT || - reason == WIFI_REASON_NO_AP_FOUND || reason == WIFI_REASON_ASSOC_FAIL || - reason == WIFI_REASON_HANDSHAKE_TIMEOUT) { - err_t err = esp_wifi_disconnect(); - if (err != ESP_OK) { - ESP_LOGV(TAG, "Disconnect failed: %s", esp_err_to_name(err)); - } - this->error_from_callback_ = true; - } - - s_sta_connecting = false; - break; - } - case ESPHOME_EVENT_ID_WIFI_STA_AUTHMODE_CHANGE: { - auto it = info.wifi_sta_authmode_change; - ESP_LOGV(TAG, "Authmode Change old=%s new=%s", get_auth_mode_str(it.old_mode), get_auth_mode_str(it.new_mode)); - // Mitigate CVE-2020-12638 - // https://lbsfilm.at/blog/wpa2-authenticationmode-downgrade-in-espressif-microprocessors - if (it.old_mode != WIFI_AUTH_OPEN && it.new_mode == WIFI_AUTH_OPEN) { - ESP_LOGW(TAG, "Potential Authmode downgrade detected, disconnecting"); - // we can't call retry_connect() from this context, so disconnect immediately - // and notify main thread with error_from_callback_ - err_t err = esp_wifi_disconnect(); - if (err != ESP_OK) { - ESP_LOGW(TAG, "Disconnect failed: %s", esp_err_to_name(err)); - } - this->error_from_callback_ = true; - } - break; - } - case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP: { - auto it = info.got_ip.ip_info; - ESP_LOGV(TAG, "static_ip=%s gateway=%s", format_ip4_addr(it.ip).c_str(), format_ip4_addr(it.gw).c_str()); - this->got_ipv4_address_ = true; -#if USE_NETWORK_IPV6 - s_sta_connecting = this->num_ipv6_addresses_ < USE_NETWORK_MIN_IPV6_ADDR_COUNT; -#else - s_sta_connecting = false; -#endif /* USE_NETWORK_IPV6 */ - break; - } -#if USE_NETWORK_IPV6 - case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP6: { - auto it = info.got_ip6.ip6_info; - ESP_LOGV(TAG, "IPv6 address=" IPV6STR, IPV62STR(it.ip)); - this->num_ipv6_addresses_++; - s_sta_connecting = !(this->got_ipv4_address_ & (this->num_ipv6_addresses_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT)); - break; - } -#endif /* USE_NETWORK_IPV6 */ - case ESPHOME_EVENT_ID_WIFI_STA_LOST_IP: { - ESP_LOGV(TAG, "Lost IP"); - this->got_ipv4_address_ = false; - break; - } - case ESPHOME_EVENT_ID_WIFI_AP_START: { - ESP_LOGV(TAG, "AP start"); - break; - } - case ESPHOME_EVENT_ID_WIFI_AP_STOP: { - ESP_LOGV(TAG, "AP stop"); -#ifdef USE_WIFI_AP - // Clear the AP interface handle to prevent use-after-free - s_ap_netif = nullptr; -#endif - break; - } - case ESPHOME_EVENT_ID_WIFI_AP_STACONNECTED: { - auto it = info.wifi_sta_connected; - auto &mac = it.bssid; - ESP_LOGV(TAG, "AP client connected MAC=%s", format_mac_address_pretty(mac).c_str()); - break; - } - case ESPHOME_EVENT_ID_WIFI_AP_STADISCONNECTED: { - auto it = info.wifi_sta_disconnected; - auto &mac = it.bssid; - ESP_LOGV(TAG, "AP client disconnected MAC=%s", format_mac_address_pretty(mac).c_str()); - break; - } - case ESPHOME_EVENT_ID_WIFI_AP_STAIPASSIGNED: { - ESP_LOGV(TAG, "AP client assigned IP"); - break; - } - case ESPHOME_EVENT_ID_WIFI_AP_PROBEREQRECVED: { - auto it = info.wifi_ap_probereqrecved; - ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", format_mac_address_pretty(it.mac).c_str(), it.rssi); - break; - } - default: - break; - } -} - -WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() { - const auto status = WiFi.status(); - if (status == WL_CONNECT_FAILED || status == WL_CONNECTION_LOST) { - return WiFiSTAConnectStatus::ERROR_CONNECT_FAILED; - } - if (status == WL_NO_SSID_AVAIL) { - return WiFiSTAConnectStatus::ERROR_NETWORK_NOT_FOUND; - } - if (s_sta_connecting) { - return WiFiSTAConnectStatus::CONNECTING; - } - if (status == WL_CONNECTED) { - return WiFiSTAConnectStatus::CONNECTED; - } - return WiFiSTAConnectStatus::IDLE; -} -bool WiFiComponent::wifi_scan_start_(bool passive) { - // enable STA - if (!this->wifi_mode_(true, {})) - return false; - - // need to use WiFi because of WiFiScanClass allocations :( - int16_t err = WiFi.scanNetworks(true, true, passive, 200); - if (err != WIFI_SCAN_RUNNING) { - ESP_LOGV(TAG, "WiFi.scanNetworks failed: %d", err); - return false; - } - - return true; -} -void WiFiComponent::wifi_scan_done_callback_() { - this->scan_result_.clear(); - - int16_t num = WiFi.scanComplete(); - if (num < 0) - return; - - this->scan_result_.reserve(static_cast(num)); - for (int i = 0; i < num; i++) { - String ssid = WiFi.SSID(i); - wifi_auth_mode_t authmode = WiFi.encryptionType(i); - int32_t rssi = WiFi.RSSI(i); - uint8_t *bssid = WiFi.BSSID(i); - int32_t channel = WiFi.channel(i); - - WiFiScanResult scan({bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]}, std::string(ssid.c_str()), - channel, rssi, authmode != WIFI_AUTH_OPEN, ssid.length() == 0); - this->scan_result_.push_back(scan); - } - WiFi.scanDelete(); - this->scan_done_ = true; -} - -#ifdef USE_WIFI_AP -bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { - esp_err_t err; - - // enable AP - if (!this->wifi_mode_({}, true)) - return false; - - // Check if the AP interface is initialized before using it - if (s_ap_netif == nullptr) { - ESP_LOGW(TAG, "AP interface not initialized"); - return false; - } - - esp_netif_ip_info_t info; - if (manual_ip.has_value()) { - info.ip = manual_ip->static_ip; - info.gw = manual_ip->gateway; - info.netmask = manual_ip->subnet; - } else { - info.ip = network::IPAddress(192, 168, 4, 1); - info.gw = network::IPAddress(192, 168, 4, 1); - info.netmask = network::IPAddress(255, 255, 255, 0); - } - - err = esp_netif_dhcps_stop(s_ap_netif); - if (err != ESP_OK && err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STOPPED) { - ESP_LOGE(TAG, "esp_netif_dhcps_stop failed: %s", esp_err_to_name(err)); - return false; - } - - err = esp_netif_set_ip_info(s_ap_netif, &info); - if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_netif_set_ip_info failed: %d", err); - return false; - } - - dhcps_lease_t lease; - lease.enable = true; - network::IPAddress start_address = network::IPAddress(&info.ip); - start_address += 99; - lease.start_ip = start_address; - ESP_LOGV(TAG, "DHCP server IP lease start: %s", start_address.str().c_str()); - start_address += 10; - lease.end_ip = start_address; - ESP_LOGV(TAG, "DHCP server IP lease end: %s", start_address.str().c_str()); - err = esp_netif_dhcps_option(s_ap_netif, ESP_NETIF_OP_SET, ESP_NETIF_REQUESTED_IP_ADDRESS, &lease, sizeof(lease)); - - if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_netif_dhcps_option failed: %d", err); - return false; - } - - err = esp_netif_dhcps_start(s_ap_netif); - - if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_netif_dhcps_start failed: %d", err); - return false; - } - - return true; -} - -bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { - // enable AP - if (!this->wifi_mode_({}, true)) - return false; - - wifi_config_t conf; - memset(&conf, 0, sizeof(conf)); - if (ap.get_ssid().size() > sizeof(conf.ap.ssid)) { - ESP_LOGE(TAG, "AP SSID too long"); - return false; - } - memcpy(reinterpret_cast(conf.ap.ssid), ap.get_ssid().c_str(), ap.get_ssid().size()); - conf.ap.channel = ap.get_channel().value_or(1); - conf.ap.ssid_hidden = ap.get_ssid().size(); - conf.ap.max_connection = 5; - conf.ap.beacon_interval = 100; - - if (ap.get_password().empty()) { - conf.ap.authmode = WIFI_AUTH_OPEN; - *conf.ap.password = 0; - } else { - conf.ap.authmode = WIFI_AUTH_WPA2_PSK; - if (ap.get_password().size() > sizeof(conf.ap.password)) { - ESP_LOGE(TAG, "AP password too long"); - return false; - } - memcpy(reinterpret_cast(conf.ap.password), ap.get_password().c_str(), ap.get_password().size()); - } - - // pairwise cipher of SoftAP, group cipher will be derived using this. - conf.ap.pairwise_cipher = WIFI_CIPHER_TYPE_CCMP; - - esp_err_t err = esp_wifi_set_config(WIFI_IF_AP, &conf); - if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_wifi_set_config failed: %d", err); - return false; - } - - yield(); - - if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) { - ESP_LOGV(TAG, "wifi_ap_ip_config_ failed"); - return false; - } - - return true; -} - -network::IPAddress WiFiComponent::wifi_soft_ap_ip() { - esp_netif_ip_info_t ip; - esp_netif_get_ip_info(s_ap_netif, &ip); - return network::IPAddress(&ip.ip); -} -#endif // USE_WIFI_AP - -bool WiFiComponent::wifi_disconnect_() { return esp_wifi_disconnect(); } - -bssid_t WiFiComponent::wifi_bssid() { - bssid_t bssid{}; - uint8_t *raw_bssid = WiFi.BSSID(); - if (raw_bssid != nullptr) { - for (size_t i = 0; i < bssid.size(); i++) - bssid[i] = raw_bssid[i]; - } - return bssid; -} -std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); } -int8_t WiFiComponent::wifi_rssi() { return WiFi.RSSI(); } -int32_t WiFiComponent::get_wifi_channel() { return WiFi.channel(); } -network::IPAddress WiFiComponent::wifi_subnet_mask_() { return network::IPAddress(WiFi.subnetMask()); } -network::IPAddress WiFiComponent::wifi_gateway_ip_() { return network::IPAddress(WiFi.gatewayIP()); } -network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return network::IPAddress(WiFi.dnsIP(num)); } - -} // namespace wifi -} // namespace esphome - -#endif // USE_ESP32_FRAMEWORK_ARDUINO -#endif diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index ae1daed8b5..540ad3a585 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -38,8 +38,7 @@ extern "C" { #include "esphome/core/log.h" #include "esphome/core/util.h" -namespace esphome { -namespace wifi { +namespace esphome::wifi { static const char *const TAG = "wifi_esp8266"; @@ -117,7 +116,7 @@ void netif_set_addr(struct netif *netif, const ip4_addr_t *ip, const ip4_addr_t }; #endif -bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { +bool WiFiComponent::wifi_sta_ip_config_(const optional &manual_ip) { // enable STA if (!this->wifi_mode_(true, {})) return false; @@ -258,8 +257,17 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { if (ap.get_password().empty()) { conf.threshold.authmode = AUTH_OPEN; } else { - // Only allow auth modes with at least WPA - conf.threshold.authmode = AUTH_WPA_PSK; + // Set threshold based on configured minimum auth mode + // Note: ESP8266 doesn't support WPA3 + switch (this->min_auth_mode_) { + case WIFI_MIN_AUTH_MODE_WPA: + conf.threshold.authmode = AUTH_WPA_PSK; + break; + case WIFI_MIN_AUTH_MODE_WPA2: + case WIFI_MIN_AUTH_MODE_WPA3: // Fall back to WPA2 for ESP8266 + conf.threshold.authmode = AUTH_WPA2_PSK; + break; + } } conf.threshold.rssi = -127; #endif @@ -273,9 +281,15 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { return false; } +#ifdef USE_WIFI_MANUAL_IP if (!this->wifi_sta_ip_config_(ap.get_manual_ip())) { return false; } +#else + if (!this->wifi_sta_ip_config_({})) { + return false; + } +#endif // setup enterprise authentication if required #ifdef USE_WIFI_WPA2_EAP @@ -301,7 +315,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { // if we have certs, this must be EAP-TLS ret = wifi_station_set_enterprise_cert_key((uint8_t *) eap.client_cert, client_cert_len + 1, (uint8_t *) eap.client_key, client_key_len + 1, - (uint8_t *) eap.password.c_str(), strlen(eap.password.c_str())); + (uint8_t *) eap.password.c_str(), eap.password.length()); if (ret) { ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_cert_key failed: %d", ret); } @@ -499,6 +513,10 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { ESP_LOGV(TAG, "Connected ssid='%s' bssid=%s channel=%u", buf, format_mac_address_pretty(it.bssid).c_str(), it.channel); s_sta_connected = true; +#ifdef USE_WIFI_CALLBACKS + global_wifi_component->wifi_connect_state_callback_.call(global_wifi_component->wifi_ssid(), + global_wifi_component->wifi_bssid()); +#endif break; } case EVENT_STAMODE_DISCONNECTED: { @@ -510,12 +528,17 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { ESP_LOGW(TAG, "Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf); s_sta_connect_not_found = true; } else { - ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, - format_mac_address_pretty(it.bssid).c_str(), LOG_STR_ARG(get_disconnect_reason_str(it.reason))); + char bssid_s[18]; + format_mac_addr_upper(it.bssid, bssid_s); + ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, bssid_s, + LOG_STR_ARG(get_disconnect_reason_str(it.reason))); s_sta_connect_error = true; } s_sta_connected = false; s_sta_connecting = false; +#ifdef USE_WIFI_CALLBACKS + global_wifi_component->wifi_connect_state_callback_.call("", bssid_t({0, 0, 0, 0, 0, 0})); +#endif break; } case EVENT_STAMODE_AUTHMODE_CHANGE: { @@ -538,6 +561,11 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { ESP_LOGV(TAG, "static_ip=%s gateway=%s netmask=%s", format_ip_addr(it.ip).c_str(), format_ip_addr(it.gw).c_str(), format_ip_addr(it.mask).c_str()); s_sta_got_ip = true; +#ifdef USE_WIFI_CALLBACKS + global_wifi_component->ip_state_callback_.call(global_wifi_component->wifi_sta_ip_addresses(), + global_wifi_component->get_dns_address(0), + global_wifi_component->get_dns_address(1)); +#endif break; } case EVENT_STAMODE_DHCP_TIMEOUT: { @@ -696,18 +724,29 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) { this->retry_connect(); return; } + + // Count the number of results first auto *head = reinterpret_cast(arg); + size_t count = 0; for (bss_info *it = head; it != nullptr; it = STAILQ_NEXT(it, next)) { - WiFiScanResult res({it->bssid[0], it->bssid[1], it->bssid[2], it->bssid[3], it->bssid[4], it->bssid[5]}, - std::string(reinterpret_cast(it->ssid), it->ssid_len), it->channel, it->rssi, - it->authmode != AUTH_OPEN, it->is_hidden != 0); - this->scan_result_.push_back(res); + count++; + } + + this->scan_result_.init(count); + for (bss_info *it = head; it != nullptr; it = STAILQ_NEXT(it, next)) { + this->scan_result_.emplace_back( + bssid_t{it->bssid[0], it->bssid[1], it->bssid[2], it->bssid[3], it->bssid[4], it->bssid[5]}, + std::string(reinterpret_cast(it->ssid), it->ssid_len), it->channel, it->rssi, it->authmode != AUTH_OPEN, + it->is_hidden != 0); } this->scan_done_ = true; +#ifdef USE_WIFI_CALLBACKS + global_wifi_component->wifi_scan_state_callback_.call(global_wifi_component->scan_result_); +#endif } #ifdef USE_WIFI_AP -bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { +bool WiFiComponent::wifi_ap_ip_config_(const optional &manual_ip) { // enable AP if (!this->wifi_mode_({}, true)) return false; @@ -815,10 +854,17 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { return false; } +#ifdef USE_WIFI_MANUAL_IP if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) { ESP_LOGV(TAG, "wifi_ap_ip_config_ failed"); return false; } +#else + if (!this->wifi_ap_ip_config_({})) { + ESP_LOGV(TAG, "wifi_ap_ip_config_ failed"); + return false; + } +#endif return true; } @@ -840,15 +886,19 @@ bssid_t WiFiComponent::wifi_bssid() { return bssid; } std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); } -int8_t WiFiComponent::wifi_rssi() { return WiFi.RSSI(); } +int8_t WiFiComponent::wifi_rssi() { + if (WiFi.status() != WL_CONNECTED) + return WIFI_RSSI_DISCONNECTED; + int8_t rssi = WiFi.RSSI(); + // Values >= 31 are error codes per NONOS SDK API, not valid RSSI readings + return rssi >= 31 ? WIFI_RSSI_DISCONNECTED : rssi; +} int32_t WiFiComponent::get_wifi_channel() { return WiFi.channel(); } network::IPAddress WiFiComponent::wifi_subnet_mask_() { return {(const ip_addr_t *) WiFi.subnetMask()}; } network::IPAddress WiFiComponent::wifi_gateway_ip_() { return {(const ip_addr_t *) WiFi.gatewayIP()}; } network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return {(const ip_addr_t *) WiFi.dnsIP(num)}; } void WiFiComponent::wifi_loop_() {} -} // namespace wifi -} // namespace esphome - +} // namespace esphome::wifi #endif #endif diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 94f1f5125f..c20c96ced0 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -1,7 +1,7 @@ #include "wifi_component.h" #ifdef USE_WIFI -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include #include @@ -27,6 +27,10 @@ #include "dhcpserver/dhcpserver.h" #endif // USE_WIFI_AP +#ifdef USE_CAPTIVE_PORTAL +#include "esphome/components/captive_portal/captive_portal.h" +#endif + #include "lwip/apps/sntp.h" #include "lwip/dns.h" #include "lwip/err.h" @@ -37,8 +41,7 @@ #include "esphome/core/log.h" #include "esphome/core/util.h" -namespace esphome { -namespace wifi { +namespace esphome::wifi { static const char *const TAG = "wifi_esp32"; @@ -304,7 +307,18 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { if (ap.get_password().empty()) { conf.sta.threshold.authmode = WIFI_AUTH_OPEN; } else { - conf.sta.threshold.authmode = WIFI_AUTH_WPA_WPA2_PSK; + // Set threshold based on configured minimum auth mode + switch (this->min_auth_mode_) { + case WIFI_MIN_AUTH_MODE_WPA: + conf.sta.threshold.authmode = WIFI_AUTH_WPA_PSK; + break; + case WIFI_MIN_AUTH_MODE_WPA2: + conf.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK; + break; + case WIFI_MIN_AUTH_MODE_WPA3: + conf.sta.threshold.authmode = WIFI_AUTH_WPA3_PSK; + break; + } } #ifdef USE_WIFI_WPA2_EAP @@ -343,8 +357,6 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { // The minimum rssi to accept in the fast scan mode conf.sta.threshold.rssi = -127; - conf.sta.threshold.authmode = WIFI_AUTH_OPEN; - wifi_config_t current_conf; esp_err_t err; err = esp_wifi_get_config(WIFI_IF_STA, ¤t_conf); @@ -367,9 +379,15 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { return false; } +#ifdef USE_WIFI_MANUAL_IP if (!this->wifi_sta_ip_config_(ap.get_manual_ip())) { return false; } +#else + if (!this->wifi_sta_ip_config_({})) { + return false; + } +#endif // setup enterprise authentication if required #ifdef USE_WIFI_WPA2_EAP @@ -404,11 +422,11 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { #if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1) err = esp_eap_client_set_certificate_and_key((uint8_t *) eap.client_cert, client_cert_len + 1, (uint8_t *) eap.client_key, client_key_len + 1, - (uint8_t *) eap.password.c_str(), strlen(eap.password.c_str())); + (uint8_t *) eap.password.c_str(), eap.password.length()); #else err = esp_wifi_sta_wpa2_ent_set_cert_key((uint8_t *) eap.client_cert, client_cert_len + 1, (uint8_t *) eap.client_key, client_key_len + 1, - (uint8_t *) eap.password.c_str(), strlen(eap.password.c_str())); + (uint8_t *) eap.password.c_str(), eap.password.length()); #endif if (err != ESP_OK) { ESP_LOGV(TAG, "set_cert_key failed %d", err); @@ -468,7 +486,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { return true; } -bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { +bool WiFiComponent::wifi_sta_ip_config_(const optional &manual_ip) { // enable STA if (!this->wifi_mode_(true, {})) return false; @@ -584,10 +602,6 @@ const char *get_auth_mode_str(uint8_t mode) { } } -std::string format_ip4_addr(const esp_ip4_addr_t &ip) { return str_snprintf(IPSTR, 15, IP2STR(&ip)); } -#if LWIP_IPV6 -std::string format_ip6_addr(const esp_ip6_addr_t &ip) { return str_snprintf(IPV6STR, 39, IPV62STR(ip)); } -#endif /* LWIP_IPV6 */ const char *get_disconnect_reason_str(uint8_t reason) { switch (reason) { case WIFI_REASON_AUTH_EXPIRE: @@ -654,12 +668,14 @@ const char *get_disconnect_reason_str(uint8_t reason) { return "Association comeback time too long"; case WIFI_REASON_SA_QUERY_TIMEOUT: return "SA query timeout"; +#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 2) case WIFI_REASON_NO_AP_FOUND_W_COMPATIBLE_SECURITY: return "No AP found with compatible security"; case WIFI_REASON_NO_AP_FOUND_IN_AUTHMODE_THRESHOLD: return "No AP found in auth mode threshold"; case WIFI_REASON_NO_AP_FOUND_IN_RSSI_THRESHOLD: return "No AP found in RSSI threshold"; +#endif case WIFI_REASON_UNSPECIFIED: default: return "Unspecified"; @@ -697,8 +713,6 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_STOP) { ESP_LOGV(TAG, "STA stop"); s_sta_started = false; - // Clear the STA interface handle to prevent use-after-free - s_sta_netif = nullptr; } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_AUTHMODE_CHANGE) { const auto &it = data->data.sta_authmode_change; @@ -713,6 +727,9 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { ESP_LOGV(TAG, "Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf, format_mac_address_pretty(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode)); s_sta_connected = true; +#ifdef USE_WIFI_CALLBACKS + this->wifi_connect_state_callback_.call(this->wifi_ssid(), this->wifi_bssid()); +#endif } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_DISCONNECTED) { const auto &it = data->data.sta_disconnected; @@ -727,28 +744,38 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { ESP_LOGI(TAG, "Disconnected ssid='%s' reason='Station Roaming'", buf); return; } else { - ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, - format_mac_address_pretty(it.bssid).c_str(), get_disconnect_reason_str(it.reason)); + char bssid_s[18]; + format_mac_addr_upper(it.bssid, bssid_s); + ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, bssid_s, + get_disconnect_reason_str(it.reason)); s_sta_connect_error = true; } s_sta_connected = false; s_sta_connecting = false; error_from_callback_ = true; +#ifdef USE_WIFI_CALLBACKS + this->wifi_connect_state_callback_.call("", bssid_t({0, 0, 0, 0, 0, 0})); +#endif } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_STA_GOT_IP) { const auto &it = data->data.ip_got_ip; #if USE_NETWORK_IPV6 esp_netif_create_ip6_linklocal(s_sta_netif); #endif /* USE_NETWORK_IPV6 */ - ESP_LOGV(TAG, "static_ip=%s gateway=%s", format_ip4_addr(it.ip_info.ip).c_str(), - format_ip4_addr(it.ip_info.gw).c_str()); + ESP_LOGV(TAG, "static_ip=" IPSTR " gateway=" IPSTR, IP2STR(&it.ip_info.ip), IP2STR(&it.ip_info.gw)); this->got_ipv4_address_ = true; +#ifdef USE_WIFI_CALLBACKS + this->ip_state_callback_.call(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); +#endif #if USE_NETWORK_IPV6 } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_GOT_IP6) { const auto &it = data->data.ip_got_ip6; - ESP_LOGV(TAG, "IPv6 address=%s", format_ip6_addr(it.ip6_info.ip).c_str()); + ESP_LOGV(TAG, "IPv6 address=" IPV6STR, IPV62STR(it.ip6_info.ip)); this->num_ipv6_addresses_++; +#ifdef USE_WIFI_CALLBACKS + this->ip_state_callback_.call(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); +#endif #endif /* USE_NETWORK_IPV6 */ } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_STA_LOST_IP) { @@ -772,23 +799,25 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { } uint16_t number = it.number; - std::vector records(number); - err = esp_wifi_scan_get_ap_records(&number, records.data()); + auto records = std::make_unique(number); + err = esp_wifi_scan_get_ap_records(&number, records.get()); if (err != ESP_OK) { ESP_LOGW(TAG, "esp_wifi_scan_get_ap_records failed: %s", esp_err_to_name(err)); return; } - records.resize(number); - scan_result_.reserve(number); + scan_result_.init(number); for (int i = 0; i < number; i++) { auto &record = records[i]; bssid_t bssid; std::copy(record.bssid, record.bssid + 6, bssid.begin()); std::string ssid(reinterpret_cast(record.ssid)); - WiFiScanResult result(bssid, ssid, record.primary, record.rssi, record.authmode != WIFI_AUTH_OPEN, ssid.empty()); - scan_result_.push_back(result); + scan_result_.emplace_back(bssid, ssid, record.primary, record.rssi, record.authmode != WIFI_AUTH_OPEN, + ssid.empty()); } +#ifdef USE_WIFI_CALLBACKS + this->wifi_scan_state_callback_.call(this->scan_result_); +#endif } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_START) { ESP_LOGV(TAG, "AP start"); @@ -797,10 +826,6 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_STOP) { ESP_LOGV(TAG, "AP stop"); s_ap_started = false; -#ifdef USE_WIFI_AP - // Clear the AP interface handle to prevent use-after-free - s_ap_netif = nullptr; -#endif } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_PROBEREQRECVED) { const auto &it = data->data.ap_probe_req_rx; @@ -816,7 +841,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_AP_STAIPASSIGNED) { const auto &it = data->data.ip_ap_staipassigned; - ESP_LOGV(TAG, "AP client assigned IP %s", format_ip4_addr(it.ip).c_str()); + ESP_LOGV(TAG, "AP client assigned IP " IPSTR, IP2STR(&it.ip)); } } @@ -870,7 +895,7 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { } #ifdef USE_WIFI_AP -bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { +bool WiFiComponent::wifi_ap_ip_config_(const optional &manual_ip) { esp_err_t err; // enable AP @@ -922,6 +947,22 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { return false; } +#if defined(USE_CAPTIVE_PORTAL) && ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 0) + // Configure DHCP Option 114 (Captive Portal URI) if captive portal is enabled + // This provides a standards-compliant way for clients to discover the captive portal + if (captive_portal::global_captive_portal != nullptr) { + static char captive_portal_uri[32]; + snprintf(captive_portal_uri, sizeof(captive_portal_uri), "http://%s", network::IPAddress(&info.ip).str().c_str()); + err = esp_netif_dhcps_option(s_ap_netif, ESP_NETIF_OP_SET, ESP_NETIF_CAPTIVEPORTAL_URI, captive_portal_uri, + strlen(captive_portal_uri)); + if (err != ESP_OK) { + ESP_LOGV(TAG, "Failed to set DHCP captive portal URI: %s", esp_err_to_name(err)); + } else { + ESP_LOGV(TAG, "DHCP Captive Portal URI set to: %s", captive_portal_uri); + } + } +#endif + err = esp_netif_dhcps_start(s_ap_netif); if (err != ESP_OK) { @@ -970,10 +1011,17 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { return false; } +#ifdef USE_WIFI_MANUAL_IP if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) { ESP_LOGE(TAG, "wifi_ap_ip_config_ failed:"); return false; } +#else + if (!this->wifi_ap_ip_config_({})) { + ESP_LOGE(TAG, "wifi_ap_ip_config_ failed:"); + return false; + } +#endif return true; } @@ -992,7 +1040,8 @@ bssid_t WiFiComponent::wifi_bssid() { wifi_ap_record_t info; esp_err_t err = esp_wifi_sta_get_ap_info(&info); if (err != ESP_OK) { - ESP_LOGW(TAG, "esp_wifi_sta_get_ap_info failed: %s", esp_err_to_name(err)); + // Very verbose only: this is expected during dump_config() before connection is established (PR #9823) + ESP_LOGVV(TAG, "esp_wifi_sta_get_ap_info failed: %s", esp_err_to_name(err)); return bssid; } std::copy(info.bssid, info.bssid + 6, bssid.begin()); @@ -1002,7 +1051,8 @@ std::string WiFiComponent::wifi_ssid() { wifi_ap_record_t info{}; esp_err_t err = esp_wifi_sta_get_ap_info(&info); if (err != ESP_OK) { - ESP_LOGW(TAG, "esp_wifi_sta_get_ap_info failed: %s", esp_err_to_name(err)); + // Very verbose only: this is expected during dump_config() before connection is established (PR #9823) + ESP_LOGVV(TAG, "esp_wifi_sta_get_ap_info failed: %s", esp_err_to_name(err)); return ""; } auto *ssid_s = reinterpret_cast(info.ssid); @@ -1013,8 +1063,9 @@ int8_t WiFiComponent::wifi_rssi() { wifi_ap_record_t info; esp_err_t err = esp_wifi_sta_get_ap_info(&info); if (err != ESP_OK) { - ESP_LOGW(TAG, "esp_wifi_sta_get_ap_info failed: %s", esp_err_to_name(err)); - return 0; + // Very verbose only: this is expected during dump_config() before connection is established (PR #9823) + ESP_LOGVV(TAG, "esp_wifi_sta_get_ap_info failed: %s", esp_err_to_name(err)); + return WIFI_RSSI_DISCONNECTED; } return info.rssi; } @@ -1051,8 +1102,6 @@ network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return network::IPAddress(dns_ip); } -} // namespace wifi -} // namespace esphome - -#endif // USE_ESP_IDF +} // namespace esphome::wifi +#endif // USE_ESP32 #endif diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index b15f710150..04d0d4fa85 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -15,8 +15,7 @@ #include "esphome/core/log.h" #include "esphome/core/util.h" -namespace esphome { -namespace wifi { +namespace esphome::wifi { static const char *const TAG = "wifi_lt"; @@ -68,7 +67,7 @@ bool WiFiComponent::wifi_sta_pre_setup_() { return true; } bool WiFiComponent::wifi_apply_power_save_() { return WiFi.setSleep(this->power_save_ != WIFI_POWER_SAVE_NONE); } -bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { +bool WiFiComponent::wifi_sta_ip_config_(const optional &manual_ip) { // enable STA if (!this->wifi_mode_(true, {})) return false; @@ -112,9 +111,15 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { WiFi.disconnect(); } +#ifdef USE_WIFI_MANUAL_IP if (!this->wifi_sta_ip_config_(ap.get_manual_ip())) { return false; } +#else + if (!this->wifi_sta_ip_config_({})) { + return false; + } +#endif this->wifi_apply_hostname_(); @@ -282,7 +287,9 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ buf[it.ssid_len] = '\0'; ESP_LOGV(TAG, "Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf, format_mac_address_pretty(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode)); - +#ifdef USE_WIFI_CALLBACKS + this->wifi_connect_state_callback_.call(this->wifi_ssid(), this->wifi_bssid()); +#endif break; } case ESPHOME_EVENT_ID_WIFI_STA_DISCONNECTED: { @@ -293,8 +300,10 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ if (it.reason == WIFI_REASON_NO_AP_FOUND) { ESP_LOGW(TAG, "Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf); } else { - ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, - format_mac_address_pretty(it.bssid).c_str(), get_disconnect_reason_str(it.reason)); + char bssid_s[18]; + format_mac_addr_upper(it.bssid, bssid_s); + ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, bssid_s, + get_disconnect_reason_str(it.reason)); } uint8_t reason = it.reason; @@ -306,6 +315,9 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ } s_sta_connecting = false; +#ifdef USE_WIFI_CALLBACKS + this->wifi_connect_state_callback_.call("", bssid_t({0, 0, 0, 0, 0, 0})); +#endif break; } case ESPHOME_EVENT_ID_WIFI_STA_AUTHMODE_CHANGE: { @@ -327,11 +339,17 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ ESP_LOGV(TAG, "static_ip=%s gateway=%s", format_ip4_addr(WiFi.localIP()).c_str(), format_ip4_addr(WiFi.gatewayIP()).c_str()); s_sta_connecting = false; +#ifdef USE_WIFI_CALLBACKS + this->ip_state_callback_.call(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); +#endif break; } case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP6: { // auto it = info.got_ip.ip_info; ESP_LOGV(TAG, "Got IPv6"); +#ifdef USE_WIFI_CALLBACKS + this->ip_state_callback_.call(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); +#endif break; } case ESPHOME_EVENT_ID_WIFI_STA_LOST_IP: { @@ -411,7 +429,7 @@ void WiFiComponent::wifi_scan_done_callback_() { if (num < 0) return; - this->scan_result_.reserve(static_cast(num)); + this->scan_result_.init(static_cast(num)); for (int i = 0; i < num; i++) { String ssid = WiFi.SSID(i); wifi_auth_mode_t authmode = WiFi.encryptionType(i); @@ -419,16 +437,19 @@ void WiFiComponent::wifi_scan_done_callback_() { uint8_t *bssid = WiFi.BSSID(i); int32_t channel = WiFi.channel(i); - WiFiScanResult scan({bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]}, std::string(ssid.c_str()), - channel, rssi, authmode != WIFI_AUTH_OPEN, ssid.length() == 0); - this->scan_result_.push_back(scan); + this->scan_result_.emplace_back(bssid_t{bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]}, + std::string(ssid.c_str()), channel, rssi, authmode != WIFI_AUTH_OPEN, + ssid.length() == 0); } WiFi.scanDelete(); this->scan_done_ = true; +#ifdef USE_WIFI_CALLBACKS + this->wifi_scan_state_callback_.call(this->scan_result_); +#endif } #ifdef USE_WIFI_AP -bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { +bool WiFiComponent::wifi_ap_ip_config_(const optional &manual_ip) { // enable AP if (!this->wifi_mode_({}, true)) return false; @@ -445,10 +466,17 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { if (!this->wifi_mode_({}, true)) return false; +#ifdef USE_WIFI_MANUAL_IP if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) { ESP_LOGV(TAG, "wifi_ap_ip_config_ failed"); return false; } +#else + if (!this->wifi_ap_ip_config_({})) { + ESP_LOGV(TAG, "wifi_ap_ip_config_ failed"); + return false; + } +#endif yield(); @@ -471,15 +499,13 @@ bssid_t WiFiComponent::wifi_bssid() { return bssid; } std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); } -int8_t WiFiComponent::wifi_rssi() { return WiFi.RSSI(); } +int8_t WiFiComponent::wifi_rssi() { return WiFi.status() == WL_CONNECTED ? WiFi.RSSI() : WIFI_RSSI_DISCONNECTED; } int32_t WiFiComponent::get_wifi_channel() { return WiFi.channel(); } network::IPAddress WiFiComponent::wifi_subnet_mask_() { return {WiFi.subnetMask()}; } network::IPAddress WiFiComponent::wifi_gateway_ip_() { return {WiFi.gatewayIP()}; } network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return {WiFi.dnsIP(num)}; } void WiFiComponent::wifi_loop_() {} -} // namespace wifi -} // namespace esphome - +} // namespace esphome::wifi #endif // USE_LIBRETINY #endif diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index bf15892cd5..326883c0c4 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -1,4 +1,3 @@ - #include "wifi_component.h" #ifdef USE_WIFI @@ -15,11 +14,14 @@ #include "esphome/core/log.h" #include "esphome/core/util.h" -namespace esphome { -namespace wifi { +namespace esphome::wifi { static const char *const TAG = "wifi_pico_w"; +// Track previous state for detecting changes +static bool s_sta_was_connected = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static bool s_sta_had_ip = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + bool WiFiComponent::wifi_mode_(optional sta, optional ap) { if (sta.has_value()) { if (sta.value()) { @@ -51,12 +53,17 @@ bool WiFiComponent::wifi_apply_power_save_() { return ret == 0; } -// TODO: The driver doesnt seem to have an API for this +// TODO: The driver doesn't seem to have an API for this bool WiFiComponent::wifi_apply_output_power_(float output_power) { return true; } bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { +#ifdef USE_WIFI_MANUAL_IP if (!this->wifi_sta_ip_config_(ap.get_manual_ip())) return false; +#else + if (!this->wifi_sta_ip_config_({})) + return false; +#endif auto ret = WiFi.begin(ap.get_ssid().c_str(), ap.get_password().c_str()); if (ret != WL_CONNECTED) @@ -67,7 +74,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { bool WiFiComponent::wifi_sta_pre_setup_() { return this->wifi_mode_(true, {}); } -bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { +bool WiFiComponent::wifi_sta_ip_config_(const optional &manual_ip) { if (!manual_ip.has_value()) { return true; } @@ -141,7 +148,7 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { } #ifdef USE_WIFI_AP -bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { +bool WiFiComponent::wifi_ap_ip_config_(const optional &manual_ip) { esphome::network::IPAddress ip_address, gateway, subnet, dns; if (manual_ip.has_value()) { ip_address = manual_ip->static_ip; @@ -161,10 +168,17 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { if (!this->wifi_mode_({}, true)) return false; +#ifdef USE_WIFI_MANUAL_IP if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) { ESP_LOGV(TAG, "wifi_ap_ip_config_ failed"); return false; } +#else + if (!this->wifi_ap_ip_config_({})) { + ESP_LOGV(TAG, "wifi_ap_ip_config_ failed"); + return false; + } +#endif WiFi.beginAP(ap.get_ssid().c_str(), ap.get_password().c_str(), ap.get_channel().value_or(1)); @@ -188,7 +202,7 @@ bssid_t WiFiComponent::wifi_bssid() { return bssid; } std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); } -int8_t WiFiComponent::wifi_rssi() { return WiFi.RSSI(); } +int8_t WiFiComponent::wifi_rssi() { return WiFi.status() == WL_CONNECTED ? WiFi.RSSI() : WIFI_RSSI_DISCONNECTED; } int32_t WiFiComponent::get_wifi_channel() { return WiFi.channel(); } network::IPAddresses WiFiComponent::wifi_sta_ip_addresses() { @@ -207,16 +221,61 @@ network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { } void WiFiComponent::wifi_loop_() { + // Handle scan completion if (this->state_ == WIFI_COMPONENT_STATE_STA_SCANNING && !cyw43_wifi_scan_active(&cyw43_state)) { this->scan_done_ = true; ESP_LOGV(TAG, "Scan done"); +#ifdef USE_WIFI_CALLBACKS + this->wifi_scan_state_callback_.call(this->scan_result_); +#endif + } + + // Poll for connection state changes + // The arduino-pico WiFi library doesn't have event callbacks like ESP8266/ESP32, + // so we need to poll the link status to detect state changes + auto status = cyw43_tcpip_link_status(&cyw43_state, CYW43_ITF_STA); + bool is_connected = (status == CYW43_LINK_UP); + + // Detect connection state change + if (is_connected && !s_sta_was_connected) { + // Just connected + s_sta_was_connected = true; + ESP_LOGV(TAG, "Connected"); +#ifdef USE_WIFI_CALLBACKS + this->wifi_connect_state_callback_.call(this->wifi_ssid(), this->wifi_bssid()); +#endif + } else if (!is_connected && s_sta_was_connected) { + // Just disconnected + s_sta_was_connected = false; + s_sta_had_ip = false; + ESP_LOGV(TAG, "Disconnected"); +#ifdef USE_WIFI_CALLBACKS + this->wifi_connect_state_callback_.call("", bssid_t({0, 0, 0, 0, 0, 0})); +#endif + } + + // Detect IP address changes (only when connected) + if (is_connected) { + bool has_ip = false; + // Check for any IP address (IPv4 or IPv6) + for (auto addr : addrList) { + has_ip = true; + break; + } + + if (has_ip && !s_sta_had_ip) { + // Just got IP address + s_sta_had_ip = true; + ESP_LOGV(TAG, "Got IP address"); +#ifdef USE_WIFI_CALLBACKS + this->ip_state_callback_.call(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); +#endif + } } } void WiFiComponent::wifi_pre_setup_() {} -} // namespace wifi -} // namespace esphome - +} // namespace esphome::wifi #endif #endif diff --git a/esphome/components/wifi_info/text_sensor.py b/esphome/components/wifi_info/text_sensor.py index 4ceb73a695..0feee3d4a9 100644 --- a/esphome/components/wifi_info/text_sensor.py +++ b/esphome/components/wifi_info/text_sensor.py @@ -1,5 +1,5 @@ import esphome.codegen as cg -from esphome.components import text_sensor +from esphome.components import text_sensor, wifi import esphome.config_validation as cv from esphome.const import ( CONF_BSSID, @@ -15,31 +15,27 @@ DEPENDENCIES = ["wifi"] wifi_info_ns = cg.esphome_ns.namespace("wifi_info") IPAddressWiFiInfo = wifi_info_ns.class_( - "IPAddressWiFiInfo", text_sensor.TextSensor, cg.PollingComponent + "IPAddressWiFiInfo", text_sensor.TextSensor, cg.Component ) ScanResultsWiFiInfo = wifi_info_ns.class_( - "ScanResultsWiFiInfo", text_sensor.TextSensor, cg.PollingComponent -) -SSIDWiFiInfo = wifi_info_ns.class_( - "SSIDWiFiInfo", text_sensor.TextSensor, cg.PollingComponent + "ScanResultsWiFiInfo", text_sensor.TextSensor, cg.Component ) +SSIDWiFiInfo = wifi_info_ns.class_("SSIDWiFiInfo", text_sensor.TextSensor, cg.Component) BSSIDWiFiInfo = wifi_info_ns.class_( - "BSSIDWiFiInfo", text_sensor.TextSensor, cg.PollingComponent + "BSSIDWiFiInfo", text_sensor.TextSensor, cg.Component ) MacAddressWifiInfo = wifi_info_ns.class_( "MacAddressWifiInfo", text_sensor.TextSensor, cg.Component ) DNSAddressWifiInfo = wifi_info_ns.class_( - "DNSAddressWifiInfo", text_sensor.TextSensor, cg.PollingComponent + "DNSAddressWifiInfo", text_sensor.TextSensor, cg.Component ) CONFIG_SCHEMA = cv.Schema( { cv.Optional(CONF_IP_ADDRESS): text_sensor.text_sensor_schema( IPAddressWiFiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC - ) - .extend(cv.polling_component_schema("1s")) - .extend( + ).extend( { cv.Optional(f"address_{x}"): text_sensor.text_sensor_schema( entity_category=ENTITY_CATEGORY_DIAGNOSTIC, @@ -49,22 +45,31 @@ CONFIG_SCHEMA = cv.Schema( ), cv.Optional(CONF_SCAN_RESULTS): text_sensor.text_sensor_schema( ScanResultsWiFiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC - ).extend(cv.polling_component_schema("60s")), + ), cv.Optional(CONF_SSID): text_sensor.text_sensor_schema( SSIDWiFiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC - ).extend(cv.polling_component_schema("1s")), + ), cv.Optional(CONF_BSSID): text_sensor.text_sensor_schema( BSSIDWiFiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC - ).extend(cv.polling_component_schema("1s")), + ), cv.Optional(CONF_MAC_ADDRESS): text_sensor.text_sensor_schema( MacAddressWifiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC ), cv.Optional(CONF_DNS_ADDRESS): text_sensor.text_sensor_schema( DNSAddressWifiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC - ).extend(cv.polling_component_schema("1s")), + ), } ) +# Keys that require WiFi callbacks +_NETWORK_INFO_KEYS = { + CONF_SSID, + CONF_BSSID, + CONF_IP_ADDRESS, + CONF_DNS_ADDRESS, + CONF_SCAN_RESULTS, +} + async def setup_conf(config, key): if key in config: @@ -74,10 +79,16 @@ async def setup_conf(config, key): async def to_code(config): + # Request WiFi callbacks for any sensor that needs them + if _NETWORK_INFO_KEYS.intersection(config): + wifi.request_wifi_callbacks() + await setup_conf(config, CONF_SSID) await setup_conf(config, CONF_BSSID) await setup_conf(config, CONF_MAC_ADDRESS) - await setup_conf(config, CONF_SCAN_RESULTS) + if CONF_SCAN_RESULTS in config: + await setup_conf(config, CONF_SCAN_RESULTS) + wifi.request_wifi_scan_results() await setup_conf(config, CONF_DNS_ADDRESS) if conf := config.get(CONF_IP_ADDRESS): wifi_info = await text_sensor.new_text_sensor(config[CONF_IP_ADDRESS]) diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.cpp b/esphome/components/wifi_info/wifi_info_text_sensor.cpp index 2612e4af8d..abd590b168 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.cpp +++ b/esphome/components/wifi_info/wifi_info_text_sensor.cpp @@ -2,18 +2,121 @@ #ifdef USE_WIFI #include "esphome/core/log.h" -namespace esphome { -namespace wifi_info { +namespace esphome::wifi_info { static const char *const TAG = "wifi_info"; +static constexpr size_t MAX_STATE_LENGTH = 255; + +/******************** + * IPAddressWiFiInfo + *******************/ + +void IPAddressWiFiInfo::setup() { + wifi::global_wifi_component->add_on_ip_state_callback( + [this](const network::IPAddresses &ips, const network::IPAddress &dns1_ip, const network::IPAddress &dns2_ip) { + this->state_callback_(ips); + }); +} + void IPAddressWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "IP Address", this); } -void ScanResultsWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "Scan Results", this); } -void SSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "SSID", this); } -void BSSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "BSSID", this); } -void MacAddressWifiInfo::dump_config() { LOG_TEXT_SENSOR("", "MAC Address", this); } + +void IPAddressWiFiInfo::state_callback_(const network::IPAddresses &ips) { + this->publish_state(ips[0].str()); + uint8_t sensor = 0; + for (const auto &ip : ips) { + if (ip.is_set()) { + if (this->ip_sensors_[sensor] != nullptr) { + this->ip_sensors_[sensor]->publish_state(ip.str()); + } + sensor++; + } + } +} + +/********************* + * DNSAddressWifiInfo + ********************/ + +void DNSAddressWifiInfo::setup() { + wifi::global_wifi_component->add_on_ip_state_callback( + [this](const network::IPAddresses &ips, const network::IPAddress &dns1_ip, const network::IPAddress &dns2_ip) { + this->state_callback_(dns1_ip, dns2_ip); + }); +} + void DNSAddressWifiInfo::dump_config() { LOG_TEXT_SENSOR("", "DNS Address", this); } -} // namespace wifi_info -} // namespace esphome +void DNSAddressWifiInfo::state_callback_(const network::IPAddress &dns1_ip, const network::IPAddress &dns2_ip) { + std::string dns_results = dns1_ip.str() + " " + dns2_ip.str(); + this->publish_state(dns_results); +} + +/********************** + * ScanResultsWiFiInfo + *********************/ + +void ScanResultsWiFiInfo::setup() { + wifi::global_wifi_component->add_on_wifi_scan_state_callback( + [this](const wifi::wifi_scan_vector_t &results) { this->state_callback_(results); }); +} + +void ScanResultsWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "Scan Results", this); } + +void ScanResultsWiFiInfo::state_callback_(const wifi::wifi_scan_vector_t &results) { + std::string scan_results; + for (const auto &scan : results) { + if (scan.get_is_hidden()) + continue; + + scan_results += scan.get_ssid(); + scan_results += ": "; + scan_results += esphome::to_string(scan.get_rssi()); + scan_results += "dB\n"; + } + // There's a limit of 255 characters per state; longer states just don't get sent so we truncate it + if (scan_results.length() > MAX_STATE_LENGTH) { + scan_results.resize(MAX_STATE_LENGTH); + } + this->publish_state(scan_results); +} + +/*************** + * SSIDWiFiInfo + **************/ + +void SSIDWiFiInfo::setup() { + wifi::global_wifi_component->add_on_wifi_connect_state_callback( + [this](const std::string &ssid, const wifi::bssid_t &bssid) { this->state_callback_(ssid); }); +} + +void SSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "SSID", this); } + +void SSIDWiFiInfo::state_callback_(const std::string &ssid) { this->publish_state(ssid); } + +/**************** + * BSSIDWiFiInfo + ***************/ + +void BSSIDWiFiInfo::setup() { + wifi::global_wifi_component->add_on_wifi_connect_state_callback( + [this](const std::string &ssid, const wifi::bssid_t &bssid) { this->state_callback_(bssid); }); +} + +void BSSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "BSSID", this); } + +void BSSIDWiFiInfo::state_callback_(const wifi::bssid_t &bssid) { + char buf[18] = "unknown"; + if (mac_address_is_valid(bssid.data())) { + format_mac_addr_upper(bssid.data(), buf); + } + this->publish_state(buf); +} +/********************* + * MacAddressWifiInfo + ********************/ + +void MacAddressWifiInfo::dump_config() { LOG_TEXT_SENSOR("", "MAC Address", this); } + +} // namespace esphome::wifi_info #endif diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.h b/esphome/components/wifi_info/wifi_info_text_sensor.h index 68b5f438e4..12666b4059 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.h +++ b/esphome/components/wifi_info/wifi_info_text_sensor.h @@ -1,129 +1,70 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/helpers.h" #include "esphome/components/text_sensor/text_sensor.h" #include "esphome/components/wifi/wifi_component.h" #ifdef USE_WIFI #include -namespace esphome { -namespace wifi_info { +namespace esphome::wifi_info { -class IPAddressWiFiInfo : public PollingComponent, public text_sensor::TextSensor { +class IPAddressWiFiInfo : public Component, public text_sensor::TextSensor { public: - void update() override { - auto ips = wifi::global_wifi_component->wifi_sta_ip_addresses(); - if (ips != this->last_ips_) { - this->last_ips_ = ips; - this->publish_state(ips[0].str()); - uint8_t sensor = 0; - for (auto &ip : ips) { - if (ip.is_set()) { - if (this->ip_sensors_[sensor] != nullptr) { - this->ip_sensors_[sensor]->publish_state(ip.str()); - } - sensor++; - } - } - } - } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + void setup() override; void dump_config() override; void add_ip_sensors(uint8_t index, text_sensor::TextSensor *s) { this->ip_sensors_[index] = s; } protected: - network::IPAddresses last_ips_; + void state_callback_(const network::IPAddresses &ips); std::array ip_sensors_; }; -class DNSAddressWifiInfo : public PollingComponent, public text_sensor::TextSensor { +class DNSAddressWifiInfo : public Component, public text_sensor::TextSensor { public: - void update() override { - auto dns_one = wifi::global_wifi_component->get_dns_address(0); - auto dns_two = wifi::global_wifi_component->get_dns_address(1); - - std::string dns_results = dns_one.str() + " " + dns_two.str(); - - if (dns_results != this->last_results_) { - this->last_results_ = dns_results; - this->publish_state(dns_results); - } - } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + void setup() override; void dump_config() override; protected: - std::string last_results_; + void state_callback_(const network::IPAddress &dns1_ip, const network::IPAddress &dns2_ip); }; -class ScanResultsWiFiInfo : public PollingComponent, public text_sensor::TextSensor { +class ScanResultsWiFiInfo : public Component, public text_sensor::TextSensor { public: - void update() override { - std::string scan_results; - for (auto &scan : wifi::global_wifi_component->get_scan_result()) { - if (scan.get_is_hidden()) - continue; - - scan_results += scan.get_ssid(); - scan_results += ": "; - scan_results += esphome::to_string(scan.get_rssi()); - scan_results += "dB\n"; - } - - if (this->last_scan_results_ != scan_results) { - this->last_scan_results_ = scan_results; - // There's a limit of 255 characters per state. - // Longer states just don't get sent so we truncate it. - this->publish_state(scan_results.substr(0, 255)); - } - } + void setup() override; float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } void dump_config() override; protected: - std::string last_scan_results_; + void state_callback_(const wifi::wifi_scan_vector_t &results); }; -class SSIDWiFiInfo : public PollingComponent, public text_sensor::TextSensor { +class SSIDWiFiInfo : public Component, public text_sensor::TextSensor { public: - void update() override { - std::string ssid = wifi::global_wifi_component->wifi_ssid(); - if (this->last_ssid_ != ssid) { - this->last_ssid_ = ssid; - this->publish_state(this->last_ssid_); - } - } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + void setup() override; void dump_config() override; protected: - std::string last_ssid_; + void state_callback_(const std::string &ssid); }; -class BSSIDWiFiInfo : public PollingComponent, public text_sensor::TextSensor { +class BSSIDWiFiInfo : public Component, public text_sensor::TextSensor { public: - void update() override { - wifi::bssid_t bssid = wifi::global_wifi_component->wifi_bssid(); - if (memcmp(bssid.data(), last_bssid_.data(), 6) != 0) { - std::copy(bssid.begin(), bssid.end(), last_bssid_.begin()); - char buf[30]; - sprintf(buf, "%02X:%02X:%02X:%02X:%02X:%02X", bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]); - this->publish_state(buf); - } - } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + void setup() override; void dump_config() override; protected: - wifi::bssid_t last_bssid_; + void state_callback_(const wifi::bssid_t &bssid); }; class MacAddressWifiInfo : public Component, public text_sensor::TextSensor { public: - void setup() override { this->publish_state(get_mac_address_pretty()); } + void setup() override { + char mac_s[18]; + this->publish_state(get_mac_address_pretty_into_buffer(mac_s)); + } void dump_config() override; }; -} // namespace wifi_info -} // namespace esphome +} // namespace esphome::wifi_info #endif diff --git a/esphome/components/wireguard/__init__.py b/esphome/components/wireguard/__init__.py index 8eff8e7b2a..50c7980215 100644 --- a/esphome/components/wireguard/__init__.py +++ b/esphome/components/wireguard/__init__.py @@ -118,7 +118,7 @@ async def to_code(config): # Workaround for crash on IDF 5+ # See https://github.com/trombik/esp_wireguard/issues/33#issuecomment-1568503651 - if CORE.using_esp_idf: + if CORE.is_esp32: add_idf_sdkconfig_option("CONFIG_LWIP_PPP_SUPPORT", True) # This flag is added here because the esp_wireguard library statically diff --git a/esphome/components/wireguard/binary_sensor.py b/esphome/components/wireguard/binary_sensor.py index 02c4862e8d..2ba59d4c39 100644 --- a/esphome/components/wireguard/binary_sensor.py +++ b/esphome/components/wireguard/binary_sensor.py @@ -1,5 +1,6 @@ import esphome.codegen as cg from esphome.components import binary_sensor +from esphome.components.const import CONF_ENABLED import esphome.config_validation as cv from esphome.const import ( CONF_STATUS, @@ -9,8 +10,6 @@ from esphome.const import ( from . import CONF_WIREGUARD_ID, Wireguard -CONF_ENABLED = "enabled" - DEPENDENCIES = ["wireguard"] CONFIG_SCHEMA = { diff --git a/esphome/components/wireguard/wireguard.h b/esphome/components/wireguard/wireguard.h index 5db9a48c90..f8f79b835d 100644 --- a/esphome/components/wireguard/wireguard.h +++ b/esphome/components/wireguard/wireguard.h @@ -148,25 +148,25 @@ std::string mask_key(const std::string &key); /// Condition to check if remote peer is online. template class WireguardPeerOnlineCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->is_peer_up(); } + bool check(const Ts &...x) override { return this->parent_->is_peer_up(); } }; /// Condition to check if Wireguard component is enabled. template class WireguardEnabledCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->is_enabled(); } + bool check(const Ts &...x) override { return this->parent_->is_enabled(); } }; /// Action to enable Wireguard component. template class WireguardEnableAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->enable(); } + void play(const Ts &...x) override { this->parent_->enable(); } }; /// Action to disable Wireguard component. template class WireguardDisableAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->disable(); } + void play(const Ts &...x) override { this->parent_->disable(); } }; } // namespace wireguard diff --git a/esphome/components/wled/wled_light_effect.cpp b/esphome/components/wled/wled_light_effect.cpp index 25577ccc11..d26b7a1750 100644 --- a/esphome/components/wled/wled_light_effect.cpp +++ b/esphome/components/wled/wled_light_effect.cpp @@ -28,7 +28,7 @@ const int DEFAULT_BLANK_TIME = 1000; static const char *const TAG = "wled_light_effect"; -WLEDLightEffect::WLEDLightEffect(const std::string &name) : AddressableLightEffect(name) {} +WLEDLightEffect::WLEDLightEffect(const char *name) : AddressableLightEffect(name) {} void WLEDLightEffect::start() { AddressableLightEffect::start(); diff --git a/esphome/components/wled/wled_light_effect.h b/esphome/components/wled/wled_light_effect.h index a591e1fd1a..6da5f4e9f9 100644 --- a/esphome/components/wled/wled_light_effect.h +++ b/esphome/components/wled/wled_light_effect.h @@ -15,7 +15,7 @@ namespace wled { class WLEDLightEffect : public light::AddressableLightEffect { public: - WLEDLightEffect(const std::string &name); + WLEDLightEffect(const char *name); void start() override; void stop() override; diff --git a/esphome/components/wts01/__init__.py b/esphome/components/wts01/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/wts01/sensor.py b/esphome/components/wts01/sensor.py new file mode 100644 index 0000000000..bf4f0262ad --- /dev/null +++ b/esphome/components/wts01/sensor.py @@ -0,0 +1,41 @@ +import esphome.codegen as cg +from esphome.components import sensor, uart +import esphome.config_validation as cv +from esphome.const import ( + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, +) + +CONF_WTS01_ID = "wts01_id" +CODEOWNERS = ["@alepee"] +DEPENDENCIES = ["uart"] + +wts01_ns = cg.esphome_ns.namespace("wts01") +WTS01Sensor = wts01_ns.class_( + "WTS01Sensor", cg.Component, uart.UARTDevice, sensor.Sensor +) + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + WTS01Sensor, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(uart.UART_DEVICE_SCHEMA) +) + +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "wts01", + baud_rate=9600, + require_rx=True, +) + + +async def to_code(config): + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) diff --git a/esphome/components/wts01/wts01.cpp b/esphome/components/wts01/wts01.cpp new file mode 100644 index 0000000000..cb910d89cf --- /dev/null +++ b/esphome/components/wts01/wts01.cpp @@ -0,0 +1,91 @@ +#include "wts01.h" +#include "esphome/core/log.h" +#include + +namespace esphome { +namespace wts01 { + +constexpr uint8_t HEADER_1 = 0x55; +constexpr uint8_t HEADER_2 = 0x01; +constexpr uint8_t HEADER_3 = 0x01; +constexpr uint8_t HEADER_4 = 0x04; + +static const char *const TAG = "wts01"; + +void WTS01Sensor::loop() { + // Process all available data at once + while (this->available()) { + uint8_t c; + if (this->read_byte(&c)) { + this->handle_char_(c); + } + } +} + +void WTS01Sensor::dump_config() { LOG_SENSOR("", "WTS01 Sensor", this); } + +void WTS01Sensor::handle_char_(uint8_t c) { + // State machine for processing the header. Reset if something doesn't match. + if (this->buffer_pos_ == 0 && c != HEADER_1) { + return; + } + + if (this->buffer_pos_ == 1 && c != HEADER_2) { + this->buffer_pos_ = 0; + return; + } + + if (this->buffer_pos_ == 2 && c != HEADER_3) { + this->buffer_pos_ = 0; + return; + } + + if (this->buffer_pos_ == 3 && c != HEADER_4) { + this->buffer_pos_ = 0; + return; + } + + // Add byte to buffer + this->buffer_[this->buffer_pos_++] = c; + + // Process complete packet + if (this->buffer_pos_ >= PACKET_SIZE) { + this->process_packet_(); + this->buffer_pos_ = 0; + } +} + +void WTS01Sensor::process_packet_() { + // Based on Tasmota implementation + // Format: 55 01 01 04 01 11 16 12 95 + // header T Td Ck - T = Temperature, Td = Temperature decimal, Ck = Checksum + uint8_t calculated_checksum = 0; + for (uint8_t i = 0; i < PACKET_SIZE - 1; i++) { + calculated_checksum += this->buffer_[i]; + } + + uint8_t received_checksum = this->buffer_[PACKET_SIZE - 1]; + if (calculated_checksum != received_checksum) { + ESP_LOGW(TAG, "WTS01 Checksum doesn't match: 0x%02X != 0x%02X", received_checksum, calculated_checksum); + return; + } + + // Extract temperature value + int8_t temp = this->buffer_[6]; + int32_t sign = 1; + + // Handle negative temperatures + if (temp < 0) { + sign = -1; + } + + // Calculate temperature (temp + decimal/100) + float temperature = static_cast(temp) + (sign * static_cast(this->buffer_[7]) / 100.0f); + + ESP_LOGV(TAG, "Received new temperature: %.2f°C", temperature); + + this->publish_state(temperature); +} + +} // namespace wts01 +} // namespace esphome diff --git a/esphome/components/wts01/wts01.h b/esphome/components/wts01/wts01.h new file mode 100644 index 0000000000..298595a5d6 --- /dev/null +++ b/esphome/components/wts01/wts01.h @@ -0,0 +1,27 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace wts01 { + +constexpr uint8_t PACKET_SIZE = 9; + +class WTS01Sensor : public sensor::Sensor, public uart::UARTDevice, public Component { + public: + void loop() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + protected: + uint8_t buffer_[PACKET_SIZE]; + uint8_t buffer_pos_{0}; + + void handle_char_(uint8_t c); + void process_packet_(); +}; + +} // namespace wts01 +} // namespace esphome diff --git a/esphome/components/xgzp68xx/sensor.py b/esphome/components/xgzp68xx/sensor.py index 74cef3bf7b..2b38392a02 100644 --- a/esphome/components/xgzp68xx/sensor.py +++ b/esphome/components/xgzp68xx/sensor.py @@ -3,6 +3,7 @@ from esphome.components import i2c, sensor import esphome.config_validation as cv from esphome.const import ( CONF_ID, + CONF_OVERSAMPLING, CONF_PRESSURE, CONF_TEMPERATURE, DEVICE_CLASS_PRESSURE, @@ -18,6 +19,17 @@ CODEOWNERS = ["@gcormier"] CONF_K_VALUE = "k_value" xgzp68xx_ns = cg.esphome_ns.namespace("xgzp68xx") +XGZP68XXOversampling = xgzp68xx_ns.enum("XGZP68XXOversampling") +OVERSAMPLING_OPTIONS = { + "256X": XGZP68XXOversampling.XGZP68XX_OVERSAMPLING_256X, + "512X": XGZP68XXOversampling.XGZP68XX_OVERSAMPLING_512X, + "1024X": XGZP68XXOversampling.XGZP68XX_OVERSAMPLING_1024X, + "2048X": XGZP68XXOversampling.XGZP68XX_OVERSAMPLING_2048X, + "4096X": XGZP68XXOversampling.XGZP68XX_OVERSAMPLING_4096X, + "8192X": XGZP68XXOversampling.XGZP68XX_OVERSAMPLING_8192X, + "16384X": XGZP68XXOversampling.XGZP68XX_OVERSAMPLING_16384X, + "32768X": XGZP68XXOversampling.XGZP68XX_OVERSAMPLING_32768X, +} XGZP68XXComponent = xgzp68xx_ns.class_( "XGZP68XXComponent", cg.PollingComponent, i2c.I2CDevice ) @@ -31,6 +43,12 @@ CONFIG_SCHEMA = ( accuracy_decimals=1, device_class=DEVICE_CLASS_PRESSURE, state_class=STATE_CLASS_MEASUREMENT, + ).extend( + { + cv.Optional(CONF_OVERSAMPLING, default="4096X"): cv.enum( + OVERSAMPLING_OPTIONS, upper=True + ), + } ), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, @@ -58,5 +76,6 @@ async def to_code(config): if pressure_config := config.get(CONF_PRESSURE): sens = await sensor.new_sensor(pressure_config) cg.add(var.set_pressure_sensor(sens)) + cg.add(var.set_pressure_oversampling(pressure_config[CONF_OVERSAMPLING])) cg.add(var.set_k_value(config[CONF_K_VALUE])) diff --git a/esphome/components/xgzp68xx/xgzp68xx.cpp b/esphome/components/xgzp68xx/xgzp68xx.cpp index 20a97cd04b..2b0824de0a 100644 --- a/esphome/components/xgzp68xx/xgzp68xx.cpp +++ b/esphome/components/xgzp68xx/xgzp68xx.cpp @@ -16,16 +16,49 @@ static const uint8_t SYSCONFIG_ADDRESS = 0xA5; static const uint8_t PCONFIG_ADDRESS = 0xA6; static const uint8_t READ_COMMAND = 0x0A; +[[maybe_unused]] static const char *oversampling_to_str(XGZP68XXOversampling oversampling) { + switch (oversampling) { + case XGZP68XX_OVERSAMPLING_256X: + return "256x"; + case XGZP68XX_OVERSAMPLING_512X: + return "512x"; + case XGZP68XX_OVERSAMPLING_1024X: + return "1024x"; + case XGZP68XX_OVERSAMPLING_2048X: + return "2048x"; + case XGZP68XX_OVERSAMPLING_4096X: + return "4096x"; + case XGZP68XX_OVERSAMPLING_8192X: + return "8192x"; + case XGZP68XX_OVERSAMPLING_16384X: + return "16384x"; + case XGZP68XX_OVERSAMPLING_32768X: + return "32768x"; + default: + return "UNKNOWN"; + } +} + void XGZP68XXComponent::update() { + // Do we need to change oversampling? + if (this->last_pressure_oversampling_ != this->pressure_oversampling_) { + uint8_t oldconfig = 0; + this->read_register(PCONFIG_ADDRESS, &oldconfig, 1); + uint8_t newconfig = (oldconfig & 0xf8) | (this->pressure_oversampling_ & 0x7); + this->write_register(PCONFIG_ADDRESS, &newconfig, 1); + ESP_LOGD(TAG, "oversampling to %s: oldconfig = 0x%x newconfig = 0x%x", + oversampling_to_str(this->pressure_oversampling_), oldconfig, newconfig); + this->last_pressure_oversampling_ = this->pressure_oversampling_; + } + // Request temp + pressure acquisition this->write_register(0x30, &READ_COMMAND, 1); // Wait 20mS per datasheet this->set_timeout("measurement", 20, [this]() { - uint8_t data[5]; - uint32_t pressure_raw; - uint16_t temperature_raw; - float pressure_in_pa, temperature; + uint8_t data[5] = {}; + uint32_t pressure_raw = 0; + uint16_t temperature_raw = 0; int success; // Read the sensor data @@ -42,23 +75,11 @@ void XGZP68XXComponent::update() { ESP_LOGV(TAG, "Got raw pressure=%" PRIu32 ", raw temperature=%u", pressure_raw, temperature_raw); ESP_LOGV(TAG, "K value is %u", this->k_value_); - // The most significant bit of both pressure and temperature will be 1 to indicate a negative value. - // This is directly from the datasheet, and the calculations below will handle this. - if (pressure_raw > pow(2, 23)) { - // Negative pressure - pressure_in_pa = (pressure_raw - pow(2, 24)) / (float) (this->k_value_); - } else { - // Positive pressure - pressure_in_pa = pressure_raw / (float) (this->k_value_); - } + // Sign extend the pressure + float pressure_in_pa = (float) (((int32_t) pressure_raw << 8) >> 8); + pressure_in_pa /= (float) (this->k_value_); - if (temperature_raw > pow(2, 15)) { - // Negative temperature - temperature = (float) (temperature_raw - pow(2, 16)) / 256.0f; - } else { - // Positive temperature - temperature = (float) temperature_raw / 256.0f; - } + float temperature = ((float) (int16_t) temperature_raw) / 256.0f; if (this->pressure_sensor_ != nullptr) this->pressure_sensor_->publish_state(pressure_in_pa); @@ -69,20 +90,27 @@ void XGZP68XXComponent::update() { } void XGZP68XXComponent::setup() { - uint8_t config; + uint8_t config1 = 0, config2 = 0; // Display some sample bits to confirm we are talking to the sensor - this->read_register(SYSCONFIG_ADDRESS, &config, 1); - ESP_LOGCONFIG(TAG, - "Gain value is %d\n" - "XGZP68xx started!", - (config >> 3) & 0b111); + if (i2c::ErrorCode::ERROR_OK != this->read_register(SYSCONFIG_ADDRESS, &config1, 1)) { + this->mark_failed(); + return; + } + if (i2c::ErrorCode::ERROR_OK != this->read_register(PCONFIG_ADDRESS, &config2, 1)) { + this->mark_failed(); + return; + } + ESP_LOGD(TAG, "sys_config 0x%x, p_config 0x%x", config1, config2); } void XGZP68XXComponent::dump_config() { ESP_LOGCONFIG(TAG, "XGZP68xx:"); LOG_SENSOR(" ", "Temperature: ", this->temperature_sensor_); LOG_SENSOR(" ", "Pressure: ", this->pressure_sensor_); + if (this->pressure_sensor_ != nullptr) { + ESP_LOGCONFIG(TAG, " Oversampling: %s", oversampling_to_str(this->pressure_oversampling_)); + } LOG_I2C_DEVICE(this); if (this->is_failed()) { ESP_LOGE(TAG, " Connection failed"); diff --git a/esphome/components/xgzp68xx/xgzp68xx.h b/esphome/components/xgzp68xx/xgzp68xx.h index 1bb7304b15..ce9cfd6b78 100644 --- a/esphome/components/xgzp68xx/xgzp68xx.h +++ b/esphome/components/xgzp68xx/xgzp68xx.h @@ -7,11 +7,29 @@ namespace esphome { namespace xgzp68xx { +/// Enum listing all oversampling options for the XGZP68XX. +enum XGZP68XXOversampling : uint8_t { + XGZP68XX_OVERSAMPLING_256X = 0b100, + XGZP68XX_OVERSAMPLING_512X = 0b101, + XGZP68XX_OVERSAMPLING_1024X = 0b000, + XGZP68XX_OVERSAMPLING_2048X = 0b001, + XGZP68XX_OVERSAMPLING_4096X = 0b010, + XGZP68XX_OVERSAMPLING_8192X = 0b011, + XGZP68XX_OVERSAMPLING_16384X = 0b110, + XGZP68XX_OVERSAMPLING_32768X = 0b111, + + XGZP68XX_OVERSAMPLING_UNKNOWN = (uint8_t) -1, +}; + class XGZP68XXComponent : public PollingComponent, public sensor::Sensor, public i2c::I2CDevice { public: SUB_SENSOR(temperature) SUB_SENSOR(pressure) void set_k_value(uint16_t k_value) { this->k_value_ = k_value; } + /// Set the pressure oversampling value. Defaults to 4096X. + void set_pressure_oversampling(XGZP68XXOversampling pressure_oversampling) { + this->pressure_oversampling_ = pressure_oversampling; + } void update() override; void setup() override; @@ -21,6 +39,8 @@ class XGZP68XXComponent : public PollingComponent, public sensor::Sensor, public /// Internal method to read the pressure from the component after it has been scheduled. void read_pressure_(); uint16_t k_value_; + XGZP68XXOversampling pressure_oversampling_{XGZP68XX_OVERSAMPLING_4096X}; + XGZP68XXOversampling last_pressure_oversampling_{XGZP68XX_OVERSAMPLING_UNKNOWN}; }; } // namespace xgzp68xx diff --git a/esphome/components/yashima/yashima.cpp b/esphome/components/yashima/yashima.cpp index a3cf53ff66..bf91420620 100644 --- a/esphome/components/yashima/yashima.cpp +++ b/esphome/components/yashima/yashima.cpp @@ -81,7 +81,9 @@ const uint32_t YASHIMA_CARRIER_FREQUENCY = 38000; climate::ClimateTraits YashimaClimate::traits() { auto traits = climate::ClimateTraits(); - traits.set_supports_current_temperature(this->sensor_ != nullptr); + if (this->sensor_ != nullptr) { + traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE); + } traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_HEAT_COOL}); if (supports_cool_) @@ -89,7 +91,6 @@ climate::ClimateTraits YashimaClimate::traits() { if (supports_heat_) traits.add_supported_mode(climate::CLIMATE_MODE_HEAT); - traits.set_supports_two_point_target_temperature(false); traits.set_visual_min_temperature(YASHIMA_TEMP_MIN); traits.set_visual_max_temperature(YASHIMA_TEMP_MAX); traits.set_visual_temperature_step(1); diff --git a/esphome/components/zephyr/__init__.py b/esphome/components/zephyr/__init__.py index c698122030..0381fbcba9 100644 --- a/esphome/components/zephyr/__init__.py +++ b/esphome/components/zephyr/__init__.py @@ -1,4 +1,5 @@ -import os +from pathlib import Path +import textwrap from typing import TypedDict import esphome.codegen as cg @@ -48,7 +49,7 @@ class ZephyrData(TypedDict): bootloader: str prj_conf: dict[str, tuple[PrjConfValueType, bool]] overlay: str - extra_build_files: dict[str, str] + extra_build_files: dict[str, Path] pm_static: list[Section] user: dict[str, list[str]] @@ -90,10 +91,10 @@ def zephyr_add_prj_conf( def zephyr_add_overlay(content): - zephyr_data()[KEY_OVERLAY] += content + zephyr_data()[KEY_OVERLAY] += textwrap.dedent(content) -def add_extra_build_file(filename: str, path: str) -> bool: +def add_extra_build_file(filename: str, path: Path) -> bool: """Add an extra build file to the project.""" extra_build_files = zephyr_data()[KEY_EXTRA_BUILD_FILES] if filename not in extra_build_files: @@ -102,7 +103,7 @@ def add_extra_build_file(filename: str, path: str) -> bool: return False -def add_extra_script(stage: str, filename: str, path: str): +def add_extra_script(stage: str, filename: str, path: Path) -> None: """Add an extra script to the project.""" key = f"{stage}:{filename}" if add_extra_build_file(filename, path): @@ -144,7 +145,7 @@ def zephyr_to_code(config): add_extra_script( "pre", "pre_build.py", - os.path.join(os.path.dirname(__file__), "pre_build.py.script"), + Path(__file__).parent / "pre_build.py.script", ) @@ -222,18 +223,28 @@ def copy_files(): ] in ["xiao_ble"]: fake_board_manifest = """ { -"frameworks": [ - "zephyr" -], -"name": "esphome nrf52", -"upload": { - "maximum_ram_size": 248832, - "maximum_size": 815104 -}, -"url": "https://esphome.io/", -"vendor": "esphome" + "frameworks": [ + "zephyr" + ], + "name": "esphome nrf52", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "speed": 115200 + }, + "url": "https://esphome.io/", + "vendor": "esphome", + "build": { + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_fwid": "0x00B6" + } + } } """ + write_file_if_changed( CORE.relative_build_path(f"boards/{zephyr_data()[KEY_BOARD]}.json"), fake_board_manifest, diff --git a/esphome/components/zephyr/core.cpp b/esphome/components/zephyr/core.cpp index ad7a148cdb..d5427a0ebf 100644 --- a/esphome/components/zephyr/core.cpp +++ b/esphome/components/zephyr/core.cpp @@ -3,9 +3,10 @@ #include #include #include -#include +#include #include "esphome/core/hal.h" #include "esphome/core/helpers.h" +#include "esphome/core/defines.h" namespace esphome { @@ -25,7 +26,14 @@ void arch_init() { wdt_config.window.max = 2000; wdt_channel_id = wdt_install_timeout(WDT, &wdt_config); if (wdt_channel_id >= 0) { - wdt_setup(WDT, WDT_OPT_PAUSE_HALTED_BY_DBG | WDT_OPT_PAUSE_IN_SLEEP); + uint8_t options = 0; +#ifdef USE_DEBUG + options |= WDT_OPT_PAUSE_HALTED_BY_DBG; +#endif +#ifdef USE_DEEP_SLEEP + options |= WDT_OPT_PAUSE_IN_SLEEP; +#endif + wdt_setup(WDT, options); } } } diff --git a/esphome/components/zephyr/gpio.cpp b/esphome/components/zephyr/gpio.cpp index 4b84910368..41b983535c 100644 --- a/esphome/components/zephyr/gpio.cpp +++ b/esphome/components/zephyr/gpio.cpp @@ -8,8 +8,8 @@ namespace zephyr { static const char *const TAG = "zephyr"; -static int flags_to_mode(gpio::Flags flags, bool inverted, bool value) { - int ret = 0; +static gpio_flags_t flags_to_mode(gpio::Flags flags, bool inverted, bool value) { + gpio_flags_t ret = 0; if (flags & gpio::FLAG_INPUT) { ret |= GPIO_INPUT; } @@ -79,7 +79,10 @@ void ZephyrGPIOPin::pin_mode(gpio::Flags flags) { if (nullptr == this->gpio_) { return; } - gpio_pin_configure(this->gpio_, this->pin_ % 32, flags_to_mode(flags, this->inverted_, this->value_)); + auto ret = gpio_pin_configure(this->gpio_, this->pin_ % 32, flags_to_mode(flags, this->inverted_, this->value_)); + if (ret != 0) { + ESP_LOGE(TAG, "gpio %u cannot be configured %d.", this->pin_, ret); + } } std::string ZephyrGPIOPin::dump_summary() const { diff --git a/esphome/components/zephyr/gpio.h b/esphome/components/zephyr/gpio.h index f512ae4648..6e8f81857a 100644 --- a/esphome/components/zephyr/gpio.h +++ b/esphome/components/zephyr/gpio.h @@ -26,10 +26,10 @@ class ZephyrGPIOPin : public InternalGPIOPin { protected: void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; uint8_t pin_; - bool inverted_; - gpio::Flags flags_; - const device *gpio_ = nullptr; - bool value_ = false; + bool inverted_{}; + gpio::Flags flags_{}; + const device *gpio_{nullptr}; + bool value_{false}; }; } // namespace zephyr diff --git a/esphome/components/zephyr_ble_server/__init__.py b/esphome/components/zephyr_ble_server/__init__.py new file mode 100644 index 0000000000..211941e984 --- /dev/null +++ b/esphome/components/zephyr_ble_server/__init__.py @@ -0,0 +1,34 @@ +import esphome.codegen as cg +from esphome.components.zephyr import zephyr_add_prj_conf +import esphome.config_validation as cv +from esphome.const import CONF_ESPHOME, CONF_ID, CONF_NAME, Framework +import esphome.final_validate as fv + +zephyr_ble_server_ns = cg.esphome_ns.namespace("zephyr_ble_server") +BLEServer = zephyr_ble_server_ns.class_("BLEServer", cg.Component) + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(BLEServer), + } + ).extend(cv.COMPONENT_SCHEMA), + cv.only_with_framework(Framework.ZEPHYR), +) + + +def _final_validate(_): + full_config = fv.full_config.get() + zephyr_add_prj_conf("BT_DEVICE_NAME", full_config[CONF_ESPHOME][CONF_NAME]) + + +FINAL_VALIDATE_SCHEMA = _final_validate + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + zephyr_add_prj_conf("BT", True) + zephyr_add_prj_conf("BT_PERIPHERAL", True) + zephyr_add_prj_conf("BT_RX_STACK_SIZE", 1536) + # zephyr_add_prj_conf("BT_LL_SW_SPLIT", True) + await cg.register_component(var, config) diff --git a/esphome/components/zephyr_ble_server/ble_server.cpp b/esphome/components/zephyr_ble_server/ble_server.cpp new file mode 100644 index 0000000000..9f7e606a90 --- /dev/null +++ b/esphome/components/zephyr_ble_server/ble_server.cpp @@ -0,0 +1,100 @@ +#ifdef USE_ZEPHYR +#include "ble_server.h" +#include "esphome/core/defines.h" +#include "esphome/core/log.h" +#include +#include + +namespace esphome::zephyr_ble_server { + +static const char *const TAG = "zephyr_ble_server"; + +static struct k_work advertise_work; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +#define DEVICE_NAME CONFIG_BT_DEVICE_NAME +#define DEVICE_NAME_LEN (sizeof(DEVICE_NAME) - 1) + +static const struct bt_data AD[] = { + BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)), + BT_DATA(BT_DATA_NAME_COMPLETE, DEVICE_NAME, DEVICE_NAME_LEN), +}; + +static const struct bt_data SD[] = { +#ifdef USE_OTA + BT_DATA_BYTES(BT_DATA_UUID128_ALL, 0x84, 0xaa, 0x60, 0x74, 0x52, 0x8a, 0x8b, 0x86, 0xd3, 0x4c, 0xb7, 0x1d, 0x1d, + 0xdc, 0x53, 0x8d), +#endif +}; + +const struct bt_le_adv_param *const ADV_PARAM = BT_LE_ADV_CONN; + +static void advertise(struct k_work *work) { + int rc = bt_le_adv_stop(); + if (rc) { + ESP_LOGE(TAG, "Advertising failed to stop (rc %d)", rc); + } + + rc = bt_le_adv_start(ADV_PARAM, AD, ARRAY_SIZE(AD), SD, ARRAY_SIZE(SD)); + if (rc) { + ESP_LOGE(TAG, "Advertising failed to start (rc %d)", rc); + return; + } + ESP_LOGI(TAG, "Advertising successfully started"); +} + +static void connected(struct bt_conn *conn, uint8_t err) { + if (err) { + ESP_LOGE(TAG, "Connection failed (err 0x%02x)", err); + } else { + ESP_LOGI(TAG, "Connected"); + } +} + +static void disconnected(struct bt_conn *conn, uint8_t reason) { + ESP_LOGI(TAG, "Disconnected (reason 0x%02x)", reason); + k_work_submit(&advertise_work); +} + +static void bt_ready(int err) { + if (err != 0) { + ESP_LOGE(TAG, "Bluetooth failed to initialise: %d", err); + } else { + k_work_submit(&advertise_work); + } +} + +BT_CONN_CB_DEFINE(conn_callbacks) = { + .connected = connected, + .disconnected = disconnected, +}; + +void BLEServer::setup() { + k_work_init(&advertise_work, advertise); + resume_(); +} + +void BLEServer::loop() { + if (this->suspended_) { + resume_(); + this->suspended_ = false; + } +} + +void BLEServer::resume_() { + int rc = bt_enable(bt_ready); + if (rc != 0) { + ESP_LOGE(TAG, "Bluetooth enable failed: %d", rc); + return; + } +} + +void BLEServer::on_shutdown() { + struct k_work_sync sync; + k_work_cancel_sync(&advertise_work, &sync); + bt_disable(); + this->suspended_ = true; +} + +} // namespace esphome::zephyr_ble_server + +#endif diff --git a/esphome/components/zephyr_ble_server/ble_server.h b/esphome/components/zephyr_ble_server/ble_server.h new file mode 100644 index 0000000000..1b32e9b58c --- /dev/null +++ b/esphome/components/zephyr_ble_server/ble_server.h @@ -0,0 +1,19 @@ +#pragma once +#ifdef USE_ZEPHYR +#include "esphome/core/component.h" + +namespace esphome::zephyr_ble_server { + +class BLEServer : public Component { + public: + void setup() override; + void loop() override; + void on_shutdown() override; + + protected: + void resume_(); + bool suspended_ = false; +}; + +} // namespace esphome::zephyr_ble_server +#endif diff --git a/esphome/components/zwave_proxy/__init__.py b/esphome/components/zwave_proxy/__init__.py new file mode 100644 index 0000000000..d88f9f7041 --- /dev/null +++ b/esphome/components/zwave_proxy/__init__.py @@ -0,0 +1,43 @@ +import esphome.codegen as cg +from esphome.components import uart +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_POWER_SAVE_MODE, CONF_WIFI +import esphome.final_validate as fv + +CODEOWNERS = ["@kbx81"] +DEPENDENCIES = ["api", "uart"] + +zwave_proxy_ns = cg.esphome_ns.namespace("zwave_proxy") +ZWaveProxy = zwave_proxy_ns.class_("ZWaveProxy", cg.Component, uart.UARTDevice) + + +def final_validate(config): + full_config = fv.full_config.get() + if (wifi_conf := full_config.get(CONF_WIFI)) and ( + wifi_conf.get(CONF_POWER_SAVE_MODE).lower() != "none" + ): + raise cv.Invalid( + f"{CONF_WIFI} {CONF_POWER_SAVE_MODE} must be set to 'none' when using Z-Wave proxy" + ) + + return config + + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ZWaveProxy), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(uart.UART_DEVICE_SCHEMA) +) + +FINAL_VALIDATE_SCHEMA = final_validate + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) + cg.add_define("USE_ZWAVE_PROXY") diff --git a/esphome/components/zwave_proxy/zwave_proxy.cpp b/esphome/components/zwave_proxy/zwave_proxy.cpp new file mode 100644 index 0000000000..a26a9b2335 --- /dev/null +++ b/esphome/components/zwave_proxy/zwave_proxy.cpp @@ -0,0 +1,346 @@ +#include "zwave_proxy.h" +#include "esphome/components/api/api_server.h" +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include "esphome/core/util.h" + +namespace esphome { +namespace zwave_proxy { + +static const char *const TAG = "zwave_proxy"; + +static constexpr uint8_t ZWAVE_COMMAND_GET_NETWORK_IDS = 0x20; +// GET_NETWORK_IDS response: [SOF][LENGTH][TYPE][CMD][HOME_ID(4)][NODE_ID][...] +static constexpr uint8_t ZWAVE_COMMAND_TYPE_RESPONSE = 0x01; // Response type field value +static constexpr uint8_t ZWAVE_MIN_GET_NETWORK_IDS_LENGTH = 9; // TYPE + CMD + HOME_ID(4) + NODE_ID + checksum +static constexpr uint32_t HOME_ID_TIMEOUT_MS = 100; // Timeout for waiting for home ID during setup + +static uint8_t calculate_frame_checksum(const uint8_t *data, uint8_t length) { + // Calculate Z-Wave frame checksum + // XOR all bytes between SOF and checksum position (exclusive) + // Initial value is 0xFF per Z-Wave protocol specification + uint8_t checksum = 0xFF; + for (uint8_t i = 1; i < length - 1; i++) { + checksum ^= data[i]; + } + return checksum; +} + +ZWaveProxy::ZWaveProxy() { global_zwave_proxy = this; } + +void ZWaveProxy::setup() { + this->setup_time_ = App.get_loop_component_start_time(); + this->send_simple_command_(ZWAVE_COMMAND_GET_NETWORK_IDS); +} + +float ZWaveProxy::get_setup_priority() const { + // Set up before API so home ID is ready when API starts + return setup_priority::BEFORE_CONNECTION; +} + +bool ZWaveProxy::can_proceed() { + // If we already have the home ID, we can proceed + if (this->home_id_ready_) { + return true; + } + + // Handle any pending responses + if (this->response_handler_()) { + ESP_LOGV(TAG, "Handled response during setup"); + } + + // Process UART data to check for home ID + this->process_uart_(); + + // Check if we got the home ID after processing + if (this->home_id_ready_) { + return true; + } + + // Wait up to HOME_ID_TIMEOUT_MS for home ID response + const uint32_t now = App.get_loop_component_start_time(); + if (now - this->setup_time_ > HOME_ID_TIMEOUT_MS) { + ESP_LOGW(TAG, "Timeout reading Home ID during setup"); + return true; // Proceed anyway after timeout + } + + return false; // Keep waiting +} + +void ZWaveProxy::loop() { + if (this->response_handler_()) { + ESP_LOGV(TAG, "Handled late response"); + } + if (this->api_connection_ != nullptr && (!this->api_connection_->is_connection_setup() || !api_is_connected())) { + ESP_LOGW(TAG, "Subscriber disconnected"); + this->api_connection_ = nullptr; // Unsubscribe if disconnected + } + + this->process_uart_(); + this->status_clear_warning(); +} + +void ZWaveProxy::process_uart_() { + while (this->available()) { + uint8_t byte; + if (!this->read_byte(&byte)) { + this->status_set_warning("UART read failed"); + return; + } + if (this->parse_byte_(byte)) { + // Check if this is a GET_NETWORK_IDS response frame + // Frame format: [SOF][LENGTH][TYPE][CMD][HOME_ID(4)][NODE_ID][...] + // We verify: + // - buffer_[0]: Start of frame marker (0x01) + // - buffer_[1]: Length field must be >= 9 to contain all required data + // - buffer_[2]: Command type (0x01 for response) + // - buffer_[3]: Command ID (0x20 for GET_NETWORK_IDS) + if (this->buffer_[3] == ZWAVE_COMMAND_GET_NETWORK_IDS && this->buffer_[2] == ZWAVE_COMMAND_TYPE_RESPONSE && + this->buffer_[1] >= ZWAVE_MIN_GET_NETWORK_IDS_LENGTH && this->buffer_[0] == ZWAVE_FRAME_TYPE_START) { + // Store the 4-byte Home ID, which starts at offset 4, and notify connected clients if it changed + // The frame parser has already validated the checksum and ensured all bytes are present + if (this->set_home_id(&this->buffer_[4])) { + this->send_homeid_changed_msg_(); + } + } + ESP_LOGV(TAG, "Sending to client: %s", YESNO(this->api_connection_ != nullptr)); + if (this->api_connection_ != nullptr) { + // Zero-copy: point directly to our buffer + this->outgoing_proto_msg_.data = this->buffer_.data(); + if (this->in_bootloader_) { + this->outgoing_proto_msg_.data_len = this->buffer_index_; + } else { + // If this is a data frame, use frame length indicator + 2 (for SoF + checksum), else assume 1 for ACK/NAK/CAN + this->outgoing_proto_msg_.data_len = this->buffer_[0] == ZWAVE_FRAME_TYPE_START ? this->buffer_[1] + 2 : 1; + } + this->api_connection_->send_message(this->outgoing_proto_msg_, api::ZWaveProxyFrame::MESSAGE_TYPE); + } + } + } +} + +void ZWaveProxy::dump_config() { + ESP_LOGCONFIG(TAG, + "Z-Wave Proxy:\n" + " Home ID: %s", + format_hex_pretty(this->home_id_.data(), this->home_id_.size(), ':', false).c_str()); +} + +void ZWaveProxy::api_connection_authenticated(api::APIConnection *conn) { + if (this->home_id_ready_) { + // If a client just authenticated & HomeID is ready, send the current HomeID + this->send_homeid_changed_msg_(conn); + } +} + +void ZWaveProxy::zwave_proxy_request(api::APIConnection *api_connection, api::enums::ZWaveProxyRequestType type) { + switch (type) { + case api::enums::ZWAVE_PROXY_REQUEST_TYPE_SUBSCRIBE: + if (this->api_connection_ != nullptr) { + ESP_LOGE(TAG, "Only one API subscription is allowed at a time"); + return; + } + this->api_connection_ = api_connection; + ESP_LOGV(TAG, "API connection is now subscribed"); + break; + case api::enums::ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE: + if (this->api_connection_ != api_connection) { + ESP_LOGV(TAG, "API connection is not subscribed"); + return; + } + this->api_connection_ = nullptr; + break; + default: + ESP_LOGW(TAG, "Unknown request type: %d", type); + break; + } +} + +bool ZWaveProxy::set_home_id(const uint8_t *new_home_id) { + if (std::memcmp(this->home_id_.data(), new_home_id, this->home_id_.size()) == 0) { + ESP_LOGV(TAG, "Home ID unchanged"); + return false; // No change + } + std::memcpy(this->home_id_.data(), new_home_id, this->home_id_.size()); + ESP_LOGI(TAG, "Home ID: %s", format_hex_pretty(this->home_id_.data(), this->home_id_.size(), ':', false).c_str()); + this->home_id_ready_ = true; + return true; // Home ID was changed +} + +void ZWaveProxy::send_frame(const uint8_t *data, size_t length) { + if (length == 1 && data[0] == this->last_response_) { + ESP_LOGV(TAG, "Skipping sending duplicate response: 0x%02X", data[0]); + return; + } + ESP_LOGVV(TAG, "Sending: %s", format_hex_pretty(data, length).c_str()); + this->write_array(data, length); +} + +void ZWaveProxy::send_homeid_changed_msg_(api::APIConnection *conn) { + api::ZWaveProxyRequest msg; + msg.type = api::enums::ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE; + msg.data = this->home_id_.data(); + msg.data_len = this->home_id_.size(); + if (conn != nullptr) { + // Send to specific connection + conn->send_message(msg, api::ZWaveProxyRequest::MESSAGE_TYPE); + } else if (api::global_api_server != nullptr) { + // We could add code to manage a second subscription type, but, since this message is + // very infrequent and small, we simply send it to all clients + api::global_api_server->on_zwave_proxy_request(msg); + } +} + +void ZWaveProxy::send_simple_command_(const uint8_t command_id) { + // Send a simple Z-Wave command with no parameters + // Frame format: [SOF][LENGTH][TYPE][CMD][CHECKSUM] + // Where LENGTH=0x03 (3 bytes: TYPE + CMD + CHECKSUM) + uint8_t cmd[] = {0x01, 0x03, 0x00, command_id, 0x00}; + cmd[4] = calculate_frame_checksum(cmd, sizeof(cmd)); + this->send_frame(cmd, sizeof(cmd)); +} + +bool ZWaveProxy::parse_byte_(uint8_t byte) { + bool frame_completed = false; + // Basic parsing logic for received frames + switch (this->parsing_state_) { + case ZWAVE_PARSING_STATE_WAIT_START: + this->parse_start_(byte); + break; + case ZWAVE_PARSING_STATE_WAIT_LENGTH: + if (!byte) { + ESP_LOGW(TAG, "Invalid LENGTH: %u", byte); + this->parsing_state_ = ZWAVE_PARSING_STATE_SEND_NAK; + return false; + } + ESP_LOGVV(TAG, "Received LENGTH: %u", byte); + this->end_frame_after_ = this->buffer_index_ + byte; + ESP_LOGVV(TAG, "Calculated EOF: %u", this->end_frame_after_); + this->buffer_[this->buffer_index_++] = byte; + this->parsing_state_ = ZWAVE_PARSING_STATE_WAIT_TYPE; + break; + case ZWAVE_PARSING_STATE_WAIT_TYPE: + this->buffer_[this->buffer_index_++] = byte; + ESP_LOGVV(TAG, "Received TYPE: 0x%02X", byte); + this->parsing_state_ = ZWAVE_PARSING_STATE_WAIT_COMMAND_ID; + break; + case ZWAVE_PARSING_STATE_WAIT_COMMAND_ID: + this->buffer_[this->buffer_index_++] = byte; + ESP_LOGVV(TAG, "Received COMMAND ID: 0x%02X", byte); + this->parsing_state_ = ZWAVE_PARSING_STATE_WAIT_PAYLOAD; + break; + case ZWAVE_PARSING_STATE_WAIT_PAYLOAD: + this->buffer_[this->buffer_index_++] = byte; + ESP_LOGVV(TAG, "Received PAYLOAD: 0x%02X", byte); + if (this->buffer_index_ >= this->end_frame_after_) { + this->parsing_state_ = ZWAVE_PARSING_STATE_WAIT_CHECKSUM; + } + break; + case ZWAVE_PARSING_STATE_WAIT_CHECKSUM: { + this->buffer_[this->buffer_index_++] = byte; + auto checksum = calculate_frame_checksum(this->buffer_.data(), this->buffer_index_); + ESP_LOGVV(TAG, "CHECKSUM Received: 0x%02X - Calculated: 0x%02X", byte, checksum); + if (checksum != byte) { + ESP_LOGW(TAG, "Bad checksum: expected 0x%02X, got 0x%02X", checksum, byte); + this->parsing_state_ = ZWAVE_PARSING_STATE_SEND_NAK; + } else { + this->parsing_state_ = ZWAVE_PARSING_STATE_SEND_ACK; + ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(this->buffer_.data(), this->buffer_index_).c_str()); + frame_completed = true; + } + this->response_handler_(); + break; + } + case ZWAVE_PARSING_STATE_READ_BL_MENU: + this->buffer_[this->buffer_index_++] = byte; + if (!byte) { + this->parsing_state_ = ZWAVE_PARSING_STATE_WAIT_START; + frame_completed = true; + } + break; + case ZWAVE_PARSING_STATE_SEND_ACK: + case ZWAVE_PARSING_STATE_SEND_NAK: + break; // Should not happen, handled in loop() + default: + ESP_LOGW(TAG, "Bad parsing state; resetting"); + this->parsing_state_ = ZWAVE_PARSING_STATE_WAIT_START; + break; + } + return frame_completed; +} + +void ZWaveProxy::parse_start_(uint8_t byte) { + this->buffer_index_ = 0; + this->parsing_state_ = ZWAVE_PARSING_STATE_WAIT_START; + switch (byte) { + case ZWAVE_FRAME_TYPE_START: + ESP_LOGVV(TAG, "Received START"); + if (this->in_bootloader_) { + ESP_LOGD(TAG, "Exited bootloader mode"); + this->in_bootloader_ = false; + } + this->buffer_[this->buffer_index_++] = byte; + this->parsing_state_ = ZWAVE_PARSING_STATE_WAIT_LENGTH; + return; + case ZWAVE_FRAME_TYPE_BL_MENU: + ESP_LOGVV(TAG, "Received BL_MENU"); + if (!this->in_bootloader_) { + ESP_LOGD(TAG, "Entered bootloader mode"); + this->in_bootloader_ = true; + } + this->buffer_[this->buffer_index_++] = byte; + this->parsing_state_ = ZWAVE_PARSING_STATE_READ_BL_MENU; + return; + case ZWAVE_FRAME_TYPE_BL_BEGIN_UPLOAD: + ESP_LOGVV(TAG, "Received BL_BEGIN_UPLOAD"); + break; + case ZWAVE_FRAME_TYPE_ACK: + ESP_LOGVV(TAG, "Received ACK"); + break; + case ZWAVE_FRAME_TYPE_NAK: + ESP_LOGW(TAG, "Received NAK"); + break; + case ZWAVE_FRAME_TYPE_CAN: + ESP_LOGW(TAG, "Received CAN"); + break; + default: + ESP_LOGW(TAG, "Unrecognized START: 0x%02X", byte); + return; + } + // Forward response (ACK/NAK/CAN) back to client for processing + if (this->api_connection_ != nullptr) { + // Store single byte in buffer and point to it + this->buffer_[0] = byte; + this->outgoing_proto_msg_.data = this->buffer_.data(); + this->outgoing_proto_msg_.data_len = 1; + this->api_connection_->send_message(this->outgoing_proto_msg_, api::ZWaveProxyFrame::MESSAGE_TYPE); + } +} + +bool ZWaveProxy::response_handler_() { + switch (this->parsing_state_) { + case ZWAVE_PARSING_STATE_SEND_ACK: + this->last_response_ = ZWAVE_FRAME_TYPE_ACK; + break; + case ZWAVE_PARSING_STATE_SEND_CAN: + this->last_response_ = ZWAVE_FRAME_TYPE_CAN; + break; + case ZWAVE_PARSING_STATE_SEND_NAK: + this->last_response_ = ZWAVE_FRAME_TYPE_NAK; + break; + default: + return false; // No response handled + } + + ESP_LOGVV(TAG, "Sending %s (0x%02X)", this->last_response_ == ZWAVE_FRAME_TYPE_ACK ? "ACK" : "NAK/CAN", + this->last_response_); + this->write_byte(this->last_response_); + this->parsing_state_ = ZWAVE_PARSING_STATE_WAIT_START; + return true; +} + +ZWaveProxy *global_zwave_proxy = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +} // namespace zwave_proxy +} // namespace esphome diff --git a/esphome/components/zwave_proxy/zwave_proxy.h b/esphome/components/zwave_proxy/zwave_proxy.h new file mode 100644 index 0000000000..20d9090d98 --- /dev/null +++ b/esphome/components/zwave_proxy/zwave_proxy.h @@ -0,0 +1,93 @@ +#pragma once + +#include "esphome/components/api/api_connection.h" +#include "esphome/components/api/api_pb2.h" +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "esphome/components/uart/uart.h" + +#include + +namespace esphome { +namespace zwave_proxy { + +static constexpr size_t MAX_ZWAVE_FRAME_SIZE = 257; // Maximum Z-Wave frame size + +enum ZWaveResponseTypes : uint8_t { + ZWAVE_FRAME_TYPE_ACK = 0x06, + ZWAVE_FRAME_TYPE_CAN = 0x18, + ZWAVE_FRAME_TYPE_NAK = 0x15, + ZWAVE_FRAME_TYPE_START = 0x01, + ZWAVE_FRAME_TYPE_BL_MENU = 0x0D, + ZWAVE_FRAME_TYPE_BL_BEGIN_UPLOAD = 0x43, +}; + +enum ZWaveParsingState : uint8_t { + ZWAVE_PARSING_STATE_WAIT_START, + ZWAVE_PARSING_STATE_WAIT_LENGTH, + ZWAVE_PARSING_STATE_WAIT_TYPE, + ZWAVE_PARSING_STATE_WAIT_COMMAND_ID, + ZWAVE_PARSING_STATE_WAIT_PAYLOAD, + ZWAVE_PARSING_STATE_WAIT_CHECKSUM, + ZWAVE_PARSING_STATE_SEND_ACK, + ZWAVE_PARSING_STATE_SEND_CAN, + ZWAVE_PARSING_STATE_SEND_NAK, + ZWAVE_PARSING_STATE_READ_BL_MENU, +}; + +enum ZWaveProxyFeature : uint32_t { + FEATURE_ZWAVE_PROXY_ENABLED = 1 << 0, +}; + +class ZWaveProxy : public uart::UARTDevice, public Component { + public: + ZWaveProxy(); + + void setup() override; + void loop() override; + void dump_config() override; + float get_setup_priority() const override; + bool can_proceed() override; + + void api_connection_authenticated(api::APIConnection *conn); + void zwave_proxy_request(api::APIConnection *api_connection, api::enums::ZWaveProxyRequestType type); + api::APIConnection *get_api_connection() { return this->api_connection_; } + + uint32_t get_feature_flags() const { return ZWaveProxyFeature::FEATURE_ZWAVE_PROXY_ENABLED; } + uint32_t get_home_id() { + return encode_uint32(this->home_id_[0], this->home_id_[1], this->home_id_[2], this->home_id_[3]); + } + bool set_home_id(const uint8_t *new_home_id); // Store a new home ID. Returns true if it changed. + + void send_frame(const uint8_t *data, size_t length); + + protected: + void send_homeid_changed_msg_(api::APIConnection *conn = nullptr); + void send_simple_command_(uint8_t command_id); + bool parse_byte_(uint8_t byte); // Returns true if frame parsing was completed (a frame is ready in the buffer) + void parse_start_(uint8_t byte); + bool response_handler_(); + void process_uart_(); // Process all available UART data + + // Pre-allocated message - always ready to send + api::ZWaveProxyFrame outgoing_proto_msg_; + std::array buffer_; // Fixed buffer for incoming data + std::array home_id_{0, 0, 0, 0}; // Fixed buffer for home ID + + // Pointers and 32-bit values (aligned together) + api::APIConnection *api_connection_{nullptr}; // Current subscribed client + uint32_t setup_time_{0}; // Time when setup() was called + + // 8-bit values (grouped together to minimize padding) + uint8_t buffer_index_{0}; // Index for populating the data buffer + uint8_t end_frame_after_{0}; // Payload reception ends after this index + uint8_t last_response_{0}; // Last response type sent + ZWaveParsingState parsing_state_{ZWAVE_PARSING_STATE_WAIT_START}; + bool in_bootloader_{false}; // True if the device is detected to be in bootloader mode + bool home_id_ready_{false}; // True when home ID has been received from Z-Wave module +}; + +extern ZWaveProxy *global_zwave_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +} // namespace zwave_proxy +} // namespace esphome diff --git a/esphome/config.py b/esphome/config.py index cf7a232d8e..1c4cdd93c6 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -12,7 +12,7 @@ from typing import Any import voluptuous as vol from esphome import core, loader, pins, yaml_util -from esphome.config_helpers import Extend, Remove +from esphome.config_helpers import Extend, Remove, merge_config, merge_dicts_ordered import esphome.config_validation as cv from esphome.const import ( CONF_ESPHOME, @@ -32,7 +32,7 @@ from esphome.log import AnsiFore, color from esphome.types import ConfigFragmentType, ConfigType from esphome.util import OrderedDict, safe_print from esphome.voluptuous_schema import ExtraKeysInvalid -from esphome.yaml_util import ESPForceValue, ESPHomeDataBase, is_secret +from esphome.yaml_util import ESPHomeDataBase, ESPLiteralValue, is_secret _LOGGER = logging.getLogger(__name__) @@ -67,6 +67,31 @@ ConfigPath = list[str | int] path_context = contextvars.ContextVar("Config path") +def _add_auto_load_steps(result: Config, loads: list[str]) -> None: + """Add AutoLoadValidationStep for each component in loads that isn't already loaded.""" + for load in loads: + if load not in result: + result.add_validation_step(AutoLoadValidationStep(load)) + + +def _process_auto_load( + result: Config, platform: ComponentManifest, path: ConfigPath +) -> None: + # Process platform's AUTO_LOAD + auto_load = platform.auto_load + if isinstance(auto_load, list): + _add_auto_load_steps(result, auto_load) + elif callable(auto_load): + import inspect + + if inspect.signature(auto_load).parameters: + result.add_validation_step( + AddDynamicAutoLoadsValidationStep(path, platform) + ) + else: + _add_auto_load_steps(result, auto_load()) + + def _process_platform_config( result: Config, component_name: str, @@ -91,9 +116,7 @@ def _process_platform_config( CORE.loaded_platforms.add(f"{component_name}/{platform_name}") # Process platform's AUTO_LOAD - for load in platform.auto_load: - if load not in result: - result.add_validation_step(AutoLoadValidationStep(load)) + _process_auto_load(result, platform, path) # Add validation steps for the platform p_domain = f"{component_name}.{platform_name}" @@ -296,27 +319,118 @@ def iter_ids(config, path=None): yield from iter_ids(item, path + [i]) elif isinstance(config, dict): for key, value in config.items(): + if len(path) == 0 and key == CONF_SUBSTITUTIONS: + # Ignore IDs in substitution definitions. + continue if isinstance(key, core.ID): yield key, path yield from iter_ids(value, path + [key]) -def recursive_check_replaceme(value): - if isinstance(value, list): - return cv.Schema([recursive_check_replaceme])(value) - if isinstance(value, dict): - return cv.Schema({cv.valid: recursive_check_replaceme})(value) - if isinstance(value, ESPForceValue): - pass +def check_replaceme(value): if isinstance(value, str) and value == "REPLACEME": raise cv.Invalid( "Found 'REPLACEME' in configuration, this is most likely an error. " "Please make sure you have replaced all fields from the sample " "configuration.\n" "If you want to use the literal REPLACEME string, " - 'please use "!force REPLACEME"' + 'please use "!literal REPLACEME"' ) - return value + + +def _get_item_id(item: Any) -> str | Extend | Remove | None: + """Attempts to get a list item's ID""" + if not isinstance(item, dict): + return None # not a dict, can't have ID + # 1.- Check regular case: + # - id: my_id + item_id = item.get(CONF_ID) + if item_id is None and len(item) == 1: + # 2.- Check single-key dict case: + # - obj: + # id: my_id + item = next(iter(item.values())) + if isinstance(item, dict): + item_id = item.get(CONF_ID) + if isinstance(item_id, Extend): + # Remove instances of Extend so they don't overwrite the original item when merging: + del item[CONF_ID] + elif not isinstance(item_id, (str, Remove)): + return None + return item_id + + +def _build_list_index( + lst: list[Any], +) -> tuple[ + OrderedDict[str | Extend | Remove, Any], list[tuple[int, str, Any]], set[str] +]: + index = OrderedDict() + extensions, removals = [], set() + for pos, item in enumerate(lst): + if item is None: + removals.add(None) + continue + item_id = _get_item_id(item) + if isinstance(item_id, Extend): + extensions.append((pos, item_id.value, item)) + continue + if isinstance(item_id, Remove): + removals.add(item_id.value) + continue + if not item_id or item_id in index: + # no id or duplicate -> pass through with identity-based key + item_id = id(item) + index[item_id] = item + return index, extensions, removals + + +def resolve_extend_remove(value: Any, is_key: bool = False) -> None: + if isinstance(value, ESPLiteralValue): + return # do not check inside literal blocks + if isinstance(value, list): + index, extensions, removals = _build_list_index(value) + if extensions or removals: + # Rebuild the original list after + # processing all extensions and removals + for pos, item_id, item in extensions: + if item_id in removals: + continue + old = index.get(item_id) + if old is None: + # Failed to find source for extension + with cv.prepend_path(pos): + raise cv.Invalid( + f"Source for extension of ID '{item_id}' was not found." + ) + index[item_id] = merge_config(old, item) + for item_id in removals: + index.pop(item_id, None) + + value[:] = index.values() + + for i, item in enumerate(value): + with cv.prepend_path(i): + resolve_extend_remove(item, False) + return + if isinstance(value, dict): + removals = [] + for k, v in value.items(): + with cv.prepend_path(k): + if isinstance(v, Remove): + removals.append(k) + continue + resolve_extend_remove(k, True) + resolve_extend_remove(v, False) + for k in removals: + value.pop(k, None) + return + if is_key: + return # do not check keys (yet) + + check_replaceme(value) + + return class ConfigValidationStep(abc.ABC): @@ -382,11 +496,15 @@ class LoadValidationStep(ConfigValidationStep): result.add_str_error(f"Component not found: {self.domain}", path) return CORE.loaded_integrations.add(self.domain) + # For platform components, normalize conf before creating MetadataValidationStep + if component.is_platform_component: + if not self.conf: + result[self.domain] = self.conf = [] + elif not isinstance(self.conf, list): + result[self.domain] = self.conf = [self.conf] # Process AUTO_LOAD - for load in component.auto_load: - if load not in result: - result.add_validation_step(AutoLoadValidationStep(load)) + _process_auto_load(result, component, path) result.add_validation_step( MetadataValidationStep([self.domain], self.domain, self.conf, component) @@ -399,12 +517,6 @@ class LoadValidationStep(ConfigValidationStep): # Remove this is as an output path result.remove_output_path([self.domain], self.domain) - # Ensure conf is a list - if not self.conf: - result[self.domain] = self.conf = [] - elif not isinstance(self.conf, list): - result[self.domain] = self.conf = [self.conf] - for i, p_config in enumerate(self.conf): path = [self.domain, i] # Construct temporary unknown output path @@ -416,19 +528,6 @@ class LoadValidationStep(ConfigValidationStep): continue p_name = p_config.get("platform") if p_name is None: - p_id = p_config.get(CONF_ID) - if isinstance(p_id, Extend): - result.add_str_error( - f"Source for extension of ID '{p_id.value}' was not found.", - path + [CONF_ID], - ) - continue - if isinstance(p_id, Remove): - result.add_str_error( - f"Source for removal of ID '{p_id.value}' was not found.", - path + [CONF_ID], - ) - continue result.add_str_error( f"'{self.domain}' requires a 'platform' key but it was not specified.", path, @@ -618,6 +717,34 @@ class MetadataValidationStep(ConfigValidationStep): result.add_validation_step(FinalValidateValidationStep(self.path, self.comp)) +class AddDynamicAutoLoadsValidationStep(ConfigValidationStep): + """Add dynamic auto loads step. + + This step is used to auto-load components where one component can alter its + AUTO_LOAD based on its configuration. + """ + + # Has to happen after normal schema is validated and before final schema validation + priority = -5.0 + + def __init__(self, path: ConfigPath, comp: ComponentManifest) -> None: + self.path = path + self.comp = comp + + def run(self, result: Config) -> None: + if result.errors: + # If result already has errors, skip this step + return + + conf = result.get_nested_item(self.path) + with result.catch_error(self.path): + auto_load = self.comp.auto_load + if not callable(auto_load): + return + loads = auto_load(conf) + _add_auto_load_steps(result, loads) + + class SchemaValidationStep(ConfigValidationStep): """Schema validation step. @@ -627,13 +754,15 @@ class SchemaValidationStep(ConfigValidationStep): def __init__( self, domain: str, path: ConfigPath, conf: ConfigType, comp: ComponentManifest ): + self.domain = domain self.path = path self.conf = conf self.comp = comp def run(self, result: Config) -> None: token = path_context.set(self.path) - with result.catch_error(self.path): + # The domain already contains the full component path (e.g., "sensor.template", "sensor.uptime") + with CORE.component_context(self.domain), result.catch_error(self.path): if self.comp.is_platform: # Remove 'platform' key for validation input_conf = OrderedDict(self.conf) @@ -844,7 +973,9 @@ class PinUseValidationCheck(ConfigValidationStep): def validate_config( - config: dict[str, Any], command_line_substitutions: dict[str, Any] + config: dict[str, Any], + command_line_substitutions: dict[str, Any], + skip_external_update: bool = False, ) -> Config: result = Config() @@ -857,7 +988,7 @@ def validate_config( result.add_output_path([CONF_PACKAGES], CONF_PACKAGES) try: - config = do_packages_pass(config) + config = do_packages_pass(config, skip_update=skip_external_update) except vol.Invalid as err: result.update(config) result.add_error(err) @@ -869,10 +1000,9 @@ def validate_config( if CONF_SUBSTITUTIONS in config or command_line_substitutions: from esphome.components import substitutions - result[CONF_SUBSTITUTIONS] = { - **(config.get(CONF_SUBSTITUTIONS) or {}), - **command_line_substitutions, - } + result[CONF_SUBSTITUTIONS] = merge_dicts_ordered( + config.get(CONF_SUBSTITUTIONS) or {}, command_line_substitutions + ) result.add_output_path([CONF_SUBSTITUTIONS], CONF_SUBSTITUTIONS) try: substitutions.do_substitution_pass(config, command_line_substitutions) @@ -882,9 +1012,10 @@ def validate_config( CORE.raw_config = config - # 1.1. Check for REPLACEME special value + # 1.1. Resolve !extend and !remove and check for REPLACEME + # After this step, there will not be any Extend or Remove values in the config anymore try: - recursive_check_replaceme(config) + resolve_extend_remove(config) except vol.Invalid as err: result.add_error(err) @@ -894,7 +1025,7 @@ def validate_config( result.add_output_path([CONF_EXTERNAL_COMPONENTS], CONF_EXTERNAL_COMPONENTS) try: - do_external_components_pass(config) + do_external_components_pass(config, skip_update=skip_external_update) except vol.Invalid as err: result.update(config) result.add_error(err) @@ -940,6 +1071,9 @@ def validate_config( # do not try to validate further as we don't know what the target is return result + # Reset the pin registry so that any target platforms with pin validations do not get the duplicate pin warning. + pins.PIN_SCHEMA_REGISTRY.reset() + for domain, conf in config.items(): result.add_validation_step(LoadValidationStep(domain, conf)) result.add_validation_step(IDPassValidationStep()) @@ -1015,7 +1149,9 @@ class InvalidYAMLError(EsphomeError): self.base_exc = base_exc -def _load_config(command_line_substitutions: dict[str, Any]) -> Config: +def _load_config( + command_line_substitutions: dict[str, Any], skip_external_update: bool = False +) -> Config: """Load the configuration file.""" try: config = yaml_util.load_yaml(CORE.config_path) @@ -1023,7 +1159,7 @@ def _load_config(command_line_substitutions: dict[str, Any]) -> Config: raise InvalidYAMLError(e) from e try: - return validate_config(config, command_line_substitutions) + return validate_config(config, command_line_substitutions, skip_external_update) except EsphomeError: raise except Exception: @@ -1031,9 +1167,11 @@ def _load_config(command_line_substitutions: dict[str, Any]) -> Config: raise -def load_config(command_line_substitutions: dict[str, Any]) -> Config: +def load_config( + command_line_substitutions: dict[str, Any], skip_external_update: bool = False +) -> Config: try: - return _load_config(command_line_substitutions) + return _load_config(command_line_substitutions, skip_external_update) except vol.Invalid as err: raise EsphomeError(f"Error while parsing config: {err}") from err @@ -1173,10 +1311,10 @@ def strip_default_ids(config): return config -def read_config(command_line_substitutions): +def read_config(command_line_substitutions, skip_external_update=False): _LOGGER.info("Reading configuration %s...", CORE.config_path) try: - res = load_config(command_line_substitutions) + res = load_config(command_line_substitutions, skip_external_update) except EsphomeError as err: _LOGGER.error("Error while reading config: %s", err) return None diff --git a/esphome/config_helpers.py b/esphome/config_helpers.py index 00cd8f9818..c0a3b99968 100644 --- a/esphome/config_helpers.py +++ b/esphome/config_helpers.py @@ -1,7 +1,6 @@ from collections.abc import Callable from esphome.const import ( - CONF_ID, CONF_LEVEL, CONF_LOGGER, KEY_CORE, @@ -10,6 +9,7 @@ from esphome.const import ( PlatformFramework, ) from esphome.core import CORE +from esphome.util import OrderedDict # Pre-build lookup map from (platform, framework) tuples to PlatformFramework enum _PLATFORM_FRAMEWORK_LOOKUP = { @@ -17,6 +17,25 @@ _PLATFORM_FRAMEWORK_LOOKUP = { } +def merge_dicts_ordered(*dicts: dict) -> OrderedDict: + """Merge multiple dicts into an OrderedDict, preserving key order. + + This is a helper to ensure that dictionary merging preserves OrderedDict type, + which is important for operations like move_to_end(). + + Args: + *dicts: Variable number of dictionaries to merge (later dicts override earlier ones) + + Returns: + OrderedDict with merged contents + """ + result = OrderedDict() + for d in dicts: + if d: + result.update(d) + return result + + class Extend: def __init__(self, value): self.value = value @@ -55,69 +74,28 @@ class Remove: return isinstance(b, Remove) and self.value == b.value -def merge_config(full_old, full_new): - def merge(old, new): - if isinstance(new, dict): - if not isinstance(old, dict): - return new - res = old.copy() - for k, v in new.items(): - if isinstance(v, Remove) and k in old: - del res[k] - else: - res[k] = merge(old[k], v) if k in old else v - return res - if isinstance(new, list): - if not isinstance(old, list): - return new - res = old.copy() - ids = { - v_id: i - for i, v in enumerate(res) - if isinstance(v, dict) - and (v_id := v.get(CONF_ID)) - and isinstance(v_id, str) - } - extend_ids = { - v_id.value: i - for i, v in enumerate(res) - if isinstance(v, dict) - and (v_id := v.get(CONF_ID)) - and isinstance(v_id, Extend) - } - - ids_to_delete = [] - for v in new: - if isinstance(v, dict) and (new_id := v.get(CONF_ID)): - if isinstance(new_id, Extend): - new_id = new_id.value - if new_id in ids: - v[CONF_ID] = new_id - res[ids[new_id]] = merge(res[ids[new_id]], v) - continue - elif isinstance(new_id, Remove): - new_id = new_id.value - if new_id in ids: - ids_to_delete.append(ids[new_id]) - continue - elif ( - new_id in extend_ids - ): # When a package is extending a non-packaged item - extend_res = res[extend_ids[new_id]] - extend_res[CONF_ID] = new_id - new_v = merge(v, extend_res) - res[extend_ids[new_id]] = new_v - continue - else: - ids[new_id] = len(res) - res.append(v) - return [v for i, v in enumerate(res) if i not in ids_to_delete] - if new is None: - return old - +def merge_config(old, new): + if isinstance(new, Remove): return new + if isinstance(new, dict): + if not isinstance(old, dict): + return new + # Preserve OrderedDict type by copying to OrderedDict if either input is OrderedDict + if isinstance(old, OrderedDict) or isinstance(new, OrderedDict): + res = OrderedDict(old) + else: + res = old.copy() + for k, v in new.items(): + res[k] = merge_config(old.get(k), v) + return res + if isinstance(new, list): + if not isinstance(old, list): + return new + return old + new + if new is None: + return old - return merge(full_old, full_new) + return new def filter_source_files_from_platform( diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 9aaeb9f9e8..a3fd271a86 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Callable from contextlib import contextmanager, suppress from dataclasses import dataclass from datetime import datetime @@ -15,16 +16,16 @@ from ipaddress import ( ip_network, ) import logging -import os +from pathlib import Path import re from string import ascii_letters, digits +import typing import uuid as uuid_ import voluptuous as vol from esphome import core import esphome.codegen as cg -from esphome.config_helpers import Extend, Remove from esphome.const import ( ALLOWED_NAME_CHARS, CONF_AVAILABILITY, @@ -244,6 +245,20 @@ RESERVED_IDS = [ "uart0", "uart1", "uart2", + # ESP32 ROM functions + "crc16_be", + "crc16_le", + "crc32_be", + "crc32_le", + "crc8_be", + "crc8_le", + "dbg_state", + "debug_timer", + "one_bits", + "recv_packet", + "send_packet", + "check_pos", + "software_reset", ] @@ -393,10 +408,13 @@ def icon(value): ) -def sub_device_id(value: str | None) -> core.ID: +def sub_device_id(value: str | None) -> core.ID | None: # Lazy import to avoid circular imports from esphome.core.config import Device + if not value: + return None + return use_id(Device)(value) @@ -607,12 +625,6 @@ def declare_id(type): if value is None: return core.ID(None, is_declaration=True, type=type) - if isinstance(value, Extend): - raise Invalid(f"Source for extension of ID '{value.value}' was not found.") - - if isinstance(value, Remove): - raise Invalid(f"Source for Removal of ID '{value.value}' was not found.") - return core.ID(validate_id_name(value), is_declaration=True, type=type) return validator @@ -1109,8 +1121,8 @@ voltage = float_with_unit("voltage", "(v|V|volt|Volts)?") distance = float_with_unit("distance", "(m)") framerate = float_with_unit("framerate", "(FPS|fps|Fps|FpS|Hz)") angle = float_with_unit("angle", "(°|deg)", optional_unit=True) -_temperature_c = float_with_unit("temperature", "(°C|° C|°|C)?") -_temperature_k = float_with_unit("temperature", "(° K|° K|K)?") +_temperature_c = float_with_unit("temperature", "(°C|° C|C|°)?") +_temperature_k = float_with_unit("temperature", "(°K|° K|K)?") _temperature_f = float_with_unit("temperature", "(°F|° F|F)?") decibel = float_with_unit("decibel", "(dB|dBm|db|dbm)", optional_unit=True) pressure = float_with_unit("pressure", "(bar|Bar)", optional_unit=True) @@ -1192,6 +1204,13 @@ def validate_bytes(value): def hostname(value): + """Validate that the value is a valid hostname. + + Maximum length is 63 characters per RFC 1035. + + Note: If this limit is changed, update MAX_NAME_WITH_SUFFIX_SIZE in + esphome/core/helpers.cpp to accommodate the new maximum length. + """ value = string(value) if re.match(r"^[a-z0-9-]{1,63}$", value, re.IGNORECASE) is not None: return value @@ -1606,34 +1625,32 @@ def dimensions(value): return dimensions([match.group(1), match.group(2)]) -def directory(value): +def directory(value: object) -> Path: value = string(value) path = CORE.relative_config_path(value) - if not os.path.exists(path): + if not path.exists(): raise Invalid( - f"Could not find directory '{path}'. Please make sure it exists (full path: {os.path.abspath(path)})." + f"Could not find directory '{path}'. Please make sure it exists (full path: {path.resolve()})." ) - if not os.path.isdir(path): + if not path.is_dir(): raise Invalid( - f"Path '{path}' is not a directory (full path: {os.path.abspath(path)})." + f"Path '{path}' is not a directory (full path: {path.resolve()})." ) - return value + return path -def file_(value): +def file_(value: object) -> Path: value = string(value) path = CORE.relative_config_path(value) - if not os.path.exists(path): + if not path.exists(): raise Invalid( - f"Could not find file '{path}'. Please make sure it exists (full path: {os.path.abspath(path)})." + f"Could not find file '{path}'. Please make sure it exists (full path: {path.resolve()})." ) - if not os.path.isfile(path): - raise Invalid( - f"Path '{path}' is not a file (full path: {os.path.abspath(path)})." - ) - return value + if not path.is_file(): + raise Invalid(f"Path '{path}' is not a file (full path: {path.resolve()}).") + return path ENTITY_ID_CHARACTERS = "abcdefghijklmnopqrstuvwxyz0123456789_" @@ -1748,16 +1765,37 @@ class SplitDefault(Optional): class OnlyWith(Optional): - """Set the default value only if the given component is loaded.""" + """Set the default value only if the given component(s) is/are loaded. - def __init__(self, key, component, default=None): + This validator allows configuration keys to have defaults that are only applied + when specific component(s) are loaded. Supports both single component names and + lists of components. + + Args: + key: Configuration key + component: Single component name (str) or list of component names. + For lists, ALL components must be loaded for the default to apply. + default: Default value to use when condition is met + + Example: + # Single component + cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(MQTTComponent) + + # Multiple components (all must be loaded) + cv.OnlyWith(CONF_ZIGBEE_ID, ["zigbee", "nrf52"]): cv.use_id(Zigbee) + """ + + def __init__(self, key, component: str | list[str], default=None) -> None: super().__init__(key) self._component = component self._default = vol.default_factory(default) @property - def default(self): - if self._component in CORE.loaded_integrations: + def default(self) -> Callable[[], typing.Any] | vol.Undefined: + if isinstance(self._component, list): + if all(c in CORE.loaded_integrations for c in self._component): + return self._default + elif self._component in CORE.loaded_integrations: return self._default return vol.UNDEFINED @@ -2180,26 +2218,3 @@ def rename_key(old_key, new_key): return config return validator - - -# Remove before 2025.11.0 -def deprecated_schema_constant(entity_type: str): - def validator(config): - type: str = "unknown" - if (id := config.get(CONF_ID)) is not None and isinstance(id, core.ID): - type = str(id.type).split("::", maxsplit=1)[0] - _LOGGER.warning( - "Using `%s.%s_SCHEMA` is deprecated and will be removed in ESPHome 2025.11.0. " - "Please use `%s.%s_schema(...)` instead. " - "If you are seeing this, report an issue to the external_component author and ask them to update it. " - "https://developers.esphome.io/blog/2025/05/14/_schema-deprecations/. " - "Component using this schema: %s", - entity_type, - entity_type.upper(), - entity_type, - entity_type, - type, - ) - return config - - return validator diff --git a/esphome/const.py b/esphome/const.py index 7d373ff26c..2b6b60d395 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2025.8.0-dev" +__version__ = "2025.12.0-dev" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( @@ -36,7 +36,30 @@ class Framework(StrEnum): class ThreadModel(StrEnum): - """Threading model identifiers for ESPHome scheduler.""" + """Threading model identifiers for ESPHome scheduler. + + ESPHome currently uses three threading models based on platform capabilities: + + SINGLE: + - Single-threaded platforms (ESP8266, RP2040) + - No RTOS task switching + - No concurrent access to scheduler data structures + - No atomics or locks required + - Minimal overhead + + MULTI_NO_ATOMICS: + - Multi-threaded platforms without hardware atomic RMW support (e.g. LibreTiny BK7231N) + - Uses FreeRTOS or another RTOS with multiple tasks + - CPU lacks exclusive load/store instructions (ARM968E-S has no LDREX/STREX) + - std::atomic cannot provide lock-free RMW; libatomic is avoided to save flash (4–8 KB) + - Scheduler uses explicit FreeRTOS mutexes for synchronization + + MULTI_ATOMICS: + - Multi-threaded platforms with hardware atomic RMW support (ESP32, Cortex-M, Host) + - CPU provides native atomic instructions (ESP32 S32C1I, ARM LDREX/STREX) + - std::atomic is used for lock-free synchronization + - Reduced contention and better performance + """ SINGLE = "ESPHOME_THREAD_SINGLE" MULTI_NO_ATOMICS = "ESPHOME_THREAD_MULTI_NO_ATOMICS" @@ -114,6 +137,7 @@ CONF_AND = "and" CONF_ANGLE = "angle" CONF_ANY = "any" CONF_AP = "ap" +CONF_API = "api" CONF_APPARENT_POWER = "apparent_power" CONF_ARDUINO_VERSION = "arduino_version" CONF_AREA = "area" @@ -173,6 +197,7 @@ CONF_CALIBRATE_LINEAR = "calibrate_linear" CONF_CALIBRATION = "calibration" CONF_CAPACITANCE = "capacitance" CONF_CAPACITY = "capacity" +CONF_CAPTURE_RESPONSE = "capture_response" CONF_CARBON_MONOXIDE = "carbon_monoxide" CONF_CARRIER_DUTY_PERCENT = "carrier_duty_percent" CONF_CARRIER_FREQUENCY = "carrier_frequency" @@ -185,6 +210,7 @@ CONF_CHARACTERISTIC_UUID = "characteristic_uuid" CONF_CHECK = "check" CONF_CHIPSET = "chipset" CONF_CLEAN_SESSION = "clean_session" +CONF_CLEAR = "clear" CONF_CLEAR_IMPEDANCE = "clear_impedance" CONF_CLIENT_CERTIFICATE = "client_certificate" CONF_CLIENT_CERTIFICATE_KEY = "client_certificate_key" @@ -333,6 +359,7 @@ CONF_ENERGY = "energy" CONF_ENTITY_CATEGORY = "entity_category" CONF_ENTITY_ID = "entity_id" CONF_ENUM_DATAPOINT = "enum_datapoint" +CONF_ENVIRONMENT_VARIABLES = "environment_variables" CONF_EQUATION = "equation" CONF_ESP8266_DISABLE_SSL_SUPPORT = "esp8266_disable_ssl_support" CONF_ESPHOME = "esphome" @@ -424,6 +451,7 @@ CONF_HEAD = "head" CONF_HEADING = "heading" CONF_HEARTBEAT = "heartbeat" CONF_HEAT_ACTION = "heat_action" +CONF_HEAT_COOL_MODE = "heat_cool_mode" CONF_HEAT_DEADBAND = "heat_deadband" CONF_HEAT_MODE = "heat_mode" CONF_HEAT_OVERRUN = "heat_overrun" @@ -467,6 +495,7 @@ CONF_IMPORT_REACTIVE_ENERGY = "import_reactive_energy" CONF_INC_PIN = "inc_pin" CONF_INCLUDE_INTERNAL = "include_internal" CONF_INCLUDES = "includes" +CONF_INCLUDES_C = "includes_c" CONF_INDEX = "index" CONF_INDOOR = "indoor" CONF_INFRARED = "infrared" @@ -523,6 +552,7 @@ CONF_LOADED_INTEGRATIONS = "loaded_integrations" CONF_LOCAL = "local" CONF_LOCK_ACTION = "lock_action" CONF_LOG = "log" +CONF_LOG_LEVEL = "log_level" CONF_LOG_TOPIC = "log_topic" CONF_LOGGER = "logger" CONF_LOGS = "logs" @@ -538,6 +568,7 @@ CONF_MANUAL_IP = "manual_ip" CONF_MANUFACTURER_ID = "manufacturer_id" CONF_MASK_DISTURBER = "mask_disturber" CONF_MAX_BRIGHTNESS = "max_brightness" +CONF_MAX_CONNECTIONS = "max_connections" CONF_MAX_COOLING_RUN_TIME = "max_cooling_run_time" CONF_MAX_CURRENT = "max_current" CONF_MAX_DURATION = "max_duration" @@ -667,9 +698,11 @@ CONF_ON_PRESET_SET = "on_preset_set" CONF_ON_PRESS = "on_press" CONF_ON_RAW_VALUE = "on_raw_value" CONF_ON_RELEASE = "on_release" +CONF_ON_RESPONSE = "on_response" CONF_ON_SHUTDOWN = "on_shutdown" CONF_ON_SPEED_SET = "on_speed_set" CONF_ON_STATE = "on_state" +CONF_ON_SUCCESS = "on_success" CONF_ON_TAG = "on_tag" CONF_ON_TAG_REMOVED = "on_tag_removed" CONF_ON_TIME = "on_time" @@ -688,6 +721,7 @@ CONF_OPEN_DRAIN = "open_drain" CONF_OPEN_DRAIN_INTERRUPT = "open_drain_interrupt" CONF_OPEN_DURATION = "open_duration" CONF_OPEN_ENDSTOP = "open_endstop" +CONF_OPENTHREAD = "openthread" CONF_OPERATION = "operation" CONF_OPTIMISTIC = "optimistic" CONF_OPTION = "option" @@ -760,6 +794,7 @@ CONF_POSITION_COMMAND_TOPIC = "position_command_topic" CONF_POSITION_STATE_TOPIC = "position_state_topic" CONF_POWER = "power" CONF_POWER_FACTOR = "power_factor" +CONF_POWER_MODE = "power_mode" CONF_POWER_ON_VALUE = "power_on_value" CONF_POWER_SAVE_MODE = "power_save_mode" CONF_POWER_SUPPLY = "power_supply" @@ -811,6 +846,7 @@ CONF_RESET_DURATION = "reset_duration" CONF_RESET_PIN = "reset_pin" CONF_RESIZE = "resize" CONF_RESOLUTION = "resolution" +CONF_RESPONSE_TEMPLATE = "response_template" CONF_RESTART = "restart" CONF_RESTORE = "restore" CONF_RESTORE_MODE = "restore_mode" @@ -1163,7 +1199,7 @@ UNIT_KILOMETER = "km" UNIT_KILOMETER_PER_HOUR = "km/h" UNIT_KILOVOLT_AMPS = "kVA" UNIT_KILOVOLT_AMPS_HOURS = "kVAh" -UNIT_KILOVOLT_AMPS_REACTIVE = "kVAR" +UNIT_KILOVOLT_AMPS_REACTIVE = "kvar" UNIT_KILOVOLT_AMPS_REACTIVE_HOURS = "kvarh" UNIT_KILOWATT = "kW" UNIT_KILOWATT_HOURS = "kWh" @@ -1264,6 +1300,7 @@ DEVICE_CLASS_PLUG = "plug" DEVICE_CLASS_PM1 = "pm1" DEVICE_CLASS_PM10 = "pm10" DEVICE_CLASS_PM25 = "pm25" +DEVICE_CLASS_PM4 = "pm4" DEVICE_CLASS_POWER = "power" DEVICE_CLASS_POWER_FACTOR = "power_factor" DEVICE_CLASS_PRECIPITATION = "precipitation" @@ -1287,6 +1324,7 @@ DEVICE_CLASS_SULPHUR_DIOXIDE = "sulphur_dioxide" DEVICE_CLASS_SWITCH = "switch" DEVICE_CLASS_TAMPER = "tamper" DEVICE_CLASS_TEMPERATURE = "temperature" +DEVICE_CLASS_TEMPERATURE_DELTA = "temperature_delta" DEVICE_CLASS_TIMESTAMP = "timestamp" DEVICE_CLASS_UPDATE = "update" DEVICE_CLASS_VIBRATION = "vibration" @@ -1330,3 +1368,7 @@ ENTITY_CATEGORY_CONFIG = "config" # The entity category for read only diagnostic values, for example RSSI, uptime or MAC Address ENTITY_CATEGORY_DIAGNOSTIC = "diagnostic" + +# The corresponding constant exists in c++ +# when update_interval is set to never, it becomes SCHEDULER_DONT_RUN milliseconds +SCHEDULER_DONT_RUN = 4294967295 diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 39c6c3def1..08753b0f2d 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -1,7 +1,9 @@ from collections import defaultdict +from contextlib import contextmanager import logging import math import os +from pathlib import Path import re from typing import TYPE_CHECKING @@ -9,6 +11,7 @@ from esphome.const import ( CONF_COMMENT, CONF_ESPHOME, CONF_ETHERNET, + CONF_OPENTHREAD, CONF_PORT, CONF_USE_ADDRESS, CONF_WEB_SERVER, @@ -28,6 +31,7 @@ from esphome.const import ( # pylint: disable=unused-import from esphome.coroutine import ( # noqa: F401 + CoroPriority, FakeAwaitable as _FakeAwaitable, FakeEventLoop as _FakeEventLoop, coroutine, @@ -37,11 +41,16 @@ from esphome.helpers import ensure_unique_string, get_str_env, is_ha_addon from esphome.util import OrderedDict if TYPE_CHECKING: + from esphome.address_cache import AddressCache + from ..cpp_generator import MockObj, MockObjClass, Statement - from ..types import ConfigType + from ..types import ConfigType, EntityMetadata _LOGGER = logging.getLogger(__name__) +# Key for tracking controller count in CORE.data for ControllerRegistry StaticVector sizing +KEY_CONTROLLER_REGISTRY_COUNT = "controller_registry_count" + class EsphomeError(Exception): """General ESPHome exception occurred.""" @@ -379,7 +388,7 @@ class DocumentLocation: @classmethod def from_mark(cls, mark): - return cls(mark.name, mark.line, mark.column) + return cls(str(mark.name), mark.line, mark.column) def __str__(self): return f"{self.document} {self.line}:{self.column}" @@ -524,6 +533,8 @@ class EsphomeCore: self.dashboard = False # True if command is run from vscode api self.vscode = False + # True if running in testing mode (disables validation checks for grouped testing) + self.testing_mode = False # The name of the node self.name: str | None = None # The friendly name of the node @@ -534,9 +545,9 @@ class EsphomeCore: # The first key to this dict should always be the integration name self.data = {} # The relative path to the configuration YAML - self.config_path: str | None = None + self.config_path: Path | None = None # The relative path to where all build files are stored - self.build_path: str | None = None + self.build_path: Path | None = None # The validated configuration, this is None until the config has been validated self.config: ConfigType | None = None # The pending tasks in the task queue (mostly for C++ generation) @@ -571,14 +582,18 @@ class EsphomeCore: # Key: platform name (e.g. "sensor", "binary_sensor"), Value: count self.platform_counts: defaultdict[str, int] = defaultdict(int) # Track entity unique IDs to handle duplicates - # Set of (device_id, platform, sanitized_name) tuples - self.unique_ids: set[tuple[str, str, str]] = set() + # Dict mapping (device_id, platform, sanitized_name) -> entity metadata + self.unique_ids: dict[tuple[str, str, str], EntityMetadata] = {} # Whether ESPHome was started in verbose mode self.verbose = False # Whether ESPHome was started in quiet mode self.quiet = False # A list of all known ID classes self.id_classes = {} + # The current component being processed during validation + self.current_component: str | None = None + # Address cache for DNS and mDNS lookups from command line arguments + self.address_cache: AddressCache | None = None def reset(self): from esphome.pins import PIN_SCHEMA_REGISTRY @@ -604,19 +619,32 @@ class EsphomeCore: self.loaded_integrations = set() self.component_ids = set() self.platform_counts = defaultdict(int) - self.unique_ids = set() + self.unique_ids = {} + self.current_component = None + self.address_cache = None PIN_SCHEMA_REGISTRY.reset() + @contextmanager + def component_context(self, component: str): + """Context manager to set the current component being processed.""" + old_component = self.current_component + self.current_component = component + try: + yield + finally: + self.current_component = old_component + @property def address(self) -> str | None: if self.config is None: raise ValueError("Config has not been loaded yet") - if CONF_WIFI in self.config: - return self.config[CONF_WIFI][CONF_USE_ADDRESS] + for network_type in (CONF_WIFI, CONF_ETHERNET, CONF_OPENTHREAD): + if network_type in self.config: + return self.config[network_type][CONF_USE_ADDRESS] - if CONF_ETHERNET in self.config: - return self.config[CONF_ETHERNET][CONF_USE_ADDRESS] + if CONF_OPENTHREAD in self.config: + return f"{self.name}.local" return None @@ -644,43 +672,46 @@ class EsphomeCore: return None @property - def config_dir(self): - return os.path.abspath(os.path.dirname(self.config_path)) + def config_dir(self) -> Path: + if self.config_path.is_dir(): + return self.config_path.absolute() + return self.config_path.absolute().parent @property - def data_dir(self): + def data_dir(self) -> Path: if is_ha_addon(): - return os.path.join("/data") + return Path("/data") if "ESPHOME_DATA_DIR" in os.environ: - return get_str_env("ESPHOME_DATA_DIR", None) + return Path(get_str_env("ESPHOME_DATA_DIR", None)) return self.relative_config_path(".esphome") @property - def config_filename(self): - return os.path.basename(self.config_path) + def config_filename(self) -> str: + return self.config_path.name - def relative_config_path(self, *path): - path_ = os.path.expanduser(os.path.join(*path)) - return os.path.join(self.config_dir, path_) + def relative_config_path(self, *path: str | Path) -> Path: + path_ = Path(*path).expanduser() + return self.config_dir / path_ - def relative_internal_path(self, *path: str) -> str: - return os.path.join(self.data_dir, *path) + def relative_internal_path(self, *path: str | Path) -> Path: + path_ = Path(*path).expanduser() + return self.data_dir / path_ - def relative_build_path(self, *path): - path_ = os.path.expanduser(os.path.join(*path)) - return os.path.join(self.build_path, path_) + def relative_build_path(self, *path: str | Path) -> Path: + path_ = Path(*path).expanduser() + return self.build_path / path_ - def relative_src_path(self, *path): + def relative_src_path(self, *path: str | Path) -> Path: return self.relative_build_path("src", *path) - def relative_pioenvs_path(self, *path): + def relative_pioenvs_path(self, *path: str | Path) -> Path: return self.relative_build_path(".pioenvs", *path) - def relative_piolibdeps_path(self, *path): + def relative_piolibdeps_path(self, *path: str | Path) -> Path: return self.relative_build_path(".piolibdeps", *path) @property - def firmware_bin(self): + def firmware_bin(self) -> Path: if self.is_libretiny: return self.relative_pioenvs_path(self.name, "firmware.uf2") return self.relative_pioenvs_path(self.name, "firmware.bin") @@ -789,6 +820,10 @@ class EsphomeCore: raise TypeError( f"Library {library} must be instance of Library, not {type(library)}" ) + + if not library.name: + raise ValueError(f"The library for {library.repository} must have a name") + short_name = ( library.name if "/" not in library.name else library.name.split("/")[-1] ) @@ -878,6 +913,11 @@ class EsphomeCore: """ self.platform_counts[platform_name] += 1 + def register_controller(self) -> None: + """Track registration of a Controller for ControllerRegistry StaticVector sizing.""" + controller_count = self.data.setdefault(KEY_CONTROLLER_REGISTRY_COUNT, 0) + self.data[KEY_CONTROLLER_REGISTRY_COUNT] = controller_count + 1 + @property def cpp_main_section(self): from esphome.cpp_generator import statement diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 73bf13ab7c..75814ae253 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -34,37 +34,20 @@ namespace esphome { static const char *const TAG = "app"; -// Helper function for insertion sort of components by setup priority +// Helper function for insertion sort of components by priority // Using insertion sort instead of std::stable_sort saves ~1.3KB of flash // by avoiding template instantiations (std::rotate, std::stable_sort, lambdas) // IMPORTANT: This sort is stable (preserves relative order of equal elements), // which is necessary to maintain user-defined component order for same priority -template static void insertion_sort_by_setup_priority(Iterator first, Iterator last) { +template +static void insertion_sort_by_priority(Iterator first, Iterator last) { for (auto it = first + 1; it != last; ++it) { auto key = *it; - float key_priority = key->get_actual_setup_priority(); + float key_priority = (key->*GetPriority)(); auto j = it - 1; // Using '<' (not '<=') ensures stability - equal priority components keep their order - while (j >= first && (*j)->get_actual_setup_priority() < key_priority) { - *(j + 1) = *j; - j--; - } - *(j + 1) = key; - } -} - -// Helper function for insertion sort of components by loop priority -// IMPORTANT: This sort is stable (preserves relative order of equal elements), -// which is required when components are re-sorted during setup() if they block -template static void insertion_sort_by_loop_priority(Iterator first, Iterator last) { - for (auto it = first + 1; it != last; ++it) { - auto key = *it; - float key_priority = key->get_loop_priority(); - auto j = it - 1; - - // Using '<' (not '<=') ensures stability - equal priority components keep their order - while (j >= first && (*j)->get_loop_priority() < key_priority) { + while (j >= first && ((*j)->*GetPriority)() < key_priority) { *(j + 1) = *j; j--; } @@ -80,7 +63,7 @@ void Application::register_component_(Component *comp) { for (auto *c : this->components_) { if (comp == c) { - ESP_LOGW(TAG, "Component %s already registered! (%p)", c->get_component_source(), c); + ESP_LOGW(TAG, "Component %s already registered! (%p)", LOG_STR_ARG(c->get_component_log_str()), c); return; } } @@ -91,7 +74,8 @@ void Application::setup() { ESP_LOGV(TAG, "Sorting components by setup priority"); // Sort by setup priority using our helper function - insertion_sort_by_setup_priority(this->components_.begin(), this->components_.end()); + insertion_sort_by_prioritycomponents_.begin()), &Component::get_actual_setup_priority>( + this->components_.begin(), this->components_.end()); // Initialize looping_components_ early so enable_pending_loops_() works during setup this->calculate_looping_components_(); @@ -108,7 +92,8 @@ void Application::setup() { continue; // Sort components 0 through i by loop priority - insertion_sort_by_loop_priority(this->components_.begin(), this->components_.begin() + i + 1); + insertion_sort_by_prioritycomponents_.begin()), &Component::get_loop_priority>( + this->components_.begin(), this->components_.begin() + i + 1); do { uint8_t new_app_state = STATUS_LED_WARNING; @@ -137,6 +122,11 @@ void Application::setup() { // Clear setup priority overrides to free memory clear_setup_priority_overrides(); +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + // Set up wake socket for waking main loop from tasks + this->setup_wake_loop_threadsafe_(); +#endif + this->schedule_dump_config(); } void Application::loop() { @@ -256,30 +246,79 @@ void Application::run_powerdown_hooks() { void Application::teardown_components(uint32_t timeout_ms) { uint32_t start_time = millis(); - // Copy all components in reverse order using reverse iterators + // Use a StaticVector instead of std::vector to avoid heap allocation + // since we know the actual size at compile time + StaticVector pending_components; + + // Copy all components in reverse order // Reverse order matches the behavior of run_safe_shutdown_hooks() above and ensures // components are torn down in the opposite order of their setup_priority (which is // used to sort components during Application::setup()) - std::vector pending_components(this->components_.rbegin(), this->components_.rend()); + size_t num_components = this->components_.size(); + for (size_t i = 0; i < num_components; ++i) { + pending_components[i] = this->components_[num_components - 1 - i]; + } uint32_t now = start_time; - while (!pending_components.empty() && (now - start_time) < timeout_ms) { + size_t pending_count = num_components; + + // Teardown Algorithm + // ================== + // We iterate through pending components, calling teardown() on each. + // Components that return false (need more time) are copied forward + // in the array. Components that return true (finished) are skipped. + // + // The compaction happens in-place during iteration: + // - still_pending tracks the write position (where to put next pending component) + // - i tracks the read position (which component we're testing) + // - When teardown() returns false, we copy component[i] to component[still_pending] + // - When teardown() returns true, we just skip it (don't increment still_pending) + // + // Example with 4 components where B can teardown immediately: + // + // Start: + // pending_components: [A, B, C, D] + // pending_count: 4 ^----------^ + // + // Iteration 1: + // i=0: A needs more time → keep at pos 0 (no copy needed) + // i=1: B finished → skip + // i=2: C needs more time → copy to pos 1 + // i=3: D needs more time → copy to pos 2 + // + // After iteration 1: + // pending_components: [A, C, D | D] + // pending_count: 3 ^--------^ + // + // Iteration 2: + // i=0: A finished → skip + // i=1: C needs more time → copy to pos 0 + // i=2: D finished → skip + // + // After iteration 2: + // pending_components: [C | C, D, D] (positions 1-3 have old values) + // pending_count: 1 ^--^ + + while (pending_count > 0 && (now - start_time) < timeout_ms) { // Feed watchdog during teardown to prevent triggering this->feed_wdt(now); - // Use iterator to safely erase elements - for (auto it = pending_components.begin(); it != pending_components.end();) { - if ((*it)->teardown()) { - // Component finished teardown, erase it - it = pending_components.erase(it); - } else { - // Component still needs time - ++it; + // Process components and compact the array, keeping only those still pending + size_t still_pending = 0; + for (size_t i = 0; i < pending_count; ++i) { + if (!pending_components[i]->teardown()) { + // Component still needs time, copy it forward + if (still_pending != i) { + pending_components[still_pending] = pending_components[i]; + } + ++still_pending; } + // Component finished teardown, skip it (don't increment still_pending) } + pending_count = still_pending; // Give some time for I/O operations if components are still pending - if (!pending_components.empty()) { + if (pending_count > 0) { this->yield_with_select_(1); } @@ -287,12 +326,12 @@ void Application::teardown_components(uint32_t timeout_ms) { now = millis(); } - if (!pending_components.empty()) { + if (pending_count > 0) { // Note: At this point, connections are either disconnected or in a bad state, // so this warning will only appear via serial rather than being transmitted to clients - for (auto *component : pending_components) { - ESP_LOGW(TAG, "%s did not complete teardown within %" PRIu32 " ms", component->get_component_source(), - timeout_ms); + for (size_t i = 0; i < pending_count; ++i) { + ESP_LOGW(TAG, "%s did not complete teardown within %" PRIu32 " ms", + LOG_STR_ARG(pending_components[i]->get_component_log_str()), timeout_ms); } } } @@ -306,26 +345,25 @@ void Application::calculate_looping_components_() { } } - // Pre-reserve vector to avoid reallocations - this->looping_components_.reserve(total_looping); + // Initialize FixedVector with exact size - no reallocation possible + this->looping_components_.init(total_looping); // Add all components with loop override that aren't already LOOP_DONE // Some components (like logger) may call disable_loop() during initialization // before setup runs, so we need to respect their LOOP_DONE state - for (auto *obj : this->components_) { - if (obj->has_overridden_loop() && - (obj->get_component_state() & COMPONENT_STATE_MASK) != COMPONENT_STATE_LOOP_DONE) { - this->looping_components_.push_back(obj); - } - } + this->add_looping_components_by_state_(false); this->looping_components_active_end_ = this->looping_components_.size(); // Then add any components that are already LOOP_DONE to the inactive section // This handles components that called disable_loop() during initialization + this->add_looping_components_by_state_(true); +} + +void Application::add_looping_components_by_state_(bool match_loop_done) { for (auto *obj : this->components_) { if (obj->has_overridden_loop() && - (obj->get_component_state() & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE) { + ((obj->get_component_state() & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE) == match_loop_done) { this->looping_components_.push_back(obj); } } @@ -424,7 +462,7 @@ void Application::enable_pending_loops_() { // Clear the pending flag and enable the loop component->pending_enable_loop_ = false; - ESP_LOGVV(TAG, "%s loop enabled from ISR", component->get_component_source()); + ESP_LOGVV(TAG, "%s loop enabled from ISR", LOG_STR_ARG(component->get_component_log_str())); component->component_state_ &= ~COMPONENT_STATE_MASK; component->component_state_ |= COMPONENT_STATE_LOOP; @@ -439,6 +477,11 @@ void Application::enable_pending_loops_() { } void Application::before_loop_tasks_(uint32_t loop_start_time) { +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + // Drain wake notifications first to clear socket for next wake + this->drain_wake_notifications_(); +#endif + // Process scheduled tasks this->scheduler.call(loop_start_time); @@ -475,11 +518,16 @@ bool Application::register_socket_fd(int fd) { if (fd < 0) return false; +#ifndef USE_ESP32 + // Only check on non-ESP32 platforms + // On ESP32 (both Arduino and ESP-IDF), CONFIG_LWIP_MAX_SOCKETS is always <= FD_SETSIZE by design + // (LWIP_SOCKET_OFFSET = FD_SETSIZE - CONFIG_LWIP_MAX_SOCKETS per lwipopts.h) + // Other platforms may not have this guarantee if (fd >= FD_SETSIZE) { - ESP_LOGE(TAG, "Cannot monitor socket fd %d: exceeds FD_SETSIZE (%d)", fd, FD_SETSIZE); - ESP_LOGE(TAG, "Socket will not be monitored for data - may cause performance issues!"); + ESP_LOGE(TAG, "fd %d exceeds FD_SETSIZE %d", fd, FD_SETSIZE); return false; } +#endif this->socket_fds_.push_back(fd); this->socket_fds_changed_ = true; @@ -538,10 +586,11 @@ void Application::yield_with_select_(uint32_t delay_ms) { // Update fd_set if socket list has changed if (this->socket_fds_changed_) { FD_ZERO(&this->base_read_fds_); + // fd bounds are already validated in register_socket_fd() or guaranteed by platform design: + // - ESP32: LwIP guarantees fd < FD_SETSIZE by design (LWIP_SOCKET_OFFSET = FD_SETSIZE - CONFIG_LWIP_MAX_SOCKETS) + // - Other platforms: register_socket_fd() validates fd < FD_SETSIZE for (int fd : this->socket_fds_) { - if (fd >= 0 && fd < FD_SETSIZE) { - FD_SET(fd, &this->base_read_fds_); - } + FD_SET(fd, &this->base_read_fds_); } this->socket_fds_changed_ = false; } @@ -586,4 +635,73 @@ void Application::yield_with_select_(uint32_t delay_ms) { Application App; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) +void Application::setup_wake_loop_threadsafe_() { + // Create UDP socket for wake notifications + this->wake_socket_fd_ = lwip_socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (this->wake_socket_fd_ < 0) { + ESP_LOGW(TAG, "Wake socket create failed: %d", errno); + return; + } + + // Bind to loopback with auto-assigned port + struct sockaddr_in addr = {}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = lwip_htonl(INADDR_LOOPBACK); + addr.sin_port = 0; // Auto-assign port + + if (lwip_bind(this->wake_socket_fd_, (struct sockaddr *) &addr, sizeof(addr)) < 0) { + ESP_LOGW(TAG, "Wake socket bind failed: %d", errno); + lwip_close(this->wake_socket_fd_); + this->wake_socket_fd_ = -1; + return; + } + + // Get the assigned address and connect to it + // Connecting a UDP socket allows using send() instead of sendto() for better performance + struct sockaddr_in wake_addr; + socklen_t len = sizeof(wake_addr); + if (lwip_getsockname(this->wake_socket_fd_, (struct sockaddr *) &wake_addr, &len) < 0) { + ESP_LOGW(TAG, "Wake socket address failed: %d", errno); + lwip_close(this->wake_socket_fd_); + this->wake_socket_fd_ = -1; + return; + } + + // Connect to self (loopback) - allows using send() instead of sendto() + // After connect(), no need to store wake_addr - the socket remembers it + if (lwip_connect(this->wake_socket_fd_, (struct sockaddr *) &wake_addr, sizeof(wake_addr)) < 0) { + ESP_LOGW(TAG, "Wake socket connect failed: %d", errno); + lwip_close(this->wake_socket_fd_); + this->wake_socket_fd_ = -1; + return; + } + + // Set non-blocking mode + int flags = lwip_fcntl(this->wake_socket_fd_, F_GETFL, 0); + lwip_fcntl(this->wake_socket_fd_, F_SETFL, flags | O_NONBLOCK); + + // Register with application's select() loop + if (!this->register_socket_fd(this->wake_socket_fd_)) { + ESP_LOGW(TAG, "Wake socket register failed"); + lwip_close(this->wake_socket_fd_); + this->wake_socket_fd_ = -1; + return; + } +} + +void Application::wake_loop_threadsafe() { + // Called from FreeRTOS task context when events need immediate processing + // Wakes up lwip_select() in main loop by writing to connected loopback socket + if (this->wake_socket_fd_ >= 0) { + const char dummy = 1; + // Non-blocking send - if it fails (unlikely), select() will wake on timeout anyway + // No error checking needed: we control both ends of this loopback socket. + // This is safe to call from FreeRTOS tasks - send() is thread-safe in lwip + // Socket is already connected to loopback address, so send() is faster than sendto() + lwip_send(this->wake_socket_fd_, &dummy, 1, 0); + } +} +#endif // defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + } // namespace esphome diff --git a/esphome/core/application.h b/esphome/core/application.h index 4120afff53..14e800342e 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -10,6 +10,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/preferences.h" #include "esphome/core/scheduler.h" +#include "esphome/core/string_ref.h" #ifdef USE_DEVICES #include "esphome/core/device.h" @@ -20,7 +21,10 @@ #ifdef USE_SOCKET_SELECT_SUPPORT #include +#ifdef USE_WAKE_LOOP_THREADSAFE +#include #endif +#endif // USE_SOCKET_SELECT_SUPPORT #ifdef USE_BINARY_SENSOR #include "esphome/components/binary_sensor/binary_sensor.h" @@ -38,7 +42,7 @@ #include "esphome/components/text_sensor/text_sensor.h" #endif #ifdef USE_FAN -#include "esphome/components/fan/fan_state.h" +#include "esphome/components/fan/fan.h" #endif #ifdef USE_CLIMATE #include "esphome/components/climate/climate.h" @@ -101,9 +105,17 @@ class Application { arch_init(); this->name_add_mac_suffix_ = name_add_mac_suffix; if (name_add_mac_suffix) { - const std::string mac_suffix = get_mac_address().substr(6); - this->name_ = name + "-" + mac_suffix; - this->friendly_name_ = friendly_name.empty() ? "" : friendly_name + " " + mac_suffix; + // MAC address length: 12 hex chars + null terminator + constexpr size_t mac_address_len = 13; + // MAC address suffix length (last 6 characters of 12-char MAC address string) + constexpr size_t mac_address_suffix_len = 6; + char mac_addr[mac_address_len]; + get_mac_address_into_buffer(mac_addr); + const char *mac_suffix_ptr = mac_addr + mac_address_suffix_len; + this->name_ = make_name_with_suffix(name, '-', mac_suffix_ptr, mac_address_suffix_len); + if (!friendly_name.empty()) { + this->friendly_name_ = make_name_with_suffix(friendly_name, ' ', mac_suffix_ptr, mac_address_suffix_len); + } } else { this->name_ = name; this->friendly_name_ = friendly_name; @@ -248,6 +260,8 @@ class Application { bool is_name_add_mac_suffix_enabled() const { return this->name_add_mac_suffix_; } std::string get_compilation_time() const { return this->compilation_time_; } + /// Get the compilation time as StringRef (for API usage) + StringRef get_compilation_time_ref() const { return StringRef(this->compilation_time_); } /// Get the cached time in milliseconds from when the current component started its loop execution inline uint32_t IRAM_ATTR HOT get_loop_component_start_time() const { return this->loop_component_start_time_; } @@ -420,6 +434,13 @@ class Application { /// Check if there's data available on a socket without blocking /// This function is thread-safe for reading, but should be called after select() has run bool is_socket_ready(int fd) const; + +#ifdef USE_WAKE_LOOP_THREADSAFE + /// Wake the main event loop from a FreeRTOS task + /// Thread-safe, can be called from task context to immediately wake select() + /// IMPORTANT: NOT safe to call from ISR context (socket operations not ISR-safe) + void wake_loop_threadsafe(); +#endif #endif protected: @@ -428,6 +449,7 @@ class Application { void register_component_(Component *comp); void calculate_looping_components_(); + void add_looping_components_by_state_(bool match_loop_done); // These methods are called by Component::disable_loop() and Component::enable_loop() // Components should not call these directly - use this->disable_loop() or this->enable_loop() @@ -444,6 +466,11 @@ class Application { /// Perform a delay while also monitoring socket file descriptors for readiness void yield_with_select_(uint32_t delay_ms); +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + void setup_wake_loop_threadsafe_(); // Create wake notification socket + inline void drain_wake_notifications_(); // Read pending wake notifications in main loop (hot path - inlined) +#endif + // === Member variables ordered by size to minimize padding === // Pointer-sized members first @@ -468,9 +495,12 @@ class Application { // - When a component is enabled, it's swapped with the first inactive component // and active_end_ is incremented // - This eliminates branch mispredictions from flag checking in the hot loop - std::vector looping_components_{}; + FixedVector looping_components_{}; #ifdef USE_SOCKET_SELECT_SUPPORT std::vector socket_fds_; // Vector of all monitored socket file descriptors +#ifdef USE_WAKE_LOOP_THREADSAFE + int wake_socket_fd_{-1}; // Shared wake notification socket for waking main loop from tasks +#endif #endif // std::string members (typically 24-32 bytes each) @@ -587,4 +617,28 @@ class Application { /// Global storage of Application pointer - only one Application can exist. extern Application App; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) +// Inline implementations for hot-path functions +// drain_wake_notifications_() is called on every loop iteration + +// Small buffer for draining wake notification bytes (1 byte sent per wake) +// Size allows draining multiple notifications per recvfrom() without wasting stack +static constexpr size_t WAKE_NOTIFY_DRAIN_BUFFER_SIZE = 16; + +inline void Application::drain_wake_notifications_() { + // Called from main loop to drain any pending wake notifications + // Must check is_socket_ready() to avoid blocking on empty socket + if (this->wake_socket_fd_ >= 0 && this->is_socket_ready(this->wake_socket_fd_)) { + char buffer[WAKE_NOTIFY_DRAIN_BUFFER_SIZE]; + // Drain all pending notifications with non-blocking reads + // Multiple wake events may have triggered multiple writes, so drain until EWOULDBLOCK + // We control both ends of this loopback socket (always write 1 byte per wake), + // so no error checking needed - any errors indicate catastrophic system failure + while (lwip_recvfrom(this->wake_socket_fd_, buffer, sizeof(buffer), 0, nullptr, nullptr) > 0) { + // Just draining, no action needed - wake has already occurred + } + } +} +#endif // defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + } // namespace esphome diff --git a/esphome/core/automation.h b/esphome/core/automation.h index e156818312..dacadd35e8 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -4,15 +4,33 @@ #include "esphome/core/defines.h" #include "esphome/core/helpers.h" #include "esphome/core/preferences.h" +#include +#include #include #include namespace esphome { +// C++20 std::index_sequence is now used for tuple unpacking +// Legacy seq<>/gens<> pattern deprecated but kept for backwards compatibility // https://stackoverflow.com/questions/7858817/unpacking-a-tuple-to-call-a-matching-function-pointer/7858971#7858971 -template struct seq {}; // NOLINT -template struct gens : gens {}; // NOLINT -template struct gens<0, S...> { using type = seq; }; // NOLINT +// Remove before 2026.6.0 +// NOLINTBEGIN(readability-identifier-naming) +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#endif + +template struct ESPDEPRECATED("Use std::index_sequence instead. Removed in 2026.6.0", "2025.12.0") seq {}; +template +struct ESPDEPRECATED("Use std::make_index_sequence instead. Removed in 2026.6.0", "2025.12.0") gens + : gens {}; +template struct gens<0, S...> { using type = seq; }; + +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic pop +#endif +// NOLINTEND(readability-identifier-naming) #define TEMPLATABLE_VALUE_(type, name) \ protected: \ @@ -27,11 +45,20 @@ template class TemplatableValue { public: TemplatableValue() : type_(NONE) {} - template::value, int> = 0> TemplatableValue(F value) : type_(VALUE) { + template TemplatableValue(F value) requires(!std::invocable) : type_(VALUE) { new (&this->value_) T(std::move(value)); } - template::value, int> = 0> TemplatableValue(F f) : type_(LAMBDA) { + // For stateless lambdas (convertible to function pointer): use function pointer + template + TemplatableValue(F f) requires std::invocable && std::convertible_to + : type_(STATELESS_LAMBDA) { + this->stateless_f_ = f; // Implicit conversion to function pointer + } + + // For stateful lambdas (not convertible to function pointer): use std::function + template + TemplatableValue(F f) requires std::invocable &&(!std::convertible_to) : type_(LAMBDA) { this->f_ = new std::function(std::move(f)); } @@ -41,6 +68,8 @@ template class TemplatableValue { new (&this->value_) T(other.value_); } else if (type_ == LAMBDA) { this->f_ = new std::function(*other.f_); + } else if (type_ == STATELESS_LAMBDA) { + this->stateless_f_ = other.stateless_f_; } } @@ -51,6 +80,8 @@ template class TemplatableValue { } else if (type_ == LAMBDA) { this->f_ = other.f_; other.f_ = nullptr; + } else if (type_ == STATELESS_LAMBDA) { + this->stateless_f_ = other.stateless_f_; } other.type_ = NONE; } @@ -78,16 +109,23 @@ template class TemplatableValue { } else if (type_ == LAMBDA) { delete this->f_; } + // STATELESS_LAMBDA/NONE: no cleanup needed (function pointer or empty, not heap-allocated) } bool has_value() { return this->type_ != NONE; } T value(X... x) { - if (this->type_ == LAMBDA) { - return (*this->f_)(x...); + switch (this->type_) { + case STATELESS_LAMBDA: + return this->stateless_f_(x...); // Direct function pointer call + case LAMBDA: + return (*this->f_)(x...); // std::function call + case VALUE: + return this->value_; + case NONE: + default: + return T{}; } - // return value also when none - return this->type_ == VALUE ? this->value_ : T{}; } optional optional_value(X... x) { @@ -109,11 +147,13 @@ template class TemplatableValue { NONE, VALUE, LAMBDA, + STATELESS_LAMBDA, } type_; union { T value_; std::function *f_; + T (*stateless_f_)(X...); }; }; @@ -124,15 +164,15 @@ template class TemplatableValue { template class Condition { public: /// Check whether this condition passes. This condition check must be instant, and not cause any delays. - virtual bool check(Ts... x) = 0; + virtual bool check(const Ts &...x) = 0; /// Call check with a tuple of values as parameter. bool check_tuple(const std::tuple &tuple) { - return this->check_tuple_(tuple, typename gens::type()); + return this->check_tuple_(tuple, std::make_index_sequence{}); } protected: - template bool check_tuple_(const std::tuple &tuple, seq /*unused*/) { + template bool check_tuple_(const std::tuple &tuple, std::index_sequence /*unused*/) { return this->check(std::get(tuple)...); } }; @@ -142,7 +182,7 @@ template class Automation; template class Trigger { public: /// Inform the parent automation that the event has triggered. - void trigger(Ts... x) { + void trigger(const Ts &...x) { if (this->automation_parent_ == nullptr) return; this->automation_parent_->trigger(x...); @@ -170,7 +210,7 @@ template class ActionList; template class Action { public: - virtual void play_complex(Ts... x) { + virtual void play_complex(const Ts &...x) { this->num_running_++; this->play(x...); this->play_next_(x...); @@ -196,9 +236,10 @@ template class Action { protected: friend ActionList; + template friend class ContinuationAction; - virtual void play(Ts... x) = 0; - void play_next_(Ts... x) { + virtual void play(const Ts &...x) = 0; + void play_next_(const Ts &...x) { if (this->num_running_ > 0) { this->num_running_--; if (this->next_ != nullptr) { @@ -206,11 +247,11 @@ template class Action { } } } - template void play_next_tuple_(const std::tuple &tuple, seq /*unused*/) { + template void play_next_tuple_(const std::tuple &tuple, std::index_sequence /*unused*/) { this->play_next_(std::get(tuple)...); } void play_next_tuple_(const std::tuple &tuple) { - this->play_next_tuple_(tuple, typename gens::type()); + this->play_next_tuple_(tuple, std::make_index_sequence{}); } virtual void stop() {} @@ -243,16 +284,18 @@ template class ActionList { } this->actions_end_ = action; } - void add_actions(const std::vector *> &actions) { + void add_actions(const std::initializer_list *> &actions) { for (auto *action : actions) { this->add_action(action); } } - void play(Ts... x) { + void play(const Ts &...x) { if (this->actions_begin_ != nullptr) this->actions_begin_->play_complex(x...); } - void play_tuple(const std::tuple &tuple) { this->play_tuple_(tuple, typename gens::type()); } + void play_tuple(const std::tuple &tuple) { + this->play_tuple_(tuple, std::make_index_sequence{}); + } void stop() { if (this->actions_begin_ != nullptr) this->actions_begin_->stop_complex(); @@ -273,7 +316,7 @@ template class ActionList { } protected: - template void play_tuple_(const std::tuple &tuple, seq /*unused*/) { + template void play_tuple_(const std::tuple &tuple, std::index_sequence /*unused*/) { this->play(std::get(tuple)...); } @@ -286,11 +329,11 @@ template class Automation { explicit Automation(Trigger *trigger) : trigger_(trigger) { this->trigger_->set_automation_parent(this); } void add_action(Action *action) { this->actions_.add_action(action); } - void add_actions(const std::vector *> &actions) { this->actions_.add_actions(actions); } + void add_actions(const std::initializer_list *> &actions) { this->actions_.add_actions(actions); } void stop() { this->actions_.stop(); } - void trigger(Ts... x) { this->actions_.play(x...); } + void trigger(const Ts &...x) { this->actions_.play(x...); } bool is_running() { return this->actions_.is_running(); } diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index 740e10700b..e8878ac251 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -5,15 +5,19 @@ #include "esphome/core/hal.h" #include "esphome/core/defines.h" #include "esphome/core/preferences.h" +#include "esphome/core/scheduler.h" +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" +#include #include namespace esphome { template class AndCondition : public Condition { public: - explicit AndCondition(const std::vector *> &conditions) : conditions_(conditions) {} - bool check(Ts... x) override { + explicit AndCondition(std::initializer_list *> conditions) : conditions_(conditions) {} + bool check(const Ts &...x) override { for (auto *condition : this->conditions_) { if (!condition->check(x...)) return false; @@ -23,13 +27,13 @@ template class AndCondition : public Condition { } protected: - std::vector *> conditions_; + FixedVector *> conditions_; }; template class OrCondition : public Condition { public: - explicit OrCondition(const std::vector *> &conditions) : conditions_(conditions) {} - bool check(Ts... x) override { + explicit OrCondition(std::initializer_list *> conditions) : conditions_(conditions) {} + bool check(const Ts &...x) override { for (auto *condition : this->conditions_) { if (condition->check(x...)) return true; @@ -39,13 +43,13 @@ template class OrCondition : public Condition { } protected: - std::vector *> conditions_; + FixedVector *> conditions_; }; template class NotCondition : public Condition { public: explicit NotCondition(Condition *condition) : condition_(condition) {} - bool check(Ts... x) override { return !this->condition_->check(x...); } + bool check(const Ts &...x) override { return !this->condition_->check(x...); } protected: Condition *condition_; @@ -53,8 +57,8 @@ template class NotCondition : public Condition { template class XorCondition : public Condition { public: - explicit XorCondition(const std::vector *> &conditions) : conditions_(conditions) {} - bool check(Ts... x) override { + explicit XorCondition(std::initializer_list *> conditions) : conditions_(conditions) {} + bool check(const Ts &...x) override { size_t result = 0; for (auto *condition : this->conditions_) { result += condition->check(x...); @@ -64,40 +68,58 @@ template class XorCondition : public Condition { } protected: - std::vector *> conditions_; + FixedVector *> conditions_; }; template class LambdaCondition : public Condition { public: explicit LambdaCondition(std::function &&f) : f_(std::move(f)) {} - bool check(Ts... x) override { return this->f_(x...); } + bool check(const Ts &...x) override { return this->f_(x...); } protected: std::function f_; }; +/// Optimized lambda condition for stateless lambdas (no capture). +/// Uses function pointer instead of std::function to reduce memory overhead. +/// Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function). +template class StatelessLambdaCondition : public Condition { + public: + explicit StatelessLambdaCondition(bool (*f)(Ts...)) : f_(f) {} + bool check(const Ts &...x) override { return this->f_(x...); } + + protected: + bool (*f_)(Ts...); +}; + template class ForCondition : public Condition, public Component { public: explicit ForCondition(Condition<> *condition) : condition_(condition) {} TEMPLATABLE_VALUE(uint32_t, time); - void loop() override { this->check_internal(); } - float get_setup_priority() const override { return setup_priority::DATA; } - bool check_internal() { - bool cond = this->condition_->check(); - if (!cond) - this->last_inactive_ = millis(); - return cond; + void loop() override { + // Safe to use cached time - only called from Application::loop() + this->check_internal_(App.get_loop_component_start_time()); } - bool check(Ts... x) override { - if (!this->check_internal()) + float get_setup_priority() const override { return setup_priority::DATA; } + + bool check(const Ts &...x) override { + auto now = millis(); + if (!this->check_internal_(now)) return false; - return millis() - this->last_inactive_ >= this->time_.value(x...); + return now - this->last_inactive_ >= this->time_.value(x...); } protected: + bool check_internal_(uint32_t now) { + bool cond = this->condition_->check(); + if (!cond) + this->last_inactive_ = now; + return cond; + } + Condition<> *condition_; uint32_t last_inactive_{0}; }; @@ -155,14 +177,35 @@ template class DelayAction : public Action, public Compon TEMPLATABLE_VALUE(uint32_t, delay) - void play_complex(Ts... x) override { - auto f = std::bind(&DelayAction::play_next_, this, x...); + void play_complex(const Ts &...x) override { this->num_running_++; - this->set_timeout("delay", this->delay_.value(x...), f); + + // If num_running_ > 1, we have multiple instances running in parallel + // In single/restart/queued modes, only one instance runs at a time + // Parallel mode uses skip_cancel=true to allow multiple delays to coexist + // WARNING: This can accumulate delays if scripts are triggered faster than they complete! + // Users should set max_runs on parallel scripts to limit concurrent executions. + // Issue #10264: This is a workaround for parallel script delays interfering with each other. + + // Optimization: For no-argument delays (most common case), use direct lambda + // instead of std::bind to avoid bind overhead (~16 bytes heap + faster execution) + if constexpr (sizeof...(Ts) == 0) { + App.scheduler.set_timer_common_( + this, Scheduler::SchedulerItem::TIMEOUT, + /* is_static_string= */ true, "delay", this->delay_.value(), [this]() { this->play_next_(); }, + /* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1); + } else { + // For delays with arguments, use std::bind to preserve argument values + // Arguments must be copied because original references may be invalid after delay + auto f = std::bind(&DelayAction::play_next_, this, x...); + App.scheduler.set_timer_common_(this, Scheduler::SchedulerItem::TIMEOUT, + /* is_static_string= */ true, "delay", this->delay_.value(x...), std::move(f), + /* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1); + } } float get_setup_priority() const override { return setup_priority::HARDWARE; } - void play(Ts... x) override { /* ignore - see play_complex */ + void play(const Ts &...x) override { /* ignore - see play_complex */ } void stop() override { this->cancel_timeout("delay"); } @@ -172,27 +215,68 @@ template class LambdaAction : public Action { public: explicit LambdaAction(std::function &&f) : f_(std::move(f)) {} - void play(Ts... x) override { this->f_(x...); } + void play(const Ts &...x) override { this->f_(x...); } protected: std::function f_; }; +/// Optimized lambda action for stateless lambdas (no capture). +/// Uses function pointer instead of std::function to reduce memory overhead. +/// Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function). +template class StatelessLambdaAction : public Action { + public: + explicit StatelessLambdaAction(void (*f)(Ts...)) : f_(f) {} + + void play(const Ts &...x) override { this->f_(x...); } + + protected: + void (*f_)(Ts...); +}; + +/// Simple continuation action that calls play_next_ on a parent action. +/// Used internally by IfAction, WhileAction, RepeatAction, etc. to chain actions. +/// Memory: 4-8 bytes (parent pointer) vs 40 bytes (LambdaAction with std::function). +template class ContinuationAction : public Action { + public: + explicit ContinuationAction(Action *parent) : parent_(parent) {} + + void play(const Ts &...x) override { this->parent_->play_next_(x...); } + + protected: + Action *parent_; +}; + +// Forward declaration for WhileLoopContinuation +template class WhileAction; + +/// Loop continuation for WhileAction that checks condition and repeats or continues. +/// Memory: 4-8 bytes (parent pointer) vs 40 bytes (LambdaAction with std::function). +template class WhileLoopContinuation : public Action { + public: + explicit WhileLoopContinuation(WhileAction *parent) : parent_(parent) {} + + void play(const Ts &...x) override; + + protected: + WhileAction *parent_; +}; + template class IfAction : public Action { public: explicit IfAction(Condition *condition) : condition_(condition) {} - void add_then(const std::vector *> &actions) { + void add_then(const std::initializer_list *> &actions) { this->then_.add_actions(actions); - this->then_.add_action(new LambdaAction([this](Ts... x) { this->play_next_(x...); })); + this->then_.add_action(new ContinuationAction(this)); } - void add_else(const std::vector *> &actions) { + void add_else(const std::initializer_list *> &actions) { this->else_.add_actions(actions); - this->else_.add_action(new LambdaAction([this](Ts... x) { this->play_next_(x...); })); + this->else_.add_action(new ContinuationAction(this)); } - void play_complex(Ts... x) override { + void play_complex(const Ts &...x) override { this->num_running_++; bool res = this->condition_->check(x...); if (res) { @@ -210,7 +294,7 @@ template class IfAction : public Action { } } - void play(Ts... x) override { /* ignore - see play_complex */ + void play(const Ts &...x) override { /* ignore - see play_complex */ } void stop() override { @@ -228,39 +312,29 @@ template class WhileAction : public Action { public: WhileAction(Condition *condition) : condition_(condition) {} - void add_then(const std::vector *> &actions) { + void add_then(const std::initializer_list *> &actions) { this->then_.add_actions(actions); - this->then_.add_action(new LambdaAction([this](Ts... x) { - if (this->num_running_ > 0 && this->condition_->check_tuple(this->var_)) { - // play again - if (this->num_running_ > 0) { - this->then_.play_tuple(this->var_); - } - } else { - // condition false, play next - this->play_next_tuple_(this->var_); - } - })); + this->then_.add_action(new WhileLoopContinuation(this)); } - void play_complex(Ts... x) override { + friend class WhileLoopContinuation; + + void play_complex(const Ts &...x) override { this->num_running_++; - // Store loop parameters - this->var_ = std::make_tuple(x...); // Initial condition check - if (!this->condition_->check_tuple(this->var_)) { + if (!this->condition_->check(x...)) { // If new condition check failed, stop loop if running this->then_.stop(); - this->play_next_tuple_(this->var_); + this->play_next_(x...); return; } if (this->num_running_ > 0) { - this->then_.play_tuple(this->var_); + this->then_.play(x...); } } - void play(Ts... x) override { /* ignore - see play_complex */ + void play(const Ts &...x) override { /* ignore - see play_complex */ } void stop() override { this->then_.stop(); } @@ -268,52 +342,97 @@ template class WhileAction : public Action { protected: Condition *condition_; ActionList then_; - std::tuple var_{}; +}; + +// Implementation of WhileLoopContinuation::play +template void WhileLoopContinuation::play(const Ts &...x) { + if (this->parent_->num_running_ > 0 && this->parent_->condition_->check(x...)) { + // play again + this->parent_->then_.play(x...); + } else { + // condition false, play next + this->parent_->play_next_(x...); + } +} + +// Forward declaration for RepeatLoopContinuation +template class RepeatAction; + +/// Loop continuation for RepeatAction that increments iteration and repeats or continues. +/// Memory: 4-8 bytes (parent pointer) vs 40 bytes (LambdaAction with std::function). +template class RepeatLoopContinuation : public Action { + public: + explicit RepeatLoopContinuation(RepeatAction *parent) : parent_(parent) {} + + void play(const uint32_t &iteration, const Ts &...x) override; + + protected: + RepeatAction *parent_; }; template class RepeatAction : public Action { public: TEMPLATABLE_VALUE(uint32_t, count) - void add_then(const std::vector *> &actions) { + void add_then(const std::initializer_list *> &actions) { this->then_.add_actions(actions); - this->then_.add_action(new LambdaAction([this](uint32_t iteration, Ts... x) { - iteration++; - if (iteration >= this->count_.value(x...)) { - this->play_next_tuple_(this->var_); - } else { - this->then_.play(iteration, x...); - } - })); + this->then_.add_action(new RepeatLoopContinuation(this)); } - void play_complex(Ts... x) override { + friend class RepeatLoopContinuation; + + void play_complex(const Ts &...x) override { this->num_running_++; - this->var_ = std::make_tuple(x...); if (this->count_.value(x...) > 0) { this->then_.play(0, x...); } else { - this->play_next_tuple_(this->var_); + this->play_next_(x...); } } - void play(Ts... x) override { /* ignore - see play_complex */ + void play(const Ts &...x) override { /* ignore - see play_complex */ } void stop() override { this->then_.stop(); } protected: ActionList then_; - std::tuple var_; }; +// Implementation of RepeatLoopContinuation::play +template void RepeatLoopContinuation::play(const uint32_t &iteration, const Ts &...x) { + uint32_t next_iteration = iteration + 1; + if (next_iteration >= this->parent_->count_.value(x...)) { + this->parent_->play_next_(x...); + } else { + this->parent_->then_.play(next_iteration, x...); + } +} + +/** Wait until a condition is true to continue execution. + * + * Uses queue-based storage to safely handle concurrent executions. + * While concurrent execution from the same trigger is uncommon, it's possible + * (e.g., rapid button presses, high-frequency sensor updates), so we use + * queue-based storage for correctness. + */ template class WaitUntilAction : public Action, public Component { public: WaitUntilAction(Condition *condition) : condition_(condition) {} TEMPLATABLE_VALUE(uint32_t, timeout_value) - void play_complex(Ts... x) override { + void setup() override { + // Start with loop disabled - only enable when there's work to do + // IMPORTANT: Only disable if num_running_ is 0, otherwise play_complex() was already + // called before our setup() (e.g., from on_boot trigger at same priority level) + // and we must not undo its enable_loop() call + if (this->num_running_ == 0) { + this->disable_loop(); + } + } + + void play_complex(const Ts &...x) override { this->num_running_++; // Check if we can continue immediately. if (this->condition_->check(x...)) { @@ -322,46 +441,73 @@ template class WaitUntilAction : public Action, public Co } return; } - this->var_ = std::make_tuple(x...); - if (this->timeout_value_.has_value()) { - auto f = std::bind(&WaitUntilAction::play_next_, this, x...); - this->set_timeout("timeout", this->timeout_value_.value(x...), f); + // Store for later processing + auto now = millis(); + auto timeout = this->timeout_value_.optional_value(x...); + this->var_queue_.emplace_back(now, timeout, std::make_tuple(x...)); + + // Do immediate check with fresh timestamp - don't call loop() synchronously! + // Let the event loop call it to avoid reentrancy issues + if (this->process_queue_(now)) { + // Only enable loop if we still have pending items + this->enable_loop(); } - - this->loop(); } void loop() override { - if (this->num_running_ == 0) - return; - - if (!this->condition_->check_tuple(this->var_)) { - return; + // Safe to use cached time - only called from Application::loop() + if (this->num_running_ > 0 && !this->process_queue_(App.get_loop_component_start_time())) { + // If queue is now empty, disable loop until next play_complex + this->disable_loop(); } + } - this->cancel_timeout("timeout"); - - this->play_next_tuple_(this->var_); + void stop() override { + this->var_queue_.clear(); + this->disable_loop(); } float get_setup_priority() const override { return setup_priority::DATA; } - void play(Ts... x) override { /* ignore - see play_complex */ + void play(const Ts &...x) override { /* ignore - see play_complex */ } - void stop() override { this->cancel_timeout("timeout"); } - protected: + // Helper: Process queue, triggering completed items and removing them + // Returns true if queue still has pending items + bool process_queue_(uint32_t now) { + // Process each queued wait_until and remove completed ones + this->var_queue_.remove_if([&](auto &queued) { + auto start = std::get(queued); + auto timeout = std::get>(queued); + auto &var = std::get>(queued); + + // Check if timeout has expired + auto expired = timeout && (now - start) >= *timeout; + + // Keep waiting if not expired and condition not met + if (!expired && !this->condition_->check_tuple(var)) { + return false; + } + + // Condition met or timed out - trigger next action + this->play_next_tuple_(var); + return true; + }); + + return !this->var_queue_.empty(); + } + Condition *condition_; - std::tuple var_{}; + std::list, std::tuple>> var_queue_{}; }; template class UpdateComponentAction : public Action { public: UpdateComponentAction(PollingComponent *component) : component_(component) {} - void play(Ts... x) override { + void play(const Ts &...x) override { if (!this->component_->is_ready()) return; this->component_->update(); @@ -375,7 +521,7 @@ template class SuspendComponentAction : public Action { public: SuspendComponentAction(PollingComponent *component) : component_(component) {} - void play(Ts... x) override { + void play(const Ts &...x) override { if (!this->component_->is_ready()) return; this->component_->stop_poller(); @@ -390,7 +536,7 @@ template class ResumeComponentAction : public Action { ResumeComponentAction(PollingComponent *component) : component_(component) {} TEMPLATABLE_VALUE(uint32_t, update_interval) - void play(Ts... x) override { + void play(const Ts &...x) override { if (!this->component_->is_ready()) { return; } diff --git a/esphome/core/color.h b/esphome/core/color.h index 5dce58a485..4b0ae5b57a 100644 --- a/esphome/core/color.h +++ b/esphome/core/color.h @@ -14,6 +14,15 @@ inline static constexpr uint8_t esp_scale8(uint8_t i, uint8_t scale) { return (uint16_t(i) * (1 + uint16_t(scale))) / 256; } +/// Scale an 8-bit value by two 8-bit scale factors with improved precision. +/// This is more accurate than calling esp_scale8() twice because it delays +/// truncation until after both multiplications, preserving intermediate precision. +/// For example: esp_scale8_twice(value, max_brightness, local_brightness) +/// gives better results than esp_scale8(esp_scale8(value, max_brightness), local_brightness) +inline static constexpr uint8_t esp_scale8_twice(uint8_t i, uint8_t scale1, uint8_t scale2) { + return (uint32_t(i) * (1 + uint32_t(scale1)) * (1 + uint32_t(scale2))) >> 16; +} + struct Color { union { struct { diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 40cda17ca3..5e6ace8873 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -16,7 +16,6 @@ namespace esphome { static const char *const TAG = "component"; -static const char *const UNSPECIFIED_MESSAGE = "unspecified"; // Global vectors for component data that doesn't belong in every instance. // Using vector instead of unordered_map for both because: @@ -34,12 +33,44 @@ static const char *const UNSPECIFIED_MESSAGE = "unspecified"; // Using namespace-scope static to avoid guard variables (saves 16 bytes total) // This is safe because ESPHome is single-threaded during initialization namespace { +struct ComponentErrorMessage { + const Component *component; + const char *message; + // Track if message is flash pointer (needs LOG_STR_ARG) or RAM pointer + // Remove before 2026.6.0 when deprecated const char* API is removed + bool is_flash_ptr; +}; + +struct ComponentPriorityOverride { + const Component *component; + float priority; +}; + // Error messages for failed components // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) -std::unique_ptr>> component_error_messages; +std::unique_ptr> component_error_messages; // Setup priority overrides - freed after setup completes // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) -std::unique_ptr>> setup_priority_overrides; +std::unique_ptr> setup_priority_overrides; + +// Helper to store error messages - reduces duplication between deprecated and new API +// Remove before 2026.6.0 when deprecated const char* API is removed +void store_component_error_message(const Component *component, const char *message, bool is_flash_ptr) { + // Lazy allocate the error messages vector if needed + if (!component_error_messages) { + component_error_messages = std::make_unique>(); + } + // Check if this component already has an error message + for (auto &entry : *component_error_messages) { + if (entry.component == component) { + entry.message = message; + entry.is_flash_ptr = is_flash_ptr; + return; + } + } + // Add new error message + component_error_messages->emplace_back(ComponentErrorMessage{component, message, is_flash_ptr}); +} } // namespace namespace setup_priority { @@ -134,16 +165,20 @@ void Component::call_dump_config() { if (this->is_failed()) { // Look up error message from global vector const char *error_msg = nullptr; + bool is_flash_ptr = false; if (component_error_messages) { - for (const auto &pair : *component_error_messages) { - if (pair.first == this) { - error_msg = pair.second; + for (const auto &entry : *component_error_messages) { + if (entry.component == this) { + error_msg = entry.message; + is_flash_ptr = entry.is_flash_ptr; break; } } } - ESP_LOGE(TAG, " %s is marked FAILED: %s", this->get_component_source(), - error_msg ? error_msg : UNSPECIFIED_MESSAGE); + // Log with appropriate format based on pointer type + ESP_LOGE(TAG, " %s is marked FAILED: %s", LOG_STR_ARG(this->get_component_log_str()), + error_msg ? (is_flash_ptr ? LOG_STR_ARG((const LogString *) error_msg) : error_msg) + : LOG_STR_LITERAL("unspecified")); } } @@ -154,14 +189,14 @@ void Component::call() { case COMPONENT_STATE_CONSTRUCTION: { // State Construction: Call setup and set state to setup this->set_component_state_(COMPONENT_STATE_SETUP); - ESP_LOGV(TAG, "Setup %s", this->get_component_source()); + ESP_LOGV(TAG, "Setup %s", LOG_STR_ARG(this->get_component_log_str())); #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG uint32_t start_time = millis(); #endif this->call_setup(); #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG uint32_t setup_time = millis() - start_time; - ESP_LOGCONFIG(TAG, "Setup %s took %ums", this->get_component_source(), (unsigned) setup_time); + ESP_LOGCONFIG(TAG, "Setup %s took %ums", LOG_STR_ARG(this->get_component_log_str()), (unsigned) setup_time); #endif break; } @@ -182,10 +217,8 @@ void Component::call() { break; } } -const char *Component::get_component_source() const { - if (this->component_source_ == nullptr) - return ""; - return this->component_source_; +const LogString *Component::get_component_log_str() const { + return this->component_source_ == nullptr ? LOG_STR("") : this->component_source_; } bool Component::should_warn_of_blocking(uint32_t blocking_time) { if (blocking_time > this->warn_if_blocking_over_) { @@ -201,7 +234,7 @@ bool Component::should_warn_of_blocking(uint32_t blocking_time) { return false; } void Component::mark_failed() { - ESP_LOGE(TAG, "%s was marked as failed", this->get_component_source()); + ESP_LOGE(TAG, "%s was marked as failed", LOG_STR_ARG(this->get_component_log_str())); this->set_component_state_(COMPONENT_STATE_FAILED); this->status_set_error(); // Also remove from loop since failed components shouldn't loop @@ -213,14 +246,14 @@ void Component::set_component_state_(uint8_t state) { } void Component::disable_loop() { if ((this->component_state_ & COMPONENT_STATE_MASK) != COMPONENT_STATE_LOOP_DONE) { - ESP_LOGVV(TAG, "%s loop disabled", this->get_component_source()); + ESP_LOGVV(TAG, "%s loop disabled", LOG_STR_ARG(this->get_component_log_str())); this->set_component_state_(COMPONENT_STATE_LOOP_DONE); App.disable_component_loop_(this); } } void Component::enable_loop() { if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE) { - ESP_LOGVV(TAG, "%s loop enabled", this->get_component_source()); + ESP_LOGVV(TAG, "%s loop enabled", LOG_STR_ARG(this->get_component_log_str())); this->set_component_state_(COMPONENT_STATE_LOOP); App.enable_component_loop_(this); } @@ -240,7 +273,7 @@ void IRAM_ATTR HOT Component::enable_loop_soon_any_context() { } void Component::reset_to_construction_state() { if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED) { - ESP_LOGI(TAG, "%s is being reset to construction state", this->get_component_source()); + ESP_LOGI(TAG, "%s is being reset to construction state", LOG_STR_ARG(this->get_component_log_str())); this->set_component_state_(COMPONENT_STATE_CONSTRUCTION); // Clear error status when resetting this->status_clear_error(); @@ -277,50 +310,64 @@ bool Component::is_ready() const { (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE || (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_SETUP; } +bool Component::is_idle() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE; } bool Component::can_proceed() { return true; } bool Component::status_has_warning() const { return this->component_state_ & STATUS_LED_WARNING; } bool Component::status_has_error() const { return this->component_state_ & STATUS_LED_ERROR; } + void Component::status_set_warning(const char *message) { // Don't spam the log. This risks missing different warning messages though. if ((this->component_state_ & STATUS_LED_WARNING) != 0) return; this->component_state_ |= STATUS_LED_WARNING; App.app_state_ |= STATUS_LED_WARNING; - ESP_LOGW(TAG, "%s set Warning flag: %s", this->get_component_source(), message ? message : UNSPECIFIED_MESSAGE); + ESP_LOGW(TAG, "%s set Warning flag: %s", LOG_STR_ARG(this->get_component_log_str()), + message ? message : LOG_STR_LITERAL("unspecified")); } +void Component::status_set_warning(const LogString *message) { + // Don't spam the log. This risks missing different warning messages though. + if ((this->component_state_ & STATUS_LED_WARNING) != 0) + return; + this->component_state_ |= STATUS_LED_WARNING; + App.app_state_ |= STATUS_LED_WARNING; + ESP_LOGW(TAG, "%s set Warning flag: %s", LOG_STR_ARG(this->get_component_log_str()), + message ? LOG_STR_ARG(message) : LOG_STR_LITERAL("unspecified")); +} +void Component::status_set_error() { this->status_set_error((const LogString *) nullptr); } void Component::status_set_error(const char *message) { if ((this->component_state_ & STATUS_LED_ERROR) != 0) return; this->component_state_ |= STATUS_LED_ERROR; App.app_state_ |= STATUS_LED_ERROR; - ESP_LOGE(TAG, "%s set Error flag: %s", this->get_component_source(), message ? message : UNSPECIFIED_MESSAGE); + ESP_LOGE(TAG, "%s set Error flag: %s", LOG_STR_ARG(this->get_component_log_str()), + message ? message : LOG_STR_LITERAL("unspecified")); if (message != nullptr) { - // Lazy allocate the error messages vector if needed - if (!component_error_messages) { - component_error_messages = std::make_unique>>(); - } - // Check if this component already has an error message - for (auto &pair : *component_error_messages) { - if (pair.first == this) { - pair.second = message; - return; - } - } - // Add new error message - component_error_messages->emplace_back(this, message); + store_component_error_message(this, message, false); + } +} +void Component::status_set_error(const LogString *message) { + if ((this->component_state_ & STATUS_LED_ERROR) != 0) + return; + this->component_state_ |= STATUS_LED_ERROR; + App.app_state_ |= STATUS_LED_ERROR; + ESP_LOGE(TAG, "%s set Error flag: %s", LOG_STR_ARG(this->get_component_log_str()), + message ? LOG_STR_ARG(message) : LOG_STR_LITERAL("unspecified")); + if (message != nullptr) { + // Store the LogString pointer directly (safe because LogString is always in flash/static memory) + store_component_error_message(this, LOG_STR_ARG(message), true); } } void Component::status_clear_warning() { if ((this->component_state_ & STATUS_LED_WARNING) == 0) return; this->component_state_ &= ~STATUS_LED_WARNING; - ESP_LOGW(TAG, "%s cleared Warning flag", this->get_component_source()); + ESP_LOGW(TAG, "%s cleared Warning flag", LOG_STR_ARG(this->get_component_log_str())); } void Component::status_clear_error() { if ((this->component_state_ & STATUS_LED_ERROR) == 0) return; this->component_state_ &= ~STATUS_LED_ERROR; - ESP_LOGE(TAG, "%s cleared Error flag", this->get_component_source()); + ESP_LOGE(TAG, "%s cleared Error flag", LOG_STR_ARG(this->get_component_log_str())); } void Component::status_momentary_warning(const std::string &name, uint32_t length) { this->status_set_warning(); @@ -331,13 +378,25 @@ void Component::status_momentary_error(const std::string &name, uint32_t length) this->set_timeout(name, length, [this]() { this->status_clear_error(); }); } void Component::dump_config() {} + +// Function implementation of LOG_UPDATE_INTERVAL macro to reduce code size +void log_update_interval(const char *tag, PollingComponent *component) { + uint32_t update_interval = component->get_update_interval(); + if (update_interval == SCHEDULER_DONT_RUN) { + ESP_LOGCONFIG(tag, " Update Interval: never"); + } else if (update_interval < 100) { + ESP_LOGCONFIG(tag, " Update Interval: %.3fs", update_interval / 1000.0f); + } else { + ESP_LOGCONFIG(tag, " Update Interval: %.1fs", update_interval / 1000.0f); + } +} float Component::get_actual_setup_priority() const { // Check if there's an override in the global vector if (setup_priority_overrides) { // Linear search is fine for small n (typically < 5 overrides) - for (const auto &pair : *setup_priority_overrides) { - if (pair.first == this) { - return pair.second; + for (const auto &entry : *setup_priority_overrides) { + if (entry.component == this) { + return entry.priority; } } } @@ -346,21 +405,21 @@ float Component::get_actual_setup_priority() const { void Component::set_setup_priority(float priority) { // Lazy allocate the vector if needed if (!setup_priority_overrides) { - setup_priority_overrides = std::make_unique>>(); + setup_priority_overrides = std::make_unique>(); // Reserve some space to avoid reallocations (most configs have < 10 overrides) setup_priority_overrides->reserve(10); } // Check if this component already has an override - for (auto &pair : *setup_priority_overrides) { - if (pair.first == this) { - pair.second = priority; + for (auto &entry : *setup_priority_overrides) { + if (entry.component == this) { + entry.priority = priority; return; } } // Add new override - setup_priority_overrides->emplace_back(this, priority); + setup_priority_overrides->emplace_back(ComponentPriorityOverride{this, priority}); } bool Component::has_overridden_loop() const { @@ -419,8 +478,9 @@ uint32_t WarnIfComponentBlockingGuard::finish() { should_warn = blocking_time > WARN_IF_BLOCKING_OVER_MS; } if (should_warn) { - const char *src = component_ == nullptr ? "" : component_->get_component_source(); - ESP_LOGW(TAG, "%s took a long time for an operation (%" PRIu32 " ms)", src, blocking_time); + ESP_LOGW(TAG, "%s took a long time for an operation (%" PRIu32 " ms)", + component_ == nullptr ? LOG_STR_LITERAL("") : LOG_STR_ARG(component_->get_component_log_str()), + blocking_time); ESP_LOGW(TAG, "Components should block for at most 30 ms"); } diff --git a/esphome/core/component.h b/esphome/core/component.h index 096c6f9c69..51a9290e8b 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -5,10 +5,15 @@ #include #include +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" #include "esphome/core/optional.h" namespace esphome { +// Forward declaration for LogString +struct LogString; + /** Default setup priorities for components of different types. * * Components should return one of these setup priorities in get_setup_priority. @@ -44,14 +49,13 @@ extern const float LATE; static const uint32_t SCHEDULER_DONT_RUN = 4294967295UL; -#define LOG_UPDATE_INTERVAL(this) \ - if (this->get_update_interval() == SCHEDULER_DONT_RUN) { \ - ESP_LOGCONFIG(TAG, " Update Interval: never"); \ - } else if (this->get_update_interval() < 100) { \ - ESP_LOGCONFIG(TAG, " Update Interval: %.3fs", this->get_update_interval() / 1000.0f); \ - } else { \ - ESP_LOGCONFIG(TAG, " Update Interval: %.1fs", this->get_update_interval() / 1000.0f); \ - } +// Forward declaration +class PollingComponent; + +// Function declaration for LOG_UPDATE_INTERVAL +void log_update_interval(const char *tag, PollingComponent *component); + +#define LOG_UPDATE_INTERVAL(this) log_update_interval(TAG, this) extern const uint8_t COMPONENT_STATE_MASK; extern const uint8_t COMPONENT_STATE_CONSTRUCTION; @@ -138,6 +142,14 @@ class Component { */ bool is_in_loop_state() const; + /** Check if this component is idle. + * Being idle means being in LOOP_DONE state. + * This means the component has completed setup, is not failed, but its loop is currently disabled. + * + * @return True if the component is idle + */ + bool is_idle() const; + /** Mark this component as failed. Any future timeouts/intervals/setup/loop will no longer be called. * * This might be useful if a component wants to indicate that a connection to its peripheral failed. @@ -146,7 +158,19 @@ class Component { */ virtual void mark_failed(); + // Remove before 2026.6.0 + ESPDEPRECATED("Use mark_failed(LOG_STR(\"static string literal\")) instead. Do NOT use .c_str() from temporary " + "strings. Will stop working in 2026.6.0", + "2025.12.0") void mark_failed(const char *message) { +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + this->status_set_error(message); +#pragma GCC diagnostic pop + this->mark_failed(); + } + + void mark_failed(const LogString *message) { this->status_set_error(message); this->mark_failed(); } @@ -203,8 +227,15 @@ class Component { bool status_has_error() const; void status_set_warning(const char *message = nullptr); + void status_set_warning(const LogString *message); - void status_set_error(const char *message = nullptr); + void status_set_error(); // Set error flag without message + // Remove before 2026.6.0 + ESPDEPRECATED("Use status_set_error(LOG_STR(\"static string literal\")) instead. Do NOT use .c_str() from temporary " + "strings. Will stop working in 2026.6.0", + "2025.12.0") + void status_set_error(const char *message); + void status_set_error(const LogString *message); void status_clear_warning(); @@ -220,12 +251,12 @@ class Component { * * This is set by the ESPHome core, and should not be called manually. */ - void set_component_source(const char *source) { component_source_ = source; } - /** Get the integration where this component was declared as a string. + void set_component_source(const LogString *source) { component_source_ = source; } + /** Get the integration where this component was declared as a LogString for logging. * - * Returns "" if source not set + * Returns LOG_STR("") if source not set */ - const char *get_component_source() const; + const LogString *get_component_log_str() const; bool should_warn_of_blocking(uint32_t blocking_time); @@ -405,7 +436,7 @@ class Component { bool cancel_defer(const std::string &name); // NOLINT // Ordered for optimal packing on 32-bit systems - const char *component_source_{nullptr}; + const LogString *component_source_{nullptr}; uint16_t warn_if_blocking_over_{WARN_IF_BLOCKING_OVER_MS}; ///< Warn if blocked for this many ms (max 65.5s) /// State of this component - each bit has a purpose: /// Bits 0-2: Component state (0x00=CONSTRUCTION, 0x01=SETUP, 0x02=LOOP, 0x03=FAILED, 0x04=LOOP_DONE) diff --git a/esphome/core/component_iterator.cpp b/esphome/core/component_iterator.cpp index 668c4a1fda..8c6a7b95b5 100644 --- a/esphome/core/component_iterator.cpp +++ b/esphome/core/component_iterator.cpp @@ -5,7 +5,7 @@ #ifdef USE_API #include "esphome/components/api/api_server.h" #endif -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS #include "esphome/components/api/user_services.h" #endif @@ -81,7 +81,7 @@ void ComponentIterator::advance() { break; #endif -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS case IteratorState::SERVICE: this->process_platform_item_(api::global_api_server->get_user_services(), &ComponentIterator::on_service); break; @@ -185,7 +185,7 @@ void ComponentIterator::advance() { bool ComponentIterator::on_end() { return true; } bool ComponentIterator::on_begin() { return true; } -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS bool ComponentIterator::on_service(api::UserServiceDescriptor *service) { return true; } #endif #ifdef USE_CAMERA diff --git a/esphome/core/component_iterator.h b/esphome/core/component_iterator.h index fdc30485bc..1b1bd80ac5 100644 --- a/esphome/core/component_iterator.h +++ b/esphome/core/component_iterator.h @@ -10,7 +10,7 @@ namespace esphome { -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS namespace api { class UserServiceDescriptor; } // namespace api @@ -45,7 +45,7 @@ class ComponentIterator { #ifdef USE_TEXT_SENSOR virtual bool on_text_sensor(text_sensor::TextSensor *text_sensor) = 0; #endif -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS virtual bool on_service(api::UserServiceDescriptor *service); #endif #ifdef USE_CAMERA @@ -122,7 +122,7 @@ class ComponentIterator { #ifdef USE_TEXT_SENSOR TEXT_SENSOR, #endif -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS SERVICE, #endif #ifdef USE_CAMERA @@ -168,8 +168,9 @@ class ComponentIterator { UPDATE, #endif MAX, - } state_{IteratorState::NONE}; + }; uint16_t at_{0}; // Supports up to 65,535 entities per type + IteratorState state_{IteratorState::NONE}; bool include_internal_{false}; template diff --git a/esphome/core/config.py b/esphome/core/config.py index 90768a4b09..0a239c5f5e 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -17,10 +17,12 @@ from esphome.const import ( CONF_COMPILE_PROCESS_LIMIT, CONF_DEBUG_SCHEDULER, CONF_DEVICES, + CONF_ENVIRONMENT_VARIABLES, CONF_ESPHOME, CONF_FRIENDLY_NAME, CONF_ID, CONF_INCLUDES, + CONF_INCLUDES_C, CONF_LIBRARIES, CONF_MIN_VERSION, CONF_NAME, @@ -39,7 +41,12 @@ from esphome.const import ( PlatformFramework, __version__ as ESPHOME_VERSION, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import ( + CORE, + KEY_CONTROLLER_REGISTRY_COUNT, + CoroPriority, + coroutine_with_priority, +) from esphome.helpers import ( copy_file_if_changed, fnv1a_32bit_hash, @@ -136,21 +143,21 @@ def validate_ids_and_references(config: ConfigType) -> ConfigType: return config -def valid_include(value): +def valid_include(value: str) -> str: # Look for "<...>" includes if value.startswith("<") and value.endswith(">"): return value try: - return cv.directory(value) + return str(cv.directory(value)) except cv.Invalid: pass - value = cv.file_(value) - _, ext = os.path.splitext(value) + path = cv.file_(value) + ext = path.suffix if ext not in VALID_INCLUDE_EXTS: raise cv.Invalid( f"Include has invalid file extension {ext} - valid extensions are {', '.join(VALID_INCLUDE_EXTS)}" ) - return value + return str(path) def valid_project_name(value: str): @@ -200,7 +207,7 @@ CONFIG_SCHEMA = cv.All( cv.Schema( { cv.Required(CONF_NAME): cv.valid_name, - cv.Optional(CONF_FRIENDLY_NAME, ""): cv.string, + cv.Optional(CONF_FRIENDLY_NAME, ""): cv.All(cv.string, cv.Length(max=120)), cv.Optional(CONF_AREA): validate_area_config, cv.Optional(CONF_COMMENT): cv.string, cv.Required(CONF_BUILD_PATH): cv.string, @@ -209,6 +216,11 @@ CONFIG_SCHEMA = cv.All( cv.string_strict: cv.Any([cv.string], cv.string), } ), + cv.Optional(CONF_ENVIRONMENT_VARIABLES, default={}): cv.Schema( + { + cv.string_strict: cv.string, + } + ), cv.Optional(CONF_ON_BOOT): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StartupTrigger), @@ -227,6 +239,7 @@ CONFIG_SCHEMA = cv.All( } ), cv.Optional(CONF_INCLUDES, default=[]): cv.ensure_list(valid_include), + cv.Optional(CONF_INCLUDES_C, default=[]): cv.ensure_list(valid_include), cv.Optional(CONF_LIBRARIES, default=[]): cv.ensure_list(cv.string_strict), cv.Optional(CONF_NAME_ADD_MAC_SUFFIX, default=False): cv.boolean, cv.Optional(CONF_DEBUG_SCHEDULER, default=False): cv.boolean, @@ -302,6 +315,17 @@ def _list_target_platforms(): return target_platforms +def _sort_includes_by_type(includes: list[str]) -> tuple[list[str], list[str]]: + system_includes = [] + other_includes = [] + for include in includes: + if include.startswith("<") and include.endswith(">"): + system_includes.append(include) + else: + other_includes.append(include) + return system_includes, other_includes + + def preload_core_config(config, result) -> str: with cv.prepend_path(CONF_ESPHOME): conf = PRELOAD_CONFIG_SCHEMA(config[CONF_ESPHOME]) @@ -311,9 +335,9 @@ def preload_core_config(config, result) -> str: CORE.data[KEY_CORE] = {} if CONF_BUILD_PATH not in conf: - build_path = get_str_env("ESPHOME_BUILD_PATH", "build") - conf[CONF_BUILD_PATH] = os.path.join(build_path, CORE.name) - CORE.build_path = CORE.relative_internal_path(conf[CONF_BUILD_PATH]) + build_path = Path(get_str_env("ESPHOME_BUILD_PATH", "build")) + conf[CONF_BUILD_PATH] = str(build_path / CORE.name) + CORE.build_path = CORE.data_dir / conf[CONF_BUILD_PATH] target_platforms = [] @@ -339,15 +363,22 @@ def preload_core_config(config, result) -> str: return target_platforms[0] -def include_file(path, basename): - parts = basename.split(os.path.sep) +def include_file(path: Path, basename: Path, is_c_header: bool = False): + parts = basename.parts dst = CORE.relative_src_path(*parts) copy_file_if_changed(path, dst) - _, ext = os.path.splitext(path) + ext = path.suffix if ext in [".h", ".hpp", ".tcc"]: # Header, add include statement - cg.add_global(cg.RawStatement(f'#include "{basename}"')) + if is_c_header: + # Wrap in extern "C" block for C headers + cg.add_global( + cg.RawStatement(f'extern "C" {{\n #include "{basename}"\n}}') + ) + else: + # Regular include + cg.add_global(cg.RawStatement(f'#include "{basename}"')) ARDUINO_GLUE_CODE = """\ @@ -359,7 +390,7 @@ ARDUINO_GLUE_CODE = """\ """ -@coroutine_with_priority(-999.0) +@coroutine_with_priority(CoroPriority.WORKAROUNDS) async def add_arduino_global_workaround(): # The Arduino framework defined these itself in the global # namespace. For the esphome codebase that is not a problem, @@ -376,32 +407,38 @@ async def add_arduino_global_workaround(): cg.add_global(cg.RawStatement(line)) -@coroutine_with_priority(-1000.0) -async def add_includes(includes): +@coroutine_with_priority(CoroPriority.FINAL) +async def add_includes(includes: list[str], is_c_header: bool = False) -> None: # Add includes at the very end, so that the included files can access global variables for include in includes: path = CORE.relative_config_path(include) - if os.path.isdir(path): + if path.is_dir(): # Directory, copy tree for p in walk_files(path): - basename = os.path.relpath(p, os.path.dirname(path)) - include_file(p, basename) + basename = p.relative_to(path.parent) + include_file(p, basename, is_c_header) else: # Copy file - basename = os.path.basename(path) - include_file(path, basename) + basename = Path(path.name) + include_file(path, basename, is_c_header) -@coroutine_with_priority(-1000.0) +@coroutine_with_priority(CoroPriority.FINAL) async def _add_platformio_options(pio_options): # Add includes at the very end, so that they override everything for key, val in pio_options.items(): - if key == "build_flags" and not isinstance(val, list): + if key in ["build_flags", "lib_ignore"] and not isinstance(val, list): val = [val] cg.add_platformio_option(key, val) -@coroutine_with_priority(30.0) +@coroutine_with_priority(CoroPriority.FINAL) +async def _add_environment_variables(env_vars: dict[str, str]) -> None: + # Set environment variables for the build process + os.environ.update(env_vars) + + +@coroutine_with_priority(CoroPriority.AUTOMATION) async def _add_automations(config): for conf in config.get(CONF_ON_BOOT, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], conf.get(CONF_PRIORITY)) @@ -423,7 +460,7 @@ async def _add_automations(config): DATETIME_SUBTYPES = {"date", "time", "datetime"} -@coroutine_with_priority(-100.0) +@coroutine_with_priority(CoroPriority.FINAL) async def _add_platform_defines() -> None: # Generate compile-time defines for platforms that have actual entities # Only add USE_* and count defines when there are entities @@ -442,7 +479,16 @@ async def _add_platform_defines() -> None: cg.add_define(f"USE_{platform_name.upper()}") -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.FINAL) +async def _add_controller_registry_define() -> None: + # Generate StaticVector size for ControllerRegistry + controller_count = CORE.data.get(KEY_CONTROLLER_REGISTRY_COUNT, 0) + if controller_count > 0: + cg.add_define("USE_CONTROLLER_REGISTRY") + cg.add_define("CONTROLLER_REGISTRY_MAX", controller_count) + + +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config: ConfigType) -> None: cg.add_global(cg.global_ns.namespace("esphome").using) # These can be used by user lambdas, put them to default scope @@ -463,6 +509,7 @@ async def to_code(config: ConfigType) -> None: cg.add_define("ESPHOME_COMPONENT_COUNT", len(CORE.component_ids)) CORE.add_job(_add_platform_defines) + CORE.add_job(_add_controller_registry_define) CORE.add_job(_add_automations, config) @@ -494,19 +541,25 @@ async def to_code(config: ConfigType) -> None: CORE.add_job(add_arduino_global_workaround) if config[CONF_INCLUDES]: - # Get the <...> includes - system_includes = [] - other_includes = [] - for include in config[CONF_INCLUDES]: - if include.startswith("<") and include.endswith(">"): - system_includes.append(include) - else: - other_includes.append(include) + system_includes, other_includes = _sort_includes_by_type(config[CONF_INCLUDES]) # <...> includes should be at the start for include in system_includes: cg.add_global(cg.RawStatement(f"#include {include}"), prepend=True) # Other includes should be at the end - CORE.add_job(add_includes, other_includes) + CORE.add_job(add_includes, other_includes, False) + + if config[CONF_INCLUDES_C]: + system_includes, other_includes = _sort_includes_by_type( + config[CONF_INCLUDES_C] + ) + # <...> includes should be at the start + for include in system_includes: + cg.add_global( + cg.RawStatement(f'extern "C" {{\n #include {include}\n}}'), + prepend=True, + ) + # Other includes should be at the end + CORE.add_job(add_includes, other_includes, True) if project_conf := config.get(CONF_PROJECT): cg.add_define("ESPHOME_PROJECT_NAME", project_conf[CONF_NAME]) @@ -522,6 +575,9 @@ async def to_code(config: ConfigType) -> None: if config[CONF_PLATFORMIO_OPTIONS]: CORE.add_job(_add_platformio_options, config[CONF_PLATFORMIO_OPTIONS]) + if config[CONF_ENVIRONMENT_VARIABLES]: + CORE.add_job(_add_environment_variables, config[CONF_ENVIRONMENT_VARIABLES]) + # Process areas all_areas: list[dict[str, str | core.ID]] = [] if CONF_AREA in config: diff --git a/esphome/core/controller.cpp b/esphome/core/controller.cpp deleted file mode 100644 index f7ff5a9734..0000000000 --- a/esphome/core/controller.cpp +++ /dev/null @@ -1,134 +0,0 @@ -#include "controller.h" -#include "esphome/core/application.h" -#include "esphome/core/log.h" - -namespace esphome { - -void Controller::setup_controller(bool include_internal) { -#ifdef USE_BINARY_SENSOR - for (auto *obj : App.get_binary_sensors()) { - if (include_internal || !obj->is_internal()) { - obj->add_full_state_callback( - [this, obj](optional previous, optional state) { this->on_binary_sensor_update(obj); }); - } - } -#endif -#ifdef USE_FAN - for (auto *obj : App.get_fans()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj]() { this->on_fan_update(obj); }); - } -#endif -#ifdef USE_LIGHT - for (auto *obj : App.get_lights()) { - if (include_internal || !obj->is_internal()) - obj->add_new_remote_values_callback([this, obj]() { this->on_light_update(obj); }); - } -#endif -#ifdef USE_SENSOR - for (auto *obj : App.get_sensors()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj](float state) { this->on_sensor_update(obj, state); }); - } -#endif -#ifdef USE_SWITCH - for (auto *obj : App.get_switches()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj](bool state) { this->on_switch_update(obj, state); }); - } -#endif -#ifdef USE_COVER - for (auto *obj : App.get_covers()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj]() { this->on_cover_update(obj); }); - } -#endif -#ifdef USE_TEXT_SENSOR - for (auto *obj : App.get_text_sensors()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj](const std::string &state) { this->on_text_sensor_update(obj, state); }); - } -#endif -#ifdef USE_CLIMATE - for (auto *obj : App.get_climates()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj](climate::Climate & /*unused*/) { this->on_climate_update(obj); }); - } -#endif -#ifdef USE_NUMBER - for (auto *obj : App.get_numbers()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj](float state) { this->on_number_update(obj, state); }); - } -#endif -#ifdef USE_DATETIME_DATE - for (auto *obj : App.get_dates()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj]() { this->on_date_update(obj); }); - } -#endif -#ifdef USE_DATETIME_TIME - for (auto *obj : App.get_times()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj]() { this->on_time_update(obj); }); - } -#endif -#ifdef USE_DATETIME_DATETIME - for (auto *obj : App.get_datetimes()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj]() { this->on_datetime_update(obj); }); - } -#endif -#ifdef USE_TEXT - for (auto *obj : App.get_texts()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj](const std::string &state) { this->on_text_update(obj, state); }); - } -#endif -#ifdef USE_SELECT - for (auto *obj : App.get_selects()) { - if (include_internal || !obj->is_internal()) { - obj->add_on_state_callback( - [this, obj](const std::string &state, size_t index) { this->on_select_update(obj, state, index); }); - } - } -#endif -#ifdef USE_LOCK - for (auto *obj : App.get_locks()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj]() { this->on_lock_update(obj); }); - } -#endif -#ifdef USE_VALVE - for (auto *obj : App.get_valves()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj]() { this->on_valve_update(obj); }); - } -#endif -#ifdef USE_MEDIA_PLAYER - for (auto *obj : App.get_media_players()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj]() { this->on_media_player_update(obj); }); - } -#endif -#ifdef USE_ALARM_CONTROL_PANEL - for (auto *obj : App.get_alarm_control_panels()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj]() { this->on_alarm_control_panel_update(obj); }); - } -#endif -#ifdef USE_EVENT - for (auto *obj : App.get_events()) { - if (include_internal || !obj->is_internal()) - obj->add_on_event_callback([this, obj](const std::string &event_type) { this->on_event(obj, event_type); }); - } -#endif -#ifdef USE_UPDATE - for (auto *obj : App.get_updates()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj]() { this->on_update(obj); }); - } -#endif -} - -} // namespace esphome diff --git a/esphome/core/controller.h b/esphome/core/controller.h index 1a5b9ea6b4..697017217d 100644 --- a/esphome/core/controller.h +++ b/esphome/core/controller.h @@ -5,7 +5,7 @@ #include "esphome/components/binary_sensor/binary_sensor.h" #endif #ifdef USE_FAN -#include "esphome/components/fan/fan_state.h" +#include "esphome/components/fan/fan.h" #endif #ifdef USE_LIGHT #include "esphome/components/light/light_state.h" @@ -69,7 +69,6 @@ namespace esphome { class Controller { public: - void setup_controller(bool include_internal = false); #ifdef USE_BINARY_SENSOR virtual void on_binary_sensor_update(binary_sensor::BinarySensor *obj){}; #endif @@ -80,22 +79,22 @@ class Controller { virtual void on_light_update(light::LightState *obj){}; #endif #ifdef USE_SENSOR - virtual void on_sensor_update(sensor::Sensor *obj, float state){}; + virtual void on_sensor_update(sensor::Sensor *obj){}; #endif #ifdef USE_SWITCH - virtual void on_switch_update(switch_::Switch *obj, bool state){}; + virtual void on_switch_update(switch_::Switch *obj){}; #endif #ifdef USE_COVER virtual void on_cover_update(cover::Cover *obj){}; #endif #ifdef USE_TEXT_SENSOR - virtual void on_text_sensor_update(text_sensor::TextSensor *obj, const std::string &state){}; + virtual void on_text_sensor_update(text_sensor::TextSensor *obj){}; #endif #ifdef USE_CLIMATE virtual void on_climate_update(climate::Climate *obj){}; #endif #ifdef USE_NUMBER - virtual void on_number_update(number::Number *obj, float state){}; + virtual void on_number_update(number::Number *obj){}; #endif #ifdef USE_DATETIME_DATE virtual void on_date_update(datetime::DateEntity *obj){}; @@ -107,10 +106,10 @@ class Controller { virtual void on_datetime_update(datetime::DateTimeEntity *obj){}; #endif #ifdef USE_TEXT - virtual void on_text_update(text::Text *obj, const std::string &state){}; + virtual void on_text_update(text::Text *obj){}; #endif #ifdef USE_SELECT - virtual void on_select_update(select::Select *obj, const std::string &state, size_t index){}; + virtual void on_select_update(select::Select *obj){}; #endif #ifdef USE_LOCK virtual void on_lock_update(lock::Lock *obj){}; @@ -125,7 +124,7 @@ class Controller { virtual void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj){}; #endif #ifdef USE_EVENT - virtual void on_event(event::Event *obj, const std::string &event_type){}; + virtual void on_event(event::Event *obj){}; #endif #ifdef USE_UPDATE virtual void on_update(update::UpdateEntity *obj){}; diff --git a/esphome/core/controller_registry.cpp b/esphome/core/controller_registry.cpp new file mode 100644 index 0000000000..0a84bb0d0d --- /dev/null +++ b/esphome/core/controller_registry.cpp @@ -0,0 +1,114 @@ +#include "esphome/core/controller_registry.h" + +#ifdef USE_CONTROLLER_REGISTRY + +#include "esphome/core/controller.h" + +namespace esphome { + +StaticVector ControllerRegistry::controllers; + +void ControllerRegistry::register_controller(Controller *controller) { controllers.push_back(controller); } + +// Macro for standard registry notification dispatch - calls on__update() +#define CONTROLLER_REGISTRY_NOTIFY(entity_type, entity_name) \ + void ControllerRegistry::notify_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \ + for (auto *controller : controllers) { \ + controller->on_##entity_name##_update(obj); \ + } \ + } + +// Macro for entities where controller method has no "_update" suffix (Event, Update) +#define CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX(entity_type, entity_name) \ + void ControllerRegistry::notify_##entity_name(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \ + for (auto *controller : controllers) { \ + controller->on_##entity_name(obj); \ + } \ + } + +#ifdef USE_BINARY_SENSOR +CONTROLLER_REGISTRY_NOTIFY(binary_sensor::BinarySensor, binary_sensor) +#endif + +#ifdef USE_FAN +CONTROLLER_REGISTRY_NOTIFY(fan::Fan, fan) +#endif + +#ifdef USE_LIGHT +CONTROLLER_REGISTRY_NOTIFY(light::LightState, light) +#endif + +#ifdef USE_SENSOR +CONTROLLER_REGISTRY_NOTIFY(sensor::Sensor, sensor) +#endif + +#ifdef USE_SWITCH +CONTROLLER_REGISTRY_NOTIFY(switch_::Switch, switch) +#endif + +#ifdef USE_COVER +CONTROLLER_REGISTRY_NOTIFY(cover::Cover, cover) +#endif + +#ifdef USE_TEXT_SENSOR +CONTROLLER_REGISTRY_NOTIFY(text_sensor::TextSensor, text_sensor) +#endif + +#ifdef USE_CLIMATE +CONTROLLER_REGISTRY_NOTIFY(climate::Climate, climate) +#endif + +#ifdef USE_NUMBER +CONTROLLER_REGISTRY_NOTIFY(number::Number, number) +#endif + +#ifdef USE_DATETIME_DATE +CONTROLLER_REGISTRY_NOTIFY(datetime::DateEntity, date) +#endif + +#ifdef USE_DATETIME_TIME +CONTROLLER_REGISTRY_NOTIFY(datetime::TimeEntity, time) +#endif + +#ifdef USE_DATETIME_DATETIME +CONTROLLER_REGISTRY_NOTIFY(datetime::DateTimeEntity, datetime) +#endif + +#ifdef USE_TEXT +CONTROLLER_REGISTRY_NOTIFY(text::Text, text) +#endif + +#ifdef USE_SELECT +CONTROLLER_REGISTRY_NOTIFY(select::Select, select) +#endif + +#ifdef USE_LOCK +CONTROLLER_REGISTRY_NOTIFY(lock::Lock, lock) +#endif + +#ifdef USE_VALVE +CONTROLLER_REGISTRY_NOTIFY(valve::Valve, valve) +#endif + +#ifdef USE_MEDIA_PLAYER +CONTROLLER_REGISTRY_NOTIFY(media_player::MediaPlayer, media_player) +#endif + +#ifdef USE_ALARM_CONTROL_PANEL +CONTROLLER_REGISTRY_NOTIFY(alarm_control_panel::AlarmControlPanel, alarm_control_panel) +#endif + +#ifdef USE_EVENT +CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX(event::Event, event) +#endif + +#ifdef USE_UPDATE +CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX(update::UpdateEntity, update) +#endif + +#undef CONTROLLER_REGISTRY_NOTIFY +#undef CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX + +} // namespace esphome + +#endif // USE_CONTROLLER_REGISTRY diff --git a/esphome/core/controller_registry.h b/esphome/core/controller_registry.h new file mode 100644 index 0000000000..640a276a0a --- /dev/null +++ b/esphome/core/controller_registry.h @@ -0,0 +1,245 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_CONTROLLER_REGISTRY + +#include "esphome/core/helpers.h" + +// Forward declarations +namespace esphome { + +class Controller; + +#ifdef USE_BINARY_SENSOR +namespace binary_sensor { +class BinarySensor; +} +#endif + +#ifdef USE_FAN +namespace fan { +class Fan; +} +#endif + +#ifdef USE_LIGHT +namespace light { +class LightState; +} +#endif + +#ifdef USE_SENSOR +namespace sensor { +class Sensor; +} +#endif + +#ifdef USE_SWITCH +namespace switch_ { +class Switch; +} +#endif + +#ifdef USE_COVER +namespace cover { +class Cover; +} +#endif + +#ifdef USE_TEXT_SENSOR +namespace text_sensor { +class TextSensor; +} +#endif + +#ifdef USE_CLIMATE +namespace climate { +class Climate; +} +#endif + +#ifdef USE_NUMBER +namespace number { +class Number; +} +#endif + +#ifdef USE_DATETIME_DATE +namespace datetime { +class DateEntity; +} +#endif + +#ifdef USE_DATETIME_TIME +namespace datetime { +class TimeEntity; +} +#endif + +#ifdef USE_DATETIME_DATETIME +namespace datetime { +class DateTimeEntity; +} +#endif + +#ifdef USE_TEXT +namespace text { +class Text; +} +#endif + +#ifdef USE_SELECT +namespace select { +class Select; +} +#endif + +#ifdef USE_LOCK +namespace lock { +class Lock; +} +#endif + +#ifdef USE_VALVE +namespace valve { +class Valve; +} +#endif + +#ifdef USE_MEDIA_PLAYER +namespace media_player { +class MediaPlayer; +} +#endif + +#ifdef USE_ALARM_CONTROL_PANEL +namespace alarm_control_panel { +class AlarmControlPanel; +} +#endif + +#ifdef USE_EVENT +namespace event { +class Event; +} +#endif + +#ifdef USE_UPDATE +namespace update { +class UpdateEntity; +} +#endif + +/** Global registry for Controllers to receive entity state updates. + * + * This singleton registry allows Controllers (APIServer, WebServer) to receive + * entity state change notifications without storing per-entity callbacks. + * + * Instead of each entity maintaining controller callbacks (32 bytes overhead per entity), + * entities call ControllerRegistry::notify_*_update() which iterates the small list + * of registered controllers (typically 2: API and WebServer). + * + * Controllers read state directly from entities using existing accessors (obj->state, etc.) + * rather than receiving it as callback parameters that were being ignored anyway. + * + * Memory savings: 32 bytes per entity (2 controllers × 16 bytes std::function overhead) + * Typical config (25 entities): ~780 bytes saved + * Large config (80 entities): ~2,540 bytes saved + */ +class ControllerRegistry { + public: + /** Register a controller to receive entity state updates. + * + * Controllers should call this in their setup() method. + * Typically only APIServer and WebServer register. + */ + static void register_controller(Controller *controller); + +#ifdef USE_BINARY_SENSOR + static void notify_binary_sensor_update(binary_sensor::BinarySensor *obj); +#endif + +#ifdef USE_FAN + static void notify_fan_update(fan::Fan *obj); +#endif + +#ifdef USE_LIGHT + static void notify_light_update(light::LightState *obj); +#endif + +#ifdef USE_SENSOR + static void notify_sensor_update(sensor::Sensor *obj); +#endif + +#ifdef USE_SWITCH + static void notify_switch_update(switch_::Switch *obj); +#endif + +#ifdef USE_COVER + static void notify_cover_update(cover::Cover *obj); +#endif + +#ifdef USE_TEXT_SENSOR + static void notify_text_sensor_update(text_sensor::TextSensor *obj); +#endif + +#ifdef USE_CLIMATE + static void notify_climate_update(climate::Climate *obj); +#endif + +#ifdef USE_NUMBER + static void notify_number_update(number::Number *obj); +#endif + +#ifdef USE_DATETIME_DATE + static void notify_date_update(datetime::DateEntity *obj); +#endif + +#ifdef USE_DATETIME_TIME + static void notify_time_update(datetime::TimeEntity *obj); +#endif + +#ifdef USE_DATETIME_DATETIME + static void notify_datetime_update(datetime::DateTimeEntity *obj); +#endif + +#ifdef USE_TEXT + static void notify_text_update(text::Text *obj); +#endif + +#ifdef USE_SELECT + static void notify_select_update(select::Select *obj); +#endif + +#ifdef USE_LOCK + static void notify_lock_update(lock::Lock *obj); +#endif + +#ifdef USE_VALVE + static void notify_valve_update(valve::Valve *obj); +#endif + +#ifdef USE_MEDIA_PLAYER + static void notify_media_player_update(media_player::MediaPlayer *obj); +#endif + +#ifdef USE_ALARM_CONTROL_PANEL + static void notify_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj); +#endif + +#ifdef USE_EVENT + static void notify_event(event::Event *obj); +#endif + +#ifdef USE_UPDATE + static void notify_update(update::UpdateEntity *obj); +#endif + + protected: + static StaticVector controllers; +}; + +} // namespace esphome + +#endif // USE_CONTROLLER_REGISTRY diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 56de0127a6..1373ea6366 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -28,11 +28,13 @@ #define USE_BUTTON #define USE_CAMERA #define USE_CLIMATE +#define USE_CONTROLLER_REGISTRY #define USE_COVER #define USE_DATETIME #define USE_DATETIME_DATE #define USE_DATETIME_DATETIME #define USE_DATETIME_TIME +#define USE_DEBUG #define USE_DEEP_SLEEP #define USE_DEVICES #define USE_DISPLAY @@ -44,10 +46,12 @@ #define USE_GRAPHICAL_DISPLAY_MENU #define USE_HOMEASSISTANT_TIME #define USE_HTTP_REQUEST_OTA_WATCHDOG_TIMEOUT 8000 // NOLINT +#define USE_IMPROV_SERIAL_NEXT_URL #define USE_JSON #define USE_LIGHT #define USE_LOCK #define USE_LOGGER +#define USE_LOGGER_RUNTIME_TAG_LEVELS #define USE_LVGL #define USE_LVGL_ANIMIMG #define USE_LVGL_ARC @@ -82,6 +86,10 @@ #define USE_LVGL_TILEVIEW #define USE_LVGL_TOUCHSCREEN #define USE_MDNS +#define USE_MDNS_STORE_SERVICES +#define MDNS_SERVICE_COUNT 3 +#define MDNS_DYNAMIC_TXT_COUNT 3 +#define SNTP_SERVER_COUNT 3 #define USE_MEDIA_PLAYER #define USE_NEXTION_TFT_UPLOAD #define USE_NUMBER @@ -100,6 +108,7 @@ #define USE_UART_DEBUGGER #define USE_UPDATE #define USE_VALVE +#define USE_ZWAVE_PROXY // Feature flags which do not work for zephyr #ifndef USE_ZEPHYR @@ -109,24 +118,33 @@ #define USE_API #define USE_API_CLIENT_CONNECTED_TRIGGER #define USE_API_CLIENT_DISCONNECTED_TRIGGER +#define USE_API_HOMEASSISTANT_ACTION_RESPONSES +#define USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON #define USE_API_HOMEASSISTANT_SERVICES #define USE_API_HOMEASSISTANT_STATES #define USE_API_NOISE #define USE_API_PLAINTEXT -#define USE_API_SERVICES +#define USE_API_USER_DEFINED_ACTIONS +#define USE_API_CUSTOM_SERVICES +#define API_MAX_SEND_QUEUE 8 #define USE_MD5 +#define USE_SHA256 #define USE_MQTT #define USE_NETWORK #define USE_ONLINE_IMAGE_BMP_SUPPORT #define USE_ONLINE_IMAGE_PNG_SUPPORT #define USE_ONLINE_IMAGE_JPEG_SUPPORT #define USE_OTA +#define USE_OTA_MD5 #define USE_OTA_PASSWORD +#define USE_OTA_SHA256 +#define ALLOW_OTA_DOWNGRADE_MD5 #define USE_OTA_STATE_CALLBACK #define USE_OTA_VERSION 2 #define USE_TIME_TIMEZONE #define USE_WIFI #define USE_WIFI_AP +#define USE_WIFI_MANUAL_IP #define USE_WIREGUARD #endif @@ -140,6 +158,7 @@ // IDF-specific feature flags #ifdef USE_ESP_IDF #define USE_MQTT_IDF_ENQUEUE +#define ESPHOME_LOOP_TASK_STACK_SIZE 8192 #endif // ESP32-specific feature flags @@ -148,17 +167,39 @@ #define USE_BLUETOOTH_PROXY #define BLUETOOTH_PROXY_MAX_CONNECTIONS 3 +#define BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE 16 #define USE_CAPTIVE_PORTAL #define USE_ESP32_BLE +#define USE_ESP32_BLE_MAX_CONNECTIONS 3 #define USE_ESP32_BLE_CLIENT #define USE_ESP32_BLE_DEVICE #define USE_ESP32_BLE_SERVER +#define USE_ESP32_BLE_UUID +#define USE_ESP32_BLE_ADVERTISING +#define USE_ESP32_BLE_SERVER_SET_VALUE_ACTION +#define USE_ESP32_BLE_SERVER_DESCRIPTOR_SET_VALUE_ACTION +#define USE_ESP32_BLE_SERVER_NOTIFY_ACTION +#define USE_ESP32_BLE_SERVER_CHARACTERISTIC_ON_WRITE +#define USE_ESP32_BLE_SERVER_DESCRIPTOR_ON_WRITE +#define USE_ESP32_BLE_SERVER_ON_CONNECT +#define USE_ESP32_BLE_SERVER_ON_DISCONNECT +#define ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT 1 +#define ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT 1 +#define ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT 2 +#define ESPHOME_ESP32_BLE_GAP_SCAN_EVENT_HANDLER_COUNT 1 +#define ESPHOME_ESP32_BLE_GATTC_EVENT_HANDLER_COUNT 1 +#define ESPHOME_ESP32_BLE_GATTS_EVENT_HANDLER_COUNT 1 +#define ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT 2 +#define USE_ESP32_CAMERA_JPEG_ENCODER +#define USE_HTTP_REQUEST_RESPONSE #define USE_I2C #define USE_IMPROV +#define USE_ESP32_IMPROV_NEXT_URL #define USE_MICROPHONE #define USE_PSRAM #define USE_SOCKET_IMPL_BSD_SOCKETS #define USE_SOCKET_SELECT_SUPPORT +#define USE_WAKE_LOOP_THREADSAFE #define USE_SPEAKER #define USE_SPI #define USE_VOICE_ASSISTANT @@ -168,10 +209,16 @@ #define USE_WEBSERVER_PORT 80 // NOLINT #define USE_WEBSERVER_SORTING #define USE_WIFI_11KV_SUPPORT +#define USE_WIFI_FAST_CONNECT +#define USE_WIFI_CALLBACKS +#define USE_WIFI_RUNTIME_POWER_SAVE +#define USB_HOST_MAX_REQUESTS 16 #ifdef USE_ARDUINO -#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 2, 1) +#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 2) #define USE_ETHERNET +#define USE_ETHERNET_KSZ8081 +#define USE_ETHERNET_MANUAL_IP #endif #ifdef USE_ESP_IDF @@ -199,6 +246,7 @@ #define USE_CAPTIVE_PORTAL #define USE_ESP8266_PREFERENCES_FLASH #define USE_HTTP_REQUEST_ESP8266_HTTPS +#define USE_HTTP_REQUEST_RESPONSE #define USE_I2C #define USE_SOCKET_IMPL_LWIP_TCP @@ -207,8 +255,10 @@ // Dummy firmware payload for shelly_dimmer #define USE_SHD_FIRMWARE_MAJOR_VERSION 56 #define USE_SHD_FIRMWARE_MINOR_VERSION 5 +// clang-format off #define USE_SHD_FIRMWARE_DATA \ {} +// clang-format on #define USE_WEBSERVER #define USE_WEBSERVER_AUTH @@ -217,6 +267,7 @@ #ifdef USE_RP2040 #define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 0) +#define USE_HTTP_REQUEST_RESPONSE #define USE_I2C #define USE_LOGGER_USB_CDC #define USE_SOCKET_IMPL_LWIP_TCP @@ -233,10 +284,19 @@ #endif #ifdef USE_HOST +#define USE_HTTP_REQUEST_RESPONSE #define USE_SOCKET_IMPL_BSD_SOCKETS #define USE_SOCKET_SELECT_SUPPORT #endif +#ifdef USE_NRF52 +#define USE_NRF52_DFU +#define USE_NRF52_REG0_VOUT 5 +#define USE_NRF52_UICR_ERASE +#define USE_SOFTDEVICE_ID 7 +#define USE_SOFTDEVICE_VERSION 1 +#endif + // Disabled feature flags // #define USE_BSEC // Requires a library with proprietary license // #define USE_BSEC2 // Requires a library with proprietary license @@ -244,6 +304,7 @@ #define USE_DASHBOARD_IMPORT // Default counts for static analysis +#define CONTROLLER_REGISTRY_MAX 2 #define ESPHOME_COMPONENT_COUNT 50 #define ESPHOME_DEVICE_COUNT 10 #define ESPHOME_AREA_COUNT 10 diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index 2ea9c77a3e..046f99d8cc 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -1,6 +1,7 @@ #include "esphome/core/entity_base.h" #include "esphome/core/application.h" #include "esphome/core/helpers.h" +#include "esphome/core/string_ref.h" namespace esphome { @@ -44,27 +45,46 @@ void EntityBase::set_icon(const char *icon) { #endif } +// Check if the object_id is dynamic (changes with MAC suffix) +bool EntityBase::is_object_id_dynamic_() const { + return !this->flags_.has_own_name && App.is_name_add_mac_suffix_enabled(); +} + // Entity Object ID std::string EntityBase::get_object_id() const { // Check if `App.get_friendly_name()` is constant or dynamic. - if (!this->flags_.has_own_name && App.is_name_add_mac_suffix_enabled()) { + if (this->is_object_id_dynamic_()) { // `App.get_friendly_name()` is dynamic. return str_sanitize(str_snake_case(App.get_friendly_name())); - } else { - // `App.get_friendly_name()` is constant. - if (this->object_id_c_str_ == nullptr) { - return ""; - } - return this->object_id_c_str_; } + // `App.get_friendly_name()` is constant. + return this->object_id_c_str_ == nullptr ? "" : this->object_id_c_str_; +} +StringRef EntityBase::get_object_id_ref_for_api_() const { + static constexpr auto EMPTY_STRING = StringRef::from_lit(""); + // Return empty for dynamic case (MAC suffix) + if (this->is_object_id_dynamic_()) { + return EMPTY_STRING; + } + // For static case, return the string or empty if null + return this->object_id_c_str_ == nullptr ? EMPTY_STRING : StringRef(this->object_id_c_str_); } void EntityBase::set_object_id(const char *object_id) { this->object_id_c_str_ = object_id; this->calc_object_id_(); } +void EntityBase::set_name_and_object_id(const char *name, const char *object_id) { + this->set_name(name); + this->object_id_c_str_ = object_id; + this->calc_object_id_(); +} + // Calculate Object ID Hash from Entity Name -void EntityBase::calc_object_id_() { this->object_id_hash_ = fnv1_hash(this->get_object_id()); } +void EntityBase::calc_object_id_() { + this->object_id_hash_ = + fnv1_hash(this->is_object_id_dynamic_() ? this->get_object_id().c_str() : this->object_id_c_str_); +} uint32_t EntityBase::get_object_id_hash() { return this->object_id_hash_; } diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index e60e0728bc..aa9b92877a 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -12,6 +12,15 @@ namespace esphome { +// Forward declaration for friend access +namespace api { +class APIConnection; +} // namespace api + +namespace web_server { +struct UrlMatch; +} // namespace web_server + enum EntityCategory : uint8_t { ENTITY_CATEGORY_NONE = 0, ENTITY_CATEGORY_CONFIG = 1, @@ -32,6 +41,9 @@ class EntityBase { std::string get_object_id() const; void set_object_id(const char *object_id); + // Set both name and object_id in one call (reduces generated code size) + void set_name_and_object_id(const char *name, const char *object_id); + // Get the unique Object ID of this Entity uint32_t get_object_id_hash(); @@ -52,6 +64,9 @@ class EntityBase { } // Get/set this entity's icon + ESPDEPRECATED( + "Use get_icon_ref() instead for better performance (avoids string copy). Will be removed in ESPHome 2026.5.0", + "2025.11.0") std::string get_icon() const; void set_icon(const char *icon); StringRef get_icon_ref() const { @@ -80,12 +95,48 @@ class EntityBase { // Set has_state - for components that need to manually set this void set_has_state(bool state) { this->flags_.has_state = state; } + /** + * @brief Get a unique hash for storing preferences/settings for this entity. + * + * This method returns a hash that uniquely identifies the entity for the purpose of + * storing preferences (such as calibration, state, etc.). Unlike get_object_id_hash(), + * this hash also incorporates the device_id (if devices are enabled), ensuring uniqueness + * across multiple devices that may have entities with the same object_id. + * + * Use this method when storing or retrieving preferences/settings that should be unique + * per device-entity pair. Use get_object_id_hash() when you need a hash that identifies + * the entity regardless of the device it belongs to. + * + * For backward compatibility, if device_id is 0 (the main device), the hash is unchanged + * from previous versions, so existing single-device configurations will continue to work. + * + * @return uint32_t The unique hash for preferences, including device_id if available. + */ + uint32_t get_preference_hash() { +#ifdef USE_DEVICES + // Combine object_id_hash with device_id to ensure uniqueness across devices + // Note: device_id is 0 for the main device, so XORing with 0 preserves the original hash + // This ensures backward compatibility for existing single-device configurations + return this->get_object_id_hash() ^ this->get_device_id(); +#else + // Without devices, just use object_id_hash as before + return this->get_object_id_hash(); +#endif + } + protected: - /// The hash_base() function has been deprecated. It is kept in this - /// class for now, to prevent external components from not compiling. - virtual uint32_t hash_base() { return 0L; } + friend class api::APIConnection; + friend struct web_server::UrlMatch; + + // Get object_id as StringRef when it's static (for API usage) + // Returns empty StringRef if object_id is dynamic (needs allocation) + StringRef get_object_id_ref_for_api_() const; + void calc_object_id_(); + /// Check if the object_id is dynamic (changes with MAC suffix) + bool is_object_id_dynamic_() const; + StringRef name_; const char *object_id_c_str_{nullptr}; #ifdef USE_ENTITY_ICON @@ -110,6 +161,9 @@ class EntityBase { class EntityBase_DeviceClass { // NOLINT(readability-identifier-naming) public: /// Get the device class, using the manual override if set. + ESPDEPRECATED("Use get_device_class_ref() instead for better performance (avoids string copy). Will be removed in " + "ESPHome 2026.5.0", + "2025.11.0") std::string get_device_class(); /// Manually set the device class. void set_device_class(const char *device_class); @@ -126,6 +180,9 @@ class EntityBase_DeviceClass { // NOLINT(readability-identifier-naming) class EntityBase_UnitOfMeasurement { // NOLINT(readability-identifier-naming) public: /// Get the unit of measurement, using the manual override if set. + ESPDEPRECATED("Use get_unit_of_measurement_ref() instead for better performance (avoids string copy). Will be " + "removed in ESPHome 2026.5.0", + "2025.11.0") std::string get_unit_of_measurement(); /// Manually set the unit of measurement. void set_unit_of_measurement(const char *unit_of_measurement); diff --git a/esphome/core/entity_helpers.py b/esphome/core/entity_helpers.py index cc388ffb4c..f360b4d809 100644 --- a/esphome/core/entity_helpers.py +++ b/esphome/core/entity_helpers.py @@ -16,7 +16,7 @@ from esphome.core import CORE, ID from esphome.cpp_generator import MockObj, add, get_variable import esphome.final_validate as fv from esphome.helpers import sanitize, snake_case -from esphome.types import ConfigType +from esphome.types import ConfigType, EntityMetadata _LOGGER = logging.getLogger(__name__) @@ -77,15 +77,13 @@ async def setup_entity(var: MockObj, config: ConfigType, platform: str) -> None: """ # Get device info device_name: str | None = None - if CONF_DEVICE_ID in config: - device_id_obj: ID = config[CONF_DEVICE_ID] + device_id_obj: ID | None + if device_id_obj := config.get(CONF_DEVICE_ID): device: MockObj = await get_variable(device_id_obj) add(var.set_device(device)) # Get device name for object ID calculation device_name = device_id_obj.id - add(var.set_name(config[CONF_NAME])) - # Calculate base object_id using the same logic as C++ # This must match the C++ behavior in esphome/core/entity_base.cpp base_object_id = get_base_entity_object_id( @@ -97,15 +95,17 @@ async def setup_entity(var: MockObj, config: ConfigType, platform: str) -> None: "Entity has empty name, using '%s' as object_id base", base_object_id ) - # Set the object ID - add(var.set_object_id(base_object_id)) + # Set both name and object_id in one call to reduce generated code size + add(var.set_name_and_object_id(config[CONF_NAME], base_object_id)) _LOGGER.debug( "Setting object_id '%s' for entity '%s' on platform '%s'", base_object_id, config[CONF_NAME], platform, ) - add(var.set_disabled_by_default(config[CONF_DISABLED_BY_DEFAULT])) + # Only set disabled_by_default if True (default is False) + if config[CONF_DISABLED_BY_DEFAULT]: + add(var.set_disabled_by_default(True)) if CONF_INTERNAL in config: add(var.set_internal(config[CONF_INTERNAL])) if CONF_ICON in config: @@ -199,8 +199,8 @@ def entity_duplicate_validator(platform: str) -> Callable[[ConfigType], ConfigTy # Get device name if entity is on a sub-device device_name = None device_id = "" # Empty string for main device - if CONF_DEVICE_ID in config: - device_id_obj = config[CONF_DEVICE_ID] + device_id_obj: ID | None + if device_id_obj := config.get(CONF_DEVICE_ID): device_name = device_id_obj.id # Use the device ID string directly for uniqueness device_id = device_id_obj.id @@ -214,14 +214,59 @@ def entity_duplicate_validator(platform: str) -> Callable[[ConfigType], ConfigTy # Check for duplicates unique_key = (device_id, platform, name_key) if unique_key in CORE.unique_ids: - device_prefix = f" on device '{device_id}'" if device_id else "" - raise cv.Invalid( - f"Duplicate {platform} entity with name '{entity_name}' found{device_prefix}. " - f"Each entity on a device must have a unique name within its platform." - ) + # Get the existing entity metadata + existing = CORE.unique_ids[unique_key] + existing_name = existing.get("name", entity_name) + existing_device = existing.get("device_id", "") + existing_id = existing.get("entity_id", "unknown") - # Add to tracking set - CORE.unique_ids.add(unique_key) + # Build detailed error message + device_prefix = f" on device '{device_id}'" if device_id else "" + existing_device_prefix = ( + f" on device '{existing_device}'" if existing_device else "" + ) + existing_component = existing.get("component", "unknown") + + # Provide more context about where the duplicate was found + conflict_msg = ( + f"Conflicts with entity '{existing_name}'{existing_device_prefix}" + ) + if existing_id != "unknown": + conflict_msg += f" (id: {existing_id})" + if existing_component != "unknown": + conflict_msg += f" from component '{existing_component}'" + + # Show both original names and their ASCII-only versions if they differ + sanitized_msg = "" + if entity_name != existing_name: + sanitized_msg = ( + f"\n Original names: '{entity_name}' and '{existing_name}'" + f"\n Both convert to ASCII ID: '{name_key}'" + "\n To fix: Add unique ASCII characters (e.g., '1', '2', or 'A', 'B')" + "\n to distinguish them" + ) + + # Skip duplicate entity name validation when testing_mode is enabled + # This flag is used for grouped component testing + if not CORE.testing_mode: + raise cv.Invalid( + f"Duplicate {platform} entity with name '{entity_name}' found{device_prefix}. " + f"{conflict_msg}. " + "Each entity on a device must have a unique name within its platform." + f"{sanitized_msg}" + ) + + # Store metadata about this entity + entity_metadata: EntityMetadata = { + "name": entity_name, + "device_id": device_id, + "platform": platform, + "entity_id": str(config.get(CONF_ID, "unknown")), + "component": CORE.current_component or "unknown", + } + + # Add to tracking dict + CORE.unique_ids[unique_key] = entity_metadata return config return validator diff --git a/esphome/core/finite_set_mask.h b/esphome/core/finite_set_mask.h new file mode 100644 index 0000000000..f9cd0377c7 --- /dev/null +++ b/esphome/core/finite_set_mask.h @@ -0,0 +1,171 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace esphome { + +/// Default bit mapping policy for contiguous enums starting at 0 +/// Provides 1:1 mapping where enum value equals bit position +template struct DefaultBitPolicy { + // Automatic bitmask type selection based on MaxBits + // ≤8 bits: uint8_t, ≤16 bits: uint16_t, otherwise: uint32_t + using mask_t = typename std::conditional<(MaxBits <= 8), uint8_t, + typename std::conditional<(MaxBits <= 16), uint16_t, uint32_t>::type>::type; + + static constexpr int MAX_BITS = MaxBits; + + static constexpr unsigned to_bit(ValueType value) { return static_cast(value); } + + static constexpr ValueType from_bit(unsigned bit) { return static_cast(bit); } +}; + +/// Generic bitmask for storing a finite set of discrete values efficiently. +/// Replaces std::set to eliminate red-black tree overhead (~586 bytes per instantiation). +/// +/// Template parameters: +/// ValueType: The type to store (typically enum, but can be any discrete bounded type) +/// BitPolicy: Policy class defining bit mapping and mask type (defaults to DefaultBitPolicy) +/// +/// BitPolicy requirements: +/// - using mask_t = // Bitmask storage type +/// - static constexpr int MAX_BITS // Maximum number of bits +/// - static constexpr unsigned to_bit(ValueType) // Convert value to bit position +/// - static constexpr ValueType from_bit(unsigned) // Convert bit position to value +/// +/// Example usage (1:1 mapping - climate enums): +/// // For contiguous enums starting at 0, use DefaultBitPolicy +/// using ClimateModeMask = FiniteSetMask>; +/// ClimateModeMask modes({CLIMATE_MODE_HEAT, CLIMATE_MODE_COOL}); +/// if (modes.count(CLIMATE_MODE_HEAT)) { ... } +/// for (auto mode : modes) { ... } +/// +/// Example usage (custom mapping - ColorMode): +/// // For custom mappings, define a custom BitPolicy +/// // See esphome/components/light/color_mode.h for complete example +/// +/// Design notes: +/// - Policy-based design allows custom bit mappings without template specialization +/// - Iterator converts bit positions to actual values during traversal +/// - All operations are constexpr-compatible for compile-time initialization +/// - Drop-in replacement for std::set with simpler API +/// +template> class FiniteSetMask { + public: + using bitmask_t = typename BitPolicy::mask_t; + + constexpr FiniteSetMask() = default; + + /// Construct from initializer list: {VALUE1, VALUE2, ...} + constexpr FiniteSetMask(std::initializer_list values) { + for (auto value : values) { + this->insert(value); + } + } + + /// Add a single value to the set (std::set compatibility) + constexpr void insert(ValueType value) { this->mask_ |= (static_cast(1) << BitPolicy::to_bit(value)); } + + /// Add multiple values from initializer list + constexpr void insert(std::initializer_list values) { + for (auto value : values) { + this->insert(value); + } + } + + /// Remove a value from the set (std::set compatibility) + constexpr void erase(ValueType value) { this->mask_ &= ~(static_cast(1) << BitPolicy::to_bit(value)); } + + /// Clear all values from the set + constexpr void clear() { this->mask_ = 0; } + + /// Check if the set contains a specific value (std::set compatibility) + /// Returns 1 if present, 0 if not (same as std::set for unique elements) + constexpr size_t count(ValueType value) const { + return (this->mask_ & (static_cast(1) << BitPolicy::to_bit(value))) != 0 ? 1 : 0; + } + + /// Count the number of values in the set + constexpr size_t size() const { + // Brian Kernighan's algorithm - efficient for sparse bitmasks + // Typical case: 2-4 modes out of 10 possible + bitmask_t n = this->mask_; + size_t count = 0; + while (n) { + n &= n - 1; // Clear the least significant set bit + count++; + } + return count; + } + + /// Check if the set is empty + constexpr bool empty() const { return this->mask_ == 0; } + + /// Iterator support for range-based for loops and API encoding + /// Iterates over set bits and converts bit positions to values + /// Optimization: removes bits from mask as we iterate + class Iterator { + public: + using iterator_category = std::forward_iterator_tag; + using value_type = ValueType; + using difference_type = std::ptrdiff_t; + using pointer = const ValueType *; + using reference = ValueType; + + constexpr explicit Iterator(bitmask_t mask) : mask_(mask) {} + + constexpr ValueType operator*() const { + // Return value for the first set bit + return BitPolicy::from_bit(find_next_set_bit(mask_, 0)); + } + + constexpr Iterator &operator++() { + // Clear the lowest set bit (Brian Kernighan's algorithm) + mask_ &= mask_ - 1; + return *this; + } + + constexpr bool operator==(const Iterator &other) const { return mask_ == other.mask_; } + + constexpr bool operator!=(const Iterator &other) const { return !(*this == other); } + + private: + bitmask_t mask_; + }; + + constexpr Iterator begin() const { return Iterator(mask_); } + constexpr Iterator end() const { return Iterator(0); } + + /// Get the raw bitmask value for optimized operations + constexpr bitmask_t get_mask() const { return this->mask_; } + + /// Check if a specific value is present in a raw bitmask + /// Useful for checking intersection results without creating temporary objects + static constexpr bool mask_contains(bitmask_t mask, ValueType value) { + return (mask & (static_cast(1) << BitPolicy::to_bit(value))) != 0; + } + + /// Get the first value from a raw bitmask + /// Used for optimizing intersection logic (e.g., "pick first suitable mode") + static constexpr ValueType first_value_from_mask(bitmask_t mask) { + return BitPolicy::from_bit(find_next_set_bit(mask, 0)); + } + + /// Find the next set bit in a bitmask starting from a given position + /// Returns the bit position, or MAX_BITS if no more bits are set + static constexpr int find_next_set_bit(bitmask_t mask, int start_bit) { + int bit = start_bit; + while (bit < BitPolicy::MAX_BITS && !(mask & (static_cast(1) << bit))) { + ++bit; + } + return bit; + } + + protected: + bitmask_t mask_{0}; +}; + +} // namespace esphome diff --git a/esphome/core/hash_base.h b/esphome/core/hash_base.h new file mode 100644 index 0000000000..c45c4df70b --- /dev/null +++ b/esphome/core/hash_base.h @@ -0,0 +1,56 @@ +#pragma once + +#include +#include +#include +#include "esphome/core/helpers.h" + +namespace esphome { + +/// Base class for hash algorithms +class HashBase { + public: + virtual ~HashBase() = default; + + /// Initialize a new hash computation + virtual void init() = 0; + + /// Add bytes of data for the hash + virtual void add(const uint8_t *data, size_t len) = 0; + void add(const char *data, size_t len) { this->add((const uint8_t *) data, len); } + + /// Compute the hash based on provided data + virtual void calculate() = 0; + + /// Retrieve the hash as bytes + void get_bytes(uint8_t *output) { memcpy(output, this->digest_, this->get_size()); } + + /// Retrieve the hash as hex characters + void get_hex(char *output) { + for (size_t i = 0; i < this->get_size(); i++) { + uint8_t byte = this->digest_[i]; + output[i * 2] = format_hex_char(byte >> 4); + output[i * 2 + 1] = format_hex_char(byte & 0x0F); + } + } + + /// Compare the hash against a provided byte-encoded hash + bool equals_bytes(const uint8_t *expected) { return memcmp(this->digest_, expected, this->get_size()) == 0; } + + /// Compare the hash against a provided hex-encoded hash + bool equals_hex(const char *expected) { + uint8_t parsed[32]; // Fixed size for max hash (SHA256 = 32 bytes) + if (!parse_hex(expected, parsed, this->get_size())) { + return false; + } + return this->equals_bytes(parsed); + } + + /// Get the size of the hash in bytes (16 for MD5, 32 for SHA256) + virtual size_t get_size() const = 0; + + protected: + uint8_t digest_[32]; // Storage sized for max(MD5=16, SHA256=32) bytes +}; + +} // namespace esphome diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index e84f5a7317..1f675563c7 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -3,6 +3,7 @@ #include "esphome/core/defines.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" +#include "esphome/core/string_ref.h" #include #include @@ -41,17 +42,28 @@ static const uint16_t CRC16_1021_BE_LUT_H[] = {0x0000, 0x1231, 0x2462, 0x3653, 0 // Mathematics -uint8_t crc8(const uint8_t *data, uint8_t len) { - uint8_t crc = 0; - +uint8_t crc8(const uint8_t *data, uint8_t len, uint8_t crc, uint8_t poly, bool msb_first) { while ((len--) != 0u) { uint8_t inbyte = *data++; - for (uint8_t i = 8; i != 0u; i--) { - bool mix = (crc ^ inbyte) & 0x01; - crc >>= 1; - if (mix) - crc ^= 0x8C; - inbyte >>= 1; + if (msb_first) { + // MSB first processing (for polynomials like 0x31, 0x07) + crc ^= inbyte; + for (uint8_t i = 8; i != 0u; i--) { + if (crc & 0x80) { + crc = (crc << 1) ^ poly; + } else { + crc <<= 1; + } + } + } else { + // LSB first processing (default for Dallas/Maxim 0x8C) + for (uint8_t i = 8; i != 0u; i--) { + bool mix = (crc ^ inbyte) & 0x01; + crc >>= 1; + if (mix) + crc ^= poly; + inbyte >>= 1; + } } } return crc; @@ -131,11 +143,13 @@ uint16_t crc16be(const uint8_t *data, uint16_t len, uint16_t crc, uint16_t poly, return refout ? (crc ^ 0xffff) : crc; } -uint32_t fnv1_hash(const std::string &str) { +uint32_t fnv1_hash(const char *str) { uint32_t hash = 2166136261UL; - for (char c : str) { - hash *= 16777619UL; - hash ^= c; + if (str) { + while (*str) { + hash *= 16777619UL; + hash ^= *str++; + } } return hash; } @@ -221,6 +235,34 @@ std::string str_sprintf(const char *fmt, ...) { return str; } +// Maximum size for name with suffix: 120 (max friendly name) + 1 (separator) + 6 (MAC suffix) + 1 (null term) +static constexpr size_t MAX_NAME_WITH_SUFFIX_SIZE = 128; + +std::string make_name_with_suffix(const char *name, size_t name_len, char sep, const char *suffix_ptr, + size_t suffix_len) { + char buffer[MAX_NAME_WITH_SUFFIX_SIZE]; + size_t total_len = name_len + 1 + suffix_len; + + // Silently truncate if needed: prioritize keeping the full suffix + if (total_len >= MAX_NAME_WITH_SUFFIX_SIZE) { + // NOTE: This calculation could underflow if suffix_len >= MAX_NAME_WITH_SUFFIX_SIZE - 2, + // but this is safe because this helper is only called with small suffixes: + // MAC suffixes (6-12 bytes), ".local" (5 bytes), etc. + name_len = MAX_NAME_WITH_SUFFIX_SIZE - suffix_len - 2; // -2 for separator and null terminator + total_len = name_len + 1 + suffix_len; + } + + memcpy(buffer, name, name_len); + buffer[name_len] = sep; + memcpy(buffer + name_len + 1, suffix_ptr, suffix_len); + buffer[total_len] = '\0'; + return std::string(buffer, total_len); +} + +std::string make_name_with_suffix(const std::string &name, char sep, const char *suffix_ptr, size_t suffix_len) { + return make_name_with_suffix(name.c_str(), name.size(), sep, suffix_ptr, suffix_len); +} + // Parsing & formatting size_t parse_hex(const char *str, size_t length, uint8_t *data, size_t count) { @@ -242,23 +284,22 @@ size_t parse_hex(const char *str, size_t length, uint8_t *data, size_t count) { } std::string format_mac_address_pretty(const uint8_t *mac) { - return str_snprintf("%02X:%02X:%02X:%02X:%02X:%02X", 17, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + char buf[18]; + format_mac_addr_upper(mac, buf); + return std::string(buf); } -static char format_hex_char(uint8_t v) { return v >= 10 ? 'a' + (v - 10) : '0' + v; } std::string format_hex(const uint8_t *data, size_t length) { std::string ret; ret.resize(length * 2); for (size_t i = 0; i < length; i++) { - ret[2 * i] = format_hex_char((data[i] & 0xF0) >> 4); + ret[2 * i] = format_hex_char(data[i] >> 4); ret[2 * i + 1] = format_hex_char(data[i] & 0x0F); } return ret; } std::string format_hex(const std::vector &data) { return format_hex(data.data(), data.size()); } -static char format_hex_pretty_char(uint8_t v) { return v >= 10 ? 'A' + (v - 10) : '0' + v; } - // Shared implementation for uint8_t and string hex formatting static std::string format_hex_pretty_uint8(const uint8_t *data, size_t length, char separator, bool show_length) { if (data == nullptr || length == 0) @@ -267,7 +308,7 @@ static std::string format_hex_pretty_uint8(const uint8_t *data, size_t length, c uint8_t multiple = separator ? 3 : 2; // 3 if separator is not \0, 2 otherwise ret.resize(multiple * length - (separator ? 1 : 0)); for (size_t i = 0; i < length; i++) { - ret[multiple * i] = format_hex_pretty_char((data[i] & 0xF0) >> 4); + ret[multiple * i] = format_hex_pretty_char(data[i] >> 4); ret[multiple * i + 1] = format_hex_pretty_char(data[i] & 0x0F); if (separator && i != length - 1) ret[multiple * i + 2] = separator; @@ -336,17 +377,34 @@ ParseOnOffState parse_on_off(const char *str, const char *on, const char *off) { return PARSE_NONE; } -std::string value_accuracy_to_string(float value, int8_t accuracy_decimals) { +static inline void normalize_accuracy_decimals(float &value, int8_t &accuracy_decimals) { if (accuracy_decimals < 0) { auto multiplier = powf(10.0f, accuracy_decimals); value = roundf(value * multiplier) / multiplier; accuracy_decimals = 0; } +} + +std::string value_accuracy_to_string(float value, int8_t accuracy_decimals) { + normalize_accuracy_decimals(value, accuracy_decimals); char tmp[32]; // should be enough, but we should maybe improve this at some point. snprintf(tmp, sizeof(tmp), "%.*f", accuracy_decimals, value); return std::string(tmp); } +std::string value_accuracy_with_uom_to_string(float value, int8_t accuracy_decimals, StringRef unit_of_measurement) { + normalize_accuracy_decimals(value, accuracy_decimals); + // Buffer sized for float (up to ~15 chars) + space + typical UOM (usually <20 chars like "μS/cm") + // snprintf truncates safely if exceeded, though ESPHome UOMs are typically short + char tmp[64]; + if (unit_of_measurement.empty()) { + snprintf(tmp, sizeof(tmp), "%.*f", accuracy_decimals, value); + } else { + snprintf(tmp, sizeof(tmp), "%.*f %s", accuracy_decimals, value, unit_of_measurement.c_str()); + } + return std::string(tmp); +} + int8_t step_to_accuracy_decimals(float step) { // use printf %g to find number of digits based on temperature step char buf[32]; @@ -578,13 +636,27 @@ bool HighFrequencyLoopRequester::is_high_frequency() { return num_requests > 0; std::string get_mac_address() { uint8_t mac[6]; get_mac_address_raw(mac); - return str_snprintf("%02x%02x%02x%02x%02x%02x", 12, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + char buf[13]; + format_mac_addr_lower_no_sep(mac, buf); + return std::string(buf); } std::string get_mac_address_pretty() { + char buf[18]; + return std::string(get_mac_address_pretty_into_buffer(buf)); +} + +void get_mac_address_into_buffer(std::span buf) { uint8_t mac[6]; get_mac_address_raw(mac); - return format_mac_address_pretty(mac); + format_mac_addr_lower_no_sep(mac, buf.data()); +} + +const char *get_mac_address_pretty_into_buffer(std::span buf) { + uint8_t mac[6]; + get_mac_address_raw(mac); + format_mac_addr_upper(mac, buf.data()); + return buf.data(); } #ifndef USE_ESP32 @@ -599,8 +671,6 @@ bool mac_address_is_valid(const uint8_t *mac) { if (mac[i] != 0) { is_all_zeros = false; } - } - for (uint8_t i = 0; i < 6; i++) { if (mac[i] != 0xFF) { is_all_ones = false; } diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index b5fe59c4fd..a43c55e06b 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -8,9 +8,11 @@ #include #include #include +#include #include #include #include +#include #include "esphome/core/optional.h" @@ -45,6 +47,9 @@ namespace esphome { +// Forward declaration to avoid circular dependency with string_ref.h +class StringRef; + /// @name STL backports ///@{ @@ -82,6 +87,16 @@ template constexpr T byteswap(T n) { return m; } template<> constexpr uint8_t byteswap(uint8_t n) { return n; } +#ifdef USE_LIBRETINY +// LibreTiny's Beken framework redefines __builtin_bswap functions as non-constexpr +template<> inline uint16_t byteswap(uint16_t n) { return __builtin_bswap16(n); } +template<> inline uint32_t byteswap(uint32_t n) { return __builtin_bswap32(n); } +template<> inline uint64_t byteswap(uint64_t n) { return __builtin_bswap64(n); } +template<> inline int8_t byteswap(int8_t n) { return n; } +template<> inline int16_t byteswap(int16_t n) { return __builtin_bswap16(n); } +template<> inline int32_t byteswap(int32_t n) { return __builtin_bswap32(n); } +template<> inline int64_t byteswap(int64_t n) { return __builtin_bswap64(n); } +#else template<> constexpr uint16_t byteswap(uint16_t n) { return __builtin_bswap16(n); } template<> constexpr uint32_t byteswap(uint32_t n) { return __builtin_bswap32(n); } template<> constexpr uint64_t byteswap(uint64_t n) { return __builtin_bswap64(n); } @@ -89,12 +104,30 @@ template<> constexpr int8_t byteswap(int8_t n) { return n; } template<> constexpr int16_t byteswap(int16_t n) { return __builtin_bswap16(n); } template<> constexpr int32_t byteswap(int32_t n) { return __builtin_bswap32(n); } template<> constexpr int64_t byteswap(int64_t n) { return __builtin_bswap64(n); } +#endif ///@} /// @name Container utilities ///@{ +/// Lightweight read-only view over a const array stored in RODATA (will typically be in flash memory) +/// Avoids copying data from flash to RAM by keeping a pointer to the flash data. +/// Similar to std::span but with minimal overhead for embedded systems. + +template class ConstVector { + public: + constexpr ConstVector(const T *data, size_t size) : data_(data), size_(size) {} + + const constexpr T &operator[](size_t i) const { return data_[i]; } + constexpr size_t size() const { return size_; } + constexpr bool empty() const { return size_ == 0; } + + protected: + const T *data_; + size_t size_; +}; + /// Minimal static vector - saves memory by avoiding std::vector overhead template class StaticVector { public: @@ -116,6 +149,16 @@ template class StaticVector { } } + // Return reference to next element and increment count (with bounds checking) + T &emplace_next() { + if (count_ >= N) { + // Should never happen with proper size calculation + // Return reference to last element to avoid crash + return data_[N - 1]; + } + return data_[count_++]; + } + size_t size() const { return count_; } bool empty() const { return count_ == 0; } @@ -135,6 +178,183 @@ template class StaticVector { const_reverse_iterator rend() const { return const_reverse_iterator(begin()); } }; +/// Fixed-capacity vector - allocates once at runtime, never reallocates +/// This avoids std::vector template overhead (_M_realloc_insert, _M_default_append) +/// when size is known at initialization but not at compile time +template class FixedVector { + private: + T *data_{nullptr}; + size_t size_{0}; + size_t capacity_{0}; + + // Helper to destroy all elements without freeing memory + void destroy_elements_() { + // Only call destructors for non-trivially destructible types + if constexpr (!std::is_trivially_destructible::value) { + for (size_t i = 0; i < size_; i++) { + data_[i].~T(); + } + } + } + + // Helper to destroy elements and free memory + void cleanup_() { + if (data_ != nullptr) { + destroy_elements_(); + // Free raw memory + ::operator delete(data_); + } + } + + // Helper to reset pointers after cleanup + void reset_() { + data_ = nullptr; + capacity_ = 0; + size_ = 0; + } + + // Helper to assign from initializer list (shared by constructor and assignment operator) + void assign_from_initializer_list_(std::initializer_list init_list) { + init(init_list.size()); + size_t idx = 0; + for (const auto &item : init_list) { + new (data_ + idx) T(item); + ++idx; + } + size_ = init_list.size(); + } + + public: + FixedVector() = default; + + /// Constructor from initializer list - allocates exact size needed + /// This enables brace initialization: FixedVector v = {1, 2, 3}; + FixedVector(std::initializer_list init_list) { assign_from_initializer_list_(init_list); } + + ~FixedVector() { cleanup_(); } + + // Disable copy operations (avoid accidental expensive copies) + FixedVector(const FixedVector &) = delete; + FixedVector &operator=(const FixedVector &) = delete; + + // Enable move semantics (allows use in move-only containers like std::vector) + FixedVector(FixedVector &&other) noexcept : data_(other.data_), size_(other.size_), capacity_(other.capacity_) { + other.reset_(); + } + + FixedVector &operator=(FixedVector &&other) noexcept { + if (this != &other) { + // Delete our current data + cleanup_(); + // Take ownership of other's data + data_ = other.data_; + size_ = other.size_; + capacity_ = other.capacity_; + // Leave other in valid empty state + other.reset_(); + } + return *this; + } + + /// Assignment from initializer list - avoids temporary and move overhead + /// This enables: FixedVector v; v = {1, 2, 3}; + FixedVector &operator=(std::initializer_list init_list) { + cleanup_(); + reset_(); + assign_from_initializer_list_(init_list); + return *this; + } + + // Allocate capacity - can be called multiple times to reinit + // IMPORTANT: After calling init(), you MUST use push_back() to add elements. + // Direct assignment via operator[] does NOT update the size counter. + void init(size_t n) { + cleanup_(); + reset_(); + if (n > 0) { + // Allocate raw memory without calling constructors + // sizeof(T) is correct here for any type T (value types, pointers, etc.) + // NOLINTNEXTLINE(bugprone-sizeof-expression) + data_ = static_cast(::operator new(n * sizeof(T))); + capacity_ = n; + } + } + + // Clear the vector (destroy all elements, reset size to 0, keep capacity) + void clear() { + destroy_elements_(); + size_ = 0; + } + + // Shrink capacity to fit current size (frees all memory) + void shrink_to_fit() { + cleanup_(); + reset_(); + } + + /// Add element without bounds checking + /// Caller must ensure sufficient capacity was allocated via init() + /// Silently ignores pushes beyond capacity (no exception or assertion) + void push_back(const T &value) { + if (size_ < capacity_) { + // Use placement new to construct the object in pre-allocated memory + new (&data_[size_]) T(value); + size_++; + } + } + + /// Add element by move without bounds checking + /// Caller must ensure sufficient capacity was allocated via init() + /// Silently ignores pushes beyond capacity (no exception or assertion) + void push_back(T &&value) { + if (size_ < capacity_) { + // Use placement new to move-construct the object in pre-allocated memory + new (&data_[size_]) T(std::move(value)); + size_++; + } + } + + /// Emplace element without bounds checking - constructs in-place with arguments + /// Caller must ensure sufficient capacity was allocated via init() + /// Returns reference to the newly constructed element + /// NOTE: Caller MUST ensure size_ < capacity_ before calling + template T &emplace_back(Args &&...args) { + // Use placement new to construct the object in pre-allocated memory + new (&data_[size_]) T(std::forward(args)...); + size_++; + return data_[size_ - 1]; + } + + /// Access first element (no bounds checking - matches std::vector behavior) + /// Caller must ensure vector is not empty (size() > 0) + T &front() { return data_[0]; } + const T &front() const { return data_[0]; } + + /// Access last element (no bounds checking - matches std::vector behavior) + /// Caller must ensure vector is not empty (size() > 0) + T &back() { return data_[size_ - 1]; } + const T &back() const { return data_[size_ - 1]; } + + size_t size() const { return size_; } + bool empty() const { return size_ == 0; } + + /// Access element without bounds checking (matches std::vector behavior) + /// Caller must ensure index is valid (i < size()) + T &operator[](size_t i) { return data_[i]; } + const T &operator[](size_t i) const { return data_[i]; } + + /// Access element with bounds checking (matches std::vector behavior) + /// Note: No exception thrown on out of bounds - caller must ensure index is valid + T &at(size_t i) { return data_[i]; } + const T &at(size_t i) const { return data_[i]; } + + // Iterator support for range-based for loops + T *begin() { return data_; } + T *end() { return data_ + size_; } + const T *begin() const { return data_; } + const T *end() const { return data_ + size_; } +}; + ///@} /// @name Mathematics @@ -145,8 +365,8 @@ template T remap(U value, U min, U max, T min_out, T max return (value - min) * (max_out - min_out) / (max - min) + min_out; } -/// Calculate a CRC-8 checksum of \p data with size \p len using the CRC-8-Dallas/Maxim polynomial. -uint8_t crc8(const uint8_t *data, uint8_t len); +/// Calculate a CRC-8 checksum of \p data with size \p len. +uint8_t crc8(const uint8_t *data, uint8_t len, uint8_t crc = 0x00, uint8_t poly = 0x8C, bool msb_first = false); /// Calculate a CRC-16 checksum of \p data with size \p len. uint16_t crc16(const uint8_t *data, uint16_t len, uint16_t crc = 0xffff, uint16_t reverse_poly = 0xa001, @@ -155,7 +375,8 @@ uint16_t crc16be(const uint8_t *data, uint16_t len, uint16_t crc = 0, uint16_t p bool refout = false); /// Calculate a FNV-1 hash of \p str. -uint32_t fnv1_hash(const std::string &str); +uint32_t fnv1_hash(const char *str); +inline uint32_t fnv1_hash(const std::string &str) { return fnv1_hash(str.c_str()); } /// Return a random 32-bit unsigned integer. uint32_t random_uint32(); @@ -281,6 +502,27 @@ 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, ...); +/// 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. +/// @param name The base name string +/// @param sep The separator character (e.g., '-', ' ', or '.') +/// @param suffix_ptr Pointer to the suffix characters +/// @param suffix_len Length of the suffix +/// @return The concatenated string: name + sep + suffix +std::string make_name_with_suffix(const std::string &name, char sep, const char *suffix_ptr, size_t suffix_len); + +/// Optimized string concatenation: name + separator + suffix (const char* overload) +/// Uses a fixed stack buffer to avoid heap allocations. +/// @param name The base name string +/// @param name_len Length of the name +/// @param sep Single character separator +/// @param suffix_ptr Pointer to the suffix characters +/// @param suffix_len Length of the suffix +/// @return The concatenated string: name + sep + suffix +std::string make_name_with_suffix(const char *name, size_t name_len, char sep, const char *suffix_ptr, + size_t suffix_len); + ///@} /// @name Parsing & formatting @@ -379,6 +621,35 @@ template::value, int> = 0> optional< return parse_hex(str.c_str(), str.length()); } +/// Convert a nibble (0-15) to lowercase hex char +inline char format_hex_char(uint8_t v) { return v >= 10 ? 'a' + (v - 10) : '0' + v; } + +/// Convert a nibble (0-15) to uppercase hex char (used for pretty printing) +/// This always uses uppercase (A-F) for pretty/human-readable output +inline char format_hex_pretty_char(uint8_t v) { return v >= 10 ? 'A' + (v - 10) : '0' + v; } + +/// Format MAC address as XX:XX:XX:XX:XX:XX (uppercase) +inline void format_mac_addr_upper(const uint8_t *mac, char *output) { + for (size_t i = 0; i < 6; i++) { + uint8_t byte = mac[i]; + output[i * 3] = format_hex_pretty_char(byte >> 4); + output[i * 3 + 1] = format_hex_pretty_char(byte & 0x0F); + if (i < 5) + output[i * 3 + 2] = ':'; + } + output[17] = '\0'; +} + +/// Format MAC address as xxxxxxxxxxxxxx (lowercase, no separators) +inline void format_mac_addr_lower_no_sep(const uint8_t *mac, char *output) { + for (size_t i = 0; i < 6; i++) { + uint8_t byte = mac[i]; + output[i * 2] = format_hex_char(byte >> 4); + output[i * 2 + 1] = format_hex_char(byte & 0x0F); + } + output[12] = '\0'; +} + /// Format the six-byte array \p mac into a MAC address. std::string format_mac_address_pretty(const uint8_t mac[6]); /// Format the byte array \p data of length \p len in lowercased hex. @@ -559,6 +830,8 @@ ParseOnOffState parse_on_off(const char *str, const char *on = nullptr, const ch /// Create a string from a value and an accuracy in decimals. std::string value_accuracy_to_string(float value, int8_t accuracy_decimals); +/// Create a string from a value, an accuracy in decimals, and a unit of measurement. +std::string value_accuracy_with_uom_to_string(float value, int8_t accuracy_decimals, StringRef unit_of_measurement); /// Derive accuracy in decimals from an increment step. int8_t step_to_accuracy_decimals(float step); @@ -786,6 +1059,15 @@ std::string get_mac_address(); /// Get the device MAC address as a string, in colon-separated uppercase hex notation. std::string get_mac_address_pretty(); +/// Get the device MAC address into the given buffer, in lowercase hex notation. +/// Assumes buffer length is 13 (12 digits for hexadecimal representation followed by null terminator). +void get_mac_address_into_buffer(std::span buf); + +/// Get the device MAC address into the given buffer, in colon-separated uppercase hex notation. +/// Buffer must be exactly 18 bytes (17 for "XX:XX:XX:XX:XX:XX" + null terminator). +/// Returns pointer to the buffer for convenience. +const char *get_mac_address_pretty_into_buffer(std::span buf); + #ifdef USE_ESP32 /// Set the MAC address to use from the provided byte array (6 bytes). void set_mac_address(uint8_t *mac); @@ -921,7 +1203,26 @@ template class RAMAllocator { template using ExternalRAMAllocator = RAMAllocator; -/// @} +/** + * Functions to constrain the range of arithmetic values. + */ + +template +concept comparable_with = requires(T a, U b) { + { a > b } -> std::convertible_to; + { a < b } -> std::convertible_to; +}; + +template U> T clamp_at_least(T value, U min) { + if (value < min) + return min; + return value; +} +template U> T clamp_at_most(T value, U max) { + if (value > max) + return max; + return value; +} /// @name Internal functions ///@{ @@ -939,18 +1240,4 @@ template::value, int> = 0> T &id(T ///@} -/// @name Deprecated functions -///@{ - -ESPDEPRECATED("hexencode() is deprecated, use format_hex_pretty() instead.", "2022.1") -inline std::string hexencode(const uint8_t *data, uint32_t len) { return format_hex_pretty(data, len); } - -template -ESPDEPRECATED("hexencode() is deprecated, use format_hex_pretty() instead.", "2022.1") -std::string hexencode(const T &data) { - return hexencode(data.data(), data.size()); -} - -///@} - } // namespace esphome diff --git a/esphome/core/macros.h b/esphome/core/macros.h index 8b2383321b..2e47453c40 100644 --- a/esphome/core/macros.h +++ b/esphome/core/macros.h @@ -6,3 +6,7 @@ #ifdef USE_ARDUINO #include #endif + +#ifdef USE_ZEPHYR +#define M_PI 3.14159265358979323846 +#endif diff --git a/esphome/core/ring_buffer.cpp b/esphome/core/ring_buffer.cpp index b77a02b2a7..6a2232599f 100644 --- a/esphome/core/ring_buffer.cpp +++ b/esphome/core/ring_buffer.cpp @@ -78,9 +78,13 @@ size_t RingBuffer::write(const void *data, size_t len) { return this->write_without_replacement(data, len, 0); } -size_t RingBuffer::write_without_replacement(const void *data, size_t len, TickType_t ticks_to_wait) { +size_t RingBuffer::write_without_replacement(const void *data, size_t len, TickType_t ticks_to_wait, + bool write_partial) { if (!xRingbufferSend(this->handle_, data, len, ticks_to_wait)) { - // Couldn't fit all the data, so only write what will fit + if (!write_partial) { + return 0; // Not enough space available and not allowed to write partial data + } + // Couldn't fit all the data, write what will fit size_t free = std::min(this->free(), len); if (xRingbufferSend(this->handle_, data, free, 0)) { return free; diff --git a/esphome/core/ring_buffer.h b/esphome/core/ring_buffer.h index bad96d3181..98a273781f 100644 --- a/esphome/core/ring_buffer.h +++ b/esphome/core/ring_buffer.h @@ -50,7 +50,8 @@ class RingBuffer { * @param ticks_to_wait Maximum number of FreeRTOS ticks to wait (default: 0) * @return Number of bytes written */ - size_t write_without_replacement(const void *data, size_t len, TickType_t ticks_to_wait = 0); + size_t write_without_replacement(const void *data, size_t len, TickType_t ticks_to_wait = 0, + bool write_partial = true); /** * @brief Returns the number of available bytes in the ring buffer. diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 6269a66543..09d50ee7c8 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -14,7 +14,19 @@ namespace esphome { static const char *const TAG = "scheduler"; -static const uint32_t MAX_LOGICALLY_DELETED_ITEMS = 10; +// Memory pool configuration constants +// Pool size of 5 matches typical usage patterns (2-4 active timers) +// - Minimal memory overhead (~250 bytes on ESP32) +// - Sufficient for most configs with a couple sensors/components +// - Still prevents heap fragmentation and allocation stalls +// - Complex setups with many timers will just allocate beyond the pool +// See https://github.com/esphome/backlog/issues/52 +static constexpr size_t MAX_POOL_SIZE = 5; + +// Maximum number of logically deleted (cancelled) items before forcing cleanup. +// Set to 5 to match the pool size - when we have as many cancelled items as our +// pool can hold, it's time to clean up and recycle them. +static constexpr uint32_t MAX_LOGICALLY_DELETED_ITEMS = 5; // Half the 32-bit range - used to detect rollovers vs normal time progression static constexpr uint32_t HALF_MAX_UINT32 = std::numeric_limits::max() / 2; // max delay to start an interval sequence @@ -65,24 +77,48 @@ static void validate_static_string(const char *name) { // Common implementation for both timeout and interval void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type type, bool is_static_string, - const void *name_ptr, uint32_t delay, std::function func, bool is_retry) { + const void *name_ptr, uint32_t delay, std::function func, bool is_retry, + bool skip_cancel) { // Get the name as const char* const char *name_cstr = this->get_name_cstr_(is_static_string, name_ptr); if (delay == SCHEDULER_DONT_RUN) { // Still need to cancel existing timer if name is not empty - LockGuard guard{this->lock_}; - this->cancel_item_locked_(component, name_cstr, type); + if (!skip_cancel) { + LockGuard guard{this->lock_}; + this->cancel_item_locked_(component, name_cstr, type); + } return; } + // Get fresh timestamp BEFORE taking lock - millis_64_ may need to acquire lock itself + const uint64_t now = this->millis_64_(millis()); + + // Take lock early to protect scheduler_item_pool_ access + LockGuard guard{this->lock_}; + // Create and populate the scheduler item - auto item = make_unique(); + std::unique_ptr item; + if (!this->scheduler_item_pool_.empty()) { + // Reuse from pool + item = std::move(this->scheduler_item_pool_.back()); + this->scheduler_item_pool_.pop_back(); +#ifdef ESPHOME_DEBUG_SCHEDULER + ESP_LOGD(TAG, "Reused item from pool (pool size now: %zu)", this->scheduler_item_pool_.size()); +#endif + } else { + // Allocate new if pool is empty + item = make_unique(); +#ifdef ESPHOME_DEBUG_SCHEDULER + ESP_LOGD(TAG, "Allocated new item (pool empty)"); +#endif + } item->component = component; item->set_name(name_cstr, !is_static_string); item->type = type; item->callback = std::move(func); - item->remove = false; + // Reset remove flag - recycled items may have been cancelled (remove=true) in previous use + this->set_item_removed_(item.get(), false); item->is_retry = is_retry; #ifndef ESPHOME_THREAD_SINGLE @@ -90,53 +126,36 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type // Single-core platforms don't need thread-safe defer handling if (delay == 0 && type == SchedulerItem::TIMEOUT) { // Put in defer queue for guaranteed FIFO execution - LockGuard guard{this->lock_}; - this->cancel_item_locked_(component, name_cstr, type); + if (!skip_cancel) { + this->cancel_item_locked_(component, name_cstr, type); + } this->defer_queue_.push_back(std::move(item)); return; } #endif /* not ESPHOME_THREAD_SINGLE */ - // Get fresh timestamp for new timer/interval - ensures accurate scheduling - const auto now = this->millis_64_(millis()); // Fresh millis() call - // Type-specific setup if (type == SchedulerItem::INTERVAL) { item->interval = delay; // first execution happens immediately after a random smallish offset // Calculate random offset (0 to min(interval/2, 5s)) uint32_t offset = (uint32_t) (std::min(delay / 2, MAX_INTERVAL_DELAY) * random_float()); - item->next_execution_ = now + offset; + item->set_next_execution(now + offset); ESP_LOGV(TAG, "Scheduler interval for %s is %" PRIu32 "ms, offset %" PRIu32 "ms", name_cstr ? name_cstr : "", delay, offset); } else { item->interval = 0; - item->next_execution_ = now + delay; + item->set_next_execution(now + delay); } #ifdef ESPHOME_DEBUG_SCHEDULER - // Validate static strings in debug mode - if (is_static_string && name_cstr != nullptr) { - validate_static_string(name_cstr); - } - - // Debug logging - const char *type_str = (type == SchedulerItem::TIMEOUT) ? "timeout" : "interval"; - if (type == SchedulerItem::TIMEOUT) { - ESP_LOGD(TAG, "set_%s(name='%s/%s', %s=%" PRIu32 ")", type_str, item->get_source(), - name_cstr ? name_cstr : "(null)", type_str, delay); - } else { - ESP_LOGD(TAG, "set_%s(name='%s/%s', %s=%" PRIu32 ", offset=%" PRIu32 ")", type_str, item->get_source(), - name_cstr ? name_cstr : "(null)", type_str, delay, static_cast(item->next_execution_ - now)); - } + this->debug_log_timer_(item.get(), is_static_string, name_cstr, type, delay, now); #endif /* ESPHOME_DEBUG_SCHEDULER */ - LockGuard guard{this->lock_}; - // For retries, check if there's a cancelled timeout first if (is_retry && name_cstr != nullptr && type == SchedulerItem::TIMEOUT && - (has_cancelled_timeout_in_container_(this->items_, component, name_cstr, /* match_retry= */ true) || - has_cancelled_timeout_in_container_(this->to_add_, component, name_cstr, /* match_retry= */ true))) { + (has_cancelled_timeout_in_container_locked_(this->items_, component, name_cstr, /* match_retry= */ true) || + has_cancelled_timeout_in_container_locked_(this->to_add_, component, name_cstr, /* match_retry= */ true))) { // Skip scheduling - the retry was cancelled #ifdef ESPHOME_DEBUG_SCHEDULER ESP_LOGD(TAG, "Skipping retry '%s' - found cancelled item", name_cstr); @@ -144,9 +163,11 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type return; } - // If name is provided, do atomic cancel-and-add + // If name is provided, do atomic cancel-and-add (unless skip_cancel is true) // Cancel existing items - this->cancel_item_locked_(component, name_cstr, type); + if (!skip_cancel) { + this->cancel_item_locked_(component, name_cstr, type); + } // Add new item directly to to_add_ // since we have the lock held this->to_add_.push_back(std::move(item)); @@ -272,47 +293,51 @@ optional HOT Scheduler::next_schedule_in(uint32_t now) { auto &item = this->items_[0]; // Convert the fresh timestamp from caller (usually Application::loop()) to 64-bit const auto now_64 = this->millis_64_(now); // 'now' from parameter - fresh from caller - if (item->next_execution_ < now_64) + const uint64_t next_exec = item->get_next_execution(); + if (next_exec < now_64) return 0; - return item->next_execution_ - now_64; + return next_exec - now_64; } -void HOT Scheduler::call(uint32_t now) { -#ifndef ESPHOME_THREAD_SINGLE - // Process defer queue first to guarantee FIFO execution order for deferred items. - // Previously, defer() used the heap which gave undefined order for equal timestamps, - // causing race conditions on multi-core systems (ESP32, BK7200). - // With the defer queue: - // - Deferred items (delay=0) go directly to defer_queue_ in set_timer_common_ - // - Items execute in exact order they were deferred (FIFO guarantee) - // - No deferred items exist in to_add_, so processing order doesn't affect correctness - // Single-core platforms don't use this queue and fall back to the heap-based approach. - // - // Note: Items cancelled via cancel_item_locked_() are marked with remove=true but still - // processed here. They are removed from the queue normally via pop_front() but skipped - // during execution by should_skip_item_(). This is intentional - no memory leak occurs. - while (!this->defer_queue_.empty()) { - // The outer check is done without a lock for performance. If the queue - // appears non-empty, we lock and process an item. We don't need to check - // empty() again inside the lock because only this thread can remove items. - std::unique_ptr item; - { - LockGuard lock(this->lock_); - item = std::move(this->defer_queue_.front()); - this->defer_queue_.pop_front(); - } - // Execute callback without holding lock to prevent deadlocks - // if the callback tries to call defer() again - if (!this->should_skip_item_(item.get())) { - this->execute_item_(item.get(), now); +void Scheduler::full_cleanup_removed_items_() { + // We hold the lock for the entire cleanup operation because: + // 1. We're rebuilding the entire items_ list, so we need exclusive access throughout + // 2. Other threads must see either the old state or the new state, not intermediate states + // 3. The operation is already expensive (O(n)), so lock overhead is negligible + // 4. No operations inside can block or take other locks, so no deadlock risk + LockGuard guard{this->lock_}; + + std::vector> valid_items; + + // Move all non-removed items to valid_items, recycle removed ones + for (auto &item : this->items_) { + if (!is_item_removed_(item.get())) { + valid_items.push_back(std::move(item)); + } else { + // Recycle removed items + this->recycle_item_(std::move(item)); } } + + // Replace items_ with the filtered list + this->items_ = std::move(valid_items); + // Rebuild the heap structure since items are no longer in heap order + std::make_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); + this->to_remove_ = 0; +} + +void HOT Scheduler::call(uint32_t now) { +#ifndef ESPHOME_THREAD_SINGLE + this->process_defer_queue_(now); #endif /* not ESPHOME_THREAD_SINGLE */ // Convert the fresh timestamp from main loop to 64-bit for scheduler operations const auto now_64 = this->millis_64_(now); // 'now' from parameter - fresh from Application::loop() this->process_to_add(); + // Track if any items were added to to_add_ during this call (intervals or from callbacks) + bool has_added_items = false; + #ifdef ESPHOME_DEBUG_SCHEDULER static uint64_t last_print = 0; @@ -322,11 +347,11 @@ void HOT Scheduler::call(uint32_t now) { #ifdef ESPHOME_THREAD_MULTI_ATOMICS const auto last_dbg = this->last_millis_.load(std::memory_order_relaxed); const auto major_dbg = this->millis_major_.load(std::memory_order_relaxed); - ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), now_64, - major_dbg, last_dbg); + ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), + this->scheduler_item_pool_.size(), now_64, major_dbg, last_dbg); #else /* not ESPHOME_THREAD_MULTI_ATOMICS */ - ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), now_64, - this->millis_major_, this->last_millis_); + ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), + this->scheduler_item_pool_.size(), now_64, this->millis_major_, this->last_millis_); #endif /* else ESPHOME_THREAD_MULTI_ATOMICS */ // Cleanup before debug output this->cleanup_(); @@ -339,9 +364,10 @@ void HOT Scheduler::call(uint32_t now) { } const char *name = item->get_name(); - ESP_LOGD(TAG, " %s '%s/%s' interval=%" PRIu32 " next_execution in %" PRIu64 "ms at %" PRIu64, - item->get_type_str(), item->get_source(), name ? name : "(null)", item->interval, - item->next_execution_ - now_64, item->next_execution_); + bool is_cancelled = is_item_removed_(item.get()); + ESP_LOGD(TAG, " %s '%s/%s' interval=%" PRIu32 " next_execution in %" PRIu64 "ms at %" PRIu64 "%s", + item->get_type_str(), LOG_STR_ARG(item->get_source()), name ? name : "(null)", item->interval, + item->get_next_execution() - now_64, item->get_next_execution(), is_cancelled ? " [CANCELLED]" : ""); old_items.push_back(std::move(item)); } @@ -356,91 +382,101 @@ void HOT Scheduler::call(uint32_t now) { } #endif /* ESPHOME_DEBUG_SCHEDULER */ - // If we have too many items to remove - if (this->to_remove_ > MAX_LOGICALLY_DELETED_ITEMS) { - // We hold the lock for the entire cleanup operation because: - // 1. We're rebuilding the entire items_ list, so we need exclusive access throughout - // 2. Other threads must see either the old state or the new state, not intermediate states - // 3. The operation is already expensive (O(n)), so lock overhead is negligible - // 4. No operations inside can block or take other locks, so no deadlock risk - LockGuard guard{this->lock_}; - - std::vector> valid_items; - - // Move all non-removed items to valid_items - for (auto &item : this->items_) { - if (!item->remove) { - valid_items.push_back(std::move(item)); - } - } - - // Replace items_ with the filtered list - this->items_ = std::move(valid_items); - // Rebuild the heap structure since items are no longer in heap order - std::make_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); - this->to_remove_ = 0; - } - // Cleanup removed items before processing + // First try to clean items from the top of the heap (fast path) this->cleanup_(); - while (!this->items_.empty()) { - // use scoping to indicate visibility of `item` variable - { - // Don't copy-by value yet - auto &item = this->items_[0]; - if (item->next_execution_ > now_64) { - // Not reached timeout yet, done for this call - break; - } - // Don't run on failed components - if (item->component != nullptr && item->component->is_failed()) { - LockGuard guard{this->lock_}; - this->pop_raw_(); - continue; - } -#ifdef ESPHOME_DEBUG_SCHEDULER - const char *item_name = item->get_name(); - ESP_LOGV(TAG, "Running %s '%s/%s' with interval=%" PRIu32 " next_execution=%" PRIu64 " (now=%" PRIu64 ")", - item->get_type_str(), item->get_source(), item_name ? item_name : "(null)", item->interval, - item->next_execution_, now_64); -#endif /* ESPHOME_DEBUG_SCHEDULER */ - // Warning: During callback(), a lot of stuff can happen, including: - // - timeouts/intervals get added, potentially invalidating vector pointers - // - timeouts/intervals get cancelled - this->execute_item_(item.get(), now); + // If we still have too many cancelled items, do a full cleanup + // This only happens if cancelled items are stuck in the middle/bottom of the heap + if (this->to_remove_ >= MAX_LOGICALLY_DELETED_ITEMS) { + this->full_cleanup_removed_items_(); + } + while (!this->items_.empty()) { + // Don't copy-by value yet + auto &item = this->items_[0]; + if (item->get_next_execution() > now_64) { + // Not reached timeout yet, done for this call + break; + } + // Don't run on failed components + if (item->component != nullptr && item->component->is_failed()) { + LockGuard guard{this->lock_}; + this->pop_raw_(); + continue; } + // Check if item is marked for removal + // This handles two cases: + // 1. Item was marked for removal after cleanup_() but before we got here + // 2. Item is marked for removal but wasn't at the front of the heap during cleanup_() +#ifdef ESPHOME_THREAD_MULTI_NO_ATOMICS + // Multi-threaded platforms without atomics: must take lock to safely read remove flag { LockGuard guard{this->lock_}; - - // new scope, item from before might have been moved in the vector - auto item = std::move(this->items_[0]); - // Only pop after function call, this ensures we were reachable - // during the function call and know if we were cancelled. - this->pop_raw_(); - - if (item->remove) { - // We were removed/cancelled in the function call, stop + if (is_item_removed_(item.get())) { + this->pop_raw_(); this->to_remove_--; continue; } - - if (item->type == SchedulerItem::INTERVAL) { - item->next_execution_ = now_64 + item->interval; - // Add new item directly to to_add_ - // since we have the lock held - this->to_add_.push_back(std::move(item)); - } } +#else + // Single-threaded or multi-threaded with atomics: can check without lock + if (is_item_removed_(item.get())) { + LockGuard guard{this->lock_}; + this->pop_raw_(); + this->to_remove_--; + continue; + } +#endif + +#ifdef ESPHOME_DEBUG_SCHEDULER + const char *item_name = item->get_name(); + ESP_LOGV(TAG, "Running %s '%s/%s' with interval=%" PRIu32 " next_execution=%" PRIu64 " (now=%" PRIu64 ")", + item->get_type_str(), LOG_STR_ARG(item->get_source()), item_name ? item_name : "(null)", item->interval, + item->get_next_execution(), now_64); +#endif /* ESPHOME_DEBUG_SCHEDULER */ + + // Warning: During callback(), a lot of stuff can happen, including: + // - timeouts/intervals get added, potentially invalidating vector pointers + // - timeouts/intervals get cancelled + now = this->execute_item_(item.get(), now); + + LockGuard guard{this->lock_}; + + auto executed_item = std::move(this->items_[0]); + // Only pop after function call, this ensures we were reachable + // during the function call and know if we were cancelled. + this->pop_raw_(); + + if (executed_item->remove) { + // We were removed/cancelled in the function call, stop + this->to_remove_--; + continue; + } + + if (executed_item->type == SchedulerItem::INTERVAL) { + executed_item->set_next_execution(now_64 + executed_item->interval); + // Add new item directly to to_add_ + // since we have the lock held + this->to_add_.push_back(std::move(executed_item)); + } else { + // Timeout completed - recycle it + this->recycle_item_(std::move(executed_item)); + } + + has_added_items |= !this->to_add_.empty(); } - this->process_to_add(); + if (has_added_items) { + this->process_to_add(); + } } void HOT Scheduler::process_to_add() { LockGuard guard{this->lock_}; for (auto &it : this->to_add_) { - if (it->remove) { + if (is_item_removed_(it.get())) { + // Recycle cancelled items + this->recycle_item_(std::move(it)); continue; } @@ -480,15 +516,19 @@ size_t HOT Scheduler::cleanup_() { } void HOT Scheduler::pop_raw_() { std::pop_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); + + // Instead of destroying, recycle the item + this->recycle_item_(std::move(this->items_.back())); + this->items_.pop_back(); } // Helper to execute a scheduler item -void HOT Scheduler::execute_item_(SchedulerItem *item, uint32_t now) { +uint32_t HOT Scheduler::execute_item_(SchedulerItem *item, uint32_t now) { App.set_current_component(item->component); WarnIfComponentBlockingGuard guard{item->component, now}; item->callback(); - guard.finish(); + return guard.finish(); } // Common implementation for cancel operations @@ -514,34 +554,32 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c // Check all containers for matching items #ifndef ESPHOME_THREAD_SINGLE - // Only check defer queue for timeouts (intervals never go there) + // Mark items in defer queue as cancelled (they'll be skipped when processed) if (type == SchedulerItem::TIMEOUT) { - for (auto &item : this->defer_queue_) { - if (this->matches_item_(item, component, name_cstr, type, match_retry)) { - item->remove = true; - total_cancelled++; - } - } + total_cancelled += + this->mark_matching_items_removed_locked_(this->defer_queue_, component, name_cstr, type, match_retry); } #endif /* not ESPHOME_THREAD_SINGLE */ // Cancel items in the main heap - for (auto &item : this->items_) { - if (this->matches_item_(item, component, name_cstr, type, match_retry)) { - item->remove = true; + // Special case: if the last item in the heap matches, we can remove it immediately + // (removing the last element doesn't break heap structure) + if (!this->items_.empty()) { + auto &last_item = this->items_.back(); + if (this->matches_item_locked_(last_item, component, name_cstr, type, match_retry)) { + this->recycle_item_(std::move(this->items_.back())); + this->items_.pop_back(); total_cancelled++; - this->to_remove_++; // Track removals for heap items } + // For other items in heap, we can only mark for removal (can't remove from middle of heap) + size_t heap_cancelled = + this->mark_matching_items_removed_locked_(this->items_, component, name_cstr, type, match_retry); + total_cancelled += heap_cancelled; + this->to_remove_ += heap_cancelled; // Track removals for heap items } // Cancel items in to_add_ - for (auto &item : this->to_add_) { - if (this->matches_item_(item, component, name_cstr, type, match_retry)) { - item->remove = true; - total_cancelled++; - // Don't track removals for to_add_ items - } - } + total_cancelled += this->mark_matching_items_removed_locked_(this->to_add_, component, name_cstr, type, match_retry); return total_cancelled > 0; } @@ -573,13 +611,12 @@ uint64_t Scheduler::millis_64_(uint32_t now) { if (now < last && (last - now) > HALF_MAX_UINT32) { this->millis_major_++; major++; + this->last_millis_ = now; #ifdef ESPHOME_DEBUG_SCHEDULER ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last); #endif /* ESPHOME_DEBUG_SCHEDULER */ - } - - // Only update if time moved forward - if (now > last) { + } else if (now > last) { + // Only update if time moved forward this->last_millis_ = now; } @@ -706,7 +743,52 @@ uint64_t Scheduler::millis_64_(uint32_t now) { bool HOT Scheduler::SchedulerItem::cmp(const std::unique_ptr &a, const std::unique_ptr &b) { - return a->next_execution_ > b->next_execution_; + // High bits are almost always equal (change only on 32-bit rollover ~49 days) + // Optimize for common case: check low bits first when high bits are equal + return (a->next_execution_high_ == b->next_execution_high_) ? (a->next_execution_low_ > b->next_execution_low_) + : (a->next_execution_high_ > b->next_execution_high_); } +void Scheduler::recycle_item_(std::unique_ptr item) { + if (!item) + return; + + if (this->scheduler_item_pool_.size() < MAX_POOL_SIZE) { + // Clear callback to release captured resources + item->callback = nullptr; + // Clear dynamic name if any + item->clear_dynamic_name(); + this->scheduler_item_pool_.push_back(std::move(item)); +#ifdef ESPHOME_DEBUG_SCHEDULER + ESP_LOGD(TAG, "Recycled item to pool (pool size now: %zu)", this->scheduler_item_pool_.size()); +#endif + } else { +#ifdef ESPHOME_DEBUG_SCHEDULER + ESP_LOGD(TAG, "Pool full (size: %zu), deleting item", this->scheduler_item_pool_.size()); +#endif + } + // else: unique_ptr will delete the item when it goes out of scope +} + +#ifdef ESPHOME_DEBUG_SCHEDULER +void Scheduler::debug_log_timer_(const SchedulerItem *item, bool is_static_string, const char *name_cstr, + SchedulerItem::Type type, uint32_t delay, uint64_t now) { + // Validate static strings in debug mode + if (is_static_string && name_cstr != nullptr) { + validate_static_string(name_cstr); + } + + // Debug logging + const char *type_str = (type == SchedulerItem::TIMEOUT) ? "timeout" : "interval"; + if (type == SchedulerItem::TIMEOUT) { + ESP_LOGD(TAG, "set_%s(name='%s/%s', %s=%" PRIu32 ")", type_str, LOG_STR_ARG(item->get_source()), + name_cstr ? name_cstr : "(null)", type_str, delay); + } else { + ESP_LOGD(TAG, "set_%s(name='%s/%s', %s=%" PRIu32 ", offset=%" PRIu32 ")", type_str, LOG_STR_ARG(item->get_source()), + name_cstr ? name_cstr : "(null)", type_str, delay, + static_cast(item->get_next_execution() - now)); + } +} +#endif /* ESPHOME_DEBUG_SCHEDULER */ + } // namespace esphome diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index a6092e1b1e..bea1503df0 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -4,7 +4,6 @@ #include #include #include -#include #ifdef ESPHOME_THREAD_MULTI_ATOMICS #include #endif @@ -21,8 +20,13 @@ struct RetryArgs; void retry_handler(const std::shared_ptr &args); class Scheduler { - // Allow retry_handler to access protected members + // Allow retry_handler to access protected members for internal retry mechanism friend void ::esphome::retry_handler(const std::shared_ptr &args); + // Allow DelayAction to call set_timer_common_ with skip_cancel=true for parallel script delays. + // This is needed to fix issue #10264 where parallel scripts with delays interfere with each other. + // We use friend instead of a public API because skip_cancel is dangerous - it can cause delays + // to accumulate and overload the scheduler if misused. + template friend class DelayAction; public: // Public API - accepts std::string for backward compatibility @@ -83,45 +87,65 @@ class Scheduler { struct SchedulerItem { // Ordered by size to minimize padding Component *component; - uint32_t interval; - // 64-bit time to handle millis() rollover. The scheduler combines the 32-bit millis() - // with a 16-bit rollover counter to create a 64-bit time that won't roll over for - // billions of years. This ensures correct scheduling even when devices run for months. - uint64_t next_execution_; - // Optimized name storage using tagged union union { const char *static_name; // For string literals (no allocation) char *dynamic_name; // For allocated strings } name_; - + uint32_t interval; + // Split time to handle millis() rollover. The scheduler combines the 32-bit millis() + // with a 16-bit rollover counter to create a 48-bit time space (using 32+16 bits). + // This is intentionally limited to 48 bits, not stored as a full 64-bit value. + // With 49.7 days per 32-bit rollover, the 16-bit counter supports + // 49.7 days × 65536 = ~8900 years. This ensures correct scheduling + // even when devices run for months. Split into two fields for better memory + // alignment on 32-bit systems. + uint32_t next_execution_low_; // Lower 32 bits of execution time (millis value) std::function callback; + uint16_t next_execution_high_; // Upper 16 bits (millis_major counter) - // Bit-packed fields to minimize padding +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + // Multi-threaded with atomics: use atomic for lock-free access + // Place atomic separately since it can't be packed with bit fields + std::atomic remove{false}; + + // Bit-packed fields (3 bits used, 5 bits padding in 1 byte) + enum Type : uint8_t { TIMEOUT, INTERVAL } type : 1; + bool name_is_dynamic : 1; // True if name was dynamically allocated (needs delete[]) + bool is_retry : 1; // True if this is a retry timeout + // 5 bits padding +#else + // Single-threaded or multi-threaded without atomics: can pack all fields together + // Bit-packed fields (4 bits used, 4 bits padding in 1 byte) enum Type : uint8_t { TIMEOUT, INTERVAL } type : 1; bool remove : 1; bool name_is_dynamic : 1; // True if name was dynamically allocated (needs delete[]) bool is_retry : 1; // True if this is a retry timeout - // 4 bits padding + // 4 bits padding +#endif // Constructor SchedulerItem() : component(nullptr), interval(0), - next_execution_(0), + next_execution_low_(0), + next_execution_high_(0), +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + // remove is initialized in the member declaration as std::atomic{false} + type(TIMEOUT), + name_is_dynamic(false), + is_retry(false) { +#else type(TIMEOUT), remove(false), name_is_dynamic(false), is_retry(false) { +#endif name_.static_name = nullptr; } // Destructor to clean up dynamic names - ~SchedulerItem() { - if (name_is_dynamic) { - delete[] name_.dynamic_name; - } - } + ~SchedulerItem() { clear_dynamic_name(); } // Delete copy operations to prevent accidental copies SchedulerItem(const SchedulerItem &) = delete; @@ -134,13 +158,19 @@ class Scheduler { // Helper to get the name regardless of storage type const char *get_name() const { return name_is_dynamic ? name_.dynamic_name : name_.static_name; } + // Helper to clear dynamic name if allocated + void clear_dynamic_name() { + if (name_is_dynamic && name_.dynamic_name) { + delete[] name_.dynamic_name; + name_.dynamic_name = nullptr; + name_is_dynamic = false; + } + } + // Helper to set name with proper ownership void set_name(const char *name, bool make_copy = false) { // Clean up old dynamic name if any - if (name_is_dynamic && name_.dynamic_name) { - delete[] name_.dynamic_name; - name_is_dynamic = false; - } + clear_dynamic_name(); if (!name) { // nullptr case - no name provided @@ -158,13 +188,27 @@ class Scheduler { } static bool cmp(const std::unique_ptr &a, const std::unique_ptr &b); - const char *get_type_str() const { return (type == TIMEOUT) ? "timeout" : "interval"; } - const char *get_source() const { return component ? component->get_component_source() : "unknown"; } + + // Note: We use 48 bits total (32 + 16), stored in a 64-bit value for API compatibility. + // The upper 16 bits of the 64-bit value are always zero, which is fine since + // millis_major_ is also 16 bits and they must match. + constexpr uint64_t get_next_execution() const { + return (static_cast(next_execution_high_) << 32) | next_execution_low_; + } + + constexpr void set_next_execution(uint64_t value) { + next_execution_low_ = static_cast(value); + // Cast to uint16_t intentionally truncates to lower 16 bits of the upper 32 bits. + // This is correct because millis_major_ that creates these values is also 16 bits. + next_execution_high_ = static_cast(value >> 32); + } + constexpr const char *get_type_str() const { return (type == TIMEOUT) ? "timeout" : "interval"; } + const LogString *get_source() const { return component ? component->get_component_log_str() : LOG_STR("unknown"); } }; // Common implementation for both timeout and interval void set_timer_common_(Component *component, SchedulerItem::Type type, bool is_static_string, const void *name_ptr, - uint32_t delay, std::function func, bool is_retry = false); + uint32_t delay, std::function func, bool is_retry = false, bool skip_cancel = false); // Common implementation for retry void set_retry_common_(Component *component, bool is_static_string, const void *name_ptr, uint32_t initial_wait_time, @@ -189,43 +233,212 @@ class Scheduler { // Common implementation for cancel operations bool cancel_item_(Component *component, bool is_static_string, const void *name_ptr, SchedulerItem::Type type); + // Helper to check if two scheduler item names match + inline bool HOT names_match_(const char *name1, const char *name2) const { + // Check pointer equality first (common for static strings), then string contents + // The core ESPHome codebase uses static strings (const char*) for component names, + // making pointer comparison effective. The std::string overloads exist only for + // compatibility with external components but are rarely used in practice. + return (name1 != nullptr && name2 != nullptr) && ((name1 == name2) || (strcmp(name1, name2) == 0)); + } + // Helper function to check if item matches criteria for cancellation - inline bool HOT matches_item_(const std::unique_ptr &item, Component *component, const char *name_cstr, - SchedulerItem::Type type, bool match_retry, bool skip_removed = true) const { + // IMPORTANT: Must be called with scheduler lock held + inline bool HOT matches_item_locked_(const std::unique_ptr &item, Component *component, + const char *name_cstr, SchedulerItem::Type type, bool match_retry, + bool skip_removed = true) const { + // THREAD SAFETY: Check for nullptr first to prevent LoadProhibited crashes. On multi-threaded + // platforms, items can be moved out of defer_queue_ during processing, leaving nullptr entries. + // PR #11305 added nullptr checks in callers (mark_matching_items_removed_locked_() and + // has_cancelled_timeout_in_container_locked_()), but this check provides defense-in-depth: helper + // functions should be safe regardless of caller behavior. + // Fixes: https://github.com/esphome/esphome/issues/11940 + if (!item) + return false; if (item->component != component || item->type != type || (skip_removed && item->remove) || (match_retry && !item->is_retry)) { return false; } - const char *item_name = item->get_name(); - if (item_name == nullptr) { - return false; - } - // Fast path: if pointers are equal - // This is effective because the core ESPHome codebase uses static strings (const char*) - // for component names. The std::string overloads exist only for compatibility with - // external components, but are rarely used in practice. - if (item_name == name_cstr) { - return true; - } - // Slow path: compare string contents - return strcmp(name_cstr, item_name) == 0; + return this->names_match_(item->get_name(), name_cstr); } // Helper to execute a scheduler item - void execute_item_(SchedulerItem *item, uint32_t now); + uint32_t execute_item_(SchedulerItem *item, uint32_t now); // Helper to check if item should be skipped - bool should_skip_item_(const SchedulerItem *item) const { - return item->remove || (item->component != nullptr && item->component->is_failed()); + bool should_skip_item_(SchedulerItem *item) const { + return is_item_removed_(item) || (item->component != nullptr && item->component->is_failed()); + } + + // Helper to recycle a SchedulerItem + void recycle_item_(std::unique_ptr item); + + // Helper to perform full cleanup when too many items are cancelled + void full_cleanup_removed_items_(); + +#ifdef ESPHOME_DEBUG_SCHEDULER + // Helper for debug logging in set_timer_common_ - extracted to reduce code size + void debug_log_timer_(const SchedulerItem *item, bool is_static_string, const char *name_cstr, + SchedulerItem::Type type, uint32_t delay, uint64_t now); +#endif /* ESPHOME_DEBUG_SCHEDULER */ + +#ifndef ESPHOME_THREAD_SINGLE + // Helper to process defer queue - inline for performance in hot path + inline void process_defer_queue_(uint32_t &now) { + // Process defer queue first to guarantee FIFO execution order for deferred items. + // Previously, defer() used the heap which gave undefined order for equal timestamps, + // causing race conditions on multi-core systems (ESP32, BK7200). + // With the defer queue: + // - Deferred items (delay=0) go directly to defer_queue_ in set_timer_common_ + // - Items execute in exact order they were deferred (FIFO guarantee) + // - No deferred items exist in to_add_, so processing order doesn't affect correctness + // Single-core platforms don't use this queue and fall back to the heap-based approach. + // + // Note: Items cancelled via cancel_item_locked_() are marked with remove=true but still + // processed here. They are skipped during execution by should_skip_item_(). + // This is intentional - no memory leak occurs. + // + // We use an index (defer_queue_front_) to track the read position instead of calling + // erase() on every pop, which would be O(n). The queue is processed once per loop - + // any items added during processing are left for the next loop iteration. + + // Snapshot the queue end point - only process items that existed at loop start + // Items added during processing (by callbacks or other threads) run next loop + // No lock needed: single consumer (main loop), stale read just means we process less this iteration + size_t defer_queue_end = this->defer_queue_.size(); + + while (this->defer_queue_front_ < defer_queue_end) { + std::unique_ptr item; + { + LockGuard lock(this->lock_); + // SAFETY: Moving out the unique_ptr leaves a nullptr in the vector at defer_queue_front_. + // This is intentional and safe because: + // 1. The vector is only cleaned up by cleanup_defer_queue_locked_() at the end of this function + // 2. Any code iterating defer_queue_ MUST check for nullptr items (see mark_matching_items_removed_locked_ + // and has_cancelled_timeout_in_container_locked_ in scheduler.h) + // 3. The lock protects concurrent access, but the nullptr remains until cleanup + item = std::move(this->defer_queue_[this->defer_queue_front_]); + this->defer_queue_front_++; + } + + // Execute callback without holding lock to prevent deadlocks + // if the callback tries to call defer() again + if (!this->should_skip_item_(item.get())) { + now = this->execute_item_(item.get(), now); + } + // Recycle the defer item after execution + this->recycle_item_(std::move(item)); + } + + // If we've consumed all items up to the snapshot point, clean up the dead space + // Single consumer (main loop), so no lock needed for this check + if (this->defer_queue_front_ >= defer_queue_end) { + LockGuard lock(this->lock_); + this->cleanup_defer_queue_locked_(); + } + } + + // Helper to cleanup defer_queue_ after processing + // IMPORTANT: Caller must hold the scheduler lock before calling this function. + inline void cleanup_defer_queue_locked_() { + // Check if new items were added by producers during processing + if (this->defer_queue_front_ >= this->defer_queue_.size()) { + // Common case: no new items - clear everything + this->defer_queue_.clear(); + } else { + // Rare case: new items were added during processing - compact the vector + // This only happens when: + // 1. A deferred callback calls defer() again, or + // 2. Another thread calls defer() while we're processing + // + // Move unprocessed items (added during this loop) to the front for next iteration + // + // SAFETY: Compacted items may include cancelled items (marked for removal via + // cancel_item_locked_() during execution). This is safe because should_skip_item_() + // checks is_item_removed_() before executing, so cancelled items will be skipped + // and recycled on the next loop iteration. + size_t remaining = this->defer_queue_.size() - this->defer_queue_front_; + for (size_t i = 0; i < remaining; i++) { + this->defer_queue_[i] = std::move(this->defer_queue_[this->defer_queue_front_ + i]); + } + this->defer_queue_.resize(remaining); + } + this->defer_queue_front_ = 0; + } +#endif /* not ESPHOME_THREAD_SINGLE */ + + // Helper to check if item is marked for removal (platform-specific) + // Returns true if item should be skipped, handles platform-specific synchronization + // For ESPHOME_THREAD_MULTI_NO_ATOMICS platforms, the caller must hold the scheduler lock before calling this + // function. + bool is_item_removed_(SchedulerItem *item) const { +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + // Multi-threaded with atomics: use atomic load for lock-free access + return item->remove.load(std::memory_order_acquire); +#else + // Single-threaded (ESPHOME_THREAD_SINGLE) or + // multi-threaded without atomics (ESPHOME_THREAD_MULTI_NO_ATOMICS): direct read + // For ESPHOME_THREAD_MULTI_NO_ATOMICS, caller MUST hold lock! + return item->remove; +#endif + } + + // Helper to set item removal flag (platform-specific) + // For ESPHOME_THREAD_MULTI_NO_ATOMICS platforms, the caller must hold the scheduler lock before calling this + // function. Uses memory_order_release when setting to true (for cancellation synchronization), + // and memory_order_relaxed when setting to false (for initialization). + void set_item_removed_(SchedulerItem *item, bool removed) { +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + // Multi-threaded with atomics: use atomic store with appropriate ordering + // Release ordering when setting to true ensures cancellation is visible to other threads + // Relaxed ordering when setting to false is sufficient for initialization + item->remove.store(removed, removed ? std::memory_order_release : std::memory_order_relaxed); +#else + // Single-threaded (ESPHOME_THREAD_SINGLE) or + // multi-threaded without atomics (ESPHOME_THREAD_MULTI_NO_ATOMICS): direct write + // For ESPHOME_THREAD_MULTI_NO_ATOMICS, caller MUST hold lock! + item->remove = removed; +#endif + } + + // Helper to mark matching items in a container as removed + // Returns the number of items marked for removal + // IMPORTANT: Must be called with scheduler lock held + template + size_t mark_matching_items_removed_locked_(Container &container, Component *component, const char *name_cstr, + SchedulerItem::Type type, bool match_retry) { + size_t count = 0; + for (auto &item : container) { + // Skip nullptr items (can happen in defer_queue_ when items are being processed) + // The defer_queue_ uses index-based processing: items are std::moved out but left in the + // vector as nullptr until cleanup. Even though this function is called with lock held, + // the vector can still contain nullptr items from the processing loop. This check prevents crashes. + if (!item) + continue; + if (this->matches_item_locked_(item, component, name_cstr, type, match_retry)) { + // Mark item for removal (platform-specific) + this->set_item_removed_(item.get(), true); + count++; + } + } + return count; } // Template helper to check if any item in a container matches our criteria + // IMPORTANT: Must be called with scheduler lock held template - bool has_cancelled_timeout_in_container_(const Container &container, Component *component, const char *name_cstr, - bool match_retry) const { + bool has_cancelled_timeout_in_container_locked_(const Container &container, Component *component, + const char *name_cstr, bool match_retry) const { for (const auto &item : container) { - if (item->remove && this->matches_item_(item, component, name_cstr, SchedulerItem::TIMEOUT, match_retry, - /* skip_removed= */ false)) { + // Skip nullptr items (can happen in defer_queue_ when items are being processed) + // The defer_queue_ uses index-based processing: items are std::moved out but left in the + // vector as nullptr until cleanup. If this function is called during defer queue processing, + // it will iterate over these nullptr items. This check prevents crashes. + if (!item) + continue; + if (is_item_removed_(item.get()) && + this->matches_item_locked_(item, component, name_cstr, SchedulerItem::TIMEOUT, match_retry, + /* skip_removed= */ false)) { return true; } } @@ -236,11 +449,24 @@ class Scheduler { std::vector> items_; std::vector> to_add_; #ifndef ESPHOME_THREAD_SINGLE - // Single-core platforms don't need the defer queue and save 40 bytes of RAM - std::deque> defer_queue_; // FIFO queue for defer() calls -#endif /* ESPHOME_THREAD_SINGLE */ + // Single-core platforms don't need the defer queue and save ~32 bytes of RAM + // Using std::vector instead of std::deque avoids 512-byte chunked allocations + // Index tracking avoids O(n) erase() calls when draining the queue each loop + std::vector> defer_queue_; // FIFO queue for defer() calls + size_t defer_queue_front_{0}; // Index of first valid item in defer_queue_ (tracks consumed items) +#endif /* ESPHOME_THREAD_SINGLE */ uint32_t to_remove_{0}; + // Memory pool for recycling SchedulerItem objects to reduce heap churn. + // Design decisions: + // - std::vector is used instead of a fixed array because many systems only need 1-2 scheduler items + // - The vector grows dynamically up to MAX_POOL_SIZE (5) only when needed, saving memory on simple setups + // - Pool size of 5 matches typical usage (2-4 timers) while keeping memory overhead low (~250 bytes on ESP32) + // - The pool significantly reduces heap fragmentation which is critical because heap allocation/deallocation + // can stall the entire system, causing timing issues and dropped events for any components that need + // to synchronize between tasks (see https://github.com/esphome/backlog/issues/52) + std::vector> scheduler_item_pool_; + #ifdef ESPHOME_THREAD_MULTI_ATOMICS /* * Multi-threaded platforms with atomic support: last_millis_ needs atomic for lock-free updates diff --git a/esphome/core/string_ref.cpp b/esphome/core/string_ref.cpp deleted file mode 100644 index ce1e33cbb7..0000000000 --- a/esphome/core/string_ref.cpp +++ /dev/null @@ -1,12 +0,0 @@ -#include "string_ref.h" - -namespace esphome { - -#ifdef USE_JSON - -// NOLINTNEXTLINE(readability-identifier-naming) -void convertToJson(const StringRef &src, JsonVariant dst) { dst.set(src.c_str()); } - -#endif // USE_JSON - -} // namespace esphome diff --git a/esphome/core/string_ref.h b/esphome/core/string_ref.h index c4320107e3..efaa17181d 100644 --- a/esphome/core/string_ref.h +++ b/esphome/core/string_ref.h @@ -130,7 +130,7 @@ inline std::string operator+(const StringRef &lhs, const char *rhs) { #ifdef USE_JSON // NOLINTNEXTLINE(readability-identifier-naming) -void convertToJson(const StringRef &src, JsonVariant dst); +inline void convertToJson(const StringRef &src, JsonVariant dst) { dst.set(src.c_str()); } #endif // USE_JSON } // namespace esphome diff --git a/esphome/core/template_lambda.h b/esphome/core/template_lambda.h new file mode 100644 index 0000000000..7b8f4374aa --- /dev/null +++ b/esphome/core/template_lambda.h @@ -0,0 +1,51 @@ +#pragma once + +#include "esphome/core/optional.h" + +namespace esphome { + +/** Lightweight wrapper for template platform lambdas (stateless function pointers only). + * + * This optimizes template platforms by storing only a function pointer (4 bytes on ESP32) + * instead of std::function (16-32 bytes). + * + * IMPORTANT: This only supports stateless lambdas (no captures). The set_template() method + * is an internal API used by YAML codegen, not intended for external use. + * + * Lambdas must return optional to support the pattern: + * return {}; // Don't publish a value + * return 42.0; // Publish this value + * + * operator() returns optional, returning nullopt when no lambda is set (nullptr check). + * + * @tparam T The return type (e.g., float for sensor values) + * @tparam Args Optional arguments for the lambda + */ +template class TemplateLambda { + public: + TemplateLambda() : f_(nullptr) {} + + /** Set the lambda function pointer. + * INTERNAL API: Only for use by YAML codegen. + * Only stateless lambdas (no captures) are supported. + */ + void set(optional (*f)(Args...)) { this->f_ = f; } + + /** Check if a lambda is set */ + bool has_value() const { return this->f_ != nullptr; } + + /** Call the lambda, returning nullopt if no lambda is set */ + optional operator()(Args &&...args) { + if (this->f_ == nullptr) + return nullopt; + return this->f_(std::forward(args)...); + } + + /** Alias for operator() for compatibility */ + optional call(Args &&...args) { return (*this)(std::forward(args)...); } + + protected: + optional (*f_)(Args...); // Function pointer (4 bytes on ESP32) +}; + +} // namespace esphome diff --git a/esphome/core/time.cpp b/esphome/core/time.cpp index f9652b5329..d30dac4394 100644 --- a/esphome/core/time.cpp +++ b/esphome/core/time.cpp @@ -46,24 +46,18 @@ struct tm ESPTime::to_c_tm() { return c_tm; } -std::string ESPTime::strftime(const std::string &format) { - std::string timestr; - timestr.resize(format.size() * 4); +std::string ESPTime::strftime(const char *format) { struct tm c_tm = this->to_c_tm(); - size_t len = ::strftime(×tr[0], timestr.size(), format.c_str(), &c_tm); - while (len == 0) { - if (timestr.size() >= 128) { - // strftime has failed for reasons unrelated to the size of the buffer - // so return a formatting error - return "ERROR"; - } - timestr.resize(timestr.size() * 2); - len = ::strftime(×tr[0], timestr.size(), format.c_str(), &c_tm); + char buf[128]; + size_t len = ::strftime(buf, sizeof(buf), format, &c_tm); + if (len > 0) { + return std::string(buf, len); } - timestr.resize(len); - return timestr; + return "ERROR"; } +std::string ESPTime::strftime(const std::string &format) { return this->strftime(format.c_str()); } + bool ESPTime::strptime(const std::string &time_to_parse, ESPTime &esp_time) { uint16_t year; uint8_t month; @@ -77,7 +71,7 @@ bool ESPTime::strptime(const std::string &time_to_parse, ESPTime &esp_time) { &hour, // NOLINT &minute, // NOLINT &second, &num) == 6 && // NOLINT - num == time_to_parse.size()) { + num == static_cast(time_to_parse.size())) { esp_time.year = year; esp_time.month = month; esp_time.day_of_month = day; @@ -87,7 +81,7 @@ bool ESPTime::strptime(const std::string &time_to_parse, ESPTime &esp_time) { } else if (sscanf(time_to_parse.c_str(), "%04hu-%02hhu-%02hhu %02hhu:%02hhu %n", &year, &month, &day, // NOLINT &hour, // NOLINT &minute, &num) == 5 && // NOLINT - num == time_to_parse.size()) { + num == static_cast(time_to_parse.size())) { esp_time.year = year; esp_time.month = month; esp_time.day_of_month = day; @@ -95,17 +89,17 @@ bool ESPTime::strptime(const std::string &time_to_parse, ESPTime &esp_time) { esp_time.minute = minute; esp_time.second = 0; } else if (sscanf(time_to_parse.c_str(), "%02hhu:%02hhu:%02hhu %n", &hour, &minute, &second, &num) == 3 && // NOLINT - num == time_to_parse.size()) { + num == static_cast(time_to_parse.size())) { esp_time.hour = hour; esp_time.minute = minute; esp_time.second = second; } else if (sscanf(time_to_parse.c_str(), "%02hhu:%02hhu %n", &hour, &minute, &num) == 2 && // NOLINT - num == time_to_parse.size()) { + num == static_cast(time_to_parse.size())) { esp_time.hour = hour; esp_time.minute = minute; esp_time.second = 0; } else if (sscanf(time_to_parse.c_str(), "%04hu-%02hhu-%02hhu %n", &year, &month, &day, &num) == 3 && // NOLINT - num == time_to_parse.size()) { + num == static_cast(time_to_parse.size())) { esp_time.year = year; esp_time.month = month; esp_time.day_of_month = day; @@ -203,27 +197,13 @@ void ESPTime::recalc_timestamp_local() { } int32_t ESPTime::timezone_offset() { - int32_t offset = 0; time_t now = ::time(nullptr); - auto local = ESPTime::from_epoch_local(now); - auto utc = ESPTime::from_epoch_utc(now); - bool negative = utc.hour > local.hour && local.day_of_year <= utc.day_of_year; - - if (utc.minute > local.minute) { - local.minute += 60; - local.hour -= 1; - } - offset += (local.minute - utc.minute) * 60; - - if (negative) { - offset -= (utc.hour - local.hour) * 3600; - } else { - if (utc.hour > local.hour) { - local.hour += 24; - } - offset += (local.hour - utc.hour) * 3600; - } - return offset; + struct tm local_tm = *::localtime(&now); + local_tm.tm_isdst = 0; // Cause mktime to ignore daylight saving time because we want to include it in the offset. + time_t local_time = mktime(&local_tm); + struct tm utc_tm = *::gmtime(&now); + time_t utc_time = mktime(&utc_tm); + return static_cast(local_time - utc_time); } bool ESPTime::operator<(const ESPTime &other) const { return this->timestamp < other.timestamp; } diff --git a/esphome/core/time.h b/esphome/core/time.h index a53fca2346..68826dabdc 100644 --- a/esphome/core/time.h +++ b/esphome/core/time.h @@ -44,17 +44,19 @@ struct ESPTime { size_t strftime(char *buffer, size_t buffer_len, const char *format); /** Convert this ESPTime struct to a string as specified by the format argument. - * @see https://www.gnu.org/software/libc/manual/html_node/Formatting-Calendar-Time.html#index-strftime + * @see https://en.cppreference.com/w/c/chrono/strftime * - * @warning This method uses dynamically allocated strings which can cause heap fragmentation with some + * @warning This method returns a dynamically allocated string which can cause heap fragmentation with some * microcontrollers. * - * @warning This method can return "ERROR" when the underlying strftime() call fails, e.g. when the - * format string contains unsupported specifiers or when the format string doesn't produce any - * output. + * @warning This method can return "ERROR" when the underlying strftime() call fails or when the + * output exceeds 128 bytes. */ std::string strftime(const std::string &format); + /// @copydoc strftime(const std::string &format) + std::string strftime(const char *format); + /// Check if this ESPTime is valid (all fields in range and year is greater than 2018) bool is_valid() const { return this->year >= 2019 && this->fields_in_range(); } @@ -82,6 +84,9 @@ struct ESPTime { */ static ESPTime from_epoch_local(time_t epoch) { struct tm *c_tm = ::localtime(&epoch); + if (c_tm == nullptr) { + return ESPTime{}; // Return an invalid ESPTime + } return ESPTime::from_c_tm(c_tm, epoch); } /** Convert an UTC epoch timestamp to a UTC time ESPTime instance. @@ -91,6 +96,9 @@ struct ESPTime { */ static ESPTime from_epoch_utc(time_t epoch) { struct tm *c_tm = ::gmtime(&epoch); + if (c_tm == nullptr) { + return ESPTime{}; // Return an invalid ESPTime + } return ESPTime::from_c_tm(c_tm, epoch); } diff --git a/esphome/coroutine.py b/esphome/coroutine.py index 8d952246f3..0331c602c5 100644 --- a/esphome/coroutine.py +++ b/esphome/coroutine.py @@ -42,7 +42,10 @@ Here everything is combined in `yield` expressions. You await other coroutines u the last `yield` expression defines what is returned. """ +from __future__ import annotations + from collections.abc import Awaitable, Callable, Generator, Iterator +import enum import functools import heapq import inspect @@ -53,6 +56,98 @@ from typing import Any _LOGGER = logging.getLogger(__name__) +class CoroPriority(enum.IntEnum): + """Execution priority stages for ESPHome code generation. + + Higher values run first. These stages ensure proper dependency + resolution during code generation. + """ + + # Platform initialization - must run first + # Examples: esp32, esp8266, rp2040 + PLATFORM = 1000 + + # Network infrastructure setup + # Examples: network (201) + NETWORK = 201 + + # Network transport layer + # Examples: async_tcp (200) + NETWORK_TRANSPORT = 200 + + # Core system components + # Examples: esphome core, most entity base components (cover, update, datetime, + # valve, alarm_control_panel, lock, event, binary_sensor, button, climate, fan, + # light, media_player, number, select, sensor, switch, text_sensor, text), + # microphone, speaker, audio_dac, touchscreen, stepper + CORE = 100 + + # Diagnostic and debugging systems + # Examples: logger (90) + DIAGNOSTICS = 90 + + # Status and monitoring systems + # Examples: status_led (80) + STATUS = 80 + + # Web server infrastructure + # Examples: web_server_base (65) + WEB_SERVER_BASE = 65 + + # Network portal services + # Examples: captive_portal (64) + CAPTIVE_PORTAL = 64 + + # Communication protocols and services + # Examples: wifi (60), ethernet (60) + COMMUNICATION = 60 + + # Network discovery and management services + # Examples: mdns (55) + NETWORK_SERVICES = 55 + + # OTA update services + # Examples: ota_updates (54) + OTA_UPDATES = 54 + + # Web-based OTA services + # Examples: web_server_ota (52) + WEB_SERVER_OTA = 52 + + # Application-level services + # Examples: safe_mode (50) + APPLICATION = 50 + + # Web and UI services + # Examples: web_server (40) + WEB = 40 + + # Automations and user logic + # Examples: esphome core automations (30) + AUTOMATION = 30 + + # Bus and peripheral setup + # Examples: i2c (1) + BUS = 1 + + # Standard component priority (default) + # Components without explicit priority run at 0 + COMPONENT = 0 + + # Components that need others to be registered first + # Examples: globals (-100) + LATE = -100 + + # Platform-specific workarounds and fixes + # Examples: add_arduino_global_workaround (-999), esp8266 pin states (-999) + WORKAROUNDS = -999 + + # Final setup that requires all components to be registered + # Examples: add_includes, _add_platformio_options, _add_platform_defines (all -1000), + # esp32_ble_tracker feature defines (-1000) + FINAL = -1000 + + def coroutine(func: Callable[..., Any]) -> Callable[..., Awaitable[Any]]: """Decorator to apply to methods to convert them to ESPHome coroutines.""" if getattr(func, "_esphome_coroutine", False): @@ -95,15 +190,16 @@ def coroutine(func: Callable[..., Any]) -> Callable[..., Awaitable[Any]]: return coro -def coroutine_with_priority(priority: float): +def coroutine_with_priority(priority: float | CoroPriority): """Decorator to apply to functions to convert them to ESPHome coroutines. :param priority: priority with which to schedule the coroutine, higher priorities run first. + Can be a float or a CoroPriority enum value. """ def decorator(func): coro = coroutine(func) - coro.priority = priority + coro.priority = float(priority) return coro return decorator @@ -173,7 +269,7 @@ class _Task: self.iterator = iterator self.original_function = original_function - def with_priority(self, priority: float) -> "_Task": + def with_priority(self, priority: float) -> _Task: return _Task(priority, self.id_number, self.iterator, self.original_function) @property diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 34e4eec1ee..1a47b346b7 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -1,5 +1,5 @@ import abc -from collections.abc import Callable, Sequence +from collections.abc import Callable import inspect import math import re @@ -13,7 +13,6 @@ from esphome.core import ( HexInt, Lambda, Library, - TimePeriod, TimePeriodMicroseconds, TimePeriodMilliseconds, TimePeriodMinutes, @@ -21,35 +20,11 @@ from esphome.core import ( TimePeriodSeconds, ) from esphome.helpers import cpp_string_escape, indent_all_but_first_and_last +from esphome.types import Expression, SafeExpType, TemplateArgsType from esphome.util import OrderedDict from esphome.yaml_util import ESPHomeDataBase -class Expression(abc.ABC): - __slots__ = () - - @abc.abstractmethod - def __str__(self): - """ - Convert expression into C++ code - """ - - -SafeExpType = ( - Expression - | bool - | str - | str - | int - | float - | TimePeriod - | type[bool] - | type[int] - | type[float] - | Sequence[Any] -) - - class RawExpression(Expression): __slots__ = ("text",) @@ -223,6 +198,8 @@ class LambdaExpression(Expression): self.return_type = safe_exp(return_type) if return_type is not None else None def __str__(self): + # Stateless lambdas (empty capture) implicitly convert to function pointers + # when assigned to function pointer types - no unary + needed cpp = f"[{self.capture}]({self.parameters})" if self.return_type is not None: cpp += f" -> {self.return_type}" @@ -253,6 +230,19 @@ class StringLiteral(Literal): return cpp_string_escape(self.string) +class LogStringLiteral(Literal): + """A string literal that uses LOG_STR() macro for flash storage on ESP8266.""" + + __slots__ = ("string",) + + def __init__(self, string: str) -> None: + super().__init__() + self.string = string + + def __str__(self) -> str: + return f"LOG_STR({cpp_string_escape(self.string)})" + + class IntLiteral(Literal): __slots__ = ("i",) @@ -360,7 +350,7 @@ def safe_exp(obj: SafeExpType) -> Expression: return IntLiteral(int(obj.total_seconds)) if isinstance(obj, TimePeriodMinutes): return IntLiteral(int(obj.total_minutes)) - if isinstance(obj, tuple | list): + if isinstance(obj, (tuple, list)): return ArrayInitializer(*[safe_exp(o) for o in obj]) if obj is bool: return bool_ @@ -562,7 +552,7 @@ def Pvariable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj": return obj -def new_Pvariable(id_: ID, *args: SafeExpType) -> Pvariable: +def new_Pvariable(id_: ID, *args: SafeExpType) -> "MockObj": """Declare a new pointer variable in the code generation by calling it's constructor with the given arguments. @@ -668,8 +658,8 @@ async def get_variable_with_full_id(id_: ID) -> tuple[ID, "MockObj"]: async def process_lambda( value: Lambda, - parameters: list[tuple[SafeExpType, str]], - capture: str = "=", + parameters: TemplateArgsType, + capture: str = "", return_type: SafeExpType = None, ) -> LambdaExpression | None: """Process the given lambda value into a LambdaExpression. diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index b61b215bdc..2698b9b3d5 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -9,7 +9,7 @@ from esphome.const import ( ) from esphome.core import CORE, ID, coroutine from esphome.coroutine import FakeAwaitable -from esphome.cpp_generator import add, get_variable +from esphome.cpp_generator import LogStringLiteral, add, get_variable from esphome.cpp_types import App from esphome.types import ConfigFragmentType, ConfigType from esphome.util import Registry, RegistryEntry @@ -76,7 +76,7 @@ async def register_component(var, config): "Error while finding name of component, please report this", exc_info=e ) if name is not None: - add(var.set_component_source(name)) + add(var.set_component_source(LogStringLiteral(name))) add(App.register_component(var)) return var diff --git a/esphome/cpp_types.py b/esphome/cpp_types.py index a0dd62cb4e..0d1813f63b 100644 --- a/esphome/cpp_types.py +++ b/esphome/cpp_types.py @@ -23,6 +23,7 @@ size_t = global_ns.namespace("size_t") const_char_ptr = global_ns.namespace("const char *") NAN = global_ns.namespace("NAN") esphome_ns = global_ns # using namespace esphome; +FixedVector = esphome_ns.class_("FixedVector") App = esphome_ns.App EntityBase = esphome_ns.class_("EntityBase") Component = esphome_ns.class_("Component") diff --git a/esphome/dashboard/const.py b/esphome/dashboard/const.py index db66cb5ead..ada5575d0e 100644 --- a/esphome/dashboard/const.py +++ b/esphome/dashboard/const.py @@ -1,9 +1,26 @@ from __future__ import annotations -EVENT_ENTRY_ADDED = "entry_added" -EVENT_ENTRY_REMOVED = "entry_removed" -EVENT_ENTRY_UPDATED = "entry_updated" -EVENT_ENTRY_STATE_CHANGED = "entry_state_changed" +from esphome.enum import StrEnum + + +class DashboardEvent(StrEnum): + """Dashboard WebSocket event types.""" + + # Server -> Client events (backend sends to frontend) + ENTRY_ADDED = "entry_added" + ENTRY_REMOVED = "entry_removed" + ENTRY_UPDATED = "entry_updated" + ENTRY_STATE_CHANGED = "entry_state_changed" + IMPORTABLE_DEVICE_ADDED = "importable_device_added" + IMPORTABLE_DEVICE_REMOVED = "importable_device_removed" + INITIAL_STATE = "initial_state" # Sent on WebSocket connection + PONG = "pong" # Response to client ping + + # Client -> Server events (frontend sends to backend) + PING = "ping" # WebSocket keepalive from client + REFRESH = "refresh" # Force backend to poll for changes + + MAX_EXECUTOR_WORKERS = 48 diff --git a/esphome/dashboard/core.py b/esphome/dashboard/core.py index 410ef0c29d..b9ec56cd00 100644 --- a/esphome/dashboard/core.py +++ b/esphome/dashboard/core.py @@ -7,13 +7,13 @@ from dataclasses import dataclass from functools import partial import json import logging -from pathlib import Path import threading from typing import Any from esphome.storage_json import ignored_devices_storage_path from ..zeroconf import DiscoveredImport +from .const import DashboardEvent from .dns import DNSCache from .entries import DashboardEntries from .settings import DashboardSettings @@ -31,7 +31,7 @@ MDNS_BOOTSTRAP_TIME = 7.5 class Event: """Dashboard Event.""" - event_type: str + event_type: DashboardEvent data: dict[str, Any] @@ -40,22 +40,24 @@ class EventBus: def __init__(self) -> None: """Initialize the Dashboard event bus.""" - self._listeners: dict[str, set[Callable[[Event], None]]] = {} + self._listeners: dict[DashboardEvent, set[Callable[[Event], None]]] = {} def async_add_listener( - self, event_type: str, listener: Callable[[Event], None] + self, event_type: DashboardEvent, listener: Callable[[Event], None] ) -> Callable[[], None]: """Add a listener to the event bus.""" self._listeners.setdefault(event_type, set()).add(listener) return partial(self._async_remove_listener, event_type, listener) def _async_remove_listener( - self, event_type: str, listener: Callable[[Event], None] + self, event_type: DashboardEvent, listener: Callable[[Event], None] ) -> None: """Remove a listener from the event bus.""" self._listeners[event_type].discard(listener) - def async_fire(self, event_type: str, event_data: dict[str, Any]) -> None: + def async_fire( + self, event_type: DashboardEvent, event_data: dict[str, Any] + ) -> None: """Fire an event.""" event = Event(event_type, event_data) @@ -108,7 +110,7 @@ class ESPHomeDashboard: await self.loop.run_in_executor(None, self.load_ignored_devices) def load_ignored_devices(self) -> None: - storage_path = Path(ignored_devices_storage_path()) + storage_path = ignored_devices_storage_path() try: with storage_path.open("r", encoding="utf-8") as f_handle: data = json.load(f_handle) @@ -117,7 +119,7 @@ class ESPHomeDashboard: pass def save_ignored_devices(self) -> None: - storage_path = Path(ignored_devices_storage_path()) + storage_path = ignored_devices_storage_path() with storage_path.open("w", encoding="utf-8") as f_handle: json.dump( {"ignored_devices": sorted(self.ignored_devices)}, indent=2, fp=f_handle diff --git a/esphome/dashboard/dns.py b/esphome/dashboard/dns.py index 98134062f4..58867f7bc1 100644 --- a/esphome/dashboard/dns.py +++ b/esphome/dashboard/dns.py @@ -28,6 +28,21 @@ class DNSCache: self._cache: dict[str, tuple[float, list[str] | Exception]] = {} self._ttl = ttl + def get_cached_addresses( + self, hostname: str, now_monotonic: float + ) -> list[str] | None: + """Get cached addresses without triggering resolution. + + Returns None if not in cache, list of addresses if found. + """ + # Normalize hostname for consistent lookups + normalized = hostname.rstrip(".").lower() + if expire_time_addresses := self._cache.get(normalized): + expire_time, addresses = expire_time_addresses + if expire_time > now_monotonic and not isinstance(addresses, Exception): + return addresses + return None + async def async_resolve( self, hostname: str, now_monotonic: float ) -> list[str] | Exception: diff --git a/esphome/dashboard/entries.py b/esphome/dashboard/entries.py index b138cfd272..95b8a7b2ae 100644 --- a/esphome/dashboard/entries.py +++ b/esphome/dashboard/entries.py @@ -5,20 +5,14 @@ from collections import defaultdict from dataclasses import dataclass from functools import lru_cache import logging -import os +from pathlib import Path from typing import TYPE_CHECKING, Any from esphome import const, util from esphome.enum import StrEnum from esphome.storage_json import StorageJSON, ext_storage_path -from .const import ( - DASHBOARD_COMMAND, - EVENT_ENTRY_ADDED, - EVENT_ENTRY_REMOVED, - EVENT_ENTRY_STATE_CHANGED, - EVENT_ENTRY_UPDATED, -) +from .const import DASHBOARD_COMMAND, DashboardEvent from .util.subprocess import async_run_system_command if TYPE_CHECKING: @@ -102,12 +96,12 @@ class DashboardEntries: # "path/to/file.yaml": DashboardEntry, # ... # } - self._entries: dict[str, DashboardEntry] = {} + self._entries: dict[Path, DashboardEntry] = {} self._loaded_entries = False self._update_lock = asyncio.Lock() self._name_to_entry: dict[str, set[DashboardEntry]] = defaultdict(set) - def get(self, path: str) -> DashboardEntry | None: + def get(self, path: Path) -> DashboardEntry | None: """Get an entry by path.""" return self._entries.get(path) @@ -192,7 +186,7 @@ class DashboardEntries: return entry.state = state self._dashboard.bus.async_fire( - EVENT_ENTRY_STATE_CHANGED, {"entry": entry, "state": state} + DashboardEvent.ENTRY_STATE_CHANGED, {"entry": entry, "state": state} ) async def async_request_update_entries(self) -> None: @@ -260,22 +254,22 @@ class DashboardEntries: for entry in added: entries[entry.path] = entry name_to_entry[entry.name].add(entry) - bus.async_fire(EVENT_ENTRY_ADDED, {"entry": entry}) + bus.async_fire(DashboardEvent.ENTRY_ADDED, {"entry": entry}) for entry in removed: del entries[entry.path] name_to_entry[entry.name].discard(entry) - bus.async_fire(EVENT_ENTRY_REMOVED, {"entry": entry}) + bus.async_fire(DashboardEvent.ENTRY_REMOVED, {"entry": entry}) for entry in updated: if (original_name := original_names[entry]) != (current_name := entry.name): name_to_entry[original_name].discard(entry) name_to_entry[current_name].add(entry) - bus.async_fire(EVENT_ENTRY_UPDATED, {"entry": entry}) + bus.async_fire(DashboardEvent.ENTRY_UPDATED, {"entry": entry}) - def _get_path_to_cache_key(self) -> dict[str, DashboardCacheKeyType]: + def _get_path_to_cache_key(self) -> dict[Path, DashboardCacheKeyType]: """Return a dict of path to cache key.""" - path_to_cache_key: dict[str, DashboardCacheKeyType] = {} + path_to_cache_key: dict[Path, DashboardCacheKeyType] = {} # # The cache key is (inode, device, mtime, size) # which allows us to avoid locking since it ensures @@ -287,12 +281,12 @@ class DashboardEntries: for file in util.list_yaml_files([self._config_dir]): try: # Prefer the json storage path if it exists - stat = os.stat(ext_storage_path(os.path.basename(file))) + stat = ext_storage_path(file.name).stat() except OSError: try: # Fallback to the yaml file if the storage # file does not exist or could not be generated - stat = os.stat(file) + stat = file.stat() except OSError: # File was deleted, ignore continue @@ -329,10 +323,10 @@ class DashboardEntry: "_to_dict", ) - def __init__(self, path: str, cache_key: DashboardCacheKeyType) -> None: + def __init__(self, path: Path, cache_key: DashboardCacheKeyType) -> None: """Initialize the DashboardEntry.""" self.path = path - self.filename: str = os.path.basename(path) + self.filename: str = path.name self._storage_path = ext_storage_path(self.filename) self.cache_key = cache_key self.storage: StorageJSON | None = None @@ -365,7 +359,7 @@ class DashboardEntry: "loaded_integrations": sorted(self.loaded_integrations), "deployed_version": self.update_old, "current_version": self.update_new, - "path": self.path, + "path": str(self.path), "comment": self.comment, "address": self.address, "web_port": self.web_port, diff --git a/esphome/dashboard/models.py b/esphome/dashboard/models.py new file mode 100644 index 0000000000..47ddddd5ce --- /dev/null +++ b/esphome/dashboard/models.py @@ -0,0 +1,76 @@ +"""Data models and builders for the dashboard.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, TypedDict + +if TYPE_CHECKING: + from esphome.zeroconf import DiscoveredImport + + from .core import ESPHomeDashboard + from .entries import DashboardEntry + + +class ImportableDeviceDict(TypedDict): + """Dictionary representation of an importable device.""" + + name: str + friendly_name: str | None + package_import_url: str + project_name: str + project_version: str + network: str + ignored: bool + + +class ConfiguredDeviceDict(TypedDict, total=False): + """Dictionary representation of a configured device.""" + + name: str + friendly_name: str | None + configuration: str + loaded_integrations: list[str] | None + deployed_version: str | None + current_version: str | None + path: str + comment: str | None + address: str | None + web_port: int | None + target_platform: str | None + + +class DeviceListResponse(TypedDict): + """Response for device list API.""" + + configured: list[ConfiguredDeviceDict] + importable: list[ImportableDeviceDict] + + +def build_importable_device_dict( + dashboard: ESPHomeDashboard, discovered: DiscoveredImport +) -> ImportableDeviceDict: + """Build the importable device dictionary.""" + return ImportableDeviceDict( + name=discovered.device_name, + friendly_name=discovered.friendly_name, + package_import_url=discovered.package_import_url, + project_name=discovered.project_name, + project_version=discovered.project_version, + network=discovered.network, + ignored=discovered.device_name in dashboard.ignored_devices, + ) + + +def build_device_list_response( + dashboard: ESPHomeDashboard, entries: list[DashboardEntry] +) -> DeviceListResponse: + """Build the device list response data.""" + configured = {entry.name for entry in entries} + return DeviceListResponse( + configured=[entry.to_dict() for entry in entries], + importable=[ + build_importable_device_dict(dashboard, res) + for res in dashboard.import_result.values() + if res.device_name not in configured + ], + ) diff --git a/esphome/dashboard/settings.py b/esphome/dashboard/settings.py index fa39b55016..6035b4a1d6 100644 --- a/esphome/dashboard/settings.py +++ b/esphome/dashboard/settings.py @@ -10,6 +10,10 @@ from esphome.helpers import get_bool_env from .util.password import password_hash +# Sentinel file name used for CORE.config_path when dashboard initializes. +# This ensures .parent returns the config directory instead of root. +_DASHBOARD_SENTINEL_FILE = "___DASHBOARD_SENTINEL___.yaml" + class DashboardSettings: """Settings for the dashboard.""" @@ -27,7 +31,7 @@ class DashboardSettings: def __init__(self) -> None: """Initialize the dashboard settings.""" - self.config_dir: str = "" + self.config_dir: Path = None self.password_hash: str = "" self.username: str = "" self.using_password: bool = False @@ -45,10 +49,15 @@ class DashboardSettings: self.using_password = bool(password) if self.using_password: self.password_hash = password_hash(password) - self.config_dir = args.configuration - self.absolute_config_dir = Path(self.config_dir).resolve() + self.config_dir = Path(args.configuration) + self.absolute_config_dir = self.config_dir.resolve() self.verbose = args.verbose - CORE.config_path = os.path.join(self.config_dir, ".") + # Set to a sentinel file so .parent gives us the config directory. + # Previously this was `os.path.join(self.config_dir, ".")` which worked because + # os.path.dirname("/config/.") returns "/config", but Path("/config/.").parent + # normalizes to Path("/config") first, then .parent returns Path("/"), breaking + # secret resolution. Using a sentinel file ensures .parent gives the correct directory. + CORE.config_path = self.config_dir / _DASHBOARD_SENTINEL_FILE @property def relative_url(self) -> str: @@ -81,9 +90,9 @@ class DashboardSettings: # Compare password in constant running time (to prevent timing attacks) return hmac.compare_digest(self.password_hash, password_hash(password)) - def rel_path(self, *args: Any) -> str: + def rel_path(self, *args: Any) -> Path: """Return a path relative to the ESPHome config folder.""" - joined_path = os.path.join(self.config_dir, *args) + joined_path = self.config_dir / Path(*args) # Raises ValueError if not relative to ESPHome config folder - Path(joined_path).resolve().relative_to(self.absolute_config_dir) + joined_path.resolve().relative_to(self.absolute_config_dir) return joined_path diff --git a/esphome/dashboard/status/mdns.py b/esphome/dashboard/status/mdns.py index f9ac7b4289..881340ab24 100644 --- a/esphome/dashboard/status/mdns.py +++ b/esphome/dashboard/status/mdns.py @@ -4,16 +4,21 @@ import asyncio import logging import typing +from zeroconf import AddressResolver, IPVersion + +from esphome.address_cache import normalize_hostname from esphome.zeroconf import ( ESPHOME_SERVICE_TYPE, AsyncEsphomeZeroconf, DashboardBrowser, DashboardImportDiscovery, DashboardStatus, + DiscoveredImport, ) -from ..const import SENTINEL +from ..const import SENTINEL, DashboardEvent from ..entries import DashboardEntry, EntryStateSource, bool_to_entry_state +from ..models import build_importable_device_dict if typing.TYPE_CHECKING: from ..core import ESPHomeDashboard @@ -50,6 +55,44 @@ class MDNSStatus: return await aiozc.async_resolve_host(host_name) return None + def get_cached_addresses(self, host_name: str) -> list[str] | None: + """Get cached addresses for a host without triggering resolution. + + Returns None if not in cache or no zeroconf available. + """ + if not self.aiozc: + _LOGGER.debug("No zeroconf instance available for %s", host_name) + return None + + # Normalize hostname and get the base name + normalized = normalize_hostname(host_name) + base_name = normalized.partition(".")[0] + + # Try to load from zeroconf cache without triggering resolution + resolver_name = f"{base_name}.local." + info = AddressResolver(resolver_name) + # Let zeroconf use its own current time for cache checking + if info.load_from_cache(self.aiozc.zeroconf): + addresses = info.parsed_scoped_addresses(IPVersion.All) + _LOGGER.debug("Found %s in zeroconf cache: %s", resolver_name, addresses) + return addresses + _LOGGER.debug("Not found in zeroconf cache: %s", resolver_name) + return None + + def _on_import_update(self, name: str, discovered: DiscoveredImport | None) -> None: + """Handle importable device updates.""" + if discovered is None: + # Device removed + self.dashboard.bus.async_fire( + DashboardEvent.IMPORTABLE_DEVICE_REMOVED, {"name": name} + ) + else: + # Device added + self.dashboard.bus.async_fire( + DashboardEvent.IMPORTABLE_DEVICE_ADDED, + {"device": build_importable_device_dict(self.dashboard, discovered)}, + ) + async def async_refresh_hosts(self) -> None: """Refresh the hosts to track.""" dashboard = self.dashboard @@ -106,7 +149,8 @@ class MDNSStatus: self._async_set_state(entry, result) stat = DashboardStatus(on_update) - imports = DashboardImportDiscovery() + + imports = DashboardImportDiscovery(self._on_import_update) dashboard.import_result = imports.import_state browser = DashboardBrowser( diff --git a/esphome/dashboard/util/file.py b/esphome/dashboard/util/file.py deleted file mode 100644 index bb263f9ad7..0000000000 --- a/esphome/dashboard/util/file.py +++ /dev/null @@ -1,63 +0,0 @@ -import logging -import os -from pathlib import Path -import tempfile - -_LOGGER = logging.getLogger(__name__) - - -def write_utf8_file( - filename: Path, - utf8_str: str, - private: bool = False, -) -> None: - """Write a file and rename it into place. - - Writes all or nothing. - """ - write_file(filename, utf8_str.encode("utf-8"), private) - - -# from https://github.com/home-assistant/core/blob/dev/homeassistant/util/file.py -def write_file( - filename: Path, - utf8_data: bytes, - private: bool = False, -) -> None: - """Write a file and rename it into place. - - Writes all or nothing. - """ - - tmp_filename = "" - missing_fchmod = False - try: - # Modern versions of Python tempfile create this file with mode 0o600 - with tempfile.NamedTemporaryFile( - mode="wb", dir=os.path.dirname(filename), delete=False - ) as fdesc: - fdesc.write(utf8_data) - tmp_filename = fdesc.name - if not private: - try: - os.fchmod(fdesc.fileno(), 0o644) - except AttributeError: - # os.fchmod is not available on Windows - missing_fchmod = True - - os.replace(tmp_filename, filename) - if missing_fchmod: - os.chmod(filename, 0o644) - finally: - if os.path.exists(tmp_filename): - try: - os.remove(tmp_filename) - except OSError as err: - # If we are cleaning up then something else went wrong, so - # we should suppress likely follow-on errors in the cleanup - _LOGGER.error( - "File replacement cleanup failed for %s while saving %s: %s", - tmp_filename, - filename, - err, - ) diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index 286dc9e1d7..804a2b99af 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -2,9 +2,12 @@ from __future__ import annotations import asyncio import base64 +import binascii from collections.abc import Callable, Iterable +import contextlib import datetime import functools +from functools import partial import gzip import hashlib import importlib @@ -48,10 +51,11 @@ from esphome.storage_json import ( from esphome.util import get_serial_ports, shlex_quote from esphome.yaml_util import FastestAvailableSafeLoader -from .const import DASHBOARD_COMMAND -from .core import DASHBOARD -from .entries import UNKNOWN_STATE, entry_state_to_bool -from .util.file import write_file +from ..helpers import write_file +from .const import DASHBOARD_COMMAND, DashboardEvent +from .core import DASHBOARD, ESPHomeDashboard, Event +from .entries import UNKNOWN_STATE, DashboardEntry, entry_state_to_bool +from .models import build_device_list_response from .util.subprocess import async_run_system_command from .util.text import friendly_name_slugify @@ -229,6 +233,7 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler): stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + close_fds=False, ) stdout_thread = threading.Thread(target=self._stdout_thread) stdout_thread.daemon = True @@ -281,11 +286,23 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler): def _stdout_thread(self) -> None: if not self._use_popen: return + line = b"" + cr = False while True: - data = self._proc.stdout.readline() + data = self._proc.stdout.read(1) if data: - data = data.replace(b"\r", b"") - self._queue.put_nowait(data) + if data == b"\r": + cr = True + elif data == b"\n": + self._queue.put_nowait(line + b"\n") + line = b"" + cr = False + elif cr: + self._queue.put_nowait(line + b"\r") + line = data + cr = False + else: + line += data if self._proc.poll() is not None: break self._proc.wait(1.0) @@ -312,6 +329,73 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler): raise NotImplementedError +def build_cache_arguments( + entry: DashboardEntry | None, + dashboard: ESPHomeDashboard, + now: float, +) -> list[str]: + """Build cache arguments for passing to CLI. + + Args: + entry: Dashboard entry for the configuration + dashboard: Dashboard instance with cache access + now: Current monotonic time for DNS cache expiry checks + + Returns: + List of cache arguments to pass to CLI + """ + cache_args: list[str] = [] + + if not entry: + return cache_args + + _LOGGER.debug( + "Building cache for entry (address=%s, name=%s)", + entry.address, + entry.name, + ) + + def add_cache_entry(hostname: str, addresses: list[str], cache_type: str) -> None: + """Add a cache entry to the command arguments.""" + if not addresses: + return + normalized = hostname.rstrip(".").lower() + cache_args.extend( + [ + f"--{cache_type}-address-cache", + f"{normalized}={','.join(sort_ip_addresses(addresses))}", + ] + ) + + # Check entry.address for cached addresses + if use_address := entry.address: + if use_address.endswith(".local"): + # mDNS cache for .local addresses + if (mdns := dashboard.mdns_status) and ( + cached := mdns.get_cached_addresses(use_address) + ): + _LOGGER.debug("mDNS cache hit for %s: %s", use_address, cached) + add_cache_entry(use_address, cached, "mdns") + # DNS cache for non-.local addresses + elif cached := dashboard.dns_cache.get_cached_addresses(use_address, now): + _LOGGER.debug("DNS cache hit for %s: %s", use_address, cached) + add_cache_entry(use_address, cached, "dns") + + # Check entry.name if we haven't already cached via address + # For mDNS devices, entry.name typically doesn't have .local suffix + if entry.name and not use_address: + mdns_name = ( + f"{entry.name}.local" if not entry.name.endswith(".local") else entry.name + ) + if (mdns := dashboard.mdns_status) and ( + cached := mdns.get_cached_addresses(mdns_name) + ): + _LOGGER.debug("mDNS cache hit for %s: %s", mdns_name, cached) + add_cache_entry(mdns_name, cached, "mdns") + + return cache_args + + class EsphomePortCommandWebSocket(EsphomeCommandWebSocket): """Base class for commands that require a port.""" @@ -324,38 +408,22 @@ class EsphomePortCommandWebSocket(EsphomeCommandWebSocket): configuration = json_message["configuration"] config_file = settings.rel_path(configuration) port = json_message["port"] + + # Build cache arguments to pass to CLI + cache_args: list[str] = [] + if ( port == "OTA" # pylint: disable=too-many-boolean-expressions and (entry := entries.get(config_file)) and entry.loaded_integrations and "api" in entry.loaded_integrations ): - if (mdns := dashboard.mdns_status) and ( - address_list := await mdns.async_resolve_host(entry.name) - ): - # Use the IP address if available but only - # if the API is loaded and the device is online - # since MQTT logging will not work otherwise - port = sort_ip_addresses(address_list)[0] - elif ( - entry.address - and ( - address_list := await dashboard.dns_cache.async_resolve( - entry.address, time.monotonic() - ) - ) - and not isinstance(address_list, Exception) - ): - # If mdns is not available, try to use the DNS cache - port = sort_ip_addresses(address_list)[0] + cache_args = build_cache_arguments(entry, dashboard, time.monotonic()) - return [ - *DASHBOARD_COMMAND, - *args, - config_file, - "--device", - port, - ] + # Cache arguments must come before the subcommand + cmd = [*DASHBOARD_COMMAND, *cache_args, *args, config_file, "--device", port] + _LOGGER.debug("Built command: %s", cmd) + return cmd class EsphomeLogsHandler(EsphomePortCommandWebSocket): @@ -426,6 +494,14 @@ class EsphomeCleanMqttHandler(EsphomeCommandWebSocket): return [*DASHBOARD_COMMAND, "clean-mqtt", config_file] +class EsphomeCleanAllHandler(EsphomeCommandWebSocket): + async def build_command(self, json_message: dict[str, Any]) -> list[str]: + clean_build_dir = json_message.get("clean_build_dir", True) + if clean_build_dir: + return [*DASHBOARD_COMMAND, "clean-all", settings.config_dir] + return [*DASHBOARD_COMMAND, "clean-all"] + + class EsphomeCleanHandler(EsphomeCommandWebSocket): async def build_command(self, json_message: dict[str, Any]) -> list[str]: config_file = settings.rel_path(json_message["configuration"]) @@ -447,6 +523,243 @@ class EsphomeUpdateAllHandler(EsphomeCommandWebSocket): return [*DASHBOARD_COMMAND, "update-all", settings.config_dir] +# Dashboard polling constants +DASHBOARD_POLL_INTERVAL = 2 # seconds +DASHBOARD_ENTRIES_UPDATE_INTERVAL = 10 # seconds +DASHBOARD_ENTRIES_UPDATE_ITERATIONS = ( + DASHBOARD_ENTRIES_UPDATE_INTERVAL // DASHBOARD_POLL_INTERVAL +) + + +class DashboardSubscriber: + """Manages dashboard event polling task lifecycle based on active subscribers.""" + + def __init__(self) -> None: + """Initialize the dashboard subscriber.""" + self._subscribers: set[DashboardEventsWebSocket] = set() + self._event_loop_task: asyncio.Task | None = None + self._refresh_event: asyncio.Event = asyncio.Event() + + def subscribe(self, subscriber: DashboardEventsWebSocket) -> Callable[[], None]: + """Subscribe to dashboard updates and start event loop if needed.""" + self._subscribers.add(subscriber) + if not self._event_loop_task or self._event_loop_task.done(): + self._event_loop_task = asyncio.create_task(self._event_loop()) + _LOGGER.info("Started dashboard event loop") + return partial(self._unsubscribe, subscriber) + + def _unsubscribe(self, subscriber: DashboardEventsWebSocket) -> None: + """Unsubscribe from dashboard updates and stop event loop if no subscribers.""" + self._subscribers.discard(subscriber) + if ( + not self._subscribers + and self._event_loop_task + and not self._event_loop_task.done() + ): + self._event_loop_task.cancel() + self._event_loop_task = None + _LOGGER.info("Stopped dashboard event loop - no subscribers") + + def request_refresh(self) -> None: + """Signal the polling loop to refresh immediately.""" + self._refresh_event.set() + + async def _event_loop(self) -> None: + """Run the event polling loop while there are subscribers.""" + dashboard = DASHBOARD + entries_update_counter = 0 + + while self._subscribers: + # Signal that we need ping updates (non-blocking) + dashboard.ping_request.set() + if settings.status_use_mqtt: + dashboard.mqtt_ping_request.set() + + # Check if it's time to update entries or if refresh was requested + entries_update_counter += 1 + if ( + entries_update_counter >= DASHBOARD_ENTRIES_UPDATE_ITERATIONS + or self._refresh_event.is_set() + ): + entries_update_counter = 0 + await dashboard.entries.async_request_update_entries() + # Clear the refresh event if it was set + self._refresh_event.clear() + + # Wait for either timeout or refresh event + try: + async with asyncio.timeout(DASHBOARD_POLL_INTERVAL): + await self._refresh_event.wait() + # If we get here, refresh was requested - continue loop immediately + except TimeoutError: + # Normal timeout - continue with regular polling + pass + + +# Global dashboard subscriber instance +DASHBOARD_SUBSCRIBER = DashboardSubscriber() + + +@websocket_class +class DashboardEventsWebSocket(tornado.websocket.WebSocketHandler): + """WebSocket handler for real-time dashboard events.""" + + _event_listeners: list[Callable[[], None]] | None = None + _dashboard_unsubscribe: Callable[[], None] | None = None + + async def get(self, *args: str, **kwargs: str) -> None: + """Handle WebSocket upgrade request.""" + if not is_authenticated(self): + self.set_status(401) + self.finish("Unauthorized") + return + await super().get(*args, **kwargs) + + async def open(self, *args: str, **kwargs: str) -> None: # pylint: disable=invalid-overridden-method + """Handle new WebSocket connection.""" + # Ensure messages are sent immediately to avoid + # a 200-500ms delay when nodelay is not set. + self.set_nodelay(True) + + # Update entries first + await DASHBOARD.entries.async_request_update_entries() + # Send initial state + self._send_initial_state() + # Subscribe to events + self._subscribe_to_events() + # Subscribe to dashboard updates + self._dashboard_unsubscribe = DASHBOARD_SUBSCRIBER.subscribe(self) + _LOGGER.debug("Dashboard status WebSocket opened") + + def _send_initial_state(self) -> None: + """Send initial device list and ping status.""" + entries = DASHBOARD.entries.async_all() + + # Send initial state + self._safe_send_message( + { + "event": DashboardEvent.INITIAL_STATE, + "data": { + "devices": build_device_list_response(DASHBOARD, entries), + "ping": { + entry.filename: entry_state_to_bool(entry.state) + for entry in entries + }, + }, + } + ) + + def _subscribe_to_events(self) -> None: + """Subscribe to dashboard events.""" + async_add_listener = DASHBOARD.bus.async_add_listener + # Subscribe to all events + self._event_listeners = [ + async_add_listener( + DashboardEvent.ENTRY_STATE_CHANGED, self._on_entry_state_changed + ), + async_add_listener( + DashboardEvent.ENTRY_ADDED, + self._make_entry_handler(DashboardEvent.ENTRY_ADDED), + ), + async_add_listener( + DashboardEvent.ENTRY_REMOVED, + self._make_entry_handler(DashboardEvent.ENTRY_REMOVED), + ), + async_add_listener( + DashboardEvent.ENTRY_UPDATED, + self._make_entry_handler(DashboardEvent.ENTRY_UPDATED), + ), + async_add_listener( + DashboardEvent.IMPORTABLE_DEVICE_ADDED, self._on_importable_added + ), + async_add_listener( + DashboardEvent.IMPORTABLE_DEVICE_REMOVED, + self._on_importable_removed, + ), + ] + + def _on_entry_state_changed(self, event: Event) -> None: + """Handle entry state change event.""" + entry = event.data["entry"] + state = event.data["state"] + self._safe_send_message( + { + "event": DashboardEvent.ENTRY_STATE_CHANGED, + "data": { + "filename": entry.filename, + "name": entry.name, + "state": entry_state_to_bool(state), + }, + } + ) + + def _make_entry_handler( + self, event_type: DashboardEvent + ) -> Callable[[Event], None]: + """Create an entry event handler.""" + + def handler(event: Event) -> None: + self._safe_send_message( + {"event": event_type, "data": {"device": event.data["entry"].to_dict()}} + ) + + return handler + + def _on_importable_added(self, event: Event) -> None: + """Handle importable device added event.""" + # Don't send if device is already configured + device_name = event.data.get("device", {}).get("name") + if device_name and DASHBOARD.entries.get_by_name(device_name): + return + self._safe_send_message( + {"event": DashboardEvent.IMPORTABLE_DEVICE_ADDED, "data": event.data} + ) + + def _on_importable_removed(self, event: Event) -> None: + """Handle importable device removed event.""" + self._safe_send_message( + {"event": DashboardEvent.IMPORTABLE_DEVICE_REMOVED, "data": event.data} + ) + + def _safe_send_message(self, message: dict[str, Any]) -> None: + """Send a message to the WebSocket client, ignoring closed errors.""" + with contextlib.suppress(tornado.websocket.WebSocketClosedError): + self.write_message(json.dumps(message)) + + def on_message(self, message: str) -> None: + """Handle incoming WebSocket messages.""" + _LOGGER.debug("WebSocket received message: %s", message) + try: + data = json.loads(message) + except json.JSONDecodeError as err: + _LOGGER.debug("Failed to parse WebSocket message: %s", err) + return + + event = data.get("event") + _LOGGER.debug("WebSocket message event: %s", event) + if event == DashboardEvent.PING: + # Send pong response for client ping + _LOGGER.debug("Received client ping, sending pong") + self._safe_send_message({"event": DashboardEvent.PONG}) + elif event == DashboardEvent.REFRESH: + # Signal the polling loop to refresh immediately + _LOGGER.debug("Received refresh request, signaling polling loop") + DASHBOARD_SUBSCRIBER.request_refresh() + + def on_close(self) -> None: + """Handle WebSocket close.""" + # Unsubscribe from dashboard updates + if self._dashboard_unsubscribe: + self._dashboard_unsubscribe() + self._dashboard_unsubscribe = None + + # Unsubscribe from events + for remove_listener in self._event_listeners or []: + remove_listener() + + _LOGGER.debug("Dashboard status WebSocket closed") + + class SerialPortRequestHandler(BaseHandler): @authenticated async def get(self) -> None: @@ -475,7 +788,17 @@ class WizardRequestHandler(BaseHandler): kwargs = { k: v for k, v in json.loads(self.request.body.decode()).items() - if k in ("name", "platform", "board", "ssid", "psk", "password") + if k + in ( + "type", + "name", + "platform", + "board", + "ssid", + "psk", + "password", + "file_content", + ) } if not kwargs["name"]: self.set_status(422) @@ -483,19 +806,65 @@ class WizardRequestHandler(BaseHandler): self.write(json.dumps({"error": "Name is required"})) return + if "type" not in kwargs: + # Default to basic wizard type for backwards compatibility + kwargs["type"] = "basic" + kwargs["friendly_name"] = kwargs["name"] kwargs["name"] = friendly_name_slugify(kwargs["friendly_name"]) - - kwargs["ota_password"] = secrets.token_hex(16) - noise_psk = secrets.token_bytes(32) - kwargs["api_encryption_key"] = base64.b64encode(noise_psk).decode() + if kwargs["type"] == "basic": + kwargs["ota_password"] = secrets.token_hex(16) + noise_psk = secrets.token_bytes(32) + kwargs["api_encryption_key"] = base64.b64encode(noise_psk).decode() + elif kwargs["type"] == "upload": + try: + kwargs["file_text"] = base64.b64decode(kwargs["file_content"]).decode( + "utf-8" + ) + except (binascii.Error, UnicodeDecodeError): + self.set_status(422) + self.set_header("content-type", "application/json") + self.write( + json.dumps({"error": "The uploaded file is not correctly encoded."}) + ) + return + elif kwargs["type"] != "empty": + self.set_status(422) + self.set_header("content-type", "application/json") + self.write( + json.dumps( + {"error": f"Invalid wizard type specified: {kwargs['type']}"} + ) + ) + return filename = f"{kwargs['name']}.yaml" destination = settings.rel_path(filename) - wizard.wizard_write(path=destination, **kwargs) - self.set_status(200) - self.set_header("content-type", "application/json") - self.write(json.dumps({"configuration": filename})) - self.finish() + + # Check if destination file already exists + if destination.exists(): + self.set_status(409) # Conflict status code + self.set_header("content-type", "application/json") + self.write( + json.dumps({"error": f"Configuration file '{filename}' already exists"}) + ) + self.finish() + return + + success = wizard.wizard_write(path=destination, **kwargs) + if success: + self.set_status(200) + self.set_header("content-type", "application/json") + self.write(json.dumps({"configuration": filename})) + self.finish() + else: + self.set_status(500) + self.set_header("content-type", "application/json") + self.write( + json.dumps( + {"error": "Failed to write configuration, see logs for details"} + ) + ) + self.finish() class ImportRequestHandler(BaseHandler): @@ -689,10 +1058,10 @@ class DownloadBinaryRequestHandler(BaseHandler): "download", f"{storage_json.name}-{file_name}", ) - path = os.path.dirname(storage_json.firmware_bin_path) - path = os.path.join(path, file_name) - if not Path(path).is_file(): + path = storage_json.firmware_bin_path.parent.joinpath(file_name) + + if not path.is_file(): args = ["esphome", "idedata", settings.rel_path(configuration)] rc, stdout, _ = await async_run_system_command(args) @@ -746,28 +1115,7 @@ class ListDevicesHandler(BaseHandler): await dashboard.entries.async_request_update_entries() entries = dashboard.entries.async_all() self.set_header("content-type", "application/json") - configured = {entry.name for entry in entries} - - self.write( - json.dumps( - { - "configured": [entry.to_dict() for entry in entries], - "importable": [ - { - "name": res.device_name, - "friendly_name": res.friendly_name, - "package_import_url": res.package_import_url, - "project_name": res.project_name, - "project_version": res.project_version, - "network": res.network, - "ignored": res.device_name in dashboard.ignored_devices, - } - for res in dashboard.import_result.values() - if res.device_name not in configured - ], - } - ) - ) + self.write(json.dumps(build_device_list_response(dashboard, entries))) class MainRequestHandler(BaseHandler): @@ -907,7 +1255,7 @@ class EditRequestHandler(BaseHandler): return filename = settings.rel_path(configuration) - if Path(filename).resolve().parent != settings.absolute_config_dir: + if filename.resolve().parent != settings.absolute_config_dir: self.send_error(404) return @@ -930,10 +1278,6 @@ class EditRequestHandler(BaseHandler): self.set_status(404) return None - def _write_file(self, filename: str, content: bytes) -> None: - """Write a file with the given content.""" - write_file(filename, content) - @authenticated @bind_config async def post(self, configuration: str | None = None) -> None: @@ -943,12 +1287,12 @@ class EditRequestHandler(BaseHandler): return filename = settings.rel_path(configuration) - if Path(filename).resolve().parent != settings.absolute_config_dir: + if filename.resolve().parent != settings.absolute_config_dir: self.send_error(404) return loop = asyncio.get_running_loop() - await loop.run_in_executor(None, self._write_file, filename, self.request.body) + await loop.run_in_executor(None, write_file, filename, self.request.body) # Ensure the StorageJSON is updated as well DASHBOARD.entries.async_schedule_storage_json_update(filename) self.set_status(200) @@ -963,15 +1307,12 @@ class ArchiveRequestHandler(BaseHandler): archive_path = archive_storage_path() mkdir_p(archive_path) - shutil.move(config_file, os.path.join(archive_path, configuration)) + shutil.move(config_file, archive_path / configuration) storage_json = StorageJSON.load(storage_path) - if storage_json is not None: + if storage_json is not None and storage_json.build_path: # Delete build folder (if exists) - name = storage_json.name - build_folder = os.path.join(settings.config_dir, name) - if build_folder is not None: - shutil.rmtree(build_folder, os.path.join(archive_path, name)) + shutil.rmtree(storage_json.build_path, ignore_errors=True) class UnArchiveRequestHandler(BaseHandler): @@ -980,7 +1321,7 @@ class UnArchiveRequestHandler(BaseHandler): def post(self, configuration: str | None = None) -> None: config_file = settings.rel_path(configuration) archive_path = archive_storage_path() - shutil.move(os.path.join(archive_path, configuration), config_file) + shutil.move(archive_path / configuration, config_file) class LoginHandler(BaseHandler): @@ -1067,7 +1408,7 @@ class SecretKeysRequestHandler(BaseHandler): for secret_filename in const.SECRETS_FILES: relative_filename = settings.rel_path(secret_filename) - if os.path.isfile(relative_filename): + if relative_filename.is_file(): filename = relative_filename break @@ -1100,16 +1441,17 @@ class JsonConfigRequestHandler(BaseHandler): @bind_config async def get(self, configuration: str | None = None) -> None: filename = settings.rel_path(configuration) - if not os.path.isfile(filename): + if not filename.is_file(): self.send_error(404) return - args = ["esphome", "config", filename, "--show-secrets"] + args = ["esphome", "config", str(filename), "--show-secrets"] - rc, stdout, _ = await async_run_system_command(args) + rc, stdout, stderr = await async_run_system_command(args) if rc != 0: - self.send_error(422) + self.set_status(422) + self.write(stderr) return data = yaml.load(stdout, Loader=SafeLoaderIgnoreUnknown) @@ -1118,7 +1460,7 @@ class JsonConfigRequestHandler(BaseHandler): self.finish() -def get_base_frontend_path() -> str: +def get_base_frontend_path() -> Path: if ENV_DEV not in os.environ: import esphome_dashboard @@ -1129,11 +1471,12 @@ def get_base_frontend_path() -> str: static_path += "/" # This path can be relative, so resolve against the root or else templates don't work - return os.path.abspath(os.path.join(os.getcwd(), static_path, "esphome_dashboard")) + path = Path(os.getcwd()) / static_path / "esphome_dashboard" + return path.resolve() -def get_static_path(*args: Iterable[str]) -> str: - return os.path.join(get_base_frontend_path(), "static", *args) +def get_static_path(*args: Iterable[str]) -> Path: + return get_base_frontend_path() / "static" / Path(*args) @functools.cache @@ -1150,8 +1493,7 @@ def get_static_file_url(name: str) -> str: return base.replace("index.js", esphome_dashboard.entrypoint()) path = get_static_path(name) - with open(path, "rb") as f_handle: - hash_ = hashlib.md5(f_handle.read()).hexdigest()[:8] + hash_ = hashlib.md5(path.read_bytes()).hexdigest()[:8] return f"{base}?hash={hash_}" @@ -1211,6 +1553,7 @@ def make_app(debug=get_bool_env(ENV_DEV)) -> tornado.web.Application: (f"{rel}compile", EsphomeCompileHandler), (f"{rel}validate", EsphomeValidateHandler), (f"{rel}clean-mqtt", EsphomeCleanMqttHandler), + (f"{rel}clean-all", EsphomeCleanAllHandler), (f"{rel}clean", EsphomeCleanHandler), (f"{rel}vscode", EsphomeVscodeHandler), (f"{rel}ace", EsphomeAceEditorHandler), @@ -1228,6 +1571,7 @@ def make_app(debug=get_bool_env(ENV_DEV)) -> tornado.web.Application: (f"{rel}wizard", WizardRequestHandler), (f"{rel}static/(.*)", StaticFileHandler, {"path": get_static_path()}), (f"{rel}devices", ListDevicesHandler), + (f"{rel}events", DashboardEventsWebSocket), (f"{rel}import", ImportRequestHandler), (f"{rel}secret_keys", SecretKeysRequestHandler), (f"{rel}json-config", JsonConfigRequestHandler), @@ -1251,7 +1595,7 @@ def start_web_server( """Start the web server listener.""" trash_path = trash_storage_path() - if os.path.exists(trash_path): + if trash_path.is_dir() and trash_path.exists(): _LOGGER.info("Renaming 'trash' folder to 'archive'") archive_path = archive_storage_path() shutil.move(trash_path, archive_path) diff --git a/esphome/espota2.py b/esphome/espota2.py index 279bafee8e..2b1b9a8328 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -1,19 +1,23 @@ from __future__ import annotations +from collections.abc import Callable import gzip import hashlib import io import logging +from pathlib import Path import random import socket import sys import time +from typing import Any from esphome.core import EsphomeError from esphome.helpers import resolve_ip_address RESPONSE_OK = 0x00 RESPONSE_REQUEST_AUTH = 0x01 +RESPONSE_REQUEST_SHA256_AUTH = 0x02 RESPONSE_HEADER_OK = 0x40 RESPONSE_AUTH_OK = 0x41 @@ -44,6 +48,7 @@ OTA_VERSION_2_0 = 2 MAGIC_BYTES = [0x6C, 0x26, 0xF7, 0x5C, 0x45] FEATURE_SUPPORTS_COMPRESSION = 0x01 +FEATURE_SUPPORTS_SHA256_AUTH = 0x02 UPLOAD_BLOCK_SIZE = 8192 @@ -51,6 +56,12 @@ UPLOAD_BUFFER_SIZE = UPLOAD_BLOCK_SIZE * 8 _LOGGER = logging.getLogger(__name__) +# Authentication method lookup table: response -> (hash_func, nonce_size, name) +_AUTH_METHODS: dict[int, tuple[Callable[..., Any], int, str]] = { + RESPONSE_REQUEST_SHA256_AUTH: (hashlib.sha256, 64, "SHA256"), + RESPONSE_REQUEST_AUTH: (hashlib.md5, 32, "MD5"), +} + class ProgressBar: def __init__(self): @@ -80,18 +91,43 @@ class OTAError(EsphomeError): pass -def recv_decode(sock, amount, decode=True): +def recv_decode( + sock: socket.socket, amount: int, decode: bool = True +) -> bytes | list[int]: + """Receive data from socket and optionally decode to list of integers. + + :param sock: Socket to receive data from. + :param amount: Number of bytes to receive. + :param decode: If True, convert bytes to list of integers, otherwise return raw bytes. + :return: List of integers if decode=True, otherwise raw bytes. + """ data = sock.recv(amount) if not decode: return data return list(data) -def receive_exactly(sock, amount, msg, expect, decode=True): - data = [] if decode else b"" +def receive_exactly( + sock: socket.socket, + amount: int, + msg: str, + expect: int | list[int] | None, + decode: bool = True, +) -> list[int] | bytes: + """Receive exactly the specified amount of data from socket with error checking. + + :param sock: Socket to receive data from. + :param amount: Exact number of bytes to receive. + :param msg: Description of what is being received for error messages. + :param expect: Expected response code(s) for validation, None to skip validation. + :param decode: If True, return list of integers, otherwise return raw bytes. + :return: List of integers if decode=True, otherwise raw bytes. + :raises OTAError: If receiving fails or response doesn't match expected. + """ + data: list[int] | bytes = [] if decode else b"" try: - data += recv_decode(sock, 1, decode=decode) + data += recv_decode(sock, 1, decode=decode) # type: ignore[operator] except OSError as err: raise OTAError(f"Error receiving acknowledge {msg}: {err}") from err @@ -103,13 +139,19 @@ def receive_exactly(sock, amount, msg, expect, decode=True): while len(data) < amount: try: - data += recv_decode(sock, amount - len(data), decode=decode) + data += recv_decode(sock, amount - len(data), decode=decode) # type: ignore[operator] except OSError as err: raise OTAError(f"Error receiving {msg}: {err}") from err return data -def check_error(data, expect): +def check_error(data: list[int] | bytes, expect: int | list[int] | None) -> None: + """Check response data for error codes and validate against expected response. + + :param data: Response data from device (first byte is the response code). + :param expect: Expected response code(s), None to skip validation. + :raises OTAError: If an error code is detected or response doesn't match expected. + """ if not expect: return dat = data[0] @@ -124,7 +166,7 @@ def check_error(data, expect): raise OTAError("Error: Authentication invalid. Is the password correct?") if dat == RESPONSE_ERROR_WRITING_FLASH: raise OTAError( - "Error: Wring OTA data to flash memory failed. See USB logs for more " + "Error: Writing OTA data to flash memory failed. See USB logs for more " "information." ) if dat == RESPONSE_ERROR_UPDATE_END: @@ -176,7 +218,16 @@ def check_error(data, expect): raise OTAError(f"Unexpected response from ESP: 0x{data[0]:02X}") -def send_check(sock, data, msg): +def send_check( + sock: socket.socket, data: list[int] | tuple[int, ...] | int | str | bytes, msg: str +) -> None: + """Send data to socket with error handling. + + :param sock: Socket to send data to. + :param data: Data to send (can be list/tuple of ints, single int, string, or bytes). + :param msg: Description of what is being sent for error messages. + :raises OTAError: If sending fails. + """ try: if isinstance(data, (list, tuple)): data = bytes(data) @@ -191,7 +242,7 @@ def send_check(sock, data, msg): def perform_ota( - sock: socket.socket, password: str, file_handle: io.IOBase, filename: str + sock: socket.socket, password: str | None, file_handle: io.IOBase, filename: Path ) -> None: file_contents = file_handle.read() file_size = len(file_contents) @@ -209,10 +260,14 @@ def perform_ota( f"Device uses unsupported OTA version {version}, this ESPHome supports {supported_versions}" ) - # Features - send_check(sock, FEATURE_SUPPORTS_COMPRESSION, "features") + # Features - send both compression and SHA256 auth support + features_to_send = FEATURE_SUPPORTS_COMPRESSION | FEATURE_SUPPORTS_SHA256_AUTH + send_check(sock, features_to_send, "features") features = receive_exactly( - sock, 1, "features", [RESPONSE_HEADER_OK, RESPONSE_SUPPORTS_COMPRESSION] + sock, + 1, + "features", + None, # Accept any response )[0] if features == RESPONSE_SUPPORTS_COMPRESSION: @@ -221,31 +276,52 @@ def perform_ota( else: upload_contents = file_contents - (auth,) = receive_exactly( - sock, 1, "auth", [RESPONSE_REQUEST_AUTH, RESPONSE_AUTH_OK] - ) - if auth == RESPONSE_REQUEST_AUTH: - if not password: + def perform_auth( + sock: socket.socket, + password: str | None, + hash_func: Callable[..., Any], + nonce_size: int, + hash_name: str, + ) -> None: + """Perform challenge-response authentication using specified hash algorithm.""" + if password is None: raise OTAError("ESP requests password, but no password given!") - nonce = receive_exactly( - sock, 32, "authentication nonce", [], decode=False - ).decode() - _LOGGER.debug("Auth: Nonce is %s", nonce) - cnonce = hashlib.md5(str(random.random()).encode()).hexdigest() - _LOGGER.debug("Auth: CNonce is %s", cnonce) + + nonce_bytes = receive_exactly( + sock, nonce_size, f"{hash_name} authentication nonce", [], decode=False + ) + assert isinstance(nonce_bytes, bytes) + nonce = nonce_bytes.decode() + _LOGGER.debug("Auth: %s Nonce is %s", hash_name, nonce) + + # Generate cnonce + cnonce = hash_func(str(random.random()).encode()).hexdigest() + _LOGGER.debug("Auth: %s CNonce is %s", hash_name, cnonce) send_check(sock, cnonce, "auth cnonce") - result_md5 = hashlib.md5() - result_md5.update(password.encode("utf-8")) - result_md5.update(nonce.encode()) - result_md5.update(cnonce.encode()) - result = result_md5.hexdigest() - _LOGGER.debug("Auth: Result is %s", result) + # Calculate challenge response + hasher = hash_func() + hasher.update(password.encode("utf-8")) + hasher.update(nonce.encode()) + hasher.update(cnonce.encode()) + result = hasher.hexdigest() + _LOGGER.debug("Auth: %s Result is %s", hash_name, result) send_check(sock, result, "auth result") receive_exactly(sock, 1, "auth result", RESPONSE_AUTH_OK) + (auth,) = receive_exactly( + sock, + 1, + "auth", + [RESPONSE_REQUEST_AUTH, RESPONSE_REQUEST_SHA256_AUTH, RESPONSE_AUTH_OK], + ) + + if auth != RESPONSE_AUTH_OK: + hash_func, nonce_size, hash_name = _AUTH_METHODS[auth] + perform_auth(sock, password, hash_func, nonce_size, hash_name) + # Set higher timeout during upload sock.settimeout(30.0) @@ -308,9 +384,17 @@ def perform_ota( time.sleep(1) -def run_ota_impl_(remote_host, remote_port, password, filename): +def run_ota_impl_( + remote_host: str | list[str], remote_port: int, password: str | None, filename: Path +) -> tuple[int, str | None]: + from esphome.core import CORE + + # Handle both single host and list of hosts try: - res = resolve_ip_address(remote_host, remote_port) + # Resolve all hosts at once for parallel DNS resolution + res = resolve_ip_address( + remote_host, remote_port, address_cache=CORE.address_cache + ) except EsphomeError as err: _LOGGER.error( "Error resolving IP address of %s. Is it connected to WiFi?", @@ -326,7 +410,7 @@ def run_ota_impl_(remote_host, remote_port, password, filename): af, socktype, _, _, sa = r _LOGGER.info("Connecting to %s port %s...", sa[0], sa[1]) sock = socket.socket(af, socktype) - sock.settimeout(10.0) + sock.settimeout(20.0) try: sock.connect(sa) except OSError as err: @@ -340,19 +424,22 @@ def run_ota_impl_(remote_host, remote_port, password, filename): perform_ota(sock, password, file_handle, filename) except OTAError as err: _LOGGER.error(str(err)) - return 1 + return 1, None finally: sock.close() - return 0 + # Successfully uploaded to sa[0] + return 0, sa[0] _LOGGER.error("Connection failed.") - return 1 + return 1, None -def run_ota(remote_host, remote_port, password, filename): +def run_ota( + remote_host: str | list[str], remote_port: int, password: str | None, filename: Path +) -> tuple[int, str | None]: try: return run_ota_impl_(remote_host, remote_port, password, filename) except OTAError as err: _LOGGER.error(err) - return 1 + return 1, None diff --git a/esphome/external_files.py b/esphome/external_files.py index 057ff52f3f..80b54ebb2f 100644 --- a/esphome/external_files.py +++ b/esphome/external_files.py @@ -2,7 +2,6 @@ from __future__ import annotations from datetime import datetime import logging -import os from pathlib import Path import requests @@ -23,11 +22,11 @@ CONTENT_DISPOSITION = "content-disposition" TEMP_DIR = "temp" -def has_remote_file_changed(url, local_file_path): - if os.path.exists(local_file_path): +def has_remote_file_changed(url: str, local_file_path: Path) -> bool: + if local_file_path.exists(): _LOGGER.debug("has_remote_file_changed: File exists at %s", local_file_path) try: - local_modification_time = os.path.getmtime(local_file_path) + local_modification_time = local_file_path.stat().st_mtime local_modification_time_str = datetime.utcfromtimestamp( local_modification_time ).strftime("%a, %d %b %Y %H:%M:%S GMT") @@ -65,9 +64,9 @@ def has_remote_file_changed(url, local_file_path): return True -def is_file_recent(file_path: str, refresh: TimePeriodSeconds) -> bool: - if os.path.exists(file_path): - creation_time = os.path.getctime(file_path) +def is_file_recent(file_path: Path, refresh: TimePeriodSeconds) -> bool: + if file_path.exists(): + creation_time = file_path.stat().st_ctime current_time = datetime.now().timestamp() return current_time - creation_time <= refresh.total_seconds return False diff --git a/esphome/git.py b/esphome/git.py index 005bcae702..4ff07ffe75 100644 --- a/esphome/git.py +++ b/esphome/git.py @@ -5,6 +5,7 @@ import hashlib import logging from pathlib import Path import re +import shutil import subprocess import urllib.parse @@ -13,13 +14,64 @@ from esphome.core import CORE, TimePeriodSeconds _LOGGER = logging.getLogger(__name__) +# Special value to indicate never refresh +NEVER_REFRESH = TimePeriodSeconds(seconds=-1) + + +class GitException(cv.Invalid): + """Base exception for git-related errors.""" + + +class GitNotInstalledError(GitException): + """Exception raised when git is not installed on the system.""" + + +class GitCommandError(GitException): + """Exception raised when a git command fails.""" + + +class GitRepositoryError(GitException): + """Exception raised when a git repository is in an invalid state.""" + + +def run_git_command(cmd: list[str], git_dir: Path | None = None) -> str: + if git_dir is not None: + _LOGGER.debug( + "Running git command with repository isolation: %s (git_dir=%s)", + " ".join(cmd), + git_dir, + ) + else: + _LOGGER.debug("Running git command: %s", " ".join(cmd)) + + # Set up environment for repository isolation if git_dir is provided + # Force git to only operate on this specific repository by setting + # GIT_DIR and GIT_WORK_TREE. This prevents git from walking up the + # directory tree to find parent repositories when the target repo's + # .git directory is corrupt. Without this, commands like 'git stash' + # could accidentally operate on parent repositories (e.g., the main + # ESPHome repo) instead of failing, causing data loss. + env: dict[str, str] | None = None + cwd: str | None = None + if git_dir is not None: + env = { + **subprocess.os.environ, + "GIT_DIR": str(Path(git_dir) / ".git"), + "GIT_WORK_TREE": str(git_dir), + } + cwd = str(git_dir) -def run_git_command(cmd, cwd=None) -> str: - _LOGGER.debug("Running git command: %s", " ".join(cmd)) try: - ret = subprocess.run(cmd, cwd=cwd, capture_output=True, check=False) + ret = subprocess.run( + cmd, + cwd=cwd, + capture_output=True, + check=False, + close_fds=False, + env=env, + ) except FileNotFoundError as err: - raise cv.Invalid( + raise GitNotInstalledError( "git is not installed but required for external_components.\n" "Please see https://git-scm.com/book/en/v2/Getting-Started-Installing-Git for installing git" ) from err @@ -28,8 +80,8 @@ def run_git_command(cmd, cwd=None) -> str: err_str = ret.stderr.decode("utf-8") lines = [x.strip() for x in err_str.splitlines()] if lines[-1].startswith("fatal:"): - raise cv.Invalid(lines[-1][len("fatal: ") :]) - raise cv.Invalid(err_str) + raise GitCommandError(lines[-1][len("fatal: ") :]) + raise GitCommandError(err_str) return ret.stdout.decode("utf-8").strip() @@ -50,6 +102,7 @@ def clone_or_update( username: str = None, password: str = None, submodules: list[str] | None = None, + _recover_broken: bool = True, ) -> tuple[Path, Callable[[], None] | None]: key = f"{url}@{ref}" @@ -70,51 +123,106 @@ def clone_or_update( # We need to fetch the PR branch first, otherwise git will complain # about missing objects _LOGGER.info("Fetching %s", ref) - run_git_command(["git", "fetch", "--", "origin", ref], str(repo_dir)) - run_git_command(["git", "reset", "--hard", "FETCH_HEAD"], str(repo_dir)) + run_git_command(["git", "fetch", "--", "origin", ref], git_dir=repo_dir) + run_git_command(["git", "reset", "--hard", "FETCH_HEAD"], git_dir=repo_dir) if submodules is not None: _LOGGER.info( - "Initialising submodules (%s) for %s", ", ".join(submodules), key + "Initializing submodules (%s) for %s", ", ".join(submodules), key ) run_git_command( - ["git", "submodule", "update", "--init"] + submodules, str(repo_dir) + ["git", "submodule", "update", "--init"] + submodules, git_dir=repo_dir ) else: # Check refresh needed + # Skip refresh if NEVER_REFRESH is specified + if refresh == NEVER_REFRESH: + _LOGGER.debug("Skipping update for %s (refresh disabled)", key) + return repo_dir, None + file_timestamp = Path(repo_dir / ".git" / "FETCH_HEAD") # On first clone, FETCH_HEAD does not exists if not file_timestamp.exists(): file_timestamp = Path(repo_dir / ".git" / "HEAD") age = datetime.now() - datetime.fromtimestamp(file_timestamp.stat().st_mtime) if refresh is None or age.total_seconds() > refresh.total_seconds: - old_sha = run_git_command(["git", "rev-parse", "HEAD"], str(repo_dir)) - _LOGGER.info("Updating %s", key) - _LOGGER.debug("Location: %s", repo_dir) - # Stash local changes (if any) - run_git_command( - ["git", "stash", "push", "--include-untracked"], str(repo_dir) - ) - # Fetch remote ref - cmd = ["git", "fetch", "--", "origin"] - if ref is not None: - cmd.append(ref) - run_git_command(cmd, str(repo_dir)) - # Hard reset to FETCH_HEAD (short-lived git ref corresponding to most recent fetch) - run_git_command(["git", "reset", "--hard", "FETCH_HEAD"], str(repo_dir)) + # Try to update the repository, recovering from broken state if needed + old_sha: str | None = None + try: + # First verify the repository is valid by checking HEAD + # Use git_dir parameter to prevent git from walking up to parent repos + old_sha = run_git_command( + ["git", "rev-parse", "HEAD"], git_dir=repo_dir + ) + + _LOGGER.info("Updating %s", key) + _LOGGER.debug("Location: %s", repo_dir) + + # Stash local changes (if any) + # Use git_dir to ensure this only affects the specific repo + run_git_command( + ["git", "stash", "push", "--include-untracked"], + git_dir=repo_dir, + ) + + # Fetch remote ref + cmd = ["git", "fetch", "--", "origin"] + if ref is not None: + cmd.append(ref) + run_git_command(cmd, git_dir=repo_dir) + + # Hard reset to FETCH_HEAD (short-lived git ref corresponding to most recent fetch) + run_git_command( + ["git", "reset", "--hard", "FETCH_HEAD"], + git_dir=repo_dir, + ) + except GitException as err: + # Repository is in a broken state or update failed + # Only attempt recovery once to prevent infinite recursion + if not _recover_broken: + _LOGGER.error( + "Repository %s recovery failed, cannot retry (already attempted once)", + key, + ) + raise + + _LOGGER.warning( + "Repository %s has issues (%s), attempting recovery", + key, + err, + ) + _LOGGER.info("Removing broken repository at %s", repo_dir) + shutil.rmtree(repo_dir) + _LOGGER.info("Successfully removed broken repository, re-cloning...") + + # Recursively call clone_or_update to re-clone + # Set _recover_broken=False to prevent infinite recursion + result = clone_or_update( + url=url, + ref=ref, + refresh=refresh, + domain=domain, + username=username, + password=password, + submodules=submodules, + _recover_broken=False, + ) + _LOGGER.info("Repository %s successfully recovered", key) + return result if submodules is not None: _LOGGER.info( "Updating submodules (%s) for %s", ", ".join(submodules), key ) run_git_command( - ["git", "submodule", "update", "--init"] + submodules, str(repo_dir) + ["git", "submodule", "update", "--init"] + submodules, + git_dir=repo_dir, ) def revert(): _LOGGER.info("Reverting changes to %s -> %s", key, old_sha) - run_git_command(["git", "reset", "--hard", old_sha], str(repo_dir)) + run_git_command(["git", "reset", "--hard", old_sha], git_dir=repo_dir) return repo_dir, revert diff --git a/esphome/helpers.py b/esphome/helpers.py index f722dc3f7c..ea6abff50a 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -1,4 +1,5 @@ -import codecs +from __future__ import annotations + from contextlib import suppress import ipaddress import logging @@ -6,11 +7,28 @@ import os from pathlib import Path import platform import re +import shutil import tempfile +from typing import TYPE_CHECKING from urllib.parse import urlparse from esphome.const import __version__ as ESPHOME_VERSION +if TYPE_CHECKING: + from esphome.address_cache import AddressCache + +# Type aliases for socket address information +AddrInfo = tuple[ + int, # family (AF_INET, AF_INET6, etc.) + int, # type (SOCK_STREAM, SOCK_DGRAM, etc.) + int, # proto (IPPROTO_TCP, etc.) + str, # canonname + tuple[str, int] | tuple[str, int, int, int], # sockaddr (IPv4 or IPv6) +] +IPv4SockAddr = tuple[str, int] # (host, port) +IPv6SockAddr = tuple[str, int, int, int] # (host, port, flowinfo, scope_id) +SockAddr = IPv4SockAddr | IPv6SockAddr + _LOGGER = logging.getLogger(__name__) IS_MACOS = platform.system() == "Darwin" @@ -114,22 +132,24 @@ def cpp_string_escape(string, encoding="utf-8"): def run_system_command(*args): import subprocess - with subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as p: + with subprocess.Popen( + args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False + ) as p: stdout, stderr = p.communicate() rc = p.returncode return rc, stdout, stderr -def mkdir_p(path): +def mkdir_p(path: Path): if not path: # Empty path - means create current dir return try: - os.makedirs(path) + path.mkdir(parents=True, exist_ok=True) except OSError as err: import errno - if err.errno == errno.EEXIST and os.path.isdir(path): + if err.errno == errno.EEXIST and path.is_dir(): pass else: from esphome.core import EsphomeError @@ -145,32 +165,7 @@ def is_ip_address(host): return False -def _resolve_with_zeroconf(host): - from esphome.core import EsphomeError - from esphome.zeroconf import EsphomeZeroconf - - try: - zc = EsphomeZeroconf() - except Exception as err: - raise EsphomeError( - "Cannot start mDNS sockets, is this a docker container without " - "host network mode?" - ) from err - try: - info = zc.resolve_host(f"{host}.") - except Exception as err: - raise EsphomeError(f"Error resolving mDNS hostname: {err}") from err - finally: - zc.close() - if info is None: - raise EsphomeError( - "Error resolving address with mDNS: Did not respond. " - "Maybe the device is offline." - ) - return info - - -def addr_preference_(res): +def addr_preference_(res: AddrInfo) -> int: # Trivial alternative to RFC6724 sorting. Put sane IPv6 first, then # Legacy IP, then IPv6 link-local addresses without an actual link. sa = res[4] @@ -182,10 +177,25 @@ def addr_preference_(res): return 1 -def resolve_ip_address(host, port): +def _add_ip_addresses_to_addrinfo( + addresses: list[str], port: int, res: list[AddrInfo] +) -> None: + """Helper to add IP addresses to addrinfo results with error handling.""" import socket - from esphome.core import EsphomeError + for addr in addresses: + try: + res += socket.getaddrinfo( + addr, port, proto=socket.IPPROTO_TCP, flags=socket.AI_NUMERICHOST + ) + except OSError: + _LOGGER.debug("Failed to parse IP address '%s'", addr) + + +def resolve_ip_address( + host: str | list[str], port: int, address_cache: AddressCache | None = None +) -> list[AddrInfo]: + import socket # There are five cases here. The host argument could be one of: # • a *list* of IP addresses discovered by MQTT, @@ -193,55 +203,84 @@ def resolve_ip_address(host, port): # • a .local hostname to be resolved by mDNS, # • a normal hostname to be resolved in DNS, or # • A URL from which we should extract the hostname. - # - # In each of the first three cases, we end up with IP addresses in - # string form which need to be converted to a 5-tuple to be used - # for the socket connection attempt. The easiest way to construct - # those is to pass the IP address string to getaddrinfo(). Which, - # coincidentally, is how we do hostname lookups in the other cases - # too. So first build a list which contains either IP addresses or - # a single hostname, then call getaddrinfo() on each element of - # that list. - errs = [] + hosts: list[str] if isinstance(host, list): - addr_list = host - elif is_ip_address(host): - addr_list = [host] + hosts = host else: - url = urlparse(host) - if url.scheme != "": - host = url.hostname + if not is_ip_address(host): + url = urlparse(host) + if url.scheme != "": + host = url.hostname + hosts = [host] - addr_list = [] - if host.endswith(".local"): - try: - _LOGGER.info("Resolving IP address of %s in mDNS", host) - addr_list = _resolve_with_zeroconf(host) - except EsphomeError as err: - errs.append(str(err)) + res: list[AddrInfo] = [] - # If not mDNS, or if mDNS failed, use normal DNS - if not addr_list: - addr_list = [host] + # Fast path: if all hosts are already IP addresses + if all(is_ip_address(h) for h in hosts): + _add_ip_addresses_to_addrinfo(hosts, port, res) + # Sort by preference + res.sort(key=addr_preference_) + return res - # Now we have a list containing either IP addresses or a hostname - res = [] - for addr in addr_list: - if not is_ip_address(addr): - _LOGGER.info("Resolving IP address of %s", host) + # Process hosts + + uncached_hosts: list[str] = [] + + for h in hosts: + if is_ip_address(h): + _add_ip_addresses_to_addrinfo([h], port, res) + elif address_cache and (cached := address_cache.get_addresses(h)): + _add_ip_addresses_to_addrinfo(cached, port, res) + else: + # Not cached, need to resolve + if address_cache and address_cache.has_cache(): + _LOGGER.info("Host %s not in cache, will need to resolve", h) + uncached_hosts.append(h) + + # If we have uncached hosts (only non-IP hostnames), resolve them + if uncached_hosts: + from aioesphomeapi.host_resolver import AddrInfo as AioAddrInfo + + from esphome.core import EsphomeError + from esphome.resolver import AsyncResolver + + resolver = AsyncResolver(uncached_hosts, port) + addr_infos: list[AioAddrInfo] = [] try: - r = socket.getaddrinfo(addr, port, proto=socket.IPPROTO_TCP) - except OSError as err: - errs.append(str(err)) - raise EsphomeError( - f"Error resolving IP address: {', '.join(errs)}" - ) from err + addr_infos = resolver.resolve() + except EsphomeError as err: + if not res: + # No pre-resolved addresses available, DNS resolution is fatal + raise + _LOGGER.info("%s (using %d already resolved IP addresses)", err, len(res)) - res = res + r + # Convert aioesphomeapi AddrInfo to our format + for addr_info in addr_infos: + sockaddr = addr_info.sockaddr + if addr_info.family == socket.AF_INET6: + # IPv6 + sockaddr_tuple = ( + sockaddr.address, + sockaddr.port, + sockaddr.flowinfo, + sockaddr.scope_id, + ) + else: + # IPv4 + sockaddr_tuple = (sockaddr.address, sockaddr.port) - # Zeroconf tends to give us link-local IPv6 addresses without specifying - # the link. Put those last in the list to be attempted. + res.append( + ( + addr_info.family, + addr_info.type, + addr_info.proto, + "", # canonname + sockaddr_tuple, + ) + ) + + # Sort by preference res.sort(key=addr_preference_) return res @@ -260,23 +299,8 @@ def sort_ip_addresses(address_list: list[str]) -> list[str]: # First "resolve" all the IP addresses to getaddrinfo() tuples of the form # (family, type, proto, canonname, sockaddr) - res: list[ - tuple[ - int, - int, - int, - str | None, - tuple[str, int] | tuple[str, int, int, int], - ] - ] = [] - for addr in address_list: - # This should always work as these are supposed to be IP addresses - try: - res += socket.getaddrinfo( - addr, 0, proto=socket.IPPROTO_TCP, flags=socket.AI_NUMERICHOST - ) - except OSError: - _LOGGER.info("Failed to parse IP address '%s'", addr) + res: list[AddrInfo] = [] + _add_ip_addresses_to_addrinfo(address_list, 0, res) # Now use that information to sort them. res.sort(key=addr_preference_) @@ -308,16 +332,15 @@ def is_ha_addon(): return get_bool_env("ESPHOME_IS_HA_ADDON") -def walk_files(path): +def walk_files(path: Path): for root, _, files in os.walk(path): for name in files: - yield os.path.join(root, name) + yield Path(root) / name -def read_file(path): +def read_file(path: Path) -> str: try: - with codecs.open(path, "r", encoding="utf-8") as f_handle: - return f_handle.read() + return path.read_text(encoding="utf-8") except OSError as err: from esphome.core import EsphomeError @@ -328,13 +351,15 @@ def read_file(path): raise EsphomeError(f"Error reading file {path}: {err}") from err -def _write_file(path: Path | str, text: str | bytes): +def _write_file( + path: Path, + text: str | bytes, + private: bool = False, +) -> None: """Atomically writes `text` to the given path. Automatically creates all parent directories. """ - if not isinstance(path, Path): - path = Path(path) data = text if isinstance(text, str): data = text.encode() @@ -342,42 +367,54 @@ def _write_file(path: Path | str, text: str | bytes): directory = path.parent directory.mkdir(exist_ok=True, parents=True) - tmp_path = None + tmp_filename: Path | None = None + missing_fchmod = False try: + # Modern versions of Python tempfile create this file with mode 0o600 with tempfile.NamedTemporaryFile( mode="wb", dir=directory, delete=False ) as f_handle: - tmp_path = f_handle.name f_handle.write(data) - # Newer tempfile implementations create the file with mode 0o600 - os.chmod(tmp_path, 0o644) - # If destination exists, will be overwritten - os.replace(tmp_path, path) + tmp_filename = Path(f_handle.name) + + if not private: + try: + os.fchmod(f_handle.fileno(), 0o644) + except AttributeError: + # os.fchmod is not available on Windows + missing_fchmod = True + shutil.move(tmp_filename, path) + if missing_fchmod: + path.chmod(0o644) finally: - if tmp_path is not None and os.path.exists(tmp_path): + if tmp_filename and tmp_filename.exists(): try: - os.remove(tmp_path) + tmp_filename.unlink() except OSError as err: - _LOGGER.error("Write file cleanup failed: %s", err) + # If we are cleaning up then something else went wrong, so + # we should suppress likely follow-on errors in the cleanup + _LOGGER.error( + "File replacement cleanup failed for %s while saving %s: %s", + tmp_filename, + path, + err, + ) -def write_file(path: Path | str, text: str): +def write_file(path: Path, text: str | bytes, private: bool = False) -> None: try: - _write_file(path, text) + _write_file(path, text, private=private) except OSError as err: from esphome.core import EsphomeError raise EsphomeError(f"Could not write file at {path}") from err -def write_file_if_changed(path: Path | str, text: str) -> bool: +def write_file_if_changed(path: Path, text: str) -> bool: """Write text to the given path, but not if the contents match already. Returns true if the file was changed. """ - if not isinstance(path, Path): - path = Path(path) - src_content = None if path.is_file(): src_content = read_file(path) @@ -387,12 +424,10 @@ def write_file_if_changed(path: Path | str, text: str) -> bool: return True -def copy_file_if_changed(src: os.PathLike, dst: os.PathLike) -> None: - import shutil - +def copy_file_if_changed(src: Path, dst: Path) -> None: if file_compare(src, dst): return - mkdir_p(os.path.dirname(dst)) + dst.parent.mkdir(parents=True, exist_ok=True) try: shutil.copyfile(src, dst) except OSError as err: @@ -417,12 +452,12 @@ def list_starts_with(list_, sub): return len(sub) <= len(list_) and all(list_[i] == x for i, x in enumerate(sub)) -def file_compare(path1: os.PathLike, path2: os.PathLike) -> bool: +def file_compare(path1: Path, path2: Path) -> bool: """Return True if the files path1 and path2 have the same contents.""" import stat try: - stat1, stat2 = os.stat(path1), os.stat(path2) + stat1, stat2 = path1.stat(), path2.stat() except OSError: # File doesn't exist or another error -> not equal return False @@ -439,7 +474,7 @@ def file_compare(path1: os.PathLike, path2: os.PathLike) -> bool: bufsize = 8 * 1024 # Read files in blocks until a mismatch is found - with open(path1, "rb") as fh1, open(path2, "rb") as fh2: + with path1.open("rb") as fh1, path2.open("rb") as fh2: while True: blob1, blob2 = fh1.read(bufsize), fh2.read(bufsize) if blob1 != blob2: diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 419a9797e3..fcb3a4f438 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -2,20 +2,28 @@ dependencies: espressif/esp-tflite-micro: version: 1.3.3~1 espressif/esp32-camera: - version: 2.1.0 + version: 2.1.1 espressif/mdns: version: 1.8.2 espressif/esp_wifi_remote: - version: 0.10.2 + version: 1.1.5 rules: - if: "target in [esp32h2, esp32p4]" espressif/eppp_link: - version: 0.2.0 + version: 1.1.3 rules: - if: "target in [esp32h2, esp32p4]" espressif/esp_hosted: - version: 2.0.11 + version: 2.6.1 rules: - if: "target in [esp32h2, esp32p4]" zorxx/multipart-parser: version: 1.0.1 + espressif/lan867x: + version: "2.0.0" + rules: + - if: "target in [esp32, esp32p4]" + espressif/esp_tinyusb: + version: "1.7.6~1" + rules: + - if: "target in [esp32s2, esp32s3, esp32p4]" diff --git a/esphome/loader.py b/esphome/loader.py index 7b2472521a..387443c032 100644 --- a/esphome/loader.py +++ b/esphome/loader.py @@ -82,11 +82,10 @@ class ComponentManifest: return getattr(self.module, "CONFLICTS_WITH", []) @property - def auto_load(self) -> list[str]: - al = getattr(self.module, "AUTO_LOAD", []) - if callable(al): - return al() - return al + def auto_load( + self, + ) -> list[str] | Callable[[], list[str]] | Callable[[ConfigType], list[str]]: + return getattr(self.module, "AUTO_LOAD", []) @property def codeowners(self) -> list[str]: @@ -192,7 +191,7 @@ def install_custom_components_meta_finder(): install_meta_finder(custom_components_dir) -def _lookup_module(domain, exception): +def _lookup_module(domain: str, exception: bool) -> ComponentManifest | None: if domain in _COMPONENT_CACHE: return _COMPONENT_CACHE[domain] @@ -219,16 +218,16 @@ def _lookup_module(domain, exception): return manif -def get_component(domain, exception=False): +def get_component(domain: str, exception: bool = False) -> ComponentManifest | None: assert "." not in domain return _lookup_module(domain, exception) -def get_platform(domain, platform): +def get_platform(domain: str, platform: str) -> ComponentManifest | None: full = f"{platform}.{domain}" return _lookup_module(full, False) -_COMPONENT_CACHE = {} +_COMPONENT_CACHE: dict[str, ComponentManifest] = {} CORE_COMPONENTS_PATH = (Path(__file__).parent / "components").resolve() _COMPONENT_CACHE["esphome"] = ComponentManifest(esphome.core.config) diff --git a/esphome/mqtt.py b/esphome/mqtt.py index f1c631697a..0d50edbc2c 100644 --- a/esphome/mqtt.py +++ b/esphome/mqtt.py @@ -30,6 +30,7 @@ from esphome.const import ( from esphome.core import CORE, EsphomeError from esphome.helpers import get_int_env, get_str_env from esphome.log import AnsiFore, color +from esphome.types import ConfigType from esphome.util import safe_print _LOGGER = logging.getLogger(__name__) @@ -120,7 +121,7 @@ def prepare( cert_file.flush() key_file.write(config[CONF_MQTT].get(CONF_CLIENT_CERTIFICATE_KEY)) key_file.flush() - context.load_cert_chain(cert_file, key_file) + context.load_cert_chain(cert_file.name, key_file.name) client.tls_set_context(context) try: @@ -154,8 +155,12 @@ def show_discover(config, username=None, password=None, client_id=None): def get_esphome_device_ip( - config, username=None, password=None, client_id=None, timeout=25 -): + config: ConfigType, + username: str | None = None, + password: str | None = None, + client_id: str | None = None, + timeout: int | float = 25, +) -> list[str]: if CONF_MQTT not in config: raise EsphomeError( "Cannot discover IP via MQTT as the config does not include the mqtt: " @@ -166,6 +171,10 @@ def get_esphome_device_ip( "Cannot discover IP via MQTT as the config does not include the device name: " "component" ) + if not config[CONF_MQTT].get(CONF_BROKER): + raise EsphomeError( + "Cannot discover IP via MQTT as the broker is not configured" + ) dev_name = config[CONF_ESPHOME][CONF_NAME] dev_ip = None diff --git a/esphome/pins.py b/esphome/pins.py index 4f9b4859a1..601c05880a 100644 --- a/esphome/pins.py +++ b/esphome/pins.py @@ -118,11 +118,11 @@ class PinRegistry(dict): parent_config = fconf.get_config_for_path(parent_path) final_val_fun(pin_config, parent_config) allow_others = pin_config.get(CONF_ALLOW_OTHER_USES, False) - if count != 1 and not allow_others: + if count != 1 and not allow_others and not CORE.testing_mode: raise cv.Invalid( f"Pin {pin_config[CONF_NUMBER]} is used in multiple places" ) - if count == 1 and allow_others: + if count == 1 and allow_others and not CORE.testing_mode: raise cv.Invalid( f"Pin {pin_config[CONF_NUMBER]} incorrectly sets {CONF_ALLOW_OTHER_USES}: true" ) diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index 21124fc859..d59523a74a 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -5,6 +5,7 @@ import os from pathlib import Path import re import subprocess +from typing import Any from esphome.const import CONF_COMPILE_PROCESS_LIMIT, CONF_ESPHOME, KEY_CORE from esphome.core import CORE, EsphomeError @@ -18,28 +19,59 @@ def patch_structhash(): # removed/added. This might have unintended consequences, but this improves compile # times greatly when adding/removing components and a simple clean build solves # all issues - from os import makedirs - from os.path import getmtime, isdir, join - from platformio.run import cli, helpers def patched_clean_build_dir(build_dir, *args): from platformio import fs from platformio.project.helpers import get_project_dir - platformio_ini = join(get_project_dir(), "platformio.ini") + platformio_ini = Path(get_project_dir()) / "platformio.ini" + + build_dir = Path(build_dir) # if project's config is modified - if isdir(build_dir) and getmtime(platformio_ini) > getmtime(build_dir): + if ( + build_dir.is_dir() + and platformio_ini.stat().st_mtime > build_dir.stat().st_mtime + ): fs.rmtree(build_dir) - if not isdir(build_dir): - makedirs(build_dir) + if not build_dir.is_dir(): + build_dir.mkdir(parents=True) helpers.clean_build_dir = patched_clean_build_dir cli.clean_build_dir = patched_clean_build_dir +def patch_file_downloader(): + """Patch PlatformIO's FileDownloader to retry on PackageException errors.""" + from platformio.package.download import FileDownloader + from platformio.package.exception import PackageException + + original_init = FileDownloader.__init__ + + def patched_init(self, *args: Any, **kwargs: Any) -> None: + max_retries = 3 + + for attempt in range(max_retries): + try: + return original_init(self, *args, **kwargs) + except PackageException as e: + if attempt < max_retries - 1: + _LOGGER.warning( + "Package download failed: %s. Retrying... (attempt %d/%d)", + str(e), + attempt + 1, + max_retries, + ) + else: + # Final attempt - re-raise + raise + return None + + FileDownloader.__init__ = patched_init + + IGNORE_LIB_WARNINGS = f"(?:{'|'.join(['Hash', 'Update'])})" FILTER_PLATFORMIO_LINES = [ r"Verbose mode can be enabled via `-v, --verbose` option.*", @@ -70,14 +102,19 @@ FILTER_PLATFORMIO_LINES = [ r" - tool-esptool.* \(.*\)", r" - toolchain-.* \(.*\)", r"Creating BIN file .*", + r"Warning! Could not find file \".*.crt\"", + r"Warning! Arduino framework as an ESP-IDF component doesn't handle the `variant` field! The default `esp32` variant will be used.", + r"Warning: DEPRECATED: 'esptool.py' is deprecated. Please use 'esptool' instead. The '.py' suffix will be removed in a future major release.", + r"Warning: esp-idf-size exited with code 2", + r"esp_idf_size: error: unrecognized arguments: --ng", ] def run_platformio_cli(*args, **kwargs) -> str | int: os.environ["PLATFORMIO_FORCE_COLOR"] = "true" - os.environ["PLATFORMIO_BUILD_DIR"] = os.path.abspath(CORE.relative_pioenvs_path()) + os.environ["PLATFORMIO_BUILD_DIR"] = str(CORE.relative_pioenvs_path().absolute()) os.environ.setdefault( - "PLATFORMIO_LIBDEPS_DIR", os.path.abspath(CORE.relative_piolibdeps_path()) + "PLATFORMIO_LIBDEPS_DIR", str(CORE.relative_piolibdeps_path().absolute()) ) # Suppress Python syntax warnings from third-party scripts during compilation os.environ.setdefault("PYTHONWARNINGS", "ignore::SyntaxWarning") @@ -92,11 +129,12 @@ def run_platformio_cli(*args, **kwargs) -> str | int: import platformio.__main__ patch_structhash() + patch_file_downloader() return run_external_command(platformio.__main__.main, *cmd, **kwargs) def run_platformio_cli_run(config, verbose, *args, **kwargs) -> str | int: - command = ["run", "-d", CORE.build_path] + command = ["run", "-d", str(CORE.build_path)] if verbose: command += ["-v"] command += list(args) @@ -128,8 +166,8 @@ def _run_idedata(config): def _load_idedata(config): - platformio_ini = Path(CORE.relative_build_path("platformio.ini")) - temp_idedata = Path(CORE.relative_internal_path("idedata", f"{CORE.name}.json")) + platformio_ini = CORE.relative_build_path("platformio.ini") + temp_idedata = CORE.relative_internal_path("idedata", f"{CORE.name}.json") changed = False if ( @@ -211,7 +249,7 @@ def _decode_pc(config, addr): return command = [idedata.addr2line_path, "-pfiaC", "-e", idedata.firmware_elf_path, addr] try: - translation = subprocess.check_output(command).decode().strip() + translation = subprocess.check_output(command, close_fds=False).decode().strip() except Exception: # pylint: disable=broad-except _LOGGER.debug("Caught exception for command %s", command, exc_info=1) return @@ -299,7 +337,7 @@ def process_stacktrace(config, line, backtrace_state): @dataclass class FlashImage: - path: str + path: Path offset: str @@ -308,17 +346,17 @@ class IDEData: self.raw = raw @property - def firmware_elf_path(self): - return self.raw["prog_path"] + def firmware_elf_path(self) -> Path: + return Path(self.raw["prog_path"]) @property - def firmware_bin_path(self) -> str: - return str(Path(self.firmware_elf_path).with_suffix(".bin")) + def firmware_bin_path(self) -> Path: + return self.firmware_elf_path.with_suffix(".bin") @property def extra_flash_images(self) -> list[FlashImage]: return [ - FlashImage(path=entry["path"], offset=entry["offset"]) + FlashImage(path=Path(entry["path"]), offset=entry["offset"]) for entry in self.raw["extra"]["flash_images"] ] @@ -336,3 +374,23 @@ class IDEData: return f"{self.cc_path[:-7]}addr2line.exe" return f"{self.cc_path[:-3]}addr2line" + + @property + def objdump_path(self) -> str: + # replace gcc at end with objdump + path = self.cc_path + return ( + f"{path[:-7]}objdump.exe" + if path.endswith(".exe") + else f"{path[:-3]}objdump" + ) + + @property + def readelf_path(self) -> str: + # replace gcc at end with readelf + path = self.cc_path + return ( + f"{path[:-7]}readelf.exe" + if path.endswith(".exe") + else f"{path[:-3]}readelf" + ) diff --git a/esphome/resolver.py b/esphome/resolver.py new file mode 100644 index 0000000000..99482aa20e --- /dev/null +++ b/esphome/resolver.py @@ -0,0 +1,67 @@ +"""DNS resolver for ESPHome using aioesphomeapi.""" + +from __future__ import annotations + +import asyncio +import threading + +from aioesphomeapi.core import ResolveAPIError, ResolveTimeoutAPIError +import aioesphomeapi.host_resolver as hr + +from esphome.core import EsphomeError + +RESOLVE_TIMEOUT = 10.0 # seconds + + +class AsyncResolver(threading.Thread): + """Resolver using aioesphomeapi that runs in a thread for faster results. + + This resolver uses aioesphomeapi's async_resolve_host to handle DNS resolution, + including proper .local domain fallback. Running in a thread allows us to get + the result immediately without waiting for asyncio.run() to complete its + cleanup cycle, which can take significant time. + """ + + def __init__(self, hosts: list[str], port: int) -> None: + """Initialize the resolver.""" + super().__init__(daemon=True) + self.hosts = hosts + self.port = port + self.result: list[hr.AddrInfo] | None = None + self.exception: Exception | None = None + self.event = threading.Event() + + async def _resolve(self) -> None: + """Resolve hostnames to IP addresses.""" + try: + self.result = await hr.async_resolve_host( + self.hosts, self.port, timeout=RESOLVE_TIMEOUT + ) + except Exception as e: # pylint: disable=broad-except + # We need to catch all exceptions to ensure the event is set + # Otherwise the thread could hang forever + self.exception = e + finally: + self.event.set() + + def run(self) -> None: + """Run the DNS resolution.""" + asyncio.run(self._resolve()) + + def resolve(self) -> list[hr.AddrInfo]: + """Start the thread and wait for the result.""" + self.start() + + if not self.event.wait( + timeout=RESOLVE_TIMEOUT + 1.0 + ): # Give it 1 second more than the resolver timeout + raise EsphomeError("Timeout resolving IP address") + + if exc := self.exception: + if isinstance(exc, ResolveTimeoutAPIError): + raise EsphomeError(f"Timeout resolving IP address: {exc}") from exc + if isinstance(exc, ResolveAPIError): + raise EsphomeError(f"Error resolving IP address: {exc}") from exc + raise exc + + return self.result diff --git a/esphome/storage_json.py b/esphome/storage_json.py index b69dc2dd3f..d5423ab1c7 100644 --- a/esphome/storage_json.py +++ b/esphome/storage_json.py @@ -1,11 +1,11 @@ from __future__ import annotations import binascii -import codecs from datetime import datetime import json import logging import os +from pathlib import Path from esphome import const from esphome.const import CONF_DISABLED, CONF_MDNS @@ -16,30 +16,35 @@ from esphome.types import CoreType _LOGGER = logging.getLogger(__name__) -def storage_path() -> str: - return os.path.join(CORE.data_dir, "storage", f"{CORE.config_filename}.json") +def storage_path() -> Path: + return CORE.data_dir / "storage" / f"{CORE.config_filename}.json" -def ext_storage_path(config_filename: str) -> str: - return os.path.join(CORE.data_dir, "storage", f"{config_filename}.json") +def ext_storage_path(config_filename: str) -> Path: + return CORE.data_dir / "storage" / f"{config_filename}.json" -def esphome_storage_path() -> str: - return os.path.join(CORE.data_dir, "esphome.json") +def esphome_storage_path() -> Path: + return CORE.data_dir / "esphome.json" -def ignored_devices_storage_path() -> str: - return os.path.join(CORE.data_dir, "ignored-devices.json") +def ignored_devices_storage_path() -> Path: + return CORE.data_dir / "ignored-devices.json" -def trash_storage_path() -> str: +def trash_storage_path() -> Path: return CORE.relative_config_path("trash") -def archive_storage_path() -> str: +def archive_storage_path() -> Path: return CORE.relative_config_path("archive") +def _to_path_if_not_none(value: str | None) -> Path | None: + """Convert a string to Path if it's not None.""" + return Path(value) if value is not None else None + + class StorageJSON: def __init__( self, @@ -52,8 +57,8 @@ class StorageJSON: address: str, web_port: int | None, target_platform: str, - build_path: str | None, - firmware_bin_path: str | None, + build_path: Path | None, + firmware_bin_path: Path | None, loaded_integrations: set[str], loaded_platforms: set[str], no_mdns: bool, @@ -107,8 +112,8 @@ class StorageJSON: "address": self.address, "web_port": self.web_port, "esp_platform": self.target_platform, - "build_path": self.build_path, - "firmware_bin_path": self.firmware_bin_path, + "build_path": str(self.build_path), + "firmware_bin_path": str(self.firmware_bin_path), "loaded_integrations": sorted(self.loaded_integrations), "loaded_platforms": sorted(self.loaded_platforms), "no_mdns": self.no_mdns, @@ -176,8 +181,8 @@ class StorageJSON: ) @staticmethod - def _load_impl(path: str) -> StorageJSON | None: - with codecs.open(path, "r", encoding="utf-8") as f_handle: + def _load_impl(path: Path) -> StorageJSON | None: + with path.open("r", encoding="utf-8") as f_handle: storage = json.load(f_handle) storage_version = storage["storage_version"] name = storage.get("name") @@ -190,8 +195,8 @@ class StorageJSON: address = storage.get("address") web_port = storage.get("web_port") esp_platform = storage.get("esp_platform") - build_path = storage.get("build_path") - firmware_bin_path = storage.get("firmware_bin_path") + build_path = _to_path_if_not_none(storage.get("build_path")) + firmware_bin_path = _to_path_if_not_none(storage.get("firmware_bin_path")) loaded_integrations = set(storage.get("loaded_integrations", [])) loaded_platforms = set(storage.get("loaded_platforms", [])) no_mdns = storage.get("no_mdns", False) @@ -217,7 +222,7 @@ class StorageJSON: ) @staticmethod - def load(path: str) -> StorageJSON | None: + def load(path: Path) -> StorageJSON | None: try: return StorageJSON._load_impl(path) except Exception: # pylint: disable=broad-except @@ -268,7 +273,7 @@ class EsphomeStorageJSON: @staticmethod def _load_impl(path: str) -> EsphomeStorageJSON | None: - with codecs.open(path, "r", encoding="utf-8") as f_handle: + with Path(path).open("r", encoding="utf-8") as f_handle: storage = json.load(f_handle) storage_version = storage["storage_version"] cookie_secret = storage.get("cookie_secret") diff --git a/esphome/types.py b/esphome/types.py index f68f503993..c474d0d076 100644 --- a/esphome/types.py +++ b/esphome/types.py @@ -1,6 +1,10 @@ """This helper module tracks commonly used types in the esphome python codebase.""" -from esphome.core import ID, EsphomeCore, Lambda +import abc +from collections.abc import Sequence +from typing import Any, TypedDict + +from esphome.core import ID, EsphomeCore, Lambda, TimePeriod ConfigFragmentType = ( str @@ -16,3 +20,39 @@ ConfigFragmentType = ( ConfigType = dict[str, ConfigFragmentType] CoreType = EsphomeCore ConfigPathType = str | int + + +class Expression(abc.ABC): + __slots__ = () + + @abc.abstractmethod + def __str__(self): + """ + Convert expression into C++ code + """ + + +SafeExpType = ( + Expression + | bool + | str + | int + | float + | TimePeriod + | type[bool] + | type[int] + | type[float] + | Sequence[Any] +) + +TemplateArgsType = list[tuple[SafeExpType, str]] + + +class EntityMetadata(TypedDict): + """Metadata stored for each entity to help with duplicate detection.""" + + name: str + device_id: str + platform: str + entity_id: str + component: str diff --git a/esphome/util.py b/esphome/util.py index 9aa0f6b9d8..d41800dc20 100644 --- a/esphome/util.py +++ b/esphome/util.py @@ -1,19 +1,30 @@ import collections +from collections.abc import Callable import io import logging -import os from pathlib import Path import re import subprocess import sys +from typing import TYPE_CHECKING, Any from esphome import const _LOGGER = logging.getLogger(__name__) +if TYPE_CHECKING: + from esphome.config_validation import Schema + from esphome.cpp_generator import MockObjClass + class RegistryEntry: - def __init__(self, name, fun, type_id, schema): + def __init__( + self, + name: str, + fun: Callable[..., Any], + type_id: "MockObjClass", + schema: "Schema", + ): self.name = name self.fun = fun self.type_id = type_id @@ -38,8 +49,8 @@ class Registry(dict[str, RegistryEntry]): self.base_schema = base_schema or {} self.type_id_key = type_id_key - def register(self, name, type_id, schema): - def decorator(fun): + def register(self, name: str, type_id: "MockObjClass", schema: "Schema"): + def decorator(fun: Callable[..., Any]): self[name] = RegistryEntry(name, fun, type_id, schema) return fun @@ -47,8 +58,8 @@ class Registry(dict[str, RegistryEntry]): class SimpleRegistry(dict): - def register(self, name, data): - def decorator(fun): + def register(self, name: str, data: Any): + def decorator(fun: Callable[..., Any]): self[name] = (fun, data) return fun @@ -85,7 +96,10 @@ def safe_input(prompt=""): return input() -def shlex_quote(s): +def shlex_quote(s: str | Path) -> str: + # Convert Path objects to strings + if isinstance(s, Path): + s = str(s) if not s: return "''" if re.search(r"[^\w@%+=:,./-]", s) is None: @@ -110,7 +124,7 @@ class RedirectText: def __getattr__(self, item): return getattr(self._out, item) - def _write_color_replace(self, s): + def _write_color_replace(self, s: str | bytes) -> None: from esphome.core import CORE if CORE.dashboard: @@ -121,7 +135,7 @@ class RedirectText: s = s.replace("\033", "\\033") self._out.write(s) - def write(self, s): + def write(self, s: str | bytes) -> int: # s is usually a str already (self._out is of type TextIOWrapper) # However, s is sometimes also a bytes object in python3. Let's make sure it's a # str @@ -223,7 +237,7 @@ def run_external_command( return retval -def run_external_process(*cmd, **kwargs): +def run_external_process(*cmd: str, **kwargs: Any) -> int | str: full_cmd = " ".join(shlex_quote(x) for x in cmd) _LOGGER.debug("Running: %s", full_cmd) filter_lines = kwargs.get("filter_lines") @@ -238,7 +252,12 @@ def run_external_process(*cmd, **kwargs): try: proc = subprocess.run( - cmd, stdout=sub_stdout, stderr=sub_stderr, encoding="utf-8", check=False + cmd, + stdout=sub_stdout, + stderr=sub_stderr, + encoding="utf-8", + check=False, + close_fds=False, ) return proc.stdout if capture_stdout else proc.returncode except KeyboardInterrupt: # pylint: disable=try-except-raise @@ -266,22 +285,28 @@ class OrderedDict(collections.OrderedDict): return dict(self).__repr__() -def list_yaml_files(folders): - files = filter_yaml_files( - [os.path.join(folder, p) for folder in folders for p in os.listdir(folder)] - ) - files.sort() - return files +def list_yaml_files(configs: list[str | Path]) -> list[Path]: + files: list[Path] = [] + for config in configs: + config = Path(config) + if not config.exists(): + raise FileNotFoundError(f"Config path '{config}' does not exist!") + if config.is_file(): + files.append(config) + else: + files.extend(config.glob("*")) + files = filter_yaml_files(files) + return sorted(files) -def filter_yaml_files(files): +def filter_yaml_files(files: list[Path]) -> list[Path]: return [ f for f in files if ( - os.path.splitext(f)[1] in (".yaml", ".yml") - and os.path.basename(f) not in ("secrets.yaml", "secrets.yml") - and not os.path.basename(f).startswith(".") + f.suffix in (".yaml", ".yml") + and f.name not in ("secrets.yaml", "secrets.yml") + and not f.name.startswith(".") ) ] diff --git a/esphome/vscode.py b/esphome/vscode.py index f5e2a20b97..53bb339a8e 100644 --- a/esphome/vscode.py +++ b/esphome/vscode.py @@ -2,7 +2,7 @@ from __future__ import annotations from io import StringIO import json -import os +from pathlib import Path from typing import Any from esphome.config import Config, _format_vol_invalid, validate_config @@ -67,24 +67,24 @@ def _read_file_content_from_json_on_stdin() -> str: return data["content"] -def _print_file_read_event(path: str) -> None: +def _print_file_read_event(path: Path) -> None: """Print a file read event.""" print( json.dumps( { "type": "read_file", - "path": path, + "path": str(path), } ) ) -def _request_and_get_stream_on_stdin(fname: str) -> StringIO: +def _request_and_get_stream_on_stdin(fname: Path) -> StringIO: _print_file_read_event(fname) return StringIO(_read_file_content_from_json_on_stdin()) -def _vscode_loader(fname: str) -> dict[str, Any]: +def _vscode_loader(fname: Path) -> dict[str, Any]: raw_yaml_stream = _request_and_get_stream_on_stdin(fname) # it is required to set the name on StringIO so document on start_mark # is set properly. Otherwise it is initialized with "" @@ -92,7 +92,7 @@ def _vscode_loader(fname: str) -> dict[str, Any]: return parse_yaml(fname, raw_yaml_stream, _vscode_loader) -def _ace_loader(fname: str) -> dict[str, Any]: +def _ace_loader(fname: Path) -> dict[str, Any]: raw_yaml_stream = _request_and_get_stream_on_stdin(fname) return parse_yaml(fname, raw_yaml_stream) @@ -120,10 +120,10 @@ def read_config(args): return CORE.vscode = True if args.ace: # Running from ESPHome Compiler dashboard, not vscode - CORE.config_path = os.path.join(args.configuration, data["file"]) + CORE.config_path = Path(args.configuration) / data["file"] loader = _ace_loader else: - CORE.config_path = data["file"] + CORE.config_path = Path(data["file"]) loader = _vscode_loader file_name = CORE.config_path diff --git a/esphome/wizard.py b/esphome/wizard.py index 8602e90222..97343eea99 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -1,6 +1,7 @@ -import os +from pathlib import Path import random import string +from typing import Literal, NotRequired, TypedDict, Unpack import unicodedata import voluptuous as vol @@ -103,11 +104,25 @@ HARDWARE_BASE_CONFIGS = { } -def sanitize_double_quotes(value): +def sanitize_double_quotes(value: str) -> str: return value.replace("\\", "\\\\").replace('"', '\\"') -def wizard_file(**kwargs): +class WizardFileKwargs(TypedDict): + """Keyword arguments for wizard_file function.""" + + name: str + platform: Literal["ESP8266", "ESP32", "RP2040", "BK72XX", "LN882X", "RTL87XX"] + board: str + ssid: NotRequired[str] + psk: NotRequired[str] + password: NotRequired[str] + ota_password: NotRequired[str] + api_encryption_key: NotRequired[str] + friendly_name: NotRequired[str] + + +def wizard_file(**kwargs: Unpack[WizardFileKwargs]) -> str: letters = string.ascii_letters + string.digits ap_name_base = kwargs["name"].replace("_", " ").title() ap_name = f"{ap_name_base} Fallback Hotspot" @@ -180,7 +195,25 @@ captive_portal: return config -def wizard_write(path, **kwargs): +class WizardWriteKwargs(TypedDict): + """Keyword arguments for wizard_write function.""" + + name: str + type: Literal["basic", "empty", "upload"] + # Required for "basic" type + board: NotRequired[str] + platform: NotRequired[str] + ssid: NotRequired[str] + psk: NotRequired[str] + password: NotRequired[str] + ota_password: NotRequired[str] + api_encryption_key: NotRequired[str] + friendly_name: NotRequired[str] + # Required for "upload" type + file_text: NotRequired[str] + + +def wizard_write(path: Path, **kwargs: Unpack[WizardWriteKwargs]) -> bool: from esphome.components.bk72xx import boards as bk72xx_boards from esphome.components.esp32 import boards as esp32_boards from esphome.components.esp8266 import boards as esp8266_boards @@ -189,34 +222,47 @@ def wizard_write(path, **kwargs): from esphome.components.rtl87xx import boards as rtl87xx_boards name = kwargs["name"] - board = kwargs["board"] + if kwargs["type"] == "empty": + file_text = "" + # Will be updated later after editing the file + hardware = "UNKNOWN" + elif kwargs["type"] == "upload": + file_text = kwargs["file_text"] + hardware = "UNKNOWN" + else: # "basic" + board = kwargs["board"] - for key in ("ssid", "psk", "password", "ota_password"): - if key in kwargs: - kwargs[key] = sanitize_double_quotes(kwargs[key]) + for key in ("ssid", "psk", "password", "ota_password"): + if key in kwargs: + kwargs[key] = sanitize_double_quotes(kwargs[key]) + if "platform" not in kwargs: + if board in esp8266_boards.BOARDS: + platform = "ESP8266" + elif board in esp32_boards.BOARDS: + platform = "ESP32" + elif board in rp2040_boards.BOARDS: + platform = "RP2040" + elif board in bk72xx_boards.BOARDS: + platform = "BK72XX" + elif board in ln882x_boards.BOARDS: + platform = "LN882X" + elif board in rtl87xx_boards.BOARDS: + platform = "RTL87XX" + else: + safe_print(color(AnsiFore.RED, f'The board "{board}" is unknown.')) + return False + kwargs["platform"] = platform + hardware = kwargs["platform"] + file_text = wizard_file(**kwargs) - if "platform" not in kwargs: - if board in esp8266_boards.BOARDS: - platform = "ESP8266" - elif board in esp32_boards.BOARDS: - platform = "ESP32" - elif board in rp2040_boards.BOARDS: - platform = "RP2040" - elif board in bk72xx_boards.BOARDS: - platform = "BK72XX" - elif board in ln882x_boards.BOARDS: - platform = "LN882X" - elif board in rtl87xx_boards.BOARDS: - platform = "RTL87XX" - else: - safe_print(color(AnsiFore.RED, f'The board "{board}" is unknown.')) - return False - kwargs["platform"] = platform - hardware = kwargs["platform"] + # Check if file already exists to prevent overwriting + if path.exists() and path.is_file(): + safe_print(color(AnsiFore.RED, f'The file "{path}" already exists.')) + return False - write_file(path, wizard_file(**kwargs)) + write_file(path, file_text) storage = StorageJSON.from_wizard(name, name, f"{name}.local", hardware) - storage_path = ext_storage_path(os.path.basename(path)) + storage_path = ext_storage_path(path.name) storage.save(storage_path) return True @@ -224,14 +270,14 @@ def wizard_write(path, **kwargs): if get_bool_env(ENV_QUICKWIZARD): - def sleep(time): + def sleep(time: float) -> None: pass else: from time import sleep -def safe_print_step(step, big): +def safe_print_step(step: int, big: str) -> None: safe_print() safe_print() safe_print(f"============= STEP {step} =============") @@ -240,14 +286,14 @@ def safe_print_step(step, big): sleep(0.25) -def default_input(text, default): +def default_input(text: str, default: str) -> str: safe_print() safe_print(f"Press ENTER for default ({default})") return safe_input(text.format(default)) or default # From https://stackoverflow.com/a/518232/8924614 -def strip_accents(value): +def strip_accents(value: str) -> str: return "".join( c for c in unicodedata.normalize("NFD", str(value)) @@ -255,7 +301,7 @@ def strip_accents(value): ) -def wizard(path): +def wizard(path: Path) -> int: from esphome.components.bk72xx import boards as bk72xx_boards from esphome.components.esp32 import boards as esp32_boards from esphome.components.esp8266 import boards as esp8266_boards @@ -263,14 +309,14 @@ def wizard(path): from esphome.components.rp2040 import boards as rp2040_boards from esphome.components.rtl87xx import boards as rtl87xx_boards - if not path.endswith(".yaml") and not path.endswith(".yml"): + if path.suffix not in (".yaml", ".yml"): safe_print( - f"Please make your configuration file {color(AnsiFore.CYAN, path)} have the extension .yaml or .yml" + f"Please make your configuration file {color(AnsiFore.CYAN, str(path))} have the extension .yaml or .yml" ) return 1 - if os.path.exists(path): + if path.exists(): safe_print( - f"Uh oh, it seems like {color(AnsiFore.CYAN, path)} already exists, please delete that file first or chose another configuration file." + f"Uh oh, it seems like {color(AnsiFore.CYAN, str(path))} already exists, please delete that file first or chose another configuration file." ) return 2 @@ -496,13 +542,14 @@ def wizard(path): ssid=ssid, psk=psk, password=password, + type="basic", ): return 1 safe_print() safe_print( color(AnsiFore.CYAN, "DONE! I've now written a new configuration file to ") - + color(AnsiFore.BOLD_CYAN, path) + + color(AnsiFore.BOLD_CYAN, str(path)) ) safe_print() safe_print("Next steps:") diff --git a/esphome/writer.py b/esphome/writer.py index b5c834722a..3124e9e12c 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -15,6 +15,8 @@ from esphome.const import ( from esphome.core import CORE, EsphomeError from esphome.helpers import ( copy_file_if_changed, + get_str_env, + is_ha_addon, read_file, walk_files, write_file_if_changed, @@ -80,13 +82,16 @@ def replace_file_content(text, pattern, repl): return content_new, count -def storage_should_clean(old: StorageJSON, new: StorageJSON) -> bool: +def storage_should_clean(old: StorageJSON | None, new: StorageJSON) -> bool: if old is None: return True if old.src_version != new.src_version: return True - return old.build_path != new.build_path + if old.build_path != new.build_path: + return True + # Check if any components have been removed + return bool(old.loaded_integrations - new.loaded_integrations) def storage_should_update_cmake_cache(old: StorageJSON, new: StorageJSON) -> bool: @@ -100,7 +105,7 @@ def storage_should_update_cmake_cache(old: StorageJSON, new: StorageJSON) -> boo return False -def update_storage_json(): +def update_storage_json() -> None: path = storage_path() old = StorageJSON.load(path) new = StorageJSON.from_esphome_core(CORE, old) @@ -108,8 +113,15 @@ def update_storage_json(): return if storage_should_clean(old, new): - _LOGGER.info("Core config, version changed, cleaning build files...") - clean_build() + if old is not None and old.loaded_integrations - new.loaded_integrations: + removed = old.loaded_integrations - new.loaded_integrations + _LOGGER.info( + "Components removed (%s), cleaning build files...", + ", ".join(sorted(removed)), + ) + else: + _LOGGER.info("Core config or version changed, cleaning build files...") + clean_build(clear_pio_cache=False) elif storage_should_update_cmake_cache(old, new): _LOGGER.info("Integrations changed, cleaning cmake cache...") clean_cmake_cache() @@ -256,7 +268,7 @@ def generate_version_h(): def write_cpp(code_s): path = CORE.relative_src_path("main.cpp") - if os.path.isfile(path): + if path.is_file(): text = read_file(path) code_format = find_begin_end( text, CPP_AUTO_GENERATE_BEGIN, CPP_AUTO_GENERATE_END @@ -282,24 +294,91 @@ def write_cpp(code_s): def clean_cmake_cache(): pioenvs = CORE.relative_pioenvs_path() - if os.path.isdir(pioenvs): - pioenvs_cmake_path = CORE.relative_pioenvs_path(CORE.name, "CMakeCache.txt") - if os.path.isfile(pioenvs_cmake_path): + if pioenvs.is_dir(): + pioenvs_cmake_path = pioenvs / CORE.name / "CMakeCache.txt" + if pioenvs_cmake_path.is_file(): _LOGGER.info("Deleting %s", pioenvs_cmake_path) - os.remove(pioenvs_cmake_path) + pioenvs_cmake_path.unlink() -def clean_build(): +def clean_build(clear_pio_cache: bool = True): import shutil + # Allow skipping cache cleaning for integration tests + if os.environ.get("ESPHOME_SKIP_CLEAN_BUILD"): + _LOGGER.warning("Skipping build cleaning (ESPHOME_SKIP_CLEAN_BUILD set)") + return + pioenvs = CORE.relative_pioenvs_path() - if os.path.isdir(pioenvs): + if pioenvs.is_dir(): _LOGGER.info("Deleting %s", pioenvs) shutil.rmtree(pioenvs) piolibdeps = CORE.relative_piolibdeps_path() - if os.path.isdir(piolibdeps): + if piolibdeps.is_dir(): _LOGGER.info("Deleting %s", piolibdeps) shutil.rmtree(piolibdeps) + dependencies_lock = CORE.relative_build_path("dependencies.lock") + if dependencies_lock.is_file(): + _LOGGER.info("Deleting %s", dependencies_lock) + dependencies_lock.unlink() + + if not clear_pio_cache: + return + + # Clean PlatformIO cache to resolve CMake compiler detection issues + # This helps when toolchain paths change or get corrupted + try: + from platformio.project.config import ProjectConfig + except ImportError: + # PlatformIO is not available, skip cache cleaning + pass + else: + config = ProjectConfig.get_instance() + cache_dir = Path(config.get("platformio", "cache_dir")) + if cache_dir.is_dir(): + _LOGGER.info("Deleting PlatformIO cache %s", cache_dir) + shutil.rmtree(cache_dir) + + +def clean_all(configuration: list[str]): + import shutil + + data_dirs = [] + for config in configuration: + item = Path(config) + if item.is_file() and item.suffix in (".yaml", ".yml"): + data_dirs.append(item.parent / ".esphome") + else: + data_dirs.append(item / ".esphome") + if is_ha_addon(): + data_dirs.append(Path("/data")) + if "ESPHOME_DATA_DIR" in os.environ: + data_dirs.append(Path(get_str_env("ESPHOME_DATA_DIR", None))) + + # Clean build dir + for dir in data_dirs: + if dir.is_dir(): + _LOGGER.info("Cleaning %s", dir) + # Don't remove storage or .json files which are needed by the dashboard + for item in dir.iterdir(): + if item.is_file() and not item.name.endswith(".json"): + item.unlink() + elif item.is_dir() and item.name != "storage": + shutil.rmtree(item) + + # Clean PlatformIO project files + try: + from platformio.project.config import ProjectConfig + except ImportError: + # PlatformIO is not available, skip cleaning + pass + else: + config = ProjectConfig.get_instance() + for pio_dir in ["cache_dir", "packages_dir", "platforms_dir", "core_dir"]: + path = Path(config.get("platformio", pio_dir)) + if path.is_dir(): + _LOGGER.info("Deleting PlatformIO %s %s", pio_dir, path) + shutil.rmtree(path) GITIGNORE_CONTENT = """# Gitignore settings for ESPHome @@ -312,6 +391,5 @@ GITIGNORE_CONTENT = """# Gitignore settings for ESPHome def write_gitignore(): path = CORE.relative_config_path(".gitignore") - if not os.path.isfile(path): - with open(file=path, mode="w", encoding="utf-8") as f: - f.write(GITIGNORE_CONTENT) + if not path.is_file(): + path.write_text(GITIGNORE_CONTENT, encoding="utf-8") diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index f26bc0502d..359b72b48f 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -1,7 +1,6 @@ from __future__ import annotations from collections.abc import Callable -import fnmatch import functools import inspect from io import BytesIO, TextIOBase, TextIOWrapper @@ -9,6 +8,7 @@ from ipaddress import _BaseAddress, _BaseNetwork import logging import math import os +from pathlib import Path from typing import Any import uuid @@ -69,7 +69,7 @@ class ESPHomeDataBase: self._content_offset = database.content_offset -class ESPForceValue: +class ESPLiteralValue: pass @@ -109,7 +109,9 @@ def _add_data_ref(fn): class ESPHomeLoaderMixin: """Loader class that keeps track of line numbers.""" - def __init__(self, name: str, yaml_loader: Callable[[str], dict[str, Any]]) -> None: + def __init__( + self, name: Path, yaml_loader: Callable[[Path], dict[str, Any]] + ) -> None: """Initialize the loader.""" self.name = name self.yaml_loader = yaml_loader @@ -254,12 +256,8 @@ class ESPHomeLoaderMixin: f"Environment variable '{node.value}' not defined", node.start_mark ) - @property - def _directory(self) -> str: - return os.path.dirname(self.name) - - def _rel_path(self, *args: str) -> str: - return os.path.join(self._directory, *args) + def _rel_path(self, *args: str) -> Path: + return self.name.parent / Path(*args) @_add_data_ref def construct_secret(self, node: yaml.Node) -> str: @@ -269,8 +267,8 @@ class ESPHomeLoaderMixin: if self.name == CORE.config_path: raise e try: - main_config_dir = os.path.dirname(CORE.config_path) - main_secret_yml = os.path.join(main_config_dir, SECRET_YAML) + main_config_dir = CORE.config_path.parent + main_secret_yml = main_config_dir / SECRET_YAML secrets = self.yaml_loader(main_secret_yml) except EsphomeError as er: raise EsphomeError(f"{e}\n{er}") from er @@ -329,7 +327,7 @@ class ESPHomeLoaderMixin: files = filter_yaml_files(_find_files(self._rel_path(node.value), "*.yaml")) mapping = OrderedDict() for fname in files: - filename = os.path.splitext(os.path.basename(fname))[0] + filename = fname.stem mapping[filename] = self.yaml_loader(fname) return mapping @@ -350,9 +348,15 @@ class ESPHomeLoaderMixin: return Lambda(str(node.value)) @_add_data_ref - def construct_force(self, node: yaml.Node) -> ESPForceValue: - obj = self.construct_scalar(node) - return add_class_to_obj(obj, ESPForceValue) + def construct_literal(self, node: yaml.Node) -> ESPLiteralValue: + obj = None + if isinstance(node, yaml.ScalarNode): + obj = self.construct_scalar(node) + elif isinstance(node, yaml.SequenceNode): + obj = self.construct_sequence(node) + elif isinstance(node, yaml.MappingNode): + obj = self.construct_mapping(node) + return add_class_to_obj(obj, ESPLiteralValue) @_add_data_ref def construct_extend(self, node: yaml.Node) -> Extend: @@ -369,8 +373,8 @@ class ESPHomeLoader(ESPHomeLoaderMixin, FastestAvailableSafeLoader): def __init__( self, stream: TextIOBase | BytesIO, - name: str, - yaml_loader: Callable[[str], dict[str, Any]], + name: Path, + yaml_loader: Callable[[Path], dict[str, Any]], ) -> None: FastestAvailableSafeLoader.__init__(self, stream) ESPHomeLoaderMixin.__init__(self, name, yaml_loader) @@ -382,8 +386,8 @@ class ESPHomePurePythonLoader(ESPHomeLoaderMixin, PurePythonLoader): def __init__( self, stream: TextIOBase | BytesIO, - name: str, - yaml_loader: Callable[[str], dict[str, Any]], + name: Path, + yaml_loader: Callable[[Path], dict[str, Any]], ) -> None: PurePythonLoader.__init__(self, stream) ESPHomeLoaderMixin.__init__(self, name, yaml_loader) @@ -409,29 +413,29 @@ for _loader in (ESPHomeLoader, ESPHomePurePythonLoader): "!include_dir_merge_named", _loader.construct_include_dir_merge_named ) _loader.add_constructor("!lambda", _loader.construct_lambda) - _loader.add_constructor("!force", _loader.construct_force) + _loader.add_constructor("!literal", _loader.construct_literal) _loader.add_constructor("!extend", _loader.construct_extend) _loader.add_constructor("!remove", _loader.construct_remove) -def load_yaml(fname: str, clear_secrets: bool = True) -> Any: +def load_yaml(fname: Path, clear_secrets: bool = True) -> Any: if clear_secrets: _SECRET_VALUES.clear() _SECRET_CACHE.clear() return _load_yaml_internal(fname) -def _load_yaml_internal(fname: str) -> Any: +def _load_yaml_internal(fname: Path) -> Any: """Load a YAML file.""" try: - with open(fname, encoding="utf-8") as f_handle: + with fname.open(encoding="utf-8") as f_handle: return parse_yaml(fname, f_handle) except (UnicodeDecodeError, OSError) as err: raise EsphomeError(f"Error reading file {fname}: {err}") from err def parse_yaml( - file_name: str, file_handle: TextIOWrapper, yaml_loader=_load_yaml_internal + file_name: Path, file_handle: TextIOWrapper, yaml_loader=_load_yaml_internal ) -> Any: """Parse a YAML file.""" try: @@ -483,9 +487,9 @@ def substitute_vars(config, vars): def _load_yaml_internal_with_type( loader_type: type[ESPHomeLoader] | type[ESPHomePurePythonLoader], - fname: str, + fname: Path, content: TextIOWrapper, - yaml_loader: Any, + yaml_loader: Callable[[Path], dict[str, Any]], ) -> Any: """Load a YAML file.""" loader = loader_type(content, fname, yaml_loader) @@ -512,13 +516,14 @@ def _is_file_valid(name: str) -> bool: return not name.startswith(".") -def _find_files(directory, pattern): +def _find_files(directory: Path, pattern): """Recursively load files in a directory.""" - for root, dirs, files in os.walk(directory, topdown=True): + for root, dirs, files in os.walk(directory): dirs[:] = [d for d in dirs if _is_file_valid(d)] - for basename in files: - if _is_file_valid(basename) and fnmatch.fnmatch(basename, pattern): - filename = os.path.join(root, basename) + for f in files: + filename = Path(f) + if _is_file_valid(f) and filename.match(pattern): + filename = Path(root) / filename yield filename @@ -627,3 +632,4 @@ ESPHomeDumper.add_multi_representer(TimePeriod, ESPHomeDumper.represent_stringif ESPHomeDumper.add_multi_representer(Lambda, ESPHomeDumper.represent_lambda) ESPHomeDumper.add_multi_representer(core.ID, ESPHomeDumper.represent_id) ESPHomeDumper.add_multi_representer(uuid.UUID, ESPHomeDumper.represent_stringify) +ESPHomeDumper.add_multi_representer(Path, ESPHomeDumper.represent_stringify) diff --git a/esphome/zeroconf.py b/esphome/zeroconf.py index fa496b3488..dc4ca77eb4 100644 --- a/esphome/zeroconf.py +++ b/esphome/zeroconf.py @@ -68,8 +68,11 @@ class DashboardBrowser(AsyncServiceBrowser): class DashboardImportDiscovery: - def __init__(self) -> None: + def __init__( + self, on_update: Callable[[str, DiscoveredImport | None], None] | None = None + ) -> None: self.import_state: dict[str, DiscoveredImport] = {} + self.on_update = on_update def browser_callback( self, @@ -85,7 +88,9 @@ class DashboardImportDiscovery: state_change, ) if state_change == ServiceStateChange.Removed: - self.import_state.pop(name, None) + removed = self.import_state.pop(name, None) + if removed and self.on_update: + self.on_update(name, None) return if state_change == ServiceStateChange.Updated and name not in self.import_state: @@ -139,7 +144,7 @@ class DashboardImportDiscovery: if friendly_name is not None: friendly_name = friendly_name.decode() - self.import_state[name] = DiscoveredImport( + discovered = DiscoveredImport( friendly_name=friendly_name, device_name=node_name, package_import_url=import_url, @@ -147,6 +152,10 @@ class DashboardImportDiscovery: project_version=project_version, network=network, ) + is_new = name not in self.import_state + self.import_state[name] = discovered + if is_new and self.on_update: + self.on_update(name, discovered) def update_device_mdns(self, node_name: str, version: str): storage_path = ext_storage_path(node_name + ".yaml") diff --git a/netlify.toml b/netlify.toml index 7783414a91..5f177e6b3a 100644 --- a/netlify.toml +++ b/netlify.toml @@ -1,3 +1,4 @@ [build] command = "script/build-api-docs" publish = "api-docs" + environment = { PYTHON_VERSION = "3.13" } diff --git a/platformio.ini b/platformio.ini index d9f2f879ec..94f58f84ab 100644 --- a/platformio.ini +++ b/platformio.ini @@ -46,6 +46,10 @@ lib_deps = ; This is using the repository until a new release is published to PlatformIO https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1 ; Sensirion Gas Index Algorithm Arduino Library lvgl/lvgl@8.4.0 ; lvgl + ; This dependency is used only in unit tests. + ; Must coincide with PLATFORMIO_GOOGLE_TEST_LIB in scripts/cpp_unit_test.py + ; See scripts/cpp_unit_test.py and tests/components/README.md + google/googletest@^1.15.2 build_flags = -DESPHOME_LOG_LEVEL=ESPHOME_LOG_LEVEL_VERY_VERBOSE -std=gnu++20 @@ -72,7 +76,6 @@ lib_deps = SPI ; spi (Arduino built-in) Wire ; i2c (Arduino built-int) heman/AsyncMqttClient-esphome@1.0.0 ; mqtt - ESP32Async/ESPAsyncWebServer@3.7.8 ; web_server_base fastled/FastLED@3.9.16 ; fastled_base freekode/TM1651@1.0.1 ; tm1651 glmnet/Dsmr@0.7 ; dsmr @@ -107,6 +110,7 @@ lib_deps = ESP8266WiFi ; wifi (Arduino built-in) Update ; ota (Arduino built-in) ESP32Async/ESPAsyncTCP@2.0.0 ; async_tcp + ESP32Async/ESPAsyncWebServer@3.7.8 ; web_server_base makuna/NeoPixelBus@2.7.3 ; neopixelbus ESP8266HTTPClient ; http_request (Arduino built-in) ESP8266mDNS ; mdns (Arduino built-in) @@ -125,11 +129,11 @@ extra_scripts = post:esphome/components/esp8266/post_build.py.script ; This are common settings for the ESP32 (all variants) using Arduino. [common:esp32-arduino] extends = common:arduino -platform = https://github.com/pioarduino/platform-espressif32/releases/download/54.03.21-2/platform-espressif32.zip +platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.31-1/platform-espressif32.zip platform_packages = - pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.2.1/esp32-3.2.1.zip + pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.2/esp32-3.3.2.zip -framework = arduino +framework = arduino, espidf ; Arduino as an ESP-IDF component lib_deps = ; order matters with lib-deps; some of the libs in common:arduino.lib_deps ; don't declare built-in libraries as dependencies, so they have to be declared first @@ -147,7 +151,7 @@ lib_deps = makuna/NeoPixelBus@2.8.0 ; neopixelbus esphome/ESP32-audioI2S@2.3.0 ; i2s_audio droscy/esp_wireguard@0.4.2 ; wireguard - esphome/esp-audio-libs@1.1.4 ; audio + esphome/esp-audio-libs@2.0.1 ; audio build_flags = ${common:arduino.build_flags} @@ -161,16 +165,16 @@ extra_scripts = post:esphome/components/esp32/post_build.py.script ; This are common settings for the ESP32 (all variants) using IDF. [common:esp32-idf] extends = common:idf -platform = https://github.com/pioarduino/platform-espressif32/releases/download/54.03.21-2/platform-espressif32.zip +platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.31-1/platform-espressif32.zip platform_packages = - pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.4.2/esp-idf-v5.4.2.zip + pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.1/esp-idf-v5.5.1.zip framework = espidf lib_deps = ${common:idf.lib_deps} droscy/esp_wireguard@0.4.2 ; wireguard kahrendt/ESPMicroSpeechFeatures@1.1.0 ; micro_wake_word - esphome/esp-audio-libs@1.1.4 ; audio + esphome/esp-audio-libs@2.0.1 ; audio build_flags = ${common:idf.build_flags} -Wno-nonnull-compare @@ -193,6 +197,7 @@ platform_packages = framework = arduino lib_deps = ${common:arduino.lib_deps} + ESP32Async/ESPAsyncWebServer@3.7.8 ; web_server_base build_flags = ${common:arduino.build_flags} -DUSE_RP2040 @@ -207,7 +212,8 @@ platform = libretiny@1.9.1 framework = arduino lib_compat_mode = soft lib_deps = - droscy/esp_wireguard@0.4.2 ; wireguard + ESP32Async/ESPAsyncWebServer@3.7.8 ; web_server_base + droscy/esp_wireguard@0.4.2 ; wireguard build_flags = ${common:arduino.build_flags} -DUSE_LIBRETINY @@ -221,8 +227,8 @@ extends = common platform = https://github.com/tomaszduda23/platform-nordicnrf52/archive/refs/tags/v10.3.0-1.zip framework = zephyr platform_packages = - platformio/framework-zephyr @ https://github.com/tomaszduda23/framework-sdk-nrf/archive/refs/tags/v2.6.1-4.zip - platformio/toolchain-gccarmnoneeabi@https://github.com/tomaszduda23/toolchain-sdk-ng/archive/refs/tags/v0.16.1-1.zip + platformio/framework-zephyr @ https://github.com/tomaszduda23/framework-sdk-nrf/archive/refs/tags/v2.6.1-7.zip + platformio/toolchain-gccarmnoneeabi@https://github.com/tomaszduda23/toolchain-sdk-ng/archive/refs/tags/v0.17.4-0.zip build_flags = ${common.build_flags} -DUSE_ZEPHYR @@ -274,6 +280,7 @@ build_unflags = [env:esp32-arduino-tidy] extends = common:esp32-arduino board = esp32dev +board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32-arduino-tidy build_flags = ${common:esp32-arduino.build_flags} ${flags:clangtidy.build_flags} @@ -357,6 +364,19 @@ build_flags = ${common:esp32-idf.build_flags} ${flags:runtime.build_flags} -DUSE_ESP32_VARIANT_ESP32C6 +build_unflags = + ${common.build_unflags} + +[env:esp32c6-idf-tidy] +extends = common:esp32-idf +board = esp32-c6-devkitc-1 +board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32c6-idf-tidy +build_flags = + ${common:esp32-idf.build_flags} + ${flags:clangtidy.build_flags} + -DUSE_ESP32_VARIANT_ESP32C6 +build_unflags = + ${common.build_unflags} ;;;;;;;; ESP32-S2 ;;;;;;;; diff --git a/pyproject.toml b/pyproject.toml index 4943c48eb0..d6aa584237 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "esphome" -license = {text = "MIT"} +license = "MIT" description = "ESPHome is a system to configure your microcontrollers by simple yet powerful configuration files and control them remotely through Home Automation systems." readme = "README.md" authors = [ @@ -15,12 +15,13 @@ classifiers = [ "Environment :: Console", "Intended Audience :: Developers", "Intended Audience :: End Users/Desktop", - "License :: OSI Approved :: MIT License", "Programming Language :: C++", "Programming Language :: Python :: 3", "Topic :: Home Automation", ] -requires-python = ">=3.11.0" + +# Python 3.14 is currently not supported by IDF <= 5.5.1, see https://github.com/esphome/esphome/issues/11502 +requires-python = ">=3.11.0,<3.14" dynamic = ["dependencies", "optional-dependencies", "version"] @@ -132,7 +133,6 @@ ignore = [ "PLW1641", # Object does not implement `__hash__` method "PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable "PLW2901", # Outer {outer_kind} variable {name} overwritten by inner {inner_kind} target - "UP038", # https://github.com/astral-sh/ruff/issues/7871 https://github.com/astral-sh/ruff/pull/16681 ] [tool.ruff.lint.isort] diff --git a/requirements.txt b/requirements.txt index 6f79e86bdf..a5c919e95f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,30 +1,28 @@ cryptography==45.0.1 voluptuous==0.15.2 -PyYAML==6.0.2 +PyYAML==6.0.3 paho-mqtt==1.6.1 colorama==0.4.6 icmplib==3.0.4 -tornado==6.5.1 +tornado==6.5.2 tzlocal==5.3.1 # from time tzdata>=2021.1 # from time pyserial==3.5 platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile -esptool==5.0.2 +esptool==5.1.0 click==8.1.7 -esphome-dashboard==20250514.0 -aioesphomeapi==37.2.4 -zeroconf==0.147.0 +esphome-dashboard==20251013.0 +aioesphomeapi==42.8.0 +zeroconf==0.148.0 puremagic==1.30 -ruamel.yaml==0.18.14 # dashboard_import +ruamel.yaml==0.18.16 # dashboard_import +ruamel.yaml.clib==0.2.15 # dashboard_import esphome-glyphsets==0.2.0 -pillow==10.4.0 +pillow==11.3.0 cairosvg==2.8.2 freetype-py==2.5.1 jinja2==3.1.6 - -# esp-idf requires this, but doesn't bundle it by default -# https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24 -kconfiglib==13.7.1 +bleak==2.0.0 # esp-idf >= 5.0 requires this pyparsing >= 3.0 diff --git a/requirements_test.txt b/requirements_test.txt index 188d2ff22c..7f6d3f8e26 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,14 +1,14 @@ -pylint==3.3.7 +pylint==4.0.3 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.12.7 # also change in .pre-commit-config.yaml when updating -pyupgrade==3.20.0 # also change in .pre-commit-config.yaml when updating +ruff==0.14.5 # also change in .pre-commit-config.yaml when updating +pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating pre-commit # Unit tests -pytest==8.4.1 -pytest-cov==6.2.1 -pytest-mock==3.14.1 -pytest-asyncio==1.1.0 +pytest==9.0.1 +pytest-cov==7.0.0 +pytest-mock==3.15.1 +pytest-asyncio==1.3.0 pytest-xdist==3.8.0 asyncmock==0.4.2 hypothesis==6.92.1 diff --git a/script/analyze_component_buses.py b/script/analyze_component_buses.py new file mode 100755 index 0000000000..27a36f889f --- /dev/null +++ b/script/analyze_component_buses.py @@ -0,0 +1,666 @@ +#!/usr/bin/env python3 +"""Analyze component test files to detect which common bus configs they use. + +This script scans component test files and extracts which common bus configurations +(i2c, spi, uart, etc.) are included via the packages mechanism. This information +is used to group components that can be tested together. + +Components can only be grouped together if they use the EXACT SAME set of common +bus configurations, ensuring that merged configs are compatible. + +Example output: +{ + "component1": { + "esp32-ard": ["i2c", "uart_19200"], + "esp32-idf": ["i2c", "uart_19200"] + }, + "component2": { + "esp32-ard": ["spi"], + "esp32-idf": ["spi"] + } +} +""" + +from __future__ import annotations + +import argparse +from functools import lru_cache +import json +from pathlib import Path +import re +import sys +from typing import Any + +# Add esphome to path +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from helpers import BASE_BUS_COMPONENTS + +from esphome import yaml_util +from esphome.config_helpers import Extend, Remove + +# Path to common bus configs +COMMON_BUS_PATH = Path("tests/test_build_components/common") + +# Package dependencies - maps packages to the packages they include +# When a component uses a package on the left, it automatically gets +# the packages on the right as well +PACKAGE_DEPENDENCIES = { + "modbus": ["uart"], # modbus packages include uart packages + # Add more package dependencies here as needed +} + +# Bus types that can be defined directly in config files +# Components defining these directly cannot be grouped (they create unique bus IDs) +DIRECT_BUS_TYPES = ( + "i2c", + "spi", + "uart", + "modbus", + "remote_transmitter", + "remote_receiver", +) + +# Signature for components with no bus requirements +# These components can be merged with any other group +NO_BUSES_SIGNATURE = "no_buses" + +# Prefix for isolated component signatures +# Isolated components have unique signatures and cannot be merged with others +ISOLATED_SIGNATURE_PREFIX = "isolated_" + +# Components that must be tested in isolation (not grouped or batched with others) +# These have known build issues that prevent grouping +# NOTE: This should be kept in sync with both test_build_components and split_components_for_ci.py +ISOLATED_COMPONENTS = { + "animation": "Has display lambda in common.yaml that requires existing display platform - breaks when merged without display", + "esphome": "Defines devices/areas in esphome: section that are referenced in other sections - breaks when merged", + "ethernet": "Defines ethernet: which conflicts with wifi: used by most components", + "ethernet_info": "Related to ethernet component which conflicts with wifi", + "gps": "TinyGPSPlus library declares millis() function that creates ambiguity with ESPHome millis() macro when merged with components using millis() in lambdas", + "lvgl": "Defines multiple SDL displays on host platform that conflict when merged with other display configs", + "mapping": "Uses dict format for image/display sections incompatible with standard list format - ESPHome merge_config cannot handle", + "openthread": "Conflicts with wifi: used by most components", + "openthread_info": "Conflicts with wifi: used by most components", + "matrix_keypad": "Needs isolation due to keypad", + "modbus_controller": "Defines multiple modbus buses for testing client/server functionality - conflicts with package modbus bus", + "neopixelbus": "RMT type conflict with ESP32 Arduino/ESP-IDF headers (enum vs struct rmt_channel_t)", + "packages": "cannot merge packages", + "tinyusb": "Conflicts with usb_host component - cannot be used together", +} + + +@lru_cache(maxsize=1) +def get_common_bus_packages() -> frozenset[str]: + """Get the list of common bus package names. + + Reads from tests/test_build_components/common/ directory + and caches the result. All bus types support component grouping + for config validation since --testing-mode bypasses runtime conflicts. + + Returns: + Frozenset of common bus package names (i2c, spi, uart, etc.) + """ + if not COMMON_BUS_PATH.exists(): + return frozenset() + + # List all directories in common/ - these are the bus package names + return frozenset(d.name for d in COMMON_BUS_PATH.iterdir() if d.is_dir()) + + +def uses_local_file_references(component_dir: Path) -> bool: + """Check if a component uses local file references via $component_dir. + + Components that reference local files cannot be grouped because each needs + a unique component_dir path pointing to their specific directory. + + Args: + component_dir: Path to the component's test directory + + Returns: + True if the component uses $component_dir for local file references + """ + common_yaml = component_dir / "common.yaml" + if not common_yaml.exists(): + return False + + try: + content = common_yaml.read_text() + except Exception: # pylint: disable=broad-exception-caught + return False + + # Pattern to match $component_dir or ${component_dir} references + # These indicate local file usage that prevents grouping + return bool(re.search(r"\$\{?component_dir\}?", content)) + + +def is_platform_component(component_dir: Path) -> bool: + """Check if a component is a platform component (abstract base class). + + Platform components have IS_PLATFORM_COMPONENT = True and cannot be + instantiated without a platform-specific implementation. These components + define abstract methods and cause linker errors if compiled standalone. + + Examples: canbus, mcp23x08_base, mcp23x17_base + + Args: + component_dir: Path to the component's test directory + + Returns: + True if this is a platform component + """ + # Check in the actual component source, not tests + # tests/components/X -> tests/components -> tests -> repo root + repo_root = component_dir.parent.parent.parent + comp_init = ( + repo_root / "esphome" / "components" / component_dir.name / "__init__.py" + ) + + if not comp_init.exists(): + return False + + try: + content = comp_init.read_text() + return "IS_PLATFORM_COMPONENT = True" in content + except Exception: # pylint: disable=broad-exception-caught + return False + + +def _contains_extend_or_remove(data: Any) -> bool: + """Recursively check if data contains Extend or Remove objects. + + Args: + data: Parsed YAML data structure + + Returns: + True if any Extend or Remove objects are found + """ + if isinstance(data, (Extend, Remove)): + return True + + if isinstance(data, dict): + for value in data.values(): + if _contains_extend_or_remove(value): + return True + + if isinstance(data, list): + for item in data: + if _contains_extend_or_remove(item): + return True + + return False + + +def analyze_yaml_file(yaml_file: Path) -> dict[str, Any]: + """Load a YAML file once and extract all needed information. + + This loads the YAML file a single time and extracts all information needed + for component analysis, avoiding multiple file reads. + + Args: + yaml_file: Path to the YAML file to analyze + + Returns: + Dictionary with keys: + - buses: set of common bus package names + - has_extend_remove: bool indicating if Extend/Remove objects are present + - has_direct_bus_config: bool indicating if buses are defined directly (not via packages) + - loaded: bool indicating if file was successfully loaded + """ + result = { + "buses": set(), + "has_extend_remove": False, + "has_direct_bus_config": False, + "loaded": False, + } + + if not yaml_file.exists(): + return result + + try: + data = yaml_util.load_yaml(yaml_file) + result["loaded"] = True + except Exception: # pylint: disable=broad-exception-caught + return result + + # Check for Extend/Remove objects + result["has_extend_remove"] = _contains_extend_or_remove(data) + + # Check if buses are defined directly (not via packages) + # Components that define i2c, spi, uart, or modbus directly in test files + # cannot be grouped because they create unique bus IDs + if isinstance(data, dict): + for bus_type in DIRECT_BUS_TYPES: + if bus_type in data: + result["has_direct_bus_config"] = True + break + + # Extract common bus packages + if not isinstance(data, dict) or "packages" not in data: + return result + + packages = data["packages"] + if not isinstance(packages, dict): + return result + + valid_buses = get_common_bus_packages() + for pkg_name in packages: + if pkg_name not in valid_buses: + continue + result["buses"].add(pkg_name) + # Add any package dependencies (e.g., modbus includes uart) + if pkg_name not in PACKAGE_DEPENDENCIES: + continue + for dep in PACKAGE_DEPENDENCIES[pkg_name]: + if dep not in valid_buses: + continue + result["buses"].add(dep) + + return result + + +def analyze_component(component_dir: Path) -> tuple[dict[str, list[str]], bool, bool]: + """Analyze a component directory to find which buses each platform uses. + + Args: + component_dir: Path to the component's test directory + + Returns: + Tuple of: + - Dictionary mapping platform to list of bus configs + Example: {"esp32-ard": ["i2c", "spi"], "esp32-idf": ["i2c"]} + - Boolean indicating if component uses !extend or !remove + - Boolean indicating if component defines buses directly (not via packages) + """ + if not component_dir.is_dir(): + return {}, False, False + + platform_buses = {} + has_extend_remove = False + has_direct_bus_config = False + + # Analyze all YAML files in the component directory + for yaml_file in component_dir.glob("*.yaml"): + analysis = analyze_yaml_file(yaml_file) + + # Track if any file uses extend/remove + if analysis["has_extend_remove"]: + has_extend_remove = True + + # Track if any file defines buses directly + if analysis["has_direct_bus_config"]: + has_direct_bus_config = True + + # For test.*.yaml files, extract platform and buses + if yaml_file.name.startswith("test.") and yaml_file.suffix == ".yaml": + # Extract platform name (e.g., test.esp32-ard.yaml -> esp32-ard) + platform = yaml_file.stem.replace("test.", "") + # Always add platform, even if it has no buses (empty list) + # This allows grouping components that don't use any shared buses + platform_buses[platform] = ( + sorted(analysis["buses"]) if analysis["buses"] else [] + ) + + return platform_buses, has_extend_remove, has_direct_bus_config + + +def analyze_all_components( + tests_dir: Path = None, +) -> tuple[dict[str, dict[str, list[str]]], set[str], set[str]]: + """Analyze all component test directories. + + Args: + tests_dir: Path to tests/components directory (defaults to auto-detect) + + Returns: + Tuple of: + - Dictionary mapping component name to platform->buses mapping + - Set of component names that cannot be grouped + - Set of component names that define buses directly (need migration warning) + """ + if tests_dir is None: + tests_dir = Path("tests/components") + + if not tests_dir.exists(): + print(f"Error: {tests_dir} does not exist", file=sys.stderr) + return {}, set(), set() + + components = {} + non_groupable = set() + direct_bus_components = set() + + for component_dir in sorted(tests_dir.iterdir()): + if not component_dir.is_dir(): + continue + + component_name = component_dir.name + platform_buses, has_extend_remove, has_direct_bus_config = analyze_component( + component_dir + ) + + if platform_buses: + components[component_name] = platform_buses + + # Note: Components using $component_dir are now groupable because the merge + # script rewrites these to absolute paths with component-specific substitutions + + # Check if component is explicitly isolated + # These have known issues that prevent grouping with other components + if component_name in ISOLATED_COMPONENTS: + non_groupable.add(component_name) + + # Check if component is a base bus component + # These ARE the bus platform implementations and define buses directly for testing + # They cannot be grouped with components that use bus packages (causes ID conflicts) + if component_name in BASE_BUS_COMPONENTS: + non_groupable.add(component_name) + + # Check if component uses !extend or !remove directives + # These rely on specific config structure and cannot be merged with other components + # The directives work within a component's own package hierarchy but break when + # merging independent components together + if has_extend_remove: + non_groupable.add(component_name) + + # Check if component defines buses directly in test files + # These create unique bus IDs and cause conflicts when merged + # Exclude base bus components (i2c, spi, uart, etc.) since they ARE the platform + if has_direct_bus_config and component_name not in BASE_BUS_COMPONENTS: + non_groupable.add(component_name) + direct_bus_components.add(component_name) + + return components, non_groupable, direct_bus_components + + +@lru_cache(maxsize=256) +def _get_bus_configs(buses: tuple[str, ...]) -> frozenset[tuple[str, str]]: + """Map bus type to set of configs for that type. + + Args: + buses: Tuple of bus package names (e.g., ("uart_9600", "i2c")) + + Returns: + Frozenset of (base_type, full_config) tuples + Example: frozenset({("uart", "uart_9600"), ("i2c", "i2c")}) + """ + # Split on underscore to get base type: "uart_9600" -> "uart", "i2c" -> "i2c" + return frozenset((bus.split("_", 1)[0], bus) for bus in buses) + + +@lru_cache(maxsize=1024) +def are_buses_compatible(buses1: tuple[str, ...], buses2: tuple[str, ...]) -> bool: + """Check if two bus tuples are compatible for merging. + + Two bus lists are compatible if they don't have conflicting configurations + for the same bus type. For example: + - ("ble", "uart") and ("i2c",) are compatible (different buses) + - ("uart_9600",) and ("uart_19200",) are NOT compatible (same bus, different configs) + - ("uart_9600",) and ("uart_9600",) are compatible (same bus, same config) + + Args: + buses1: First tuple of bus package names + buses2: Second tuple of bus package names + + Returns: + True if buses can be merged without conflicts + """ + configs1 = _get_bus_configs(buses1) + configs2 = _get_bus_configs(buses2) + + # Group configs by base type + bus_types1: dict[str, set[str]] = {} + for base_type, full_config in configs1: + if base_type not in bus_types1: + bus_types1[base_type] = set() + bus_types1[base_type].add(full_config) + + bus_types2: dict[str, set[str]] = {} + for base_type, full_config in configs2: + if base_type not in bus_types2: + bus_types2[base_type] = set() + bus_types2[base_type].add(full_config) + + # Check for conflicts: same bus type with different configs + for bus_type, configs in bus_types1.items(): + if bus_type not in bus_types2: + continue # No conflict - different bus types + # Same bus type - check if configs match + if configs != bus_types2[bus_type]: + return False # Conflict - same bus type, different configs + + return True # No conflicts found + + +def merge_compatible_bus_groups( + grouped_components: dict[tuple[str, str], list[str]], +) -> dict[tuple[str, str], list[str]]: + """Merge groups with compatible (non-conflicting) buses. + + This function takes groups keyed by (platform, bus_signature) and merges + groups that share the same platform and have compatible bus configurations. + Two groups can be merged if their buses don't conflict - meaning they don't + have different configurations for the same bus type. + + For example: + - ["ble"] + ["uart"] = compatible (different buses) + - ["uart_9600"] + ["uart_19200"] = incompatible (same bus, different configs) + - ["uart_9600"] + ["uart_9600"] = compatible (same bus, same config) + + Args: + grouped_components: Dictionary mapping (platform, signature) to list of component names + + Returns: + Dictionary with same structure but with compatible groups merged + """ + merged_groups: dict[tuple[str, str], list[str]] = {} + processed_keys: set[tuple[str, str]] = set() + + for (platform1, sig1), comps1 in sorted(grouped_components.items()): + if (platform1, sig1) in processed_keys: + continue + + # Skip NO_BUSES_SIGNATURE - kept separate for flexible batch distribution + # These components have no bus requirements and can be added to any batch + # as "fillers" for load balancing across CI runners + if sig1 == NO_BUSES_SIGNATURE: + merged_groups[(platform1, sig1)] = comps1 + processed_keys.add((platform1, sig1)) + continue + + # Skip isolated components - they can't be merged with others + if sig1.startswith(ISOLATED_SIGNATURE_PREFIX): + merged_groups[(platform1, sig1)] = comps1 + processed_keys.add((platform1, sig1)) + continue + + # Start with this group's components + merged_comps: list[str] = list(comps1) + merged_sig: str = sig1 + processed_keys.add((platform1, sig1)) + + # Get buses for this group as tuple for caching + buses1: tuple[str, ...] = tuple(sorted(sig1.split("+"))) + + # Try to merge with other groups on same platform + for (platform2, sig2), comps2 in sorted(grouped_components.items()): + if (platform2, sig2) in processed_keys: + continue + if platform2 != platform1: + continue # Different platforms can't be merged + if sig2 == NO_BUSES_SIGNATURE: + continue # Keep separate for flexible batch distribution + if sig2.startswith(ISOLATED_SIGNATURE_PREFIX): + continue # Isolated components can't be merged + + # Check if buses are compatible + buses2: tuple[str, ...] = tuple(sorted(sig2.split("+"))) + if are_buses_compatible(buses1, buses2): + # Compatible! Merge this group + merged_comps.extend(comps2) + processed_keys.add((platform2, sig2)) + # Update merged signature to include all unique buses + all_buses: set[str] = set(buses1) | set(buses2) + merged_sig = "+".join(sorted(all_buses)) + buses1 = tuple(sorted(all_buses)) # Update for next iteration + + # Store merged group + merged_groups[(platform1, merged_sig)] = merged_comps + + return merged_groups + + +def create_grouping_signature( + platform_buses: dict[str, list[str]], platform: str +) -> str: + """Create a signature string for grouping components. + + Components with the same signature can be grouped together for testing. + All valid bus types can be grouped since --testing-mode bypasses runtime + conflicts during config validation. + + Args: + platform_buses: Mapping of platform to list of buses + platform: The specific platform to create signature for + + Returns: + Signature string (e.g., "i2c" or "uart") or empty if no valid buses + """ + buses = platform_buses.get(platform, []) + if not buses: + return "" + + # Only include valid bus types in signature + common_buses = get_common_bus_packages() + valid_buses = [b for b in buses if b in common_buses] + if not valid_buses: + return "" + + return "+".join(sorted(valid_buses)) + + +def group_components_by_signature( + components: dict[str, dict[str, list[str]]], platform: str +) -> dict[str, list[str]]: + """Group components by their bus signature for a specific platform. + + Args: + components: Component analysis results from analyze_all_components() + platform: Platform to group for (e.g., "esp32-ard") + + Returns: + Dictionary mapping signature to list of component names + Example: {"i2c+uart_19200": ["comp1", "comp2"], "spi": ["comp3"]} + """ + signature_groups: dict[str, list[str]] = {} + + for component_name, platform_buses in components.items(): + if platform not in platform_buses: + continue + + signature = create_grouping_signature(platform_buses, platform) + if not signature: + continue + + if signature not in signature_groups: + signature_groups[signature] = [] + signature_groups[signature].append(component_name) + + return signature_groups + + +def main() -> None: + """Main entry point.""" + parser = argparse.ArgumentParser( + description="Analyze component test files to detect common bus usage" + ) + parser.add_argument( + "--components", + "-c", + nargs="+", + help="Specific components to analyze (default: all)", + ) + parser.add_argument( + "--platform", + "-p", + help="Show grouping for a specific platform", + ) + parser.add_argument( + "--json", + action="store_true", + help="Output as JSON", + ) + parser.add_argument( + "--group", + action="store_true", + help="Show component groupings by bus signature", + ) + + args = parser.parse_args() + + # Analyze components + tests_dir = Path("tests/components") + + if args.components: + # Analyze only specified components + components = {} + non_groupable = set() + direct_bus_components = set() + for comp in args.components: + comp_dir = tests_dir / comp + platform_buses, has_extend_remove, has_direct_bus_config = ( + analyze_component(comp_dir) + ) + if platform_buses: + components[comp] = platform_buses + # Note: Components using $component_dir are now groupable + if comp in ISOLATED_COMPONENTS: + non_groupable.add(comp) + if comp in BASE_BUS_COMPONENTS: + non_groupable.add(comp) + if has_direct_bus_config and comp not in BASE_BUS_COMPONENTS: + non_groupable.add(comp) + direct_bus_components.add(comp) + else: + # Analyze all components + components, non_groupable, direct_bus_components = analyze_all_components( + tests_dir + ) + + # Output results + if args.group and args.platform: + # Show groupings for a specific platform + groups = group_components_by_signature(components, args.platform) + + if args.json: + print(json.dumps(groups, indent=2)) + else: + print(f"Component groupings for {args.platform}:") + print() + for signature, comp_list in sorted(groups.items()): + print(f" {signature}:") + for comp in sorted(comp_list): + print(f" - {comp}") + print() + elif args.json: + # JSON output + print(json.dumps(components, indent=2)) + else: + # Human-readable output + for component, platform_buses in sorted(components.items()): + non_groupable_marker = ( + " [NON-GROUPABLE]" if component in non_groupable else "" + ) + print(f"{component}{non_groupable_marker}:") + for platform, buses in sorted(platform_buses.items()): + bus_str = ", ".join(buses) + print(f" {platform}: {bus_str}") + print() + print(f"Total components analyzed: {len(components)}") + if non_groupable: + print(f"Non-groupable components (use local files): {len(non_groupable)}") + for comp in sorted(non_groupable): + print(f" - {comp}") + + +if __name__ == "__main__": + main() diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index fa2f87d98d..b07a249c8d 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -3,7 +3,6 @@ from __future__ import annotations from abc import ABC, abstractmethod from enum import IntEnum -import os from pathlib import Path import re from subprocess import call @@ -12,6 +11,7 @@ from typing import Any import aioesphomeapi.api_options_pb2 as pb import google.protobuf.descriptor_pb2 as descriptor +from google.protobuf.descriptor_pb2 import FieldDescriptorProto class WireType(IntEnum): @@ -149,7 +149,7 @@ class TypeInfo(ABC): @property def repeated(self) -> bool: """Check if the field is repeated.""" - return self._field.label == 3 + return self._field.label == FieldDescriptorProto.LABEL_REPEATED @property def wire_type(self) -> WireType: @@ -338,7 +338,12 @@ def create_field_type_info( needs_encode: bool = True, ) -> TypeInfo: """Create the appropriate TypeInfo instance for a field, handling repeated fields and custom options.""" - if field.label == 3: # repeated + if field.label == FieldDescriptorProto.LABEL_REPEATED: + # Check if this repeated field has fixed_array_with_length_define option + if ( + fixed_size := get_field_opt(field, pb.fixed_array_with_length_define) + ) is not None: + return FixedArrayWithLengthRepeatedType(field, fixed_size) # Check if this repeated field has fixed_array_size option if (fixed_size := get_field_opt(field, pb.fixed_array_size)) is not None: return FixedArrayRepeatedType(field, fixed_size) @@ -349,12 +354,33 @@ def create_field_type_info( return FixedArrayRepeatedType(field, size_define) return RepeatedTypeInfo(field) - # Check for fixed_array_size option on bytes fields - if ( - field.type == 12 - and (fixed_size := get_field_opt(field, pb.fixed_array_size)) is not None - ): - return FixedArrayBytesType(field, fixed_size) + # Check for mutually exclusive options on bytes fields + if field.type == 12: + has_pointer_to_buffer = get_field_opt(field, pb.pointer_to_buffer, False) + fixed_size = get_field_opt(field, pb.fixed_array_size, None) + + if has_pointer_to_buffer and fixed_size is not None: + raise ValueError( + f"Field '{field.name}' has both pointer_to_buffer and fixed_array_size. " + "These options are mutually exclusive. Use pointer_to_buffer for zero-copy " + "or fixed_array_size for traditional array storage." + ) + + if has_pointer_to_buffer: + # Zero-copy pointer approach - no size needed, will use size_t for length + return PointerToBytesBufferType(field, None) + + if fixed_size is not None: + # Traditional fixed array approach with copy + return FixedArrayBytesType(field, fixed_size) + + # Check for pointer_to_buffer option on string fields + if field.type == 9: + has_pointer_to_buffer = get_field_opt(field, pb.pointer_to_buffer, False) + + if has_pointer_to_buffer: + # Zero-copy pointer approach for strings + return PointerToBytesBufferType(field, None) # Special handling for bytes fields if field.type == 12: @@ -436,7 +462,7 @@ class Int64Type(TypeInfo): wire_type = WireType.VARINT # Uses wire type 0 def dump(self, name: str) -> str: - o = f'snprintf(buffer, sizeof(buffer), "%lld", {name});\n' + o = f'snprintf(buffer, sizeof(buffer), "%" PRId64, {name});\n' o += "out.append(buffer);" return o @@ -456,7 +482,7 @@ class UInt64Type(TypeInfo): wire_type = WireType.VARINT # Uses wire type 0 def dump(self, name: str) -> str: - o = f'snprintf(buffer, sizeof(buffer), "%llu", {name});\n' + o = f'snprintf(buffer, sizeof(buffer), "%" PRIu64, {name});\n' o += "out.append(buffer);" return o @@ -496,7 +522,7 @@ class Fixed64Type(TypeInfo): wire_type = WireType.FIXED64 # Uses wire type 1 def dump(self, name: str) -> str: - o = f'snprintf(buffer, sizeof(buffer), "%llu", {name});\n' + o = f'snprintf(buffer, sizeof(buffer), "%" PRIu64, {name});\n' o += "out.append(buffer);" return o @@ -814,6 +840,91 @@ class BytesType(TypeInfo): return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical bytes +class PointerToBytesBufferType(TypeInfo): + """Type for bytes fields that use pointer_to_buffer option for zero-copy.""" + + @classmethod + def can_use_dump_field(cls) -> bool: + return False + + def __init__( + self, field: descriptor.FieldDescriptorProto, size: int | None = None + ) -> None: + super().__init__(field) + # Size is not used for pointer_to_buffer - we always use size_t for length + self.array_size = 0 + + @property + def cpp_type(self) -> str: + return "const uint8_t*" + + @property + def default_value(self) -> str: + return "nullptr" + + @property + def reference_type(self) -> str: + return "const uint8_t*" + + @property + def const_reference_type(self) -> str: + return "const uint8_t*" + + @property + def public_content(self) -> list[str]: + # Use uint16_t for length - max packet size is well below 65535 + # Add pointer and length fields + return [ + f"const uint8_t* {self.field_name}{{nullptr}};", + f"uint16_t {self.field_name}_len{{0}};", + ] + + @property + def encode_content(self) -> str: + return f"buffer.encode_bytes({self.number}, this->{self.field_name}, this->{self.field_name}_len);" + + @property + def decode_length_content(self) -> str | None: + # Decode directly stores the pointer to avoid allocation + return f"""case {self.number}: {{ + // Use raw data directly to avoid allocation + this->{self.field_name} = value.data(); + this->{self.field_name}_len = value.size(); + break; + }}""" + + @property + def decode_length(self) -> str | None: + # This is handled in decode_length_content + return None + + @property + def wire_type(self) -> WireType: + """Get the wire type for this bytes field.""" + return WireType.LENGTH_DELIMITED # Uses wire type 2 + + def dump(self, name: str) -> str: + return ( + f"format_hex_pretty(this->{self.field_name}, this->{self.field_name}_len)" + ) + + @property + def dump_content(self) -> str: + # Custom dump that doesn't use dump_field template + return ( + f'out.append(" {self.name}: ");\n' + + f"out.append({self.dump(self.field_name)});\n" + + 'out.append("\\n");' + ) + + def get_size_calculation(self, name: str, force: bool = False) -> str: + return f"size.add_length({self.number}, this->{self.field_name}_len);" + + def get_estimated_size(self) -> int: + # field ID + length varint + typical data (assume small for pointer fields) + return self.calculate_field_id_size() + 2 + 16 + + class FixedArrayBytesType(TypeInfo): """Special type for fixed-size byte arrays.""" @@ -843,10 +954,17 @@ class FixedArrayBytesType(TypeInfo): @property def public_content(self) -> list[str]: + len_type = ( + "uint8_t" + if self.array_size <= 255 + else "uint16_t" + if self.array_size <= 65535 + else "size_t" + ) # Add both the array and length fields return [ f"uint8_t {self.field_name}[{self.array_size}]{{}};", - f"uint8_t {self.field_name}_len{{0}};", + f"{len_type} {self.field_name}_len{{0}};", ] @property @@ -988,7 +1106,7 @@ class SFixed64Type(TypeInfo): wire_type = WireType.FIXED64 # Uses wire type 1 def dump(self, name: str) -> str: - o = f'snprintf(buffer, sizeof(buffer), "%lld", {name});\n' + o = f'snprintf(buffer, sizeof(buffer), "%" PRId64, {name});\n' o += "out.append(buffer);" return o @@ -1032,7 +1150,7 @@ class SInt64Type(TypeInfo): wire_type = WireType.VARINT # Uses wire type 0 def dump(self, name: str) -> str: - o = f'snprintf(buffer, sizeof(buffer), "%lld", {name});\n' + o = f'snprintf(buffer, sizeof(buffer), "%" PRId64, {name});\n' o += "out.append(buffer);" return o @@ -1044,7 +1162,11 @@ class SInt64Type(TypeInfo): def _generate_array_dump_content( - ti, field_name: str, name: str, is_bool: bool = False + ti, + field_name: str, + name: str, + is_bool: bool = False, + is_const_char_ptr: bool = False, ) -> str: """Generate dump content for array types (repeated or fixed array). @@ -1052,9 +1174,14 @@ def _generate_array_dump_content( """ o = f"for (const auto {'' if is_bool else '&'}it : {field_name}) {{\n" # Check if underlying type can use dump_field - if type(ti).can_use_dump_field(): + if is_const_char_ptr: + # Special case for const char* - use it directly + o += f' dump_field(out, "{name}", it, 4);\n' + elif ti.can_use_dump_field(): # For types that have dump_field overloads, use them with extra indent - o += f' dump_field(out, "{name}", {ti.dump_field_value("it")}, 4);\n' + # std::vector iterators return proxy objects, need explicit cast + value_expr = "static_cast(it)" if is_bool else ti.dump_field_value("it") + o += f' dump_field(out, "{name}", {value_expr}, 4);\n' else: # For complex types (messages, bytes), use the old pattern o += f' out.append(" {name}: ");\n' @@ -1084,6 +1211,12 @@ class FixedArrayRepeatedType(TypeInfo): validate_field_type(field.type, field.name) self._ti: TypeInfo = TYPE_INFO[field.type](field) + def _encode_element(self, element: str) -> str: + """Helper to generate encode statement for a single element.""" + if isinstance(self._ti, EnumType): + return f"buffer.{self._ti.encode_func}({self.number}, static_cast({element}), true);" + return f"buffer.{self._ti.encode_func}({self.number}, {element}, true);" + @property def cpp_type(self) -> str: return f"std::array<{self._ti.cpp_type}, {self.array_size}>" @@ -1111,19 +1244,13 @@ class FixedArrayRepeatedType(TypeInfo): @property def encode_content(self) -> str: - # Helper to generate encode statement for a single element - def encode_element(element: str) -> str: - if isinstance(self._ti, EnumType): - return f"buffer.{self._ti.encode_func}({self.number}, static_cast({element}), true);" - return f"buffer.{self._ti.encode_func}({self.number}, {element}, true);" - # If skip_zero is enabled, wrap encoding in a zero check if self.skip_zero: if self.is_define: # When using a define, we need to use a loop-based approach o = f"for (const auto &it : this->{self.field_name}) {{\n" o += " if (it != 0) {\n" - o += f" {encode_element('it')}\n" + o += f" {self._encode_element('it')}\n" o += " }\n" o += "}" return o @@ -1132,7 +1259,7 @@ class FixedArrayRepeatedType(TypeInfo): [f"this->{self.field_name}[{i}] != 0" for i in range(self.array_size)] ) encode_lines = [ - f" {encode_element(f'this->{self.field_name}[{i}]')}" + f" {self._encode_element(f'this->{self.field_name}[{i}]')}" for i in range(self.array_size) ] return f"if ({non_zero_checks}) {{\n" + "\n".join(encode_lines) + "\n}" @@ -1140,23 +1267,23 @@ class FixedArrayRepeatedType(TypeInfo): # When using a define, always use loop-based approach if self.is_define: o = f"for (const auto &it : this->{self.field_name}) {{\n" - o += f" {encode_element('it')}\n" + o += f" {self._encode_element('it')}\n" o += "}" return o # Unroll small arrays for efficiency if self.array_size == 1: - return encode_element(f"this->{self.field_name}[0]") + return self._encode_element(f"this->{self.field_name}[0]") if self.array_size == 2: return ( - encode_element(f"this->{self.field_name}[0]") + self._encode_element(f"this->{self.field_name}[0]") + "\n " - + encode_element(f"this->{self.field_name}[1]") + + self._encode_element(f"this->{self.field_name}[1]") ) # Use loops for larger arrays o = f"for (const auto &it : this->{self.field_name}) {{\n" - o += f" {encode_element('it')}\n" + o += f" {self._encode_element('it')}\n" o += "}" return o @@ -1230,12 +1357,80 @@ class FixedArrayRepeatedType(TypeInfo): return underlying_size * self.array_size +class FixedArrayWithLengthRepeatedType(FixedArrayRepeatedType): + """Special type for fixed-size repeated fields with variable length tracking. + + Similar to FixedArrayRepeatedType but generates an additional length field + to track how many elements are actually in use. Only encodes/sends elements + up to the current length. + + Fixed arrays with length are only supported for encoding (SOURCE_SERVER) since + we cannot control how many items we receive when decoding. + """ + + @property + def public_content(self) -> list[str]: + # Return both the array and the length field + return [ + f"{self.cpp_type} {self.field_name}{{}};", + f"uint16_t {self.field_name}_len{{0}};", + ] + + @property + def encode_content(self) -> str: + # Always use a loop up to the current length + o = f"for (uint16_t i = 0; i < this->{self.field_name}_len; i++) {{\n" + o += f" {self._encode_element(f'this->{self.field_name}[i]')}\n" + o += "}" + return o + + @property + def dump_content(self) -> str: + # Dump only the active elements + o = f"for (uint16_t i = 0; i < this->{self.field_name}_len; i++) {{\n" + # Check if underlying type can use dump_field + if self._ti.can_use_dump_field(): + o += f' dump_field(out, "{self.name}", {self._ti.dump_field_value(f"this->{self.field_name}[i]")}, 4);\n' + else: + o += f' out.append(" {self.name}: ");\n' + o += indent(self._ti.dump(f"this->{self.field_name}[i]")) + "\n" + o += ' out.append("\\n");\n' + o += "}" + return o + + def get_size_calculation(self, name: str, force: bool = False) -> str: + # Calculate size only for active elements + o = f"for (uint16_t i = 0; i < {name}_len; i++) {{\n" + o += f" {self._ti.get_size_calculation(f'{name}[i]', True)}\n" + o += "}" + return o + + def get_estimated_size(self) -> int: + # For fixed arrays with length, estimate based on typical usage + # Assume on average half the array is used + underlying_size = self._ti.get_estimated_size() + if self.is_define: + # When using a define, estimate 8 elements as typical + return underlying_size * 8 + return underlying_size * ( + self.array_size // 2 if self.array_size > 2 else self.array_size + ) + + class RepeatedTypeInfo(TypeInfo): def __init__(self, field: descriptor.FieldDescriptorProto) -> None: super().__init__(field) # Check if this is a pointer field by looking for container_pointer option self._container_type = get_field_opt(field, pb.container_pointer, "") - self._use_pointer = bool(self._container_type) + # Check for non-template container pointer + self._container_no_template = get_field_opt( + field, pb.container_pointer_no_template, "" + ) + self._use_pointer = bool(self._container_type) or bool( + self._container_no_template + ) + # Check if this should use FixedVector instead of std::vector + self._use_fixed_vector = get_field_opt(field, pb.fixed_vector, False) # For repeated fields, we need to get the base type info # but we can't call create_field_type_info as it would cause recursion @@ -1252,13 +1447,21 @@ class RepeatedTypeInfo(TypeInfo): @property def cpp_type(self) -> str: + if self._container_no_template: + # Non-template container: use type as-is without appending template parameters + return f"const {self._container_no_template}*" if self._use_pointer and self._container_type: # For pointer fields, use the specified container type - # If the container type already includes the element type (e.g., std::set) - # use it as-is, otherwise append the element type + # Two cases: + # 1. "std::set" - Full type with template params, use as-is + # 2. "std::set" - No <>, append the element type if "<" in self._container_type and ">" in self._container_type: + # Has template parameters specified, use as-is return f"const {self._container_type}*" + # No <> at all, append element type return f"const {self._container_type}<{self._ti.cpp_type}>*" + if self._use_fixed_vector: + return f"FixedVector<{self._ti.cpp_type}>" return f"std::vector<{self._ti.cpp_type}>" @property @@ -1337,11 +1540,16 @@ class RepeatedTypeInfo(TypeInfo): def encode_content(self) -> str: if self._use_pointer: # For pointer fields, just dereference (pointer should never be null in our use case) - o = f"for (const auto &it : *this->{self.field_name}) {{\n" - if isinstance(self._ti, EnumType): - o += f" buffer.{self._ti.encode_func}({self.number}, static_cast(it), true);\n" + # Special handling for const char* elements (when container_no_template contains "const char") + if "const char" in self._container_no_template: + o = f"for (const char *it : *this->{self.field_name}) {{\n" + o += f" buffer.{self._ti.encode_func}({self.number}, it, strlen(it), true);\n" else: - o += f" buffer.{self._ti.encode_func}({self.number}, it, true);\n" + o = f"for (const auto &it : *this->{self.field_name}) {{\n" + if isinstance(self._ti, EnumType): + o += f" buffer.{self._ti.encode_func}({self.number}, static_cast(it), true);\n" + else: + o += f" buffer.{self._ti.encode_func}({self.number}, it, true);\n" o += "}" return o o = f"for (auto {'' if self._ti_is_bool else '&'}it : this->{self.field_name}) {{\n" @@ -1354,10 +1562,18 @@ class RepeatedTypeInfo(TypeInfo): @property def dump_content(self) -> str: + # Check if this is const char* elements + is_const_char_ptr = ( + self._use_pointer and "const char" in self._container_no_template + ) if self._use_pointer: # For pointer fields, dereference and use the existing helper return _generate_array_dump_content( - self._ti, f"*this->{self.field_name}", self.name, is_bool=False + self._ti, + f"*this->{self.field_name}", + self.name, + is_bool=False, + is_const_char_ptr=is_const_char_ptr, ) return _generate_array_dump_content( self._ti, f"this->{self.field_name}", self.name, is_bool=self._ti_is_bool @@ -1392,9 +1608,15 @@ class RepeatedTypeInfo(TypeInfo): o += f" size.add_precalculated_size({size_expr} * {bytes_per_element});\n" else: # Other types need the actual value - auto_ref = "" if self._ti_is_bool else "&" - o += f" for (const auto {auto_ref}it : {container_ref}) {{\n" - o += f" {self._ti.get_size_calculation('it', True)}\n" + # Special handling for const char* elements + if self._use_pointer and "const char" in self._container_no_template: + field_id_size = self.calculate_field_id_size() + o += f" for (const char *it : {container_ref}) {{\n" + o += f" size.add_length_force({field_id_size}, strlen(it));\n" + else: + auto_ref = "" if self._ti_is_bool else "&" + o += f" for (const auto {auto_ref}it : {container_ref}) {{\n" + o += f" {self._ti.get_size_calculation('it', True)}\n" o += " }\n" o += "}" @@ -1676,13 +1898,16 @@ def build_message_type( # Add estimated size constant estimated_size = calculate_message_estimated_size(desc) - # Validate that estimated_size fits in uint8_t - if estimated_size > 255: - raise ValueError( - f"Estimated size {estimated_size} for {desc.name} exceeds uint8_t maximum (255)" - ) + # Use a type appropriate for estimated_size + estimated_size_type = ( + "uint8_t" + if estimated_size <= 255 + else "uint16_t" + if estimated_size <= 65535 + else "size_t" + ) public_content.append( - f"static constexpr uint8_t ESTIMATED_SIZE = {estimated_size};" + f"static constexpr {estimated_size_type} ESTIMATED_SIZE = {estimated_size};" ) # Add message_name method inline in header @@ -1693,6 +1918,9 @@ def build_message_type( ) public_content.append("#endif") + # Collect fixed_vector fields for custom decode generation + fixed_vector_fields = [] + for field in desc.field: # Skip deprecated fields completely if field.options.deprecated: @@ -1701,7 +1929,7 @@ def build_message_type( # Validate that fixed_array_size is only used in encode-only messages if ( needs_decode - and field.label == 3 + and field.label == FieldDescriptorProto.LABEL_REPEATED and get_field_opt(field, pb.fixed_array_size) is not None ): raise ValueError( @@ -1711,6 +1939,27 @@ def build_message_type( f"since we cannot trust or control the number of items received from clients." ) + # Validate that fixed_array_with_length_define is only used in encode-only messages + if ( + needs_decode + and field.label == FieldDescriptorProto.LABEL_REPEATED + and get_field_opt(field, pb.fixed_array_with_length_define) is not None + ): + raise ValueError( + f"Message '{desc.name}' uses fixed_array_with_length_define on field '{field.name}' " + f"but has source={SOURCE_NAMES[source]}. " + f"Fixed arrays with length are only supported for SOURCE_SERVER (encode-only) messages " + f"since we cannot trust or control the number of items received from clients." + ) + + # Collect fixed_vector repeated fields for custom decode generation + if ( + needs_decode + and field.label == FieldDescriptorProto.LABEL_REPEATED + and get_field_opt(field, pb.fixed_vector, False) + ): + fixed_vector_fields.append((field.name, field.number)) + ti = create_field_type_info(field, needs_decode, needs_encode) # Skip field declarations for fields that are in the base class @@ -1819,6 +2068,22 @@ def build_message_type( prot = "bool decode_64bit(uint32_t field_id, Proto64Bit value) override;" protected_content.insert(0, prot) + # Generate custom decode() override for messages with FixedVector fields + if fixed_vector_fields: + # Generate the decode() implementation in cpp + o = f"void {desc.name}::decode(const uint8_t *buffer, size_t length) {{\n" + # Count and init each FixedVector field + for field_name, field_number in fixed_vector_fields: + o += f" uint32_t count_{field_name} = ProtoDecodableMessage::count_repeated_field(buffer, length, {field_number});\n" + o += f" this->{field_name}.init(count_{field_name});\n" + # Call parent decode to populate the fields + o += " ProtoDecodableMessage::decode(buffer, length);\n" + o += "}\n" + cpp += o + # Generate the decode() declaration in header (public method) + prot = "void decode(const uint8_t *buffer, size_t length) override;" + public_content.append(prot) + # Only generate encode method if this message needs encoding and has fields if needs_encode and encode: o = f"void {desc.name}::encode(ProtoWriteBuffer buffer) const {{" @@ -1874,7 +2139,7 @@ def build_message_type( dump_impl += "}\n" if base_class: - out = f"class {desc.name} : public {base_class} {{\n" + out = f"class {desc.name} final : public {base_class} {{\n" else: # Check if message has any non-deprecated fields has_fields = any(not field.options.deprecated for field in desc.field) @@ -1883,7 +2148,7 @@ def build_message_type( base_class = "ProtoDecodableMessage" else: base_class = "ProtoMessage" - out = f"class {desc.name} : public {base_class} {{\n" + out = f"class {desc.name} final : public {base_class} {{\n" out += " public:\n" out += indent("\n".join(public_content)) + "\n" out += "\n" @@ -2281,7 +2546,7 @@ static void dump_field(std::string &out, const char *field_name, float value, in static void dump_field(std::string &out, const char *field_name, uint64_t value, int indent = 2) { char buffer[64]; append_field_prefix(out, field_name, indent); - snprintf(buffer, 64, "%llu", value); + snprintf(buffer, 64, "%" PRIu64, value); append_with_newline(out, buffer); } @@ -2303,6 +2568,12 @@ static void dump_field(std::string &out, const char *field_name, StringRef value out.append("\\n"); } +static void dump_field(std::string &out, const char *field_name, const char *value, int indent = 2) { + append_field_prefix(out, field_name, indent); + out.append("'").append(value).append("'"); + out.append("\\n"); +} + template static void dump_field(std::string &out, const char *field_name, T value, int indent = 2) { append_field_prefix(out, field_name, indent); @@ -2526,6 +2797,10 @@ static const char *const TAG = "api.service"; hpp_protected = "" cpp += "\n" + # Build a mapping of message input types to their authentication requirements + message_auth_map: dict[str, bool] = {} + message_conn_map: dict[str, bool] = {} + m = serv.method[0] for m in serv.method: func = m.name @@ -2537,6 +2812,10 @@ static const char *const TAG = "api.service"; needs_conn = get_opt(m, pb.needs_setup_connection, True) needs_auth = get_opt(m, pb.needs_authentication, True) + # Store authentication requirements for message types + message_auth_map[inp] = needs_auth + message_conn_map[inp] = needs_conn + ifdef = message_ifdef_map.get(inp, ifdefs.get(inp)) if ifdef is not None: @@ -2554,33 +2833,14 @@ static const char *const TAG = "api.service"; cpp += f"void {class_name}::{on_func}(const {inp} &msg) {{\n" - # Start with authentication/connection check if needed - if needs_auth or needs_conn: - # Determine which check to use - if needs_auth: - check_func = "this->check_authenticated_()" - else: - check_func = "this->check_connection_setup_()" - - if is_void: - # For void methods, just wrap with auth check - body = f"if ({check_func}) {{\n" - body += f" this->{func}(msg);\n" - body += "}\n" - else: - # For non-void methods, combine auth check and send response check - body = f"if ({check_func} && !this->send_{func}_response(msg)) {{\n" - body += " this->on_fatal_error();\n" - body += "}\n" + # No authentication check here - it's done in read_message + body = "" + if is_void: + body += f"this->{func}(msg);\n" else: - # No auth check needed, just call the handler - body = "" - if is_void: - body += f"this->{func}(msg);\n" - else: - body += f"if (!this->send_{func}_response(msg)) {{\n" - body += " this->on_fatal_error();\n" - body += "}\n" + body += f"if (!this->send_{func}_response(msg)) {{\n" + body += " this->on_fatal_error();\n" + body += "}\n" cpp += indent(body) + "\n" + "}\n" @@ -2589,6 +2849,65 @@ static const char *const TAG = "api.service"; hpp_protected += "#endif\n" cpp += "#endif\n" + # Generate optimized read_message with authentication checking + # Categorize messages by their authentication requirements + no_conn_ids: set[int] = set() + conn_only_ids: set[int] = set() + + for id_, (_, _, case_msg_name) in cases: + if case_msg_name in message_auth_map: + needs_auth = message_auth_map[case_msg_name] + needs_conn = message_conn_map[case_msg_name] + + if not needs_conn: + no_conn_ids.add(id_) + elif not needs_auth: + conn_only_ids.add(id_) + + # Generate override if we have messages that skip checks + if no_conn_ids or conn_only_ids: + # Helper to generate case statements with ifdefs + def generate_cases(ids: set[int], comment: str) -> str: + result = "" + for id_ in sorted(ids): + _, ifdef, msg_name = RECEIVE_CASES[id_] + if ifdef: + result += f"#ifdef {ifdef}\n" + result += f" case {msg_name}::MESSAGE_TYPE: {comment}\n" + if ifdef: + result += "#endif\n" + return result + + hpp_protected += " void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;\n" + + cpp += f"\nvoid {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {{\n" + cpp += " // Check authentication/connection requirements for messages\n" + cpp += " switch (msg_type) {\n" + + # Messages that don't need any checks + if no_conn_ids: + cpp += generate_cases(no_conn_ids, "// No setup required") + cpp += " break; // Skip all checks for these messages\n" + + # Messages that only need connection setup + if conn_only_ids: + cpp += generate_cases(conn_only_ids, "// Connection setup only") + cpp += " if (!this->check_connection_setup_()) {\n" + cpp += " return; // Connection not setup\n" + cpp += " }\n" + cpp += " break;\n" + + cpp += " default:\n" + cpp += " // All other messages require authentication (which includes connection check)\n" + cpp += " if (!this->check_authenticated_()) {\n" + cpp += " return; // Authentication failed\n" + cpp += " }\n" + cpp += " break;\n" + cpp += " }\n\n" + cpp += " // Call base implementation to process the message\n" + cpp += f" {class_name}Base::read_message(msg_size, msg_type, msg_data);\n" + cpp += "}\n" + hpp += " protected:\n" hpp += hpp_protected hpp += "};\n" @@ -2614,8 +2933,8 @@ static const char *const TAG = "api.service"; import clang_format def exec_clang_format(path: Path) -> None: - clang_format_path = os.path.join( - os.path.dirname(clang_format.__file__), "data", "bin", "clang-format" + clang_format_path = ( + Path(clang_format.__file__).parent / "data" / "bin" / "clang-format" ) call([clang_format_path, "-i", path]) diff --git a/script/build_codeowners.py b/script/build_codeowners.py index 4581620095..10ca1295b7 100755 --- a/script/build_codeowners.py +++ b/script/build_codeowners.py @@ -39,7 +39,7 @@ esphome/core/* @esphome/core parts = [BASE] # Fake some directory so that get_component works -CORE.config_path = str(root) +CORE.config_path = root CORE.data[KEY_CORE] = {KEY_TARGET_FRAMEWORK: None, KEY_TARGET_PLATFORM: None} codeowners = defaultdict(list) @@ -82,7 +82,7 @@ for path in components_dir.iterdir(): for path, owners in sorted(codeowners.items()): - owners = sorted(set(owners)) + owners = sorted(set(owners), key=str.casefold) if not owners: continue for owner in owners: diff --git a/script/build_language_schema.py b/script/build_language_schema.py index ff6e898902..c9501cb193 100755 --- a/script/build_language_schema.py +++ b/script/build_language_schema.py @@ -1,9 +1,9 @@ #!/usr/bin/env python3 import argparse -import glob import inspect import json import os +from pathlib import Path import re import voluptuous as vol @@ -70,14 +70,14 @@ def get_component_names(): component_names = ["esphome", "sensor", "esp32", "esp8266"] skip_components = [] - for d in os.listdir(CORE_COMPONENTS_PATH): + for d in CORE_COMPONENTS_PATH.iterdir(): if ( - not d.startswith("__") - and os.path.isdir(os.path.join(CORE_COMPONENTS_PATH, d)) - and d not in component_names - and d not in skip_components + not d.name.startswith("__") + and d.is_dir() + and d.name not in component_names + and d.name not in skip_components ): - component_names.append(d) + component_names.append(d.name) return sorted(component_names) @@ -121,7 +121,7 @@ from esphome.util import Registry # noqa: E402 def write_file(name, obj): - full_path = os.path.join(args.output_path, name + ".json") + full_path = Path(args.output_path) / f"{name}.json" if JSON_DUMP_PRETTY: json_str = json.dumps(obj, indent=2) else: @@ -131,9 +131,10 @@ def write_file(name, obj): def delete_extra_files(keep_names): - for d in os.listdir(args.output_path): - if d.endswith(".json") and d[:-5] not in keep_names: - os.remove(os.path.join(args.output_path, d)) + output_path = Path(args.output_path) + for d in output_path.iterdir(): + if d.suffix == ".json" and d.stem not in keep_names: + d.unlink() print(f"Deleted {d}") @@ -299,7 +300,7 @@ def fix_remote_receiver(): remote_receiver_schema["CONFIG_SCHEMA"] = { "type": "schema", "schema": { - "extends": ["binary_sensor.BINARY_SENSOR_SCHEMA", "core.COMPONENT_SCHEMA"], + "extends": ["binary_sensor._BINARY_SENSOR_SCHEMA", "core.COMPONENT_SCHEMA"], "config_vars": output["remote_base"].pop("binary"), }, } @@ -367,13 +368,11 @@ def get_logger_tags(): "scheduler", "api.service", ] - for x in os.walk(CORE_COMPONENTS_PATH): - for y in glob.glob(os.path.join(x[0], "*.cpp")): - with open(y, encoding="utf-8") as file: - data = file.read() - match = pattern.search(data) - if match: - tags.append(match.group(1)) + for file in CORE_COMPONENTS_PATH.rglob("*.cpp"): + data = file.read_text() + match = pattern.search(data) + if match: + tags.append(match.group(1)) return tags diff --git a/script/ci-custom.py b/script/ci-custom.py index 6f3c513f42..106aa438fe 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -6,6 +6,7 @@ import collections import fnmatch import functools import os.path +from pathlib import Path import re import sys import time @@ -70,17 +71,18 @@ ignore_types = ( ".apng", ".gif", ".webp", + ".bin", ) LINT_FILE_CHECKS = [] LINT_CONTENT_CHECKS = [] LINT_POST_CHECKS = [] -EXECUTABLE_BIT = {} +EXECUTABLE_BIT: dict[str, int] = {} -errors = collections.defaultdict(list) +errors: collections.defaultdict[Path, list] = collections.defaultdict(list) -def add_errors(fname, errs): +def add_errors(fname: Path, errs: list[tuple[int, int, str] | None]) -> None: if not isinstance(errs, list): errs = [errs] for err in errs: @@ -246,8 +248,8 @@ def lint_ext_check(fname): ".github/copilot-instructions.md", ] ) -def lint_executable_bit(fname): - ex = EXECUTABLE_BIT[fname] +def lint_executable_bit(fname: Path) -> str | None: + ex = EXECUTABLE_BIT[str(fname)] if ex != 100644: return ( f"File has invalid executable bit {ex}. If running from a windows machine please " @@ -500,13 +502,14 @@ def lint_constants_usage(): continue errs.append( f"Constant {highlight(constant)} is defined in {len(uses)} files. Please move all definitions of the " - f"constant to const.py (Uses: {', '.join(uses)})" + f"constant to const.py (Uses: {', '.join(str(u) for u in uses)}) in a separate PR. " + "See https://developers.esphome.io/contributing/code/#python" ) return errs -def relative_cpp_search_text(fname, content): - parts = fname.split("/") +def relative_cpp_search_text(fname: Path, content) -> str: + parts = fname.parts integration = parts[2] return f'#include "esphome/components/{integration}' @@ -523,8 +526,8 @@ def lint_relative_cpp_import(fname, line, col, content): ) -def relative_py_search_text(fname, content): - parts = fname.split("/") +def relative_py_search_text(fname: Path, content: str) -> str: + parts = fname.parts integration = parts[2] return f"esphome.components.{integration}" @@ -590,10 +593,8 @@ def lint_relative_py_import(fname, line, col, content): "esphome/components/http_request/httplib.h", ], ) -def lint_namespace(fname, content): - expected_name = re.match( - r"^esphome/components/([^/]+)/.*", fname.replace(os.path.sep, "/") - ).group(1) +def lint_namespace(fname: Path, content: str) -> str | None: + expected_name = fname.parts[2] # Check for both old style and C++17 nested namespace syntax search_old = f"namespace {expected_name}" search_new = f"namespace esphome::{expected_name}" @@ -732,9 +733,9 @@ def main(): files.sort() for fname in files: - _, ext = os.path.splitext(fname) + fname = Path(fname) run_checks(LINT_FILE_CHECKS, fname, fname) - if ext in ignore_types: + if fname.suffix in ignore_types: continue try: with codecs.open(fname, "r", encoding="utf-8") as f_handle: diff --git a/script/ci_add_metadata_to_json.py b/script/ci_add_metadata_to_json.py new file mode 100755 index 0000000000..687b5131c0 --- /dev/null +++ b/script/ci_add_metadata_to_json.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +"""Add metadata to memory analysis JSON file. + +This script adds components and platform metadata to an existing +memory analysis JSON file. Used by CI to ensure all required fields are present +for the comment script. +""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path +import sys + + +def main() -> int: + """Main entry point.""" + parser = argparse.ArgumentParser( + description="Add metadata to memory analysis JSON file" + ) + parser.add_argument( + "--json-file", + required=True, + help="Path to JSON file to update", + ) + parser.add_argument( + "--components", + required=True, + help='JSON array of component names (e.g., \'["api", "wifi"]\')', + ) + parser.add_argument( + "--platform", + required=True, + help="Platform name", + ) + + args = parser.parse_args() + + # Load existing JSON + json_path = Path(args.json_file) + if not json_path.exists(): + print(f"Error: JSON file not found: {args.json_file}", file=sys.stderr) + return 1 + + try: + with open(json_path, encoding="utf-8") as f: + data = json.load(f) + except (json.JSONDecodeError, OSError) as e: + print(f"Error loading JSON: {e}", file=sys.stderr) + return 1 + + # Parse components + try: + components = json.loads(args.components) + if not isinstance(components, list): + print("Error: --components must be a JSON array", file=sys.stderr) + return 1 + # Element-level validation: ensure each component is a non-empty string + for idx, comp in enumerate(components): + if not isinstance(comp, str) or not comp.strip(): + print( + f"Error: component at index {idx} is not a non-empty string: {comp!r}", + file=sys.stderr, + ) + return 1 + except json.JSONDecodeError as e: + print(f"Error parsing components: {e}", file=sys.stderr) + return 1 + + # Add metadata + data["components"] = components + data["platform"] = args.platform + + # Write back + try: + with open(json_path, "w", encoding="utf-8") as f: + json.dump(data, f, indent=2) + print(f"Added metadata to {args.json_file}", file=sys.stderr) + except OSError as e: + print(f"Error writing JSON: {e}", file=sys.stderr) + return 1 + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/script/ci_helpers.py b/script/ci_helpers.py new file mode 100755 index 0000000000..48b0e4bbfe --- /dev/null +++ b/script/ci_helpers.py @@ -0,0 +1,23 @@ +"""Common helper functions for CI scripts.""" + +from __future__ import annotations + +import os + + +def write_github_output(outputs: dict[str, str | int]) -> None: + """Write multiple outputs to GITHUB_OUTPUT or stdout. + + When running in GitHub Actions, writes to the GITHUB_OUTPUT file. + When running locally, writes to stdout for debugging. + + Args: + outputs: Dictionary of key-value pairs to write + """ + github_output = os.environ.get("GITHUB_OUTPUT") + if github_output: + with open(github_output, "a", encoding="utf-8") as f: + f.writelines(f"{key}={value}\n" for key, value in outputs.items()) + else: + for key, value in outputs.items(): + print(f"{key}={value}") diff --git a/script/ci_memory_impact_comment.py b/script/ci_memory_impact_comment.py new file mode 100755 index 0000000000..1331a44d03 --- /dev/null +++ b/script/ci_memory_impact_comment.py @@ -0,0 +1,643 @@ +#!/usr/bin/env python3 +"""Post or update a PR comment with memory impact analysis results. + +This script creates or updates a GitHub PR comment with memory usage changes. +It uses the GitHub CLI (gh) to manage comments and maintains a single comment +that gets updated on subsequent runs. +""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path +import subprocess +import sys + +from jinja2 import Environment, FileSystemLoader + +# Add esphome to path for analyze_memory import +sys.path.insert(0, str(Path(__file__).parent.parent)) + +# pylint: disable=wrong-import-position + +# Comment marker to identify our memory impact comments +COMMENT_MARKER = "" + + +def run_gh_command(args: list[str], operation: str) -> subprocess.CompletedProcess: + """Run a gh CLI command with error handling. + + Args: + args: Command arguments (including 'gh') + operation: Description of the operation for error messages + + Returns: + CompletedProcess result + + Raises: + subprocess.CalledProcessError: If command fails (with detailed error output) + """ + try: + return subprocess.run( + args, + check=True, + capture_output=True, + text=True, + ) + except subprocess.CalledProcessError as e: + print( + f"ERROR: {operation} failed with exit code {e.returncode}", file=sys.stderr + ) + print(f"ERROR: Command: {' '.join(args)}", file=sys.stderr) + print(f"ERROR: stdout: {e.stdout}", file=sys.stderr) + print(f"ERROR: stderr: {e.stderr}", file=sys.stderr) + raise + + +# Thresholds for emoji significance indicators (percentage) +OVERALL_CHANGE_THRESHOLD = 1.0 # Overall RAM/Flash changes +COMPONENT_CHANGE_THRESHOLD = 3.0 # Component breakdown changes + +# Display limits for tables +MAX_COMPONENT_BREAKDOWN_ROWS = 20 # Maximum components to show in breakdown table +MAX_CHANGED_SYMBOLS_ROWS = 30 # Maximum changed symbols to show +MAX_NEW_SYMBOLS_ROWS = 15 # Maximum new symbols to show +MAX_REMOVED_SYMBOLS_ROWS = 15 # Maximum removed symbols to show + +# Symbol display formatting +SYMBOL_DISPLAY_MAX_LENGTH = 100 # Max length before using
tag +SYMBOL_DISPLAY_TRUNCATE_LENGTH = 97 # Length to truncate in summary + +# Component change noise threshold +COMPONENT_CHANGE_NOISE_THRESHOLD = 2 # Ignore component changes ≤ this many bytes + +# Template directory +TEMPLATE_DIR = Path(__file__).parent / "templates" + + +def load_analysis_json(json_path: str) -> dict | None: + """Load memory analysis results from JSON file. + + Args: + json_path: Path to analysis JSON file + + Returns: + Dictionary with analysis results or None if file doesn't exist/can't be loaded + """ + json_file = Path(json_path) + if not json_file.exists(): + print(f"Analysis JSON not found: {json_path}", file=sys.stderr) + return None + + try: + with open(json_file, encoding="utf-8") as f: + return json.load(f) + except (json.JSONDecodeError, OSError) as e: + print(f"Failed to load analysis JSON: {e}", file=sys.stderr) + return None + + +def format_bytes(bytes_value: int) -> str: + """Format bytes value with comma separators. + + Args: + bytes_value: Number of bytes + + Returns: + Formatted string with comma separators (e.g., "1,234 bytes") + """ + return f"{bytes_value:,} bytes" + + +def format_change(before: int, after: int, threshold: float | None = None) -> str: + """Format memory change with delta and percentage. + + Args: + before: Memory usage before change (in bytes) + after: Memory usage after change (in bytes) + threshold: Optional percentage threshold for "significant" change. + If provided, adds supplemental emoji (🎉/🚨/🔸/✅) to chart icons. + If None, only shows chart icons (📈/📉/➡️). + + Returns: + Formatted string with delta and percentage + """ + delta = after - before + percentage = 0.0 if before == 0 else (delta / before) * 100 + + # Always use chart icons to show direction + if delta > 0: + delta_str = f"+{delta:,} bytes" + trend_icon = "📈" + # Add supplemental emoji based on threshold if provided + if threshold is not None: + significance = "🚨" if abs(percentage) > threshold else "🔸" + emoji = f"{trend_icon} {significance}" + else: + emoji = trend_icon + elif delta < 0: + delta_str = f"{delta:,} bytes" + trend_icon = "📉" + # Add supplemental emoji based on threshold if provided + if threshold is not None: + significance = "🎉" if abs(percentage) > threshold else "✅" + emoji = f"{trend_icon} {significance}" + else: + emoji = trend_icon + else: + delta_str = "+0 bytes" + emoji = "➡️" + + # Format percentage with sign + if percentage > 0: + pct_str = f"+{percentage:.2f}%" + elif percentage < 0: + pct_str = f"{percentage:.2f}%" + else: + pct_str = "0.00%" + + return f"{emoji} {delta_str} ({pct_str})" + + +def prepare_symbol_changes_data( + target_symbols: dict | None, pr_symbols: dict | None +) -> dict | None: + """Prepare symbol changes data for template rendering. + + Args: + target_symbols: Symbol name to size mapping for target branch + pr_symbols: Symbol name to size mapping for PR branch + + Returns: + Dictionary with changed, new, and removed symbols, or None if no changes + """ + if not target_symbols or not pr_symbols: + return None + + # Find all symbols that exist in both branches or only in one + all_symbols = set(target_symbols.keys()) | set(pr_symbols.keys()) + + # Track changes + changed_symbols: list[ + tuple[str, int, int, int] + ] = [] # (symbol, target_size, pr_size, delta) + new_symbols: list[tuple[str, int]] = [] # (symbol, size) + removed_symbols: list[tuple[str, int]] = [] # (symbol, size) + + for symbol in all_symbols: + target_size = target_symbols.get(symbol, 0) + pr_size = pr_symbols.get(symbol, 0) + + if target_size == 0 and pr_size > 0: + # New symbol + new_symbols.append((symbol, pr_size)) + elif target_size > 0 and pr_size == 0: + # Removed symbol + removed_symbols.append((symbol, target_size)) + elif target_size != pr_size: + # Changed symbol + delta = pr_size - target_size + changed_symbols.append((symbol, target_size, pr_size, delta)) + + if not changed_symbols and not new_symbols and not removed_symbols: + return None + + # Sort by size/delta + changed_symbols.sort(key=lambda x: abs(x[3]), reverse=True) + new_symbols.sort(key=lambda x: x[1], reverse=True) + removed_symbols.sort(key=lambda x: x[1], reverse=True) + + return { + "changed_symbols": changed_symbols, + "new_symbols": new_symbols, + "removed_symbols": removed_symbols, + } + + +def prepare_component_breakdown_data( + target_analysis: dict | None, pr_analysis: dict | None +) -> list[tuple[str, int, int, int]] | None: + """Prepare component breakdown data for template rendering. + + Args: + target_analysis: Component memory breakdown for target branch + pr_analysis: Component memory breakdown for PR branch + + Returns: + List of tuples (component, target_flash, pr_flash, delta), or None if no changes + """ + if not target_analysis or not pr_analysis: + return None + + # Combine all components from both analyses + all_components = set(target_analysis.keys()) | set(pr_analysis.keys()) + + # Filter to components that have changed (ignoring noise) + changed_components: list[ + tuple[str, int, int, int] + ] = [] # (comp, target_flash, pr_flash, delta) + for comp in all_components: + target_mem = target_analysis.get(comp, {}) + pr_mem = pr_analysis.get(comp, {}) + + target_flash = target_mem.get("flash_total", 0) + pr_flash = pr_mem.get("flash_total", 0) + + # Only include if component has meaningful change (above noise threshold) + delta = pr_flash - target_flash + if abs(delta) > COMPONENT_CHANGE_NOISE_THRESHOLD: + changed_components.append((comp, target_flash, pr_flash, delta)) + + if not changed_components: + return None + + # Sort by absolute delta (largest changes first) + changed_components.sort(key=lambda x: abs(x[3]), reverse=True) + + return changed_components + + +def create_comment_body( + components: list[str], + platform: str, + target_ram: int, + target_flash: int, + pr_ram: int, + pr_flash: int, + target_analysis: dict | None = None, + pr_analysis: dict | None = None, + target_symbols: dict | None = None, + pr_symbols: dict | None = None, +) -> str: + """Create the comment body with memory impact analysis using Jinja2 templates. + + Args: + components: List of component names (merged config) + platform: Platform name + target_ram: RAM usage in target branch + target_flash: Flash usage in target branch + pr_ram: RAM usage in PR branch + pr_flash: Flash usage in PR branch + target_analysis: Optional component breakdown for target branch + pr_analysis: Optional component breakdown for PR branch + target_symbols: Optional symbol map for target branch + pr_symbols: Optional symbol map for PR branch + + Returns: + Formatted comment body + """ + # Set up Jinja2 environment + env = Environment( + loader=FileSystemLoader(TEMPLATE_DIR), + trim_blocks=True, + lstrip_blocks=True, + ) + + # Register custom filters + env.filters["format_bytes"] = format_bytes + env.filters["format_change"] = format_change + + # Prepare template context + context = { + "comment_marker": COMMENT_MARKER, + "platform": platform, + "target_ram": format_bytes(target_ram), + "pr_ram": format_bytes(pr_ram), + "target_flash": format_bytes(target_flash), + "pr_flash": format_bytes(pr_flash), + "ram_change": format_change( + target_ram, pr_ram, threshold=OVERALL_CHANGE_THRESHOLD + ), + "flash_change": format_change( + target_flash, pr_flash, threshold=OVERALL_CHANGE_THRESHOLD + ), + "component_change_threshold": COMPONENT_CHANGE_THRESHOLD, + } + + # Format components list + if len(components) == 1: + context["components_str"] = f"`{components[0]}`" + context["config_note"] = "a representative test configuration" + else: + context["components_str"] = ", ".join(f"`{c}`" for c in sorted(components)) + context["config_note"] = ( + f"a merged configuration with {len(components)} components" + ) + + # Prepare component breakdown if available + component_breakdown = "" + if target_analysis and pr_analysis: + changed_components = prepare_component_breakdown_data( + target_analysis, pr_analysis + ) + if changed_components: + template = env.get_template("ci_memory_impact_component_breakdown.j2") + component_breakdown = template.render( + changed_components=changed_components, + format_bytes=format_bytes, + format_change=format_change, + component_change_threshold=COMPONENT_CHANGE_THRESHOLD, + max_rows=MAX_COMPONENT_BREAKDOWN_ROWS, + ) + + # Prepare symbol changes if available + symbol_changes = "" + if target_symbols and pr_symbols: + symbol_data = prepare_symbol_changes_data(target_symbols, pr_symbols) + if symbol_data: + template = env.get_template("ci_memory_impact_symbol_changes.j2") + symbol_changes = template.render( + **symbol_data, + format_bytes=format_bytes, + format_change=format_change, + max_changed_rows=MAX_CHANGED_SYMBOLS_ROWS, + max_new_rows=MAX_NEW_SYMBOLS_ROWS, + max_removed_rows=MAX_REMOVED_SYMBOLS_ROWS, + symbol_max_length=SYMBOL_DISPLAY_MAX_LENGTH, + symbol_truncate_length=SYMBOL_DISPLAY_TRUNCATE_LENGTH, + ) + + if not target_analysis or not pr_analysis: + print("No ELF files provided, skipping detailed analysis", file=sys.stderr) + + context["component_breakdown"] = component_breakdown + context["symbol_changes"] = symbol_changes + + # Render main template + template = env.get_template("ci_memory_impact_comment_template.j2") + return template.render(**context) + + +def find_existing_comment(pr_number: str) -> str | None: + """Find existing memory impact comment on the PR. + + Args: + pr_number: PR number + + Returns: + Comment numeric ID if found, None otherwise + + Raises: + subprocess.CalledProcessError: If gh command fails + """ + print(f"DEBUG: Looking for existing comment on PR #{pr_number}", file=sys.stderr) + + # Use gh api to get comments directly - this returns the numeric id field + result = run_gh_command( + [ + "gh", + "api", + f"/repos/{{owner}}/{{repo}}/issues/{pr_number}/comments", + "--jq", + ".[] | {id, body}", + ], + operation="Get PR comments", + ) + + print( + f"DEBUG: gh api comments output (first 500 chars):\n{result.stdout[:500]}", + file=sys.stderr, + ) + + # Parse comments and look for our marker + comment_count = 0 + for line in result.stdout.strip().split("\n"): + if not line: + continue + + try: + comment = json.loads(line) + comment_count += 1 + comment_id = comment.get("id") + print( + f"DEBUG: Checking comment {comment_count}: id={comment_id}", + file=sys.stderr, + ) + + body = comment.get("body", "") + if COMMENT_MARKER in body: + print( + f"DEBUG: Found existing comment with id={comment_id}", + file=sys.stderr, + ) + # Return the numeric id + return str(comment_id) + print("DEBUG: Comment does not contain marker", file=sys.stderr) + except json.JSONDecodeError as e: + print(f"DEBUG: JSON decode error: {e}", file=sys.stderr) + continue + + print( + f"DEBUG: No existing comment found (checked {comment_count} comments)", + file=sys.stderr, + ) + return None + + +def update_existing_comment(comment_id: str, comment_body: str) -> None: + """Update an existing comment. + + Args: + comment_id: Comment ID to update + comment_body: New comment body text + + Raises: + subprocess.CalledProcessError: If gh command fails + """ + print(f"DEBUG: Updating existing comment {comment_id}", file=sys.stderr) + print(f"DEBUG: Comment body length: {len(comment_body)} bytes", file=sys.stderr) + result = run_gh_command( + [ + "gh", + "api", + f"/repos/{{owner}}/{{repo}}/issues/comments/{comment_id}", + "-X", + "PATCH", + "-f", + f"body={comment_body}", + ], + operation="Update PR comment", + ) + print(f"DEBUG: Update response: {result.stdout}", file=sys.stderr) + + +def create_new_comment(pr_number: str, comment_body: str) -> None: + """Create a new PR comment. + + Args: + pr_number: PR number + comment_body: Comment body text + + Raises: + subprocess.CalledProcessError: If gh command fails + """ + print(f"DEBUG: Posting new comment on PR #{pr_number}", file=sys.stderr) + print(f"DEBUG: Comment body length: {len(comment_body)} bytes", file=sys.stderr) + result = run_gh_command( + ["gh", "pr", "comment", pr_number, "--body", comment_body], + operation="Create PR comment", + ) + print(f"DEBUG: Post response: {result.stdout}", file=sys.stderr) + + +def post_or_update_comment(pr_number: str, comment_body: str) -> None: + """Post a new comment or update existing one. + + Args: + pr_number: PR number + comment_body: Comment body text + + Raises: + subprocess.CalledProcessError: If gh command fails + """ + # Look for existing comment + existing_comment_id = find_existing_comment(pr_number) + + if existing_comment_id and existing_comment_id != "None": + update_existing_comment(existing_comment_id, comment_body) + else: + create_new_comment(pr_number, comment_body) + + print("Comment posted/updated successfully", file=sys.stderr) + + +def main() -> int: + """Main entry point.""" + parser = argparse.ArgumentParser( + description="Post or update PR comment with memory impact analysis" + ) + parser.add_argument("--pr-number", required=True, help="PR number") + parser.add_argument( + "--target-json", + required=True, + help="Path to target branch analysis JSON file", + ) + parser.add_argument( + "--pr-json", + required=True, + help="Path to PR branch analysis JSON file", + ) + + args = parser.parse_args() + + # Load analysis JSON files (all data comes from JSON for security) + target_data: dict | None = load_analysis_json(args.target_json) + if not target_data: + print("Error: Failed to load target analysis JSON", file=sys.stderr) + sys.exit(1) + + pr_data: dict | None = load_analysis_json(args.pr_json) + if not pr_data: + print("Error: Failed to load PR analysis JSON", file=sys.stderr) + sys.exit(1) + + # Extract detailed analysis if available + target_analysis: dict | None = None + pr_analysis: dict | None = None + target_symbols: dict | None = None + pr_symbols: dict | None = None + + if target_data.get("detailed_analysis"): + target_analysis = target_data["detailed_analysis"].get("components") + target_symbols = target_data["detailed_analysis"].get("symbols") + + if pr_data.get("detailed_analysis"): + pr_analysis = pr_data["detailed_analysis"].get("components") + pr_symbols = pr_data["detailed_analysis"].get("symbols") + + # Extract all values from JSON files (prevents shell injection from PR code) + components = target_data.get("components") + platform = target_data.get("platform") + target_ram = target_data.get("ram_bytes") + target_flash = target_data.get("flash_bytes") + pr_ram = pr_data.get("ram_bytes") + pr_flash = pr_data.get("flash_bytes") + + # Validate required fields and types + missing_fields: list[str] = [] + type_errors: list[str] = [] + + if components is None: + missing_fields.append("components") + elif not isinstance(components, list): + type_errors.append( + f"components must be a list, got {type(components).__name__}" + ) + else: + for idx, comp in enumerate(components): + if not isinstance(comp, str): + type_errors.append( + f"components[{idx}] must be a string, got {type(comp).__name__}" + ) + if platform is None: + missing_fields.append("platform") + elif not isinstance(platform, str): + type_errors.append(f"platform must be a string, got {type(platform).__name__}") + + if target_ram is None: + missing_fields.append("target.ram_bytes") + elif not isinstance(target_ram, int): + type_errors.append( + f"target.ram_bytes must be an integer, got {type(target_ram).__name__}" + ) + + if target_flash is None: + missing_fields.append("target.flash_bytes") + elif not isinstance(target_flash, int): + type_errors.append( + f"target.flash_bytes must be an integer, got {type(target_flash).__name__}" + ) + + if pr_ram is None: + missing_fields.append("pr.ram_bytes") + elif not isinstance(pr_ram, int): + type_errors.append( + f"pr.ram_bytes must be an integer, got {type(pr_ram).__name__}" + ) + + if pr_flash is None: + missing_fields.append("pr.flash_bytes") + elif not isinstance(pr_flash, int): + type_errors.append( + f"pr.flash_bytes must be an integer, got {type(pr_flash).__name__}" + ) + + if missing_fields or type_errors: + if missing_fields: + print( + f"Error: JSON files missing required fields: {', '.join(missing_fields)}", + file=sys.stderr, + ) + if type_errors: + print( + f"Error: Type validation failed: {'; '.join(type_errors)}", + file=sys.stderr, + ) + print(f"Target JSON keys: {list(target_data.keys())}", file=sys.stderr) + print(f"PR JSON keys: {list(pr_data.keys())}", file=sys.stderr) + sys.exit(1) + + # Create comment body + # Note: Memory totals (RAM/Flash) are summed across all builds if multiple were run. + comment_body = create_comment_body( + components=components, + platform=platform, + target_ram=target_ram, + target_flash=target_flash, + pr_ram=pr_ram, + pr_flash=pr_flash, + target_analysis=target_analysis, + pr_analysis=pr_analysis, + target_symbols=target_symbols, + pr_symbols=pr_symbols, + ) + + # Post or update comment + post_or_update_comment(args.pr_number, comment_body) + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/script/ci_memory_impact_extract.py b/script/ci_memory_impact_extract.py new file mode 100755 index 0000000000..77d59417e3 --- /dev/null +++ b/script/ci_memory_impact_extract.py @@ -0,0 +1,281 @@ +#!/usr/bin/env python3 +"""Extract memory usage statistics from ESPHome build output. + +This script parses the PlatformIO build output to extract RAM and flash +usage statistics for a compiled component. It's used by the CI workflow to +compare memory usage between branches. + +The script reads compile output from stdin and looks for the standard +PlatformIO output format: + RAM: [==== ] 36.1% (used 29548 bytes from 81920 bytes) + Flash: [=== ] 34.0% (used 348511 bytes from 1023984 bytes) + +Optionally performs detailed memory analysis if a build directory is provided. +""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path +import re +import sys + +# Add esphome to path +sys.path.insert(0, str(Path(__file__).parent.parent)) + +# pylint: disable=wrong-import-position +from esphome.analyze_memory import MemoryAnalyzer +from esphome.platformio_api import IDEData +from script.ci_helpers import write_github_output + +# Regex patterns for extracting memory usage from PlatformIO output +_RAM_PATTERN = re.compile(r"RAM:\s+\[.*?\]\s+\d+\.\d+%\s+\(used\s+(\d+)\s+bytes") +_FLASH_PATTERN = re.compile(r"Flash:\s+\[.*?\]\s+\d+\.\d+%\s+\(used\s+(\d+)\s+bytes") +_BUILD_PATH_PATTERN = re.compile(r"Build path: (.+)") + + +def extract_from_compile_output( + output_text: str, +) -> tuple[int | None, int | None, str | None]: + """Extract memory usage and build directory from PlatformIO compile output. + + Supports multiple builds (for component groups or isolated components). + When test_build_components.py creates multiple builds, this sums the + memory usage across all builds. + + Looks for lines like: + RAM: [==== ] 36.1% (used 29548 bytes from 81920 bytes) + Flash: [=== ] 34.0% (used 348511 bytes from 1023984 bytes) + + Also extracts build directory from lines like: + INFO Compiling app... Build path: /path/to/build + + Args: + output_text: Compile output text (may contain multiple builds) + + Returns: + Tuple of (total_ram_bytes, total_flash_bytes, build_dir) or (None, None, None) if not found + """ + # Find all RAM and Flash matches (may be multiple builds) + ram_matches = _RAM_PATTERN.findall(output_text) + flash_matches = _FLASH_PATTERN.findall(output_text) + + if not ram_matches or not flash_matches: + return None, None, None + + # Sum all builds (handles multiple component groups) + total_ram = sum(int(match) for match in ram_matches) + total_flash = sum(int(match) for match in flash_matches) + + # Extract build directory from ESPHome's explicit build path output + # Look for: INFO Compiling app... Build path: /path/to/build + # Note: Multiple builds reuse the same build path (each overwrites the previous) + build_dir = None + if match := _BUILD_PATH_PATTERN.search(output_text): + build_dir = match.group(1).strip() + + return total_ram, total_flash, build_dir + + +def run_detailed_analysis(build_dir: str) -> dict | None: + """Run detailed memory analysis on build directory. + + Args: + build_dir: Path to ESPHome build directory + + Returns: + Dictionary with analysis results or None if analysis fails + """ + build_path = Path(build_dir) + if not build_path.exists(): + print(f"Build directory not found: {build_dir}", file=sys.stderr) + return None + + # Find firmware.elf + elf_path = None + for elf_candidate in [ + build_path / "firmware.elf", + build_path / ".pioenvs" / build_path.name / "firmware.elf", + ]: + if elf_candidate.exists(): + elf_path = str(elf_candidate) + break + + if not elf_path: + print(f"firmware.elf not found in {build_dir}", file=sys.stderr) + return None + + # Find idedata.json - check multiple locations + device_name = build_path.name + idedata_candidates = [ + # In .pioenvs for test builds + build_path / ".pioenvs" / device_name / "idedata.json", + # In .esphome/idedata for regular builds + Path.home() / ".esphome" / "idedata" / f"{device_name}.json", + # Check parent directories for .esphome/idedata (for test_build_components) + build_path.parent.parent.parent / "idedata" / f"{device_name}.json", + ] + + idedata = None + for idedata_path in idedata_candidates: + if not idedata_path.exists(): + continue + try: + with open(idedata_path, encoding="utf-8") as f: + raw_data = json.load(f) + idedata = IDEData(raw_data) + print(f"Loaded idedata from: {idedata_path}", file=sys.stderr) + break + except (json.JSONDecodeError, OSError) as e: + print( + f"Warning: Failed to load idedata from {idedata_path}: {e}", + file=sys.stderr, + ) + + analyzer = MemoryAnalyzer(elf_path, idedata=idedata) + components = analyzer.analyze() + + # Convert to JSON-serializable format + result = { + "components": { + name: { + "text": mem.text_size, + "rodata": mem.rodata_size, + "data": mem.data_size, + "bss": mem.bss_size, + "flash_total": mem.flash_total, + "ram_total": mem.ram_total, + "symbol_count": mem.symbol_count, + } + for name, mem in components.items() + }, + "symbols": {}, + } + + # Build symbol map + for section in analyzer.sections.values(): + for symbol_name, size, _ in section.symbols: + if size > 0: + demangled = analyzer._demangle_symbol(symbol_name) + result["symbols"][demangled] = size + + return result + + +def main() -> int: + """Main entry point.""" + parser = argparse.ArgumentParser( + description="Extract memory usage from ESPHome build output" + ) + parser.add_argument( + "--output-env", + action="store_true", + help="Output to GITHUB_OUTPUT environment file", + ) + parser.add_argument( + "--build-dir", + help="Optional build directory for detailed memory analysis (overrides auto-detection)", + ) + parser.add_argument( + "--output-json", + help="Optional path to save detailed analysis JSON", + ) + parser.add_argument( + "--output-build-dir", + help="Optional path to write the detected build directory", + ) + + args = parser.parse_args() + + # Read compile output from stdin + compile_output = sys.stdin.read() + + # Extract memory usage and build directory + ram_bytes, flash_bytes, detected_build_dir = extract_from_compile_output( + compile_output + ) + + if ram_bytes is None or flash_bytes is None: + print("Failed to extract memory usage from compile output", file=sys.stderr) + print("Expected lines like:", file=sys.stderr) + print( + " RAM: [==== ] 36.1% (used 29548 bytes from 81920 bytes)", + file=sys.stderr, + ) + print( + " Flash: [=== ] 34.0% (used 348511 bytes from 1023984 bytes)", + file=sys.stderr, + ) + return 1 + + # Count how many builds were found + num_builds = len(_RAM_PATTERN.findall(compile_output)) + + if num_builds > 1: + print( + f"Found {num_builds} builds - summing memory usage across all builds", + file=sys.stderr, + ) + print( + "WARNING: Detailed analysis will only cover the last build", + file=sys.stderr, + ) + + print(f"Total RAM: {ram_bytes} bytes", file=sys.stderr) + print(f"Total Flash: {flash_bytes} bytes", file=sys.stderr) + + # Determine which build directory to use (explicit arg overrides auto-detection) + build_dir = args.build_dir or detected_build_dir + + if detected_build_dir: + print(f"Detected build directory: {detected_build_dir}", file=sys.stderr) + if num_builds > 1: + print( + f" (using last of {num_builds} builds for detailed analysis)", + file=sys.stderr, + ) + + # Write build directory to file if requested + if args.output_build_dir and build_dir: + build_dir_path = Path(args.output_build_dir) + build_dir_path.parent.mkdir(parents=True, exist_ok=True) + build_dir_path.write_text(build_dir) + print(f"Wrote build directory to {args.output_build_dir}", file=sys.stderr) + + # Run detailed analysis if build directory available + detailed_analysis = None + if build_dir: + print(f"Running detailed analysis on {build_dir}", file=sys.stderr) + detailed_analysis = run_detailed_analysis(build_dir) + + # Save JSON output if requested + if args.output_json: + output_data = { + "ram_bytes": ram_bytes, + "flash_bytes": flash_bytes, + "detailed_analysis": detailed_analysis, + } + + output_path = Path(args.output_json) + output_path.parent.mkdir(parents=True, exist_ok=True) + with open(output_path, "w", encoding="utf-8") as f: + json.dump(output_data, f, indent=2) + print(f"Saved analysis to {args.output_json}", file=sys.stderr) + + if args.output_env: + # Output to GitHub Actions + write_github_output( + { + "ram_usage": ram_bytes, + "flash_usage": flash_bytes, + } + ) + else: + print(f"{ram_bytes},{flash_bytes}") + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/script/clang-format b/script/clang-format index d62a5b59c7..028d752c55 100755 --- a/script/clang-format +++ b/script/clang-format @@ -31,7 +31,11 @@ def run_format(executable, args, queue, lock, failed_files): invocation.append(path) proc = subprocess.run( - invocation, capture_output=True, encoding="utf-8", check=False + invocation, + capture_output=True, + encoding="utf-8", + check=False, + close_fds=False, ) if proc.returncode != 0: with lock: diff --git a/script/clang-tidy b/script/clang-tidy index 2c4a2e36ac..142b616119 100755 --- a/script/clang-tidy +++ b/script/clang-tidy @@ -158,7 +158,11 @@ def run_tidy(executable, args, options, tmpdir, path_queue, lock, failed_files): invocation.extend(options) proc = subprocess.run( - invocation, capture_output=True, encoding="utf-8", check=False + invocation, + capture_output=True, + encoding="utf-8", + check=False, + close_fds=False, ) if proc.returncode != 0: with lock: @@ -320,9 +324,11 @@ def main(): print("Applying fixes ...") try: try: - subprocess.call(["clang-apply-replacements-18", tmpdir]) + subprocess.call( + ["clang-apply-replacements-18", tmpdir], close_fds=False + ) except FileNotFoundError: - subprocess.call(["clang-apply-replacements", tmpdir]) + subprocess.call(["clang-apply-replacements", tmpdir], close_fds=False) except FileNotFoundError: print( "Error please install clang-apply-replacements-18 or clang-apply-replacements.\n", diff --git a/script/clang_tidy_hash.py b/script/clang_tidy_hash.py index 19eb2a825e..d0d8438437 100755 --- a/script/clang_tidy_hash.py +++ b/script/clang_tidy_hash.py @@ -48,9 +48,10 @@ def parse_requirement_line(line: str) -> tuple[str, str] | None: return None -def get_clang_tidy_version_from_requirements() -> str: +def get_clang_tidy_version_from_requirements(repo_root: Path | None = None) -> str: """Get clang-tidy version from requirements_dev.txt""" - requirements_path = Path(__file__).parent.parent / "requirements_dev.txt" + repo_root = _ensure_repo_root(repo_root) + requirements_path = repo_root / "requirements_dev.txt" lines = read_file_lines(requirements_path) for line in lines: @@ -68,30 +69,49 @@ def read_file_bytes(path: Path) -> bytes: return f.read() -def calculate_clang_tidy_hash() -> str: +def get_repo_root() -> Path: + """Get the repository root directory.""" + return Path(__file__).parent.parent + + +def _ensure_repo_root(repo_root: Path | None) -> Path: + """Ensure repo_root is a Path, using default if None.""" + return repo_root if repo_root is not None else get_repo_root() + + +def calculate_clang_tidy_hash(repo_root: Path | None = None) -> str: """Calculate hash of clang-tidy configuration and version""" + repo_root = _ensure_repo_root(repo_root) + hasher = hashlib.sha256() # Hash .clang-tidy file - clang_tidy_path = Path(__file__).parent.parent / ".clang-tidy" + clang_tidy_path = repo_root / ".clang-tidy" content = read_file_bytes(clang_tidy_path) hasher.update(content) # Hash clang-tidy version from requirements_dev.txt - version = get_clang_tidy_version_from_requirements() + version = get_clang_tidy_version_from_requirements(repo_root) hasher.update(version.encode()) # Hash the entire platformio.ini file - platformio_path = Path(__file__).parent.parent / "platformio.ini" + platformio_path = repo_root / "platformio.ini" platformio_content = read_file_bytes(platformio_path) hasher.update(platformio_content) + # Hash sdkconfig.defaults file + sdkconfig_path = repo_root / "sdkconfig.defaults" + if sdkconfig_path.exists(): + sdkconfig_content = read_file_bytes(sdkconfig_path) + hasher.update(sdkconfig_content) + return hasher.hexdigest() -def read_stored_hash() -> str | None: +def read_stored_hash(repo_root: Path | None = None) -> str | None: """Read the stored hash from file""" - hash_file = Path(__file__).parent.parent / ".clang-tidy.hash" + repo_root = _ensure_repo_root(repo_root) + hash_file = repo_root / ".clang-tidy.hash" if hash_file.exists(): lines = read_file_lines(hash_file) return lines[0].strip() if lines else None @@ -104,9 +124,10 @@ def write_file_content(path: Path, content: str) -> None: f.write(content) -def write_hash(hash_value: str) -> None: +def write_hash(hash_value: str, repo_root: Path | None = None) -> None: """Write hash to file""" - hash_file = Path(__file__).parent.parent / ".clang-tidy.hash" + repo_root = _ensure_repo_root(repo_root) + hash_file = repo_root / ".clang-tidy.hash" # Strip any trailing newlines to ensure consistent formatting write_file_content(hash_file, hash_value.strip() + "\n") @@ -134,8 +155,28 @@ def main() -> None: stored_hash = read_stored_hash() if args.check: - # Exit 0 if full scan needed (hash changed or no hash file) - sys.exit(0 if current_hash != stored_hash else 1) + # Check if hash changed OR if .clang-tidy.hash was updated in this PR + # This is used in CI to determine if a full clang-tidy scan is needed + hash_changed = current_hash != stored_hash + + # Lazy import to avoid requiring dependencies that aren't needed for other modes + from helpers import changed_files # noqa: E402 + + hash_file_updated = ".clang-tidy.hash" in changed_files() + + # Exit 0 if full scan needed + sys.exit(0 if (hash_changed or hash_file_updated) else 1) + + elif args.verify: + # Verify that hash file is up to date with current configuration + # This is used in pre-commit and CI checks to ensure hash was updated + if current_hash != stored_hash: + print("ERROR: Clang-tidy configuration has changed but hash not updated!") + print(f"Expected: {current_hash}") + print(f"Found: {stored_hash}") + print("\nPlease run: script/clang_tidy_hash.py --update") + sys.exit(1) + print("Hash verification passed") elif args.update: write_hash(current_hash) @@ -151,15 +192,6 @@ def main() -> None: print("Clang-tidy hash unchanged") sys.exit(0) - elif args.verify: - if current_hash != stored_hash: - print("ERROR: Clang-tidy configuration has changed but hash not updated!") - print(f"Expected: {current_hash}") - print(f"Found: {stored_hash}") - print("\nPlease run: script/clang_tidy_hash.py --update") - sys.exit(1) - print("Hash verification passed") - else: print(f"Current hash: {current_hash}") print(f"Stored hash: {stored_hash}") diff --git a/script/cpp_unit_test.py b/script/cpp_unit_test.py new file mode 100755 index 0000000000..e97b5bd7b0 --- /dev/null +++ b/script/cpp_unit_test.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +import argparse +import hashlib +import os +from pathlib import Path +import subprocess +import sys + +from helpers import get_all_components, get_all_dependencies, root_path + +from esphome.__main__ import command_compile, parse_args +from esphome.config import validate_config +from esphome.core import CORE +from esphome.platformio_api import get_idedata + +# This must coincide with the version in /platformio.ini +PLATFORMIO_GOOGLE_TEST_LIB = "google/googletest@^1.15.2" + +# Path to /tests/components +COMPONENTS_TESTS_DIR: Path = Path(root_path) / "tests" / "components" + + +def hash_components(components: list[str]) -> str: + key = ",".join(components) + return hashlib.sha256(key.encode()).hexdigest()[:16] + + +def filter_components_without_tests(components: list[str]) -> list[str]: + """Filter out components that do not have a corresponding test file. + + This is done by checking if the component's directory contains at + least a .cpp file. + """ + filtered_components: list[str] = [] + for component in components: + test_dir = COMPONENTS_TESTS_DIR / component + if test_dir.is_dir() and any(test_dir.glob("*.cpp")): + filtered_components.append(component) + else: + print( + f"WARNING: No tests found for component '{component}', skipping.", + file=sys.stderr, + ) + return filtered_components + + +def create_test_config(config_name: str, includes: list[str]) -> dict: + """Create ESPHome test configuration for C++ unit tests. + + Args: + config_name: Unique name for this test configuration + includes: List of include folders for the test build + + Returns: + Configuration dict for ESPHome + """ + return { + "esphome": { + "name": config_name, + "friendly_name": "CPP Unit Tests", + "libraries": PLATFORMIO_GOOGLE_TEST_LIB, + "platformio_options": { + "build_type": "debug", + "build_unflags": [ + "-Os", # remove size-opt flag + ], + "build_flags": [ + "-Og", # optimize for debug + ], + "debug_build_flags": [ # only for debug builds + "-g3", # max debug info + "-ggdb3", + ], + }, + "includes": includes, + }, + "host": {}, + "logger": {"level": "DEBUG"}, + } + + +def run_tests(selected_components: list[str]) -> int: + # Skip tests on Windows + if os.name == "nt": + print("Skipping esphome tests on Windows", file=sys.stderr) + return 1 + + # Remove components that do not have tests + components = filter_components_without_tests(selected_components) + + if len(components) == 0: + print( + "No components specified or no tests found for the specified components.", + file=sys.stderr, + ) + return 0 + + components = sorted(components) + + # Obtain possible dependencies for the requested components: + components_with_dependencies = sorted(get_all_dependencies(set(components))) + + # Build a list of include folders, one folder per component containing tests. + # A special replacement main.cpp is located in /tests/components/main.cpp + includes: list[str] = ["main.cpp"] + components + + # Create a unique name for this config based on the actual components being tested + # to maximize cache during testing + config_name: str = "cpptests-" + hash_components(components) + + config = create_test_config(config_name, includes) + + CORE.config_path = COMPONENTS_TESTS_DIR / "dummy.yaml" + CORE.dashboard = None + + # Validate config will expand the above with defaults: + config = validate_config(config, {}) + + # Add all components and dependencies to the base configuration after validation, so their files + # are added to the build. + config.update({key: {} for key in components_with_dependencies}) + + print(f"Testing components: {', '.join(components)}") + CORE.config = config + args = parse_args(["program", "compile", str(CORE.config_path)]) + try: + exit_code: int = command_compile(args, config) + + if exit_code != 0: + print(f"Error compiling unit tests for {', '.join(components)}") + return exit_code + except Exception as e: + print( + f"Error compiling unit tests for {', '.join(components)}. Check path. : {e}" + ) + return 2 + + # After a successful compilation, locate the executable and run it: + idedata = get_idedata(config) + if idedata is None: + print("Cannot find executable") + return 1 + + program_path: str = idedata.raw["prog_path"] + run_cmd: list[str] = [program_path] + run_proc = subprocess.run(run_cmd, check=False) + return run_proc.returncode + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Run C++ unit tests for ESPHome components." + ) + parser.add_argument( + "components", + nargs="*", + help="List of components to test. Use --all to test all known components.", + ) + parser.add_argument("--all", action="store_true", help="Test all known components.") + + args = parser.parse_args() + + if args.all: + components: list[str] = get_all_components() + else: + components: list[str] = args.components + + sys.exit(run_tests(components)) + + +if __name__ == "__main__": + main() diff --git a/script/determine-jobs.py b/script/determine-jobs.py index e26bc29c2f..5cc3f2570a 100755 --- a/script/determine-jobs.py +++ b/script/determine-jobs.py @@ -10,7 +10,13 @@ what files have changed. It outputs JSON with the following structure: "clang_format": true/false, "python_linters": true/false, "changed_components": ["component1", "component2", ...], - "component_test_count": 5 + "component_test_count": 5, + "memory_impact": { + "should_run": "true/false", + "components": ["component1", "component2", ...], + "platform": "esp32-idf", + "use_merged_config": "true" + } } The CI workflow uses this information to: @@ -20,6 +26,7 @@ The CI workflow uses this information to: - Skip or run Python linters (ruff, flake8, pylint, pyupgrade) - Determine which components to test individually - Decide how to split component tests (if there are many) +- Run memory impact analysis whenever there are changed components (merged config), and also for core-only changes Usage: python script/determine-jobs.py [-b BRANCH] @@ -31,6 +38,9 @@ Options: from __future__ import annotations import argparse +from collections import Counter +from enum import StrEnum +from functools import cache import json import os from pathlib import Path @@ -40,14 +50,84 @@ from typing import Any from helpers import ( CPP_FILE_EXTENSIONS, - ESPHOME_COMPONENTS_PATH, + ESPHOME_TESTS_COMPONENTS_PATH, PYTHON_FILE_EXTENSIONS, changed_files, + core_changed, + filter_component_and_test_cpp_files, + filter_component_and_test_files, get_all_dependencies, + get_changed_components, + get_component_from_path, + get_component_test_files, get_components_from_integration_fixtures, - parse_list_components_output, + get_components_with_dependencies, + get_cpp_changed_components, + get_target_branch, + git_ls_files, + parse_test_filename, root_path, ) +from split_components_for_ci import create_intelligent_batches + +# Threshold for splitting clang-tidy jobs +# For small PRs (< 65 files), use nosplit for faster CI +# For large PRs (>= 65 files), use split for better parallelization +CLANG_TIDY_SPLIT_THRESHOLD = 65 + +# Component test batch size (weighted) +# Isolated components count as 10x, groupable components count as 1x +COMPONENT_TEST_BATCH_SIZE = 40 + + +class Platform(StrEnum): + """Platform identifiers for memory impact analysis.""" + + ESP8266_ARD = "esp8266-ard" + ESP32_IDF = "esp32-idf" + ESP32_C3_IDF = "esp32-c3-idf" + ESP32_C6_IDF = "esp32-c6-idf" + ESP32_S2_IDF = "esp32-s2-idf" + ESP32_S3_IDF = "esp32-s3-idf" + + +# Memory impact analysis constants +MEMORY_IMPACT_FALLBACK_COMPONENT = "api" # Representative component for core changes +MEMORY_IMPACT_FALLBACK_PLATFORM = Platform.ESP32_IDF # Most representative platform +MEMORY_IMPACT_MAX_COMPONENTS = 40 # Max components before results become nonsensical + +# Platform-specific components that can only be built on their respective platforms +# These components contain platform-specific code and cannot be cross-compiled +# Regular components (wifi, logger, api, etc.) are cross-platform and not listed here +PLATFORM_SPECIFIC_COMPONENTS = frozenset( + { + "esp32", # ESP32 platform implementation + "esp8266", # ESP8266 platform implementation + "rp2040", # Raspberry Pi Pico / RP2040 platform implementation + "bk72xx", # Beken BK72xx platform implementation (uses LibreTiny) + "rtl87xx", # Realtek RTL87xx platform implementation (uses LibreTiny) + "ln882x", # Winner Micro LN882x platform implementation (uses LibreTiny) + "host", # Host platform (for testing on development machine) + "nrf52", # Nordic nRF52 platform implementation + } +) + +# Platform preference order for memory impact analysis +# This order is used when no platform-specific hints are detected from filenames +# Priority rationale: +# 1. ESP32-C6 IDF - Newest platform, supports Thread/Zigbee +# 2. ESP8266 Arduino - Most memory constrained (best for detecting memory impact), +# 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 +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) + Platform.ESP32_IDF, # ESP32 IDF platform (primary ESP32 platform, most representative) + Platform.ESP32_C3_IDF, # ESP32-C3 IDF + Platform.ESP32_S2_IDF, # ESP32-S2 IDF + Platform.ESP32_S3_IDF, # ESP32-S3 IDF +] def should_run_integration_tests(branch: str | None = None) -> bool: @@ -90,10 +170,9 @@ def should_run_integration_tests(branch: str | None = None) -> bool: """ files = changed_files(branch) - # Check if any core files changed (esphome/core/*) - for file in files: - if file.startswith("esphome/core/"): - return True + if core_changed(files): + # If any core files changed, run integration tests + return True # Check if any integration test files changed if any("tests/integration" in file for file in files): @@ -105,16 +184,33 @@ def should_run_integration_tests(branch: str | None = None) -> bool: # Check if any required components changed for file in files: - if file.startswith(ESPHOME_COMPONENTS_PATH): - parts = file.split("/") - if len(parts) >= 3: - component = parts[2] - if component in all_required_components: - return True + component = get_component_from_path(file) + if component and component in all_required_components: + return True return False +@cache +def _is_clang_tidy_full_scan() -> bool: + """Check if clang-tidy configuration changed (requires full scan). + + Returns: + True if full scan is needed (hash changed), False otherwise. + """ + try: + result = subprocess.run( + [os.path.join(root_path, "script", "clang_tidy_hash.py"), "--check"], + capture_output=True, + check=False, + ) + # Exit 0 means hash changed (full scan needed) + return result.returncode == 0 + except Exception: + # If hash check fails, run full scan to be safe + return True + + def should_run_clang_tidy(branch: str | None = None) -> bool: """Determine if clang-tidy should run based on changed files. @@ -151,17 +247,7 @@ def should_run_clang_tidy(branch: str | None = None) -> bool: True if clang-tidy should run, False otherwise. """ # First check if clang-tidy configuration changed (full scan needed) - try: - result = subprocess.run( - [os.path.join(root_path, "script", "clang_tidy_hash.py"), "--check"], - capture_output=True, - check=False, - ) - # Exit 0 means hash changed (full scan needed) - if result.returncode == 0: - return True - except Exception: - # If hash check fails, run clang-tidy to be safe + if _is_clang_tidy_full_scan(): return True # Check if .clang-tidy.hash file itself was changed @@ -173,6 +259,22 @@ def should_run_clang_tidy(branch: str | None = None) -> bool: return _any_changed_file_endswith(branch, CPP_FILE_EXTENSIONS) +def count_changed_cpp_files(branch: str | None = None) -> int: + """Count the number of changed C++ files. + + This is used to determine whether to split clang-tidy jobs or run them as a single job. + For PRs with < 65 changed C++ files, running a single job is faster than splitting. + + Args: + branch: Branch to compare against. If None, uses default. + + Returns: + Number of changed C++ files. + """ + files = changed_files(branch) + return sum(1 for file in files if file.endswith(CPP_FILE_EXTENSIONS)) + + def should_run_clang_format(branch: str | None = None) -> bool: """Determine if clang-format should run based on changed files. @@ -207,11 +309,343 @@ def should_run_python_linters(branch: str | None = None) -> bool: return _any_changed_file_endswith(branch, PYTHON_FILE_EXTENSIONS) +def determine_cpp_unit_tests( + branch: str | None = None, +) -> tuple[bool, list[str]]: + """Determine if C++ unit tests should run based on changed files. + + This function is used by the CI workflow to skip C++ unit tests when + no relevant files have changed, saving CI time and resources. + + C++ unit tests will run when any of the following conditions are met: + + 1. Any C++ core source files changed (esphome/core/*), in which case + all cpp unit tests run. + 2. A test file for a component changed, which triggers tests for that + component. + 3. The code for a component changed, which triggers tests for that + component and all components that depend on it. + + Args: + branch: Branch to compare against. If None, uses default. + + Returns: + Tuple of (run_all, components) where: + - run_all: True if all tests should run, False otherwise + - components: List of specific components to test (empty if run_all) + """ + files = changed_files(branch) + if core_changed(files): + return (True, []) + + # Filter to only C++ files + cpp_files = list(filter(filter_component_and_test_cpp_files, files)) + return (False, get_cpp_changed_components(cpp_files)) + + def _any_changed_file_endswith(branch: str | None, extensions: tuple[str, ...]) -> bool: """Check if a changed file ends with any of the specified extensions.""" return any(file.endswith(extensions) for file in changed_files(branch)) +@cache +def _component_has_tests(component: str) -> bool: + """Check if a component has test files. + + Cached to avoid repeated filesystem operations for the same component. + + Args: + component: Component name to check + + Returns: + True if the component has test YAML files + """ + return bool(get_component_test_files(component, all_variants=True)) + + +def _select_platform_by_preference( + platforms: list[Platform] | set[Platform], +) -> Platform: + """Select the most preferred platform from a list/set based on MEMORY_IMPACT_PLATFORM_PREFERENCE. + + Args: + platforms: List or set of platforms to choose from + + Returns: + The most preferred platform (earliest in MEMORY_IMPACT_PLATFORM_PREFERENCE) + """ + return min(platforms, key=MEMORY_IMPACT_PLATFORM_PREFERENCE.index) + + +def _select_platform_by_count( + platform_counts: Counter[Platform], +) -> Platform: + """Select platform by count, using MEMORY_IMPACT_PLATFORM_PREFERENCE as tiebreaker. + + Args: + platform_counts: Counter mapping platforms to their counts + + Returns: + Platform with highest count, breaking ties by preference order + """ + return min( + platform_counts.keys(), + key=lambda p: ( + -platform_counts[p], # Negative to prefer higher counts + MEMORY_IMPACT_PLATFORM_PREFERENCE.index(p), + ), + ) + + +def _detect_platform_hint_from_filename(filename: str) -> Platform | None: + """Detect platform hint from filename patterns. + + Detects platform-specific files using patterns like: + - wifi_component_esp_idf.cpp, *_idf.h -> ESP32 IDF variants + - wifi_component_esp8266.cpp, *_esp8266.h -> ESP8266_ARD + - *_esp32*.cpp -> ESP32 IDF (generic) + - *_libretiny.cpp, *_retiny.* -> LibreTiny (not in preference list) + - *_pico.cpp, *_rp2040.* -> RP2040 (not in preference list) + + Args: + filename: File path to check + + Returns: + Platform enum if a specific platform is detected, None otherwise + """ + filename_lower = filename.lower() + + # ESP-IDF platforms (check specific variants first) + if "esp_idf" in filename_lower or "_idf" in filename_lower: + # Check for specific ESP32 variants + if "c6" in filename_lower or "esp32c6" in filename_lower: + return Platform.ESP32_C6_IDF + if "c3" in filename_lower or "esp32c3" in filename_lower: + return Platform.ESP32_C3_IDF + if "s2" in filename_lower or "esp32s2" in filename_lower: + return Platform.ESP32_S2_IDF + if "s3" in filename_lower or "esp32s3" in filename_lower: + return Platform.ESP32_S3_IDF + # Default to ESP32 IDF for generic esp_idf files + return Platform.ESP32_IDF + + # ESP8266 Arduino + if "esp8266" in filename_lower: + return Platform.ESP8266_ARD + + # Generic ESP32 (without _idf suffix, could be Arduino or shared code) + # Prefer IDF as it's the modern platform + if "esp32" in filename_lower: + return Platform.ESP32_IDF + + # LibreTiny and RP2040 are not in MEMORY_IMPACT_PLATFORM_PREFERENCE + # so we don't return them as hints + # if "retiny" in filename_lower or "libretiny" in filename_lower: + # return None # No specific LibreTiny platform preference + # if "pico" in filename_lower or "rp2040" in filename_lower: + # return None # No RP2040 platform preference + + return None + + +def detect_memory_impact_config( + branch: str | None = None, +) -> dict[str, Any]: + """Determine memory impact analysis configuration. + + Always runs memory impact analysis when there are changed components, + building a merged configuration with all changed components (like + test_build_components.py does) to get comprehensive memory analysis. + + When platform-specific files are detected (e.g., wifi_component_esp_idf.cpp), + prefers that platform for testing to ensure the most relevant memory analysis. + + For core C++ file changes without component changes, runs a fallback + analysis using a representative component to measure the impact. + + Args: + branch: Branch to compare against + + Returns: + Dictionary with memory impact analysis parameters: + - should_run: "true" or "false" + - components: list of component names to analyze + - platform: platform name for the merged build + - use_merged_config: "true" (always use merged config) + """ + # Skip memory impact analysis for release* or beta* branches + # These branches typically contain many merged changes from dev, and building + # all components at once would produce nonsensical memory impact results. + # Memory impact analysis is most useful for focused PRs targeting dev. + target_branch = get_target_branch() + if target_branch and ( + target_branch.startswith("release") or target_branch.startswith("beta") + ): + print( + f"Memory impact: Skipping analysis for target branch {target_branch} " + f"(would try to build all components at once, giving nonsensical results)", + file=sys.stderr, + ) + return {"should_run": "false"} + + # Get actually changed files (not dependencies) + files = changed_files(branch) + + # Find all changed components (excluding core) + # Also collect platform hints from platform-specific filenames + changed_component_set: set[str] = set() + has_core_cpp_changes = False + platform_hints: list[Platform] = [] + + for file in files: + component = get_component_from_path(file) + if component: + # Add all changed components, including base bus components + # Base bus components (uart, i2c, spi, etc.) should still be analyzed + # when directly changed, even though they're also used as dependencies + changed_component_set.add(component) + # Check if this is a platform-specific file + if platform_hint := _detect_platform_hint_from_filename(file): + platform_hints.append(platform_hint) + elif file.startswith("esphome/") and file.endswith(CPP_FILE_EXTENSIONS): + # Core ESPHome C++ files changed (not component-specific) + # Only C++ files affect memory usage + has_core_cpp_changes = True + + # If no components changed but core C++ changed, test representative component + force_fallback_platform = False + if not changed_component_set and has_core_cpp_changes: + print( + f"Memory impact: No components changed, but core C++ files changed. " + f"Testing {MEMORY_IMPACT_FALLBACK_COMPONENT} component on {MEMORY_IMPACT_FALLBACK_PLATFORM}.", + file=sys.stderr, + ) + changed_component_set.add(MEMORY_IMPACT_FALLBACK_COMPONENT) + force_fallback_platform = True # Use fallback platform (most representative) + elif not changed_component_set: + # No components and no core C++ changes + return {"should_run": "false"} + + # Find components that have tests and collect their supported platforms + components_with_tests: list[str] = [] + component_platforms_map: dict[ + str, set[Platform] + ] = {} # Track which platforms each component supports + + for component in sorted(changed_component_set): + # Look for test files on preferred platforms + test_files = get_component_test_files(component, all_variants=True) + if not test_files: + continue + + # Check if component has tests for any preferred platform + available_platforms = [ + platform + for test_file in test_files + if (platform := parse_test_filename(test_file)[1]) != "all" + and platform in MEMORY_IMPACT_PLATFORM_PREFERENCE + ] + + if not available_platforms: + continue + + component_platforms_map[component] = set(available_platforms) + components_with_tests.append(component) + + # If no components have tests, don't run memory impact + if not components_with_tests: + return {"should_run": "false"} + + # Skip memory impact analysis if too many components changed + # Building 40+ components at once produces nonsensical memory impact results + # This typically happens with large refactorings or batch updates + if len(components_with_tests) > MEMORY_IMPACT_MAX_COMPONENTS: + print( + f"Memory impact: Skipping analysis for {len(components_with_tests)} components " + f"(limit is {MEMORY_IMPACT_MAX_COMPONENTS}, would give nonsensical results)", + file=sys.stderr, + ) + return {"should_run": "false"} + + # Find common platforms supported by ALL components + # This ensures we can build all components together in a merged config + common_platforms = set(MEMORY_IMPACT_PLATFORM_PREFERENCE) + for component, platforms in component_platforms_map.items(): + common_platforms &= platforms + + # Select the most preferred platform from the common set + # Priority order: + # 1. Platform hints from filenames (e.g., wifi_component_esp_idf.cpp suggests ESP32_IDF) + # 2. Core changes use fallback platform (most representative of codebase) + # 3. Common platforms supported by all components + # 4. Most commonly supported platform + if platform_hints: + # Use most common platform hint that's also supported by all components + hint_counts = Counter(platform_hints) + # Filter to only hints that are in common_platforms (if any common platforms exist) + valid_hints = ( + [h for h in hint_counts if h in common_platforms] + if common_platforms + else list(hint_counts.keys()) + ) + if valid_hints: + platform = _select_platform_by_count( + Counter({p: hint_counts[p] for p in valid_hints}) + ) + elif common_platforms: + # Hints exist but none match common platforms, use common platform logic + platform = _select_platform_by_preference(common_platforms) + else: + # Use the most common hint even if it's not in common platforms + platform = _select_platform_by_count(hint_counts) + elif force_fallback_platform: + platform = MEMORY_IMPACT_FALLBACK_PLATFORM + elif common_platforms: + # Pick the most preferred platform that all components support + platform = _select_platform_by_preference(common_platforms) + else: + # No common platform - pick the most commonly supported platform + # Count how many components support each platform + platform_counts = Counter( + p for platforms in component_platforms_map.values() for p in platforms + ) + platform = _select_platform_by_count(platform_counts) + + # Filter out platform-specific components that are incompatible with selected platform + # Platform components (esp32, esp8266, rp2040, etc.) can only build on their own platform + # Other components (wifi, logger, etc.) are cross-platform and can build anywhere + compatible_components = [ + component + for component in components_with_tests + if component not in PLATFORM_SPECIFIC_COMPONENTS + or platform in component_platforms_map.get(component, set()) + ] + + # If no components are compatible with the selected platform, don't run + if not compatible_components: + return {"should_run": "false"} + + # Debug output + print("Memory impact analysis:", file=sys.stderr) + print(f" Changed components: {sorted(changed_component_set)}", file=sys.stderr) + print(f" Components with tests: {components_with_tests}", file=sys.stderr) + print( + f" Component platforms: {dict(sorted(component_platforms_map.items()))}", + file=sys.stderr, + ) + print(f" Platform hints from filenames: {platform_hints}", file=sys.stderr) + print(f" Common platforms: {sorted(common_platforms)}", file=sys.stderr) + print(f" Selected platform: {platform}", file=sys.stderr) + print(f" Compatible components: {compatible_components}", file=sys.stderr) + + return { + "should_run": "true", + "components": compatible_components, + "platform": platform, + "use_merged_config": "true", + } + + def main() -> None: """Main function that determines which CI jobs to run.""" parser = argparse.ArgumentParser( @@ -227,24 +661,146 @@ def main() -> None: run_clang_tidy = should_run_clang_tidy(args.branch) run_clang_format = should_run_clang_format(args.branch) run_python_linters = should_run_python_linters(args.branch) + changed_cpp_file_count = count_changed_cpp_files(args.branch) - # Get changed components using list-components.py for exact compatibility - script_path = Path(__file__).parent / "list-components.py" - cmd = [sys.executable, str(script_path), "--changed"] - if args.branch: - cmd.extend(["-b", args.branch]) + # Get changed components + # get_changed_components() returns: + # None: Core files changed (need full scan) + # []: No components changed + # [list]: Changed components (already includes dependencies) + changed_components_result = get_changed_components() - result = subprocess.run(cmd, capture_output=True, text=True, check=True) - changed_components = parse_list_components_output(result.stdout) + # Always analyze component files, even if core files changed + # This is needed for component testing and memory impact analysis + changed = changed_files(args.branch) + component_files = [f for f in changed if filter_component_and_test_files(f)] + + directly_changed_components = get_components_with_dependencies( + component_files, False + ) + + if changed_components_result is None: + # Core files changed - will trigger full clang-tidy scan + # But we still need to track changed components for testing and memory analysis + changed_components = get_components_with_dependencies(component_files, True) + is_core_change = True + else: + # Use the result from get_changed_components() which includes dependencies + changed_components = changed_components_result + is_core_change = False + + # Filter to only components that have test files + # Components without tests shouldn't generate CI test jobs + changed_components_with_tests = [ + component for component in changed_components if _component_has_tests(component) + ] + + # Get directly changed components with tests (for isolated testing) + # These will be tested WITHOUT --testing-mode in CI to enable full validation + # (pin conflicts, etc.) since they contain the actual changes being reviewed + directly_changed_with_tests = { + component + for component in directly_changed_components + if _component_has_tests(component) + } + + # Get dependency-only components (for grouped testing) + dependency_only_components = [ + component + for component in changed_components_with_tests + if component not in directly_changed_components + ] + + # Detect components for memory impact analysis (merged config) + memory_impact = detect_memory_impact_config(args.branch) + + # Determine clang-tidy mode based on actual files that will be checked + if run_clang_tidy: + # Full scan needed if: hash changed OR core files changed + is_full_scan = _is_clang_tidy_full_scan() or is_core_change + + if is_full_scan: + # Full scan checks all files - always use split mode for efficiency + clang_tidy_mode = "split" + files_to_check_count = -1 # Sentinel value for "all files" + else: + # Targeted scan - calculate actual files that will be checked + # This accounts for component dependencies, not just directly changed files + if changed_components: + # Count C++ files in all changed components (including dependencies) + all_cpp_files = list(git_ls_files(["*.cpp"]).keys()) + component_set = set(changed_components) + files_to_check_count = sum( + 1 + for f in all_cpp_files + if get_component_from_path(f) in component_set + ) + else: + # If no components changed, use the simple count of changed C++ files + files_to_check_count = changed_cpp_file_count + + if files_to_check_count < CLANG_TIDY_SPLIT_THRESHOLD: + clang_tidy_mode = "nosplit" + else: + clang_tidy_mode = "split" + else: + clang_tidy_mode = "disabled" + files_to_check_count = 0 # Build output + # Determine which C++ unit tests to run + cpp_run_all, cpp_components = determine_cpp_unit_tests(args.branch) + + # Split components into batches for CI testing + # This intelligently groups components with similar bus configurations + component_test_batches: list[str] + if changed_components_with_tests: + tests_dir = Path(root_path) / ESPHOME_TESTS_COMPONENTS_PATH + + # For beta/release branches, group all components for faster CI + # (no isolation, all components are groupable) + target_branch = get_target_branch() + is_release_branch = target_branch and ( + target_branch.startswith("release") or target_branch.startswith("beta") + ) + + if is_release_branch: + # For beta/release: Don't isolate any components - group everything + # This allows components to be merged into single builds + batch_directly_changed = set() # Empty set - no isolation + else: + # Normal PR: only directly changed components are isolated + batch_directly_changed = directly_changed_with_tests + + batches, _ = create_intelligent_batches( + components=changed_components_with_tests, + tests_dir=tests_dir, + batch_size=COMPONENT_TEST_BATCH_SIZE, + directly_changed=batch_directly_changed, + ) + # Convert batches to space-separated strings for CI matrix + component_test_batches = [" ".join(batch) for batch in batches] + else: + component_test_batches = [] + output: dict[str, Any] = { "integration_tests": run_integration, "clang_tidy": run_clang_tidy, + "clang_tidy_mode": clang_tidy_mode, "clang_format": run_clang_format, "python_linters": run_python_linters, "changed_components": changed_components, - "component_test_count": len(changed_components), + "changed_components_with_tests": changed_components_with_tests, + "directly_changed_components_with_tests": list(directly_changed_with_tests), + "dependency_only_components_with_tests": dependency_only_components, + "component_test_count": len(changed_components_with_tests), + "directly_changed_count": len(directly_changed_with_tests), + "dependency_only_count": len(dependency_only_components), + "changed_cpp_file_count": changed_cpp_file_count, + "memory_impact": memory_impact, + "cpp_unit_tests_run_all": cpp_run_all, + "cpp_unit_tests_components": cpp_components, + "component_test_batches": component_test_batches, } # Output as JSON diff --git a/script/extract_automations.py b/script/extract_automations.py index 943eb7110a..4e650ce25f 100755 --- a/script/extract_automations.py +++ b/script/extract_automations.py @@ -2,19 +2,14 @@ import json -from helpers import git_ls_files +from helpers import get_all_component_files, get_components_with_dependencies from esphome.automation import ACTION_REGISTRY, CONDITION_REGISTRY from esphome.pins import PIN_SCHEMA_REGISTRY -list_components = __import__("list-components") - - if __name__ == "__main__": - files = git_ls_files() - files = filter(list_components.filter_component_files, files) - - components = list_components.get_components(files, True) + files = get_all_component_files() + components = get_components_with_dependencies(files, True) dump = { "actions": sorted(list(ACTION_REGISTRY.keys())), diff --git a/script/generate-esp32-boards.py b/script/generate-esp32-boards.py index 3f444ed455..81b78b04be 100755 --- a/script/generate-esp32-boards.py +++ b/script/generate-esp32-boards.py @@ -1,14 +1,19 @@ #!/usr/bin/env python3 +import argparse import json -import os +from pathlib import Path import subprocess +import sys import tempfile -from esphome.components.esp32 import ESP_IDF_PLATFORM_VERSION as ver +from esphome.components.esp32 import PLATFORM_VERSION_LOOKUP +from esphome.helpers import write_file_if_changed +ver = PLATFORM_VERSION_LOOKUP["recommended"] version_str = f"{ver.major}.{ver.minor:02d}.{ver.patch:02d}" -print(f"ESP32 Platform Version: {version_str}") +root = Path(__file__).parent.parent +boards_file_path = root / "esphome" / "components" / "esp32" / "boards.py" def get_boards(): @@ -17,6 +22,9 @@ def get_boards(): [ "git", "clone", + "-q", + "-c", + "advice.detachedHead=false", "--depth", "1", "--branch", @@ -26,16 +34,14 @@ def get_boards(): ], check=True, ) - boards_file = os.path.join(tempdir, "boards") + boards_directory = Path(tempdir) / "boards" boards = {} - for fname in os.listdir(boards_file): - if not fname.endswith(".json"): - continue - with open(os.path.join(boards_file, fname), encoding="utf-8") as f: + for fname in boards_directory.glob("*.json"): + with fname.open(encoding="utf-8") as f: board_info = json.load(f) mcu = board_info["build"]["mcu"] name = board_info["name"] - board = fname[:-5] + board = fname.stem variant = mcu.upper() boards[board] = { "name": name, @@ -47,33 +53,47 @@ def get_boards(): TEMPLATE = """ "%s": { "name": "%s", "variant": %s, - }, -""" + },""" -def main(): +def main(check: bool): boards = get_boards() # open boards.py, delete existing BOARDS variable and write the new boards dict - boards_file_path = os.path.join( - os.path.dirname(__file__), "..", "esphome", "components", "esp32", "boards.py" - ) - with open(boards_file_path, encoding="UTF-8") as f: - lines = f.readlines() + existing_content = boards_file_path.read_text(encoding="UTF-8") - with open(boards_file_path, "w", encoding="UTF-8") as f: - for line in lines: - if line.startswith("BOARDS = {"): - f.write("BOARDS = {\n") - f.writelines( - TEMPLATE % (board, info["name"], info["variant"]) - for board, info in sorted(boards.items()) - ) - f.write("}\n") - break + parts: list[str] = [] + for line in existing_content.splitlines(): + if line == "BOARDS = {": + parts.append(line) + parts.extend( + TEMPLATE % (board, info["name"], info["variant"]) + for board, info in sorted(boards.items()) + ) + parts.append("}") + parts.append("# DO NOT ADD ANYTHING BELOW THIS LINE") + break - f.write(line) + parts.append(line) + + parts.append("") + content = "\n".join(parts) + + if check: + if existing_content != content: + print("boards.py file is not up to date.") + print("Please run `script/generate-esp32-boards.py`") + sys.exit(1) + print("boards.py file is up to date") + elif write_file_if_changed(boards_file_path, content): + print("ESP32 boards updated successfully.") if __name__ == "__main__": - main() - print("ESP32 boards updated successfully.") + parser = argparse.ArgumentParser() + parser.add_argument( + "--check", + help="Check if the boards.py file is up to date.", + action="store_true", + ) + args = parser.parse_args() + main(args.check) diff --git a/script/helpers.py b/script/helpers.py index b346f3a461..1039ef39ac 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -1,12 +1,15 @@ from __future__ import annotations +from collections.abc import Callable from functools import cache +import hashlib import json import os import os.path from pathlib import Path import re import subprocess +import sys import time from typing import Any @@ -23,12 +26,37 @@ CPP_FILE_EXTENSIONS = (".cpp", ".h", ".hpp", ".cc", ".cxx", ".c", ".tcc") # Python file extensions PYTHON_FILE_EXTENSIONS = (".py", ".pyi") +# Combined C++ and Python file extensions for convenience +CPP_AND_PYTHON_FILE_EXTENSIONS = (*CPP_FILE_EXTENSIONS, *PYTHON_FILE_EXTENSIONS) + # YAML file extensions YAML_FILE_EXTENSIONS = (".yaml", ".yml") # Component path prefix ESPHOME_COMPONENTS_PATH = "esphome/components/" +# Test components path prefix +ESPHOME_TESTS_COMPONENTS_PATH = "tests/components/" + +# Tuple of component and test paths for efficient startswith checks +COMPONENT_AND_TESTS_PATHS = (ESPHOME_COMPONENTS_PATH, ESPHOME_TESTS_COMPONENTS_PATH) + +# Base bus components - these ARE the bus implementations and should not +# be flagged as needing migration since they are the platform/base components +BASE_BUS_COMPONENTS = { + "i2c", + "spi", + "uart", + "modbus", + "canbus", + "remote_transmitter", + "remote_receiver", +} + +# Cache version for components graph +# Increment this when the cache format or graph building logic changes +COMPONENTS_GRAPH_CACHE_VERSION = 1 + def parse_list_components_output(output: str) -> list[str]: """Parse the output from list-components.py script. @@ -46,16 +74,81 @@ def parse_list_components_output(output: str) -> list[str]: return [c.strip() for c in output.strip().split("\n") if c.strip()] +def parse_test_filename(test_file: Path) -> tuple[str, str]: + """Parse test filename to extract test name and platform. + + Test files follow the naming pattern: test..yaml or test-..yaml + + Args: + test_file: Path to test file + + Returns: + Tuple of (test_name, platform) + """ + parts = test_file.stem.split(".") + if len(parts) == 2: + return parts[0], parts[1] # test, platform + return parts[0], "all" + + +def get_component_from_path(file_path: str) -> str | None: + """Extract component name from a file path. + + Args: + file_path: Path to a file (e.g., "esphome/components/wifi/wifi.cpp" + or "tests/components/uart/test.esp32-idf.yaml") + + Returns: + Component name if path is in components or tests directory, None otherwise + """ + if file_path.startswith(ESPHOME_COMPONENTS_PATH) or file_path.startswith( + ESPHOME_TESTS_COMPONENTS_PATH + ): + parts = file_path.split("/") + if len(parts) >= 3 and parts[2]: + # Verify that parts[2] is actually a component directory, not a file + # like .gitignore or README.md in the components directory itself + component_name = parts[2] + if "." not in component_name: + return component_name + return None + + +def get_component_test_files( + component: str, *, all_variants: bool = False +) -> list[Path]: + """Get test files for a component. + + Args: + component: Component name (e.g., "wifi") + all_variants: If True, returns all test files including variants (test-*.yaml). + If False, returns only base test files (test.*.yaml). + Default is False. + + Returns: + List of test file paths for the component, or empty list if none exist + """ + tests_dir = Path(root_path) / "tests" / "components" / component + if not tests_dir.exists(): + return [] + + if all_variants: + # Match both test.*.yaml and test-*.yaml patterns + return list(tests_dir.glob("test[.-]*.yaml")) + # Match only test.*.yaml (base tests) + return list(tests_dir.glob("test.*.yaml")) + + def styled(color: str | tuple[str, ...], msg: str, reset: bool = True) -> str: prefix = "".join(color) if isinstance(color, tuple) else color suffix = colorama.Style.RESET_ALL if reset else "" return prefix + msg + suffix -def print_error_for_file(file: str, body: str | None) -> None: +def print_error_for_file(file: str | Path, body: str | None) -> None: print( styled(colorama.Fore.GREEN, "### File ") - + styled((colorama.Fore.GREEN, colorama.Style.BRIGHT), file) + + styled((colorama.Fore.GREEN, colorama.Style.BRIGHT), str(file)) ) print() if body is not None: @@ -103,6 +196,20 @@ def splitlines_no_ends(string: str) -> list[str]: return [s.strip() for s in string.splitlines()] +@cache +def _get_github_event_data() -> dict | None: + """Read and parse GitHub event file (cached). + + Returns: + Parsed event data dictionary, or None if not available + """ + github_event_path = os.environ.get("GITHUB_EVENT_PATH") + if github_event_path and os.path.exists(github_event_path): + with open(github_event_path) as f: + return json.load(f) + return None + + def _get_pr_number_from_github_env() -> str | None: """Extract PR number from GitHub environment variables. @@ -115,13 +222,30 @@ def _get_pr_number_from_github_env() -> str | None: return github_ref.split("/pull/")[1].split("/")[0] # Fallback to GitHub event file - github_event_path = os.environ.get("GITHUB_EVENT_PATH") - if github_event_path and os.path.exists(github_event_path): - with open(github_event_path) as f: - event_data = json.load(f) - pr_data = event_data.get("pull_request", {}) - if pr_number := pr_data.get("number"): - return str(pr_number) + if event_data := _get_github_event_data(): + pr_data = event_data.get("pull_request", {}) + if pr_number := pr_data.get("number"): + return str(pr_number) + + return None + + +def get_target_branch() -> str | None: + """Get the target branch from GitHub environment variables. + + Returns: + Target branch name (e.g., "dev", "release", "beta"), or None if not in PR context + """ + # First try GITHUB_BASE_REF (set for pull_request events) + if base_ref := os.environ.get("GITHUB_BASE_REF"): + return base_ref + + # Fallback to GitHub event file + if event_data := _get_github_event_data(): + pr_data = event_data.get("pull_request", {}) + base_data = pr_data.get("base", {}) + if ref := base_data.get("ref"): + return ref return None @@ -139,9 +263,24 @@ def _get_changed_files_github_actions() -> list[str] | None: if event_name == "pull_request": pr_number = _get_pr_number_from_github_env() if pr_number: - # Use GitHub CLI to get changed files directly + # Try gh pr diff first (faster for small PRs) cmd = ["gh", "pr", "diff", pr_number, "--name-only"] - return _get_changed_files_from_command(cmd) + try: + return _get_changed_files_from_command(cmd) + except Exception as e: + # If it fails due to the 300 file limit, use the API method + if "maximum" in str(e) and "files" in str(e): + cmd = [ + "gh", + "api", + f"repos/esphome/esphome/pulls/{pr_number}/files", + "--paginate", + "--jq", + ".[].filename", + ] + return _get_changed_files_from_command(cmd) + # Re-raise for other errors + raise # For pushes (including squash-and-merge) elif event_name == "push": @@ -218,7 +357,10 @@ def get_changed_components() -> list[str] | None: for f in changed ) if core_cpp_changed: - print("Core C++/header files changed - will run full clang-tidy scan") + print( + "Core C++/header files changed - will run full clang-tidy scan", + file=sys.stderr, + ) return None # Use list-components.py to get changed components @@ -232,7 +374,10 @@ def get_changed_components() -> list[str] | None: return parse_list_components_output(result.stdout) except subprocess.CalledProcessError: # If the script fails, fall back to full scan - print("Could not determine changed components - will run full clang-tidy scan") + print( + "Could not determine changed components - will run full clang-tidy scan", + file=sys.stderr, + ) return None @@ -284,14 +429,14 @@ def _filter_changed_ci(files: list[str]) -> list[str]: if f in changed and not f.startswith(ESPHOME_COMPONENTS_PATH) ] if not files: - print("No files changed") + print("No files changed", file=sys.stderr) return files # Scenario 3: Specific components changed # Action: Check ALL files in each changed component # Convert component list to set for O(1) lookups component_set = set(components) - print(f"Changed components: {', '.join(sorted(components))}") + print(f"Changed components: {', '.join(sorted(components))}", file=sys.stderr) # The 'files' parameter contains ALL files in the codebase that clang-tidy would check. # We filter this down to only files in the changed components. @@ -299,11 +444,9 @@ def _filter_changed_ci(files: list[str]) -> list[str]: # because changes in one file can affect other files in the same component. filtered_files = [] for f in files: - if f.startswith(ESPHOME_COMPONENTS_PATH): - # Check if file belongs to any of the changed components - parts = f.split("/") - if len(parts) >= 3 and parts[2] in component_set: - filtered_files.append(f) + component = get_component_from_path(f) + if component and component in component_set: + filtered_files.append(f) return filtered_files @@ -498,7 +641,7 @@ def get_all_dependencies(component_names: set[str]) -> set[str]: # Set up fake config path for component loading root = Path(__file__).parent.parent - CORE.config_path = str(root) + CORE.config_path = root CORE.data[KEY_CORE] = {} # Keep finding dependencies until no new ones are found @@ -514,7 +657,16 @@ def get_all_dependencies(component_names: set[str]) -> set[str]: new_components.update(dep.split(".")[0] for dep in comp.dependencies) # Add auto_load components - new_components.update(comp.auto_load) + auto_load = comp.auto_load + if callable(auto_load): + import inspect + + if inspect.signature(auto_load).parameters: + auto_load = auto_load(None) + else: + auto_load = auto_load() + + new_components.update(auto_load) # Check if we found any new components new_components -= all_components @@ -538,7 +690,7 @@ def get_components_from_integration_fixtures() -> set[str]: fixtures_dir = Path(__file__).parent.parent / "tests" / "integration" / "fixtures" for yaml_file in fixtures_dir.glob("*.yaml"): - config: dict[str, any] | None = yaml_util.load_yaml(str(yaml_file)) + config: dict[str, any] | None = yaml_util.load_yaml(yaml_file) if not config: continue @@ -555,3 +707,373 @@ def get_components_from_integration_fixtures() -> set[str]: components.add(item["platform"]) return components + + +def filter_component_and_test_files(file_path: str) -> bool: + """Check if a file path is a component or test file. + + Args: + file_path: Path to check + + Returns: + True if the file is in a component or test directory + """ + return file_path.startswith(COMPONENT_AND_TESTS_PATHS) or ( + file_path.startswith(ESPHOME_TESTS_COMPONENTS_PATH) + and file_path.endswith(YAML_FILE_EXTENSIONS) + ) + + +def filter_component_and_test_cpp_files(file_path: str) -> bool: + """Check if a file is a C++ source file in component or test directories. + + Args: + file_path: Path to check + + Returns: + True if the file is a C++ source/header file in component or test directories + """ + return file_path.endswith(CPP_FILE_EXTENSIONS) and file_path.startswith( + COMPONENT_AND_TESTS_PATHS + ) + + +def extract_component_names_from_files(files: list[str]) -> list[str]: + """Extract unique component names from a list of file paths. + + Args: + files: List of file paths + + Returns: + List of unique component names (preserves order) + """ + return list( + dict.fromkeys(comp for file in files if (comp := get_component_from_path(file))) + ) + + +def add_item_to_components_graph( + components_graph: dict[str, list[str]], parent: str, child: str +) -> None: + """Add a dependency relationship to the components graph. + + Args: + components_graph: Graph mapping parent components to their children + parent: Parent component name + child: Child component name (dependent) + """ + if not parent.startswith("__") and parent != child: + if parent not in components_graph: + components_graph[parent] = [] + if child not in components_graph[parent]: + components_graph[parent].append(child) + + +def resolve_auto_load( + auto_load: list[str] | Callable[[], list[str]] | Callable[[dict | None], list[str]], + config: dict | None = None, +) -> list[str]: + """Resolve AUTO_LOAD to a list, handling callables with or without config parameter. + + Args: + auto_load: The AUTO_LOAD value (list or callable) + config: Optional config to pass to callable AUTO_LOAD functions + + Returns: + List of component names to auto-load + """ + if not callable(auto_load): + return auto_load + + import inspect + + if inspect.signature(auto_load).parameters: + return auto_load(config) + return auto_load() + + +@cache +def get_components_graph_cache_key() -> str: + """Generate cache key based on all component Python file hashes. + + Uses git ls-files with sha1 hashes to generate a stable cache key that works + across different machines and CI runs. This is faster and more reliable than + reading file contents or using modification times. + + Returns: + SHA256 hex string uniquely identifying the current component state + """ + + # Use git ls-files -s to get sha1 hashes of all component Python files + # Format: + # This is fast and works consistently across CI and local dev + # We hash all .py files because AUTO_LOAD, DEPENDENCIES, etc. can be defined + # in any Python file, not just __init__.py + cmd = ["git", "ls-files", "-s", "esphome/components/**/*.py"] + result = subprocess.run( + cmd, capture_output=True, text=True, check=True, cwd=root_path, close_fds=False + ) + + # Hash the git output (includes file paths and their sha1 hashes) + # This changes only when component Python files actually change + hasher = hashlib.sha256() + hasher.update(result.stdout.encode()) + + return hasher.hexdigest() + + +def create_components_graph() -> dict[str, list[str]]: + """Create a graph of component dependencies (cached). + + This function is expensive (5-6 seconds) because it imports all ESPHome components + to extract their DEPENDENCIES and AUTO_LOAD metadata. The result is cached based + on component file modification times, so unchanged components don't trigger a rebuild. + + Returns: + Dictionary mapping parent components to their children (dependencies) + """ + # Check cache first - use fixed filename since GitHub Actions cache doesn't support wildcards + cache_file = Path(temp_folder) / "components_graph.json" + + if cache_file.exists(): + try: + cached_data = json.loads(cache_file.read_text()) + except (OSError, json.JSONDecodeError): + # Cache file corrupted or unreadable, rebuild + pass + else: + # Verify cache version matches + if cached_data.get("_version") == COMPONENTS_GRAPH_CACHE_VERSION: + # Verify cache is for current component state + cache_key = get_components_graph_cache_key() + if cached_data.get("_cache_key") == cache_key: + return cached_data.get("graph", {}) + # Cache key mismatch - stale cache, rebuild + # Cache version mismatch - incompatible format, rebuild + + from esphome import const + from esphome.core import CORE + from esphome.loader import ComponentManifest, get_component, get_platform + + # The root directory of the repo + root = Path(root_path) + components_dir = root / ESPHOME_COMPONENTS_PATH + # Fake some directory so that get_component works + CORE.config_path = root + # Various configuration to capture different outcomes used by `AUTO_LOAD` function. + KEY_CORE = const.KEY_CORE + KEY_TARGET_FRAMEWORK = const.KEY_TARGET_FRAMEWORK + KEY_TARGET_PLATFORM = const.KEY_TARGET_PLATFORM + PLATFORM_ESP32 = const.PLATFORM_ESP32 + PLATFORM_ESP8266 = const.PLATFORM_ESP8266 + + TARGET_CONFIGURATIONS = [ + {KEY_TARGET_FRAMEWORK: None, KEY_TARGET_PLATFORM: None}, + {KEY_TARGET_FRAMEWORK: "arduino", KEY_TARGET_PLATFORM: None}, + {KEY_TARGET_FRAMEWORK: "esp-idf", KEY_TARGET_PLATFORM: None}, + {KEY_TARGET_FRAMEWORK: None, KEY_TARGET_PLATFORM: PLATFORM_ESP32}, + {KEY_TARGET_FRAMEWORK: None, KEY_TARGET_PLATFORM: PLATFORM_ESP8266}, + ] + CORE.data[KEY_CORE] = TARGET_CONFIGURATIONS[0] + + components_graph = {} + platforms = [] + components: list[tuple[ComponentManifest, str, Path]] = [] + + for path in components_dir.iterdir(): + if not path.is_dir(): + continue + if not (path / "__init__.py").is_file(): + continue + name = path.name + comp = get_component(name) + if comp is None: + raise RuntimeError( + f"Cannot find component {name}. Make sure current path is pip installed ESPHome" + ) + + components.append((comp, name, path)) + if comp.is_platform_component: + platforms.append(name) + + platforms = set(platforms) + + for comp, name, path in components: + for dependency in comp.dependencies: + add_item_to_components_graph( + components_graph, dependency.split(".")[0], name + ) + + for target_config in TARGET_CONFIGURATIONS: + CORE.data[KEY_CORE] = target_config + for item in resolve_auto_load(comp.auto_load, config=None): + add_item_to_components_graph(components_graph, item, name) + # restore config + CORE.data[KEY_CORE] = TARGET_CONFIGURATIONS[0] + + for platform_path in path.iterdir(): + platform_name = platform_path.stem + if platform_name == name or platform_name not in platforms: + continue + platform = get_platform(platform_name, name) + if platform is None: + continue + + add_item_to_components_graph(components_graph, platform_name, name) + + for dependency in platform.dependencies: + add_item_to_components_graph( + components_graph, dependency.split(".")[0], name + ) + + for target_config in TARGET_CONFIGURATIONS: + CORE.data[KEY_CORE] = target_config + for item in resolve_auto_load(platform.auto_load, config={}): + add_item_to_components_graph(components_graph, item, name) + # restore config + CORE.data[KEY_CORE] = TARGET_CONFIGURATIONS[0] + + # Save to cache with version and cache key for validation + cache_data = { + "_version": COMPONENTS_GRAPH_CACHE_VERSION, + "_cache_key": get_components_graph_cache_key(), + "graph": components_graph, + } + cache_file.parent.mkdir(exist_ok=True) + cache_file.write_text(json.dumps(cache_data)) + + return components_graph + + +def find_children_of_component( + components_graph: dict[str, list[str]], component_name: str, depth: int = 0 +) -> list[str]: + """Find all components that depend on the given component (recursively). + + Args: + components_graph: Graph mapping parent components to their children + component_name: Component name to find children for + depth: Current recursion depth (max 10) + + Returns: + List of all dependent component names (may contain duplicates removed at end) + """ + if component_name not in components_graph: + return [] + + children = [] + + for child in components_graph[component_name]: + children.append(child) + if depth < 10: + children.extend( + find_children_of_component(components_graph, child, depth + 1) + ) + # Remove duplicate values + return list(set(children)) + + +def get_components_with_dependencies( + files: list[str], get_dependencies: bool = False +) -> list[str]: + """Get component names from files, optionally including their dependencies. + + Args: + files: List of file paths + get_dependencies: If True, include all dependent components + + Returns: + Sorted list of component names + """ + components = extract_component_names_from_files(files) + + if get_dependencies: + components_graph = create_components_graph() + + all_components = components.copy() + for c in components: + all_components.extend(find_children_of_component(components_graph, c)) + # Remove duplicate values + all_changed_components = list(set(all_components)) + + return sorted(all_changed_components) + + return sorted(components) + + +def get_all_component_files() -> list[str]: + """Get all component and test files from git. + + Returns: + List of all component and test file paths + """ + files = git_ls_files() + return list(filter(filter_component_and_test_files, files)) + + +def get_all_components() -> list[str]: + """Get all component names. + + This function uses git to find all component files and extracts the component names. + It returns the same list as calling list-components.py without arguments. + + Returns: + List of all component names + """ + return get_components_with_dependencies(get_all_component_files(), False) + + +def core_changed(files: list[str]) -> bool: + """Check if any core C++ or Python files have changed. + + Args: + files: List of file paths to check + + Returns: + True if any core C++ or Python files have changed + """ + return any( + f.startswith("esphome/core/") and f.endswith(CPP_AND_PYTHON_FILE_EXTENSIONS) + for f in files + ) + + +def get_cpp_changed_components(files: list[str]) -> list[str]: + """Get components that have changed C++ files or tests. + + This function analyzes a list of changed files and determines which components + are affected. It handles two scenarios: + + 1. Test files changed (tests/components//*.cpp): + - Adds the component to the affected list + - Only that component needs to be tested + + 2. Component C++ files changed (esphome/components//*): + - Adds the component to the affected list + - Also adds all components that depend on this component (recursively) + - This ensures that changes propagate to dependent components + + Args: + files: List of file paths to analyze (should be C++ files) + + Returns: + Sorted list of component names that need C++ unit tests run + """ + components_graph = create_components_graph() + affected: set[str] = set() + for file in files: + if not file.endswith(CPP_FILE_EXTENSIONS): + continue + if file.startswith(ESPHOME_TESTS_COMPONENTS_PATH): + parts = file.split("/") + if len(parts) >= 4: + component_dir = Path(ESPHOME_TESTS_COMPONENTS_PATH) / parts[2] + if component_dir.is_dir(): + affected.add(parts[2]) + elif file.startswith(ESPHOME_COMPONENTS_PATH): + parts = file.split("/") + if len(parts) >= 4: + component = parts[2] + affected.update(find_children_of_component(components_graph, component)) + affected.add(component) + return sorted(affected) diff --git a/script/helpers_zephyr.py b/script/helpers_zephyr.py index 922f1171b4..f72b335e64 100644 --- a/script/helpers_zephyr.py +++ b/script/helpers_zephyr.py @@ -25,6 +25,7 @@ int main() { return 0;} Path(zephyr_dir / "prj.conf").write_text( """ CONFIG_NEWLIB_LIBC=y +CONFIG_BT=y CONFIG_ADC=y """, encoding="utf-8", diff --git a/script/list-components.py b/script/list-components.py index 66212f44e7..31a1609f88 100755 --- a/script/list-components.py +++ b/script/list-components.py @@ -1,158 +1,14 @@ #!/usr/bin/env python3 import argparse -from pathlib import Path -import sys -from helpers import changed_files, git_ls_files - -from esphome.const import ( - KEY_CORE, - KEY_TARGET_FRAMEWORK, - KEY_TARGET_PLATFORM, - PLATFORM_ESP32, - PLATFORM_ESP8266, +from helpers import ( + changed_files, + filter_component_and_test_cpp_files, + filter_component_and_test_files, + get_all_component_files, + get_components_with_dependencies, + get_cpp_changed_components, ) -from esphome.core import CORE -from esphome.loader import get_component, get_platform - - -def filter_component_files(str): - return str.startswith("esphome/components/") | str.startswith("tests/components/") - - -def get_all_component_files() -> list[str]: - """Get all component files from git.""" - files = git_ls_files() - return list(filter(filter_component_files, files)) - - -def extract_component_names_array_from_files_array(files): - components = [] - for file in files: - file_parts = file.split("/") - if len(file_parts) >= 4: - component_name = file_parts[2] - if component_name not in components: - components.append(component_name) - return components - - -def add_item_to_components_graph(components_graph, parent, child): - if not parent.startswith("__") and parent != child: - if parent not in components_graph: - components_graph[parent] = [] - if child not in components_graph[parent]: - components_graph[parent].append(child) - - -def create_components_graph(): - # The root directory of the repo - root = Path(__file__).parent.parent - components_dir = root / "esphome" / "components" - # Fake some directory so that get_component works - CORE.config_path = str(root) - # Various configuration to capture different outcomes used by `AUTO_LOAD` function. - TARGET_CONFIGURATIONS = [ - {KEY_TARGET_FRAMEWORK: None, KEY_TARGET_PLATFORM: None}, - {KEY_TARGET_FRAMEWORK: "arduino", KEY_TARGET_PLATFORM: None}, - {KEY_TARGET_FRAMEWORK: "esp-idf", KEY_TARGET_PLATFORM: None}, - {KEY_TARGET_FRAMEWORK: None, KEY_TARGET_PLATFORM: PLATFORM_ESP32}, - {KEY_TARGET_FRAMEWORK: None, KEY_TARGET_PLATFORM: PLATFORM_ESP8266}, - ] - CORE.data[KEY_CORE] = TARGET_CONFIGURATIONS[0] - - components_graph = {} - platforms = [] - components = [] - - for path in components_dir.iterdir(): - if not path.is_dir(): - continue - if not (path / "__init__.py").is_file(): - continue - name = path.name - comp = get_component(name) - if comp is None: - print( - f"Cannot find component {name}. Make sure current path is pip installed ESPHome" - ) - sys.exit(1) - - components.append((comp, name, path)) - if comp.is_platform_component: - platforms.append(name) - - platforms = set(platforms) - - for comp, name, path in components: - for dependency in comp.dependencies: - add_item_to_components_graph( - components_graph, dependency.split(".")[0], name - ) - - for target_config in TARGET_CONFIGURATIONS: - CORE.data[KEY_CORE] = target_config - for auto_load in comp.auto_load: - add_item_to_components_graph(components_graph, auto_load, name) - # restore config - CORE.data[KEY_CORE] = TARGET_CONFIGURATIONS[0] - - for platform_path in path.iterdir(): - platform_name = platform_path.stem - if platform_name == name or platform_name not in platforms: - continue - platform = get_platform(platform_name, name) - if platform is None: - continue - - add_item_to_components_graph(components_graph, platform_name, name) - - for dependency in platform.dependencies: - add_item_to_components_graph( - components_graph, dependency.split(".")[0], name - ) - - for target_config in TARGET_CONFIGURATIONS: - CORE.data[KEY_CORE] = target_config - for auto_load in platform.auto_load: - add_item_to_components_graph(components_graph, auto_load, name) - # restore config - CORE.data[KEY_CORE] = TARGET_CONFIGURATIONS[0] - - return components_graph - - -def find_children_of_component(components_graph, component_name, depth=0): - if component_name not in components_graph: - return [] - - children = [] - - for child in components_graph[component_name]: - children.append(child) - if depth < 10: - children.extend( - find_children_of_component(components_graph, child, depth + 1) - ) - # Remove duplicate values - return list(set(children)) - - -def get_components(files: list[str], get_dependencies: bool = False): - components = extract_component_names_array_from_files_array(files) - - if get_dependencies: - components_graph = create_components_graph() - - all_components = components.copy() - for c in components: - all_components.extend(find_children_of_component(components_graph, c)) - # Remove duplicate values - all_changed_components = list(set(all_components)) - - return sorted(all_changed_components) - - return sorted(components) def main(): @@ -161,33 +17,112 @@ def main(): "-c", "--changed", action="store_true", - help="List all components required for testing based on changes", + help="List all components with dependencies (used by clang-tidy). " + "When base test infrastructure changes, returns ALL components.", + ) + parser.add_argument( + "--changed-direct", + action="store_true", + help="List only directly changed components, ignoring infrastructure changes " + "(used by CI for isolation decisions)", + ) + parser.add_argument( + "--changed-with-deps", + action="store_true", + help="Output JSON with both directly changed and all changed components " + "(with dependencies), ignoring infrastructure changes (used by CI for test determination)", ) parser.add_argument( "-b", "--branch", help="Branch to compare changed files against" ) + parser.add_argument( + "--cpp-changed", + action="store_true", + help="List components with changed C++ files", + ) args = parser.parse_args() - if args.branch and not args.changed: - parser.error("--branch requires --changed") + if args.branch and not ( + args.changed + or args.changed_direct + or args.changed_with_deps + or args.cpp_changed + ): + parser.error( + "--branch requires --changed, --changed-direct, --changed-with-deps, or --cpp-changed" + ) - if args.changed: - # When --changed is passed, only get the changed files + if ( + args.changed + or args.changed_direct + or args.changed_with_deps + or args.cpp_changed + ): + # When --changed* is passed, only get the changed files changed = changed_files(args.branch) - # If any base test file(s) changed, there's no need to filter out components - if any("tests/test_build_components" in file for file in changed): - # Need to get all component files + # If any base test file(s) changed, we need to check all components + # BUT only for --changed (used by clang-tidy for comprehensive checking) + # NOT for --changed-direct or --changed-with-deps (used by CI for targeted testing) + # + # Flag usage: + # - --changed: Used by clang-tidy (script/helpers.py get_changed_components) + # Returns: All components with dependencies when base test files change + # Reason: Test infrastructure changes may affect any component + # + # - --changed-direct: Used by CI isolation (script/determine-jobs.py) + # Returns: Only components with actual code changes (not infrastructure) + # Reason: Only directly changed components need isolated testing + # + # - --changed-with-deps: Used by CI test determination (script/determine-jobs.py) + # Returns: Components with code changes + their dependencies (not infrastructure) + # Reason: CI needs to test changed components and their dependents + # + # - --cpp-changed: Used by CI to determine if any C++ files changed (script/determine-jobs.py) + # Returns: Only components with changed C++ files + # Reason: Only components with C++ changes need C++ testing + + base_test_changed = any( + "tests/test_build_components" in file for file in changed + ) + + if base_test_changed and not args.changed_direct and not args.changed_with_deps: + # Base test infrastructure changed - load all component files + # This is for --changed (clang-tidy) which needs comprehensive checking files = get_all_component_files() else: - # Only look at changed component files - files = [f for f in changed if filter_component_files(f)] + # Only look at changed component files (ignore infrastructure changes) + # For --changed-direct: only actual component code changes matter (for isolation) + # For --changed-with-deps: only actual component code changes matter (for testing) + files = [f for f in changed if filter_component_and_test_files(f)] else: # Get all component files files = get_all_component_files() - for c in get_components(files, args.changed): - print(c) + if args.changed_with_deps: + # Return JSON with both directly changed and all changed components + import json + + directly_changed = get_components_with_dependencies(files, False) + all_changed = get_components_with_dependencies(files, True) + output = { + "directly_changed": directly_changed, + "all_changed": all_changed, + } + print(json.dumps(output)) + elif args.changed_direct: + # Return only directly changed components (without dependencies) + for c in get_components_with_dependencies(files, False): + print(c) + elif args.cpp_changed: + # Only look at changed cpp files + files = list(filter(filter_component_and_test_cpp_files, changed)) + for c in get_cpp_changed_components(files): + print(c) + else: + # Return all changed components (with dependencies) - default behavior + for c in get_components_with_dependencies(files, args.changed): + print(c) if __name__ == "__main__": diff --git a/script/merge_component_configs.py b/script/merge_component_configs.py new file mode 100755 index 0000000000..59774edba9 --- /dev/null +++ b/script/merge_component_configs.py @@ -0,0 +1,443 @@ +#!/usr/bin/env python3 +"""Merge multiple component test configurations into a single test file. + +This script combines multiple component test files that use the same common bus +configurations into a single merged test file. This allows testing multiple +compatible components together, reducing CI build time. + +The merger handles: +- Component-specific substitutions (prefixing to avoid conflicts) +- Multiple instances of component configurations +- Shared common bus packages (included only once) +- Platform-specific configurations +- Uses ESPHome's built-in merge_config for proper YAML merging +""" + +from __future__ import annotations + +import argparse +from functools import lru_cache +from pathlib import Path +import re +import sys +from typing import Any + +# Add esphome to path so we can import from it +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from esphome import yaml_util +from esphome.config_helpers import merge_config +from script.analyze_component_buses import PACKAGE_DEPENDENCIES, get_common_bus_packages + +# Prefix for dependency markers in package tracking +# Used to mark packages that are included transitively (e.g., uart via modbus) +DEPENDENCY_MARKER_PREFIX = "_dep_" + + +def load_yaml_file(yaml_file: Path) -> dict: + """Load YAML file using ESPHome's YAML loader. + + Args: + yaml_file: Path to the YAML file + + Returns: + Parsed YAML as dictionary + """ + if not yaml_file.exists(): + raise FileNotFoundError(f"YAML file not found: {yaml_file}") + + return yaml_util.load_yaml(yaml_file) + + +@lru_cache(maxsize=256) +def get_component_packages( + component_name: str, platform: str, tests_dir_str: str +) -> dict: + """Get packages dict from a component's test file with caching. + + This function is cached to avoid re-loading and re-parsing the same file + multiple times when extracting packages during cross-bus merging. + + Args: + component_name: Name of the component + platform: Platform name (e.g., "esp32-idf") + tests_dir_str: String path to tests/components directory (must be string for cache hashability) + + Returns: + Dictionary with 'packages' key containing the raw packages dict from the YAML, + or empty dict if no packages section exists + """ + tests_dir = Path(tests_dir_str) + test_file = tests_dir / component_name / f"test.{platform}.yaml" + comp_data = load_yaml_file(test_file) + + if "packages" not in comp_data or not isinstance(comp_data["packages"], dict): + return {} + + return comp_data["packages"] + + +def extract_packages_from_yaml(data: dict) -> dict[str, str]: + """Extract COMMON BUS package includes from parsed YAML. + + Only extracts packages that are from test_build_components/common/, + ignoring component-specific packages. + + Args: + data: Parsed YAML dictionary + + Returns: + Dictionary mapping package name to include path (as string representation) + Only includes common bus packages (i2c, spi, uart, etc.) + """ + if "packages" not in data: + return {} + + packages_value = data["packages"] + if not isinstance(packages_value, dict): + # List format doesn't include common bus packages (those use dict format) + return {} + + # Get common bus package names (cached) + common_bus_packages = get_common_bus_packages() + packages = {} + + # Dictionary format: packages: {name: value} + for name, value in packages_value.items(): + # Only include common bus packages, ignore component-specific ones + if name not in common_bus_packages: + continue + packages[name] = str(value) + # Also track package dependencies (e.g., modbus includes uart) + if name not in PACKAGE_DEPENDENCIES: + continue + for dep in PACKAGE_DEPENDENCIES[name]: + if dep not in common_bus_packages: + continue + # Mark as included via dependency + packages[f"{DEPENDENCY_MARKER_PREFIX}{dep}"] = f"(included via {name})" + + return packages + + +def prefix_substitutions_in_dict( + data: Any, prefix: str, exclude: set[str] | None = None +) -> Any: + """Recursively prefix all substitution references in a data structure. + + Args: + data: YAML data structure (dict, list, or scalar) + prefix: Prefix to add to substitution names + exclude: Set of substitution names to exclude from prefixing + + Returns: + Data structure with prefixed substitution references + """ + if exclude is None: + exclude = set() + + def replace_sub(text: str) -> str: + """Replace substitution references in a string.""" + + def replace_match(match): + sub_name = match.group(1) + if sub_name in exclude: + return match.group(0) + # Always use braced format in output for consistency + return f"${{{prefix}_{sub_name}}}" + + # Match both ${substitution} and $substitution formats + return re.sub(r"\$\{?(\w+)\}?", replace_match, text) + + if isinstance(data, dict): + result = {} + for key, value in data.items(): + result[key] = prefix_substitutions_in_dict(value, prefix, exclude) + return result + if isinstance(data, list): + return [prefix_substitutions_in_dict(item, prefix, exclude) for item in data] + if isinstance(data, str): + return replace_sub(data) + return data + + +def deduplicate_by_id(data: dict) -> dict: + """Deduplicate list items with the same ID. + + Keeps only the first occurrence of each ID. If items with the same ID + are identical, this silently deduplicates. If they differ, the first + one is kept (ESPHome's validation will catch if this causes issues). + + Args: + data: Parsed config dictionary + + Returns: + Config with deduplicated lists + """ + if not isinstance(data, dict): + return data + + result = {} + for key, value in data.items(): + if isinstance(value, list): + # Check for items with 'id' field + seen_ids = set() + deduped_list = [] + + for item in value: + if isinstance(item, dict) and "id" in item: + item_id = item["id"] + if item_id not in seen_ids: + seen_ids.add(item_id) + deduped_list.append(item) + # else: skip duplicate ID (keep first occurrence) + else: + # No ID, just add it + deduped_list.append(item) + + result[key] = deduped_list + elif isinstance(value, dict): + # Recursively deduplicate nested dicts + result[key] = deduplicate_by_id(value) + else: + result[key] = value + + return result + + +def merge_component_configs( + component_names: list[str], + platform: str, + tests_dir: Path, + output_file: Path, +) -> None: + """Merge multiple component test configs into a single file. + + Args: + component_names: List of component names to merge + platform: Platform to merge for (e.g., "esp32-ard") + tests_dir: Path to tests/components directory + output_file: Path to output merged config file + """ + if not component_names: + raise ValueError("No components specified") + + # Track packages to ensure they're identical + all_packages = None + + # Start with empty config + merged_config_data = {} + + # Convert tests_dir to string for caching + tests_dir_str = str(tests_dir) + + # Process each component + for comp_name in component_names: + comp_dir = tests_dir / comp_name + test_file = comp_dir / f"test.{platform}.yaml" + + if not test_file.exists(): + raise FileNotFoundError(f"Test file not found: {test_file}") + + # Load the component's test file + comp_data = load_yaml_file(test_file) + + # Merge packages from all components (cross-bus merging) + # Components can have different packages (e.g., one with ble, another with uart) + # as long as they don't conflict (checked by are_buses_compatible before calling this) + comp_packages = extract_packages_from_yaml(comp_data) + + if all_packages is None: + # First component - initialize package dict + all_packages = comp_packages if comp_packages else {} + elif comp_packages: + # Merge packages - combine all unique package types + # If both have the same package type, verify they're identical + for pkg_name, pkg_config in comp_packages.items(): + if pkg_name in all_packages: + # Same package type - verify config matches + if all_packages[pkg_name] != pkg_config: + raise ValueError( + f"Component {comp_name} has conflicting config for package '{pkg_name}'. " + f"Expected: {all_packages[pkg_name]}, Got: {pkg_config}. " + f"Components with conflicting bus configs cannot be merged." + ) + else: + # New package type - add it + all_packages[pkg_name] = pkg_config + + # Handle $component_dir by replacing with absolute path + # This allows components that use local file references to be grouped + comp_abs_dir = str(comp_dir.absolute()) + + # Save top-level substitutions BEFORE expanding packages + # In ESPHome, top-level substitutions override package substitutions + top_level_subs = ( + comp_data["substitutions"].copy() + if "substitutions" in comp_data and comp_data["substitutions"] is not None + else {} + ) + + # Expand packages - but we'll restore substitution priority after + if "packages" in comp_data: + packages_value = comp_data["packages"] + + if isinstance(packages_value, dict): + # Dict format - check each package + common_bus_packages = get_common_bus_packages() + for pkg_name, pkg_value in list(packages_value.items()): + if pkg_name in common_bus_packages: + continue + if not isinstance(pkg_value, dict): + continue + # Component-specific package - expand its content into top level + comp_data = merge_config(comp_data, pkg_value) + elif isinstance(packages_value, list): + # List format - expand all package includes + for pkg_value in packages_value: + if not isinstance(pkg_value, dict): + continue + comp_data = merge_config(comp_data, pkg_value) + + # Remove all packages (common will be re-added at the end) + del comp_data["packages"] + + # Restore top-level substitution priority + # Top-level substitutions override any from packages + if "substitutions" not in comp_data or comp_data["substitutions"] is None: + comp_data["substitutions"] = {} + + # Merge: package subs as base, top-level subs override + comp_data["substitutions"].update(top_level_subs) + + # Now prefix the final merged substitutions + comp_data["substitutions"] = { + f"{comp_name}_{sub_name}": sub_value + for sub_name, sub_value in comp_data["substitutions"].items() + } + + # Add component_dir substitution with absolute path for this component + comp_data["substitutions"][f"{comp_name}_component_dir"] = comp_abs_dir + + # Prefix substitution references throughout the config + comp_data = prefix_substitutions_in_dict(comp_data, comp_name) + + # Use ESPHome's merge_config to merge this component into the result + # merge_config handles list merging with ID-based deduplication automatically + merged_config_data = merge_config(merged_config_data, comp_data) + + # Add merged packages back (union of all component packages) + # IMPORTANT: Only include common bus packages (spi, i2c, uart, etc.) + # Do NOT re-add component-specific packages as they contain unprefixed $component_dir refs + if all_packages: + # Build packages dict from merged all_packages + # all_packages is a dict mapping package_name -> str(package_value) + # We need to reconstruct the actual package values by loading them from any component + # Since packages with the same name must have identical configs (verified above), + # we can load the package value from the first component that has each package + common_bus_packages = get_common_bus_packages() + merged_packages: dict[str, Any] = {} + + # Collect packages that are included as dependencies + # If modbus is present, uart is included via modbus.packages.uart + packages_to_skip: set[str] = set() + for pkg_name in all_packages: + if pkg_name.startswith(DEPENDENCY_MARKER_PREFIX): + # Extract the actual package name (remove _dep_ prefix) + dep_name = pkg_name[len(DEPENDENCY_MARKER_PREFIX) :] + packages_to_skip.add(dep_name) + + for pkg_name in all_packages: + # Skip dependency markers + if pkg_name.startswith(DEPENDENCY_MARKER_PREFIX): + continue + # Skip non-common-bus packages + if pkg_name not in common_bus_packages: + continue + # Skip packages that are included as dependencies of other packages + # This prevents duplicate definitions (e.g., uart via modbus + uart separately) + if pkg_name in packages_to_skip: + continue + + # Find a component that has this package and extract its value + # Uses cached lookup to avoid re-loading the same files + for comp_name in component_names: + comp_packages = get_component_packages( + comp_name, platform, tests_dir_str + ) + if pkg_name in comp_packages: + merged_packages[pkg_name] = comp_packages[pkg_name] + break + + if merged_packages: + merged_config_data["packages"] = merged_packages + + # Deduplicate items with same ID (keeps first occurrence) + merged_config_data = deduplicate_by_id(merged_config_data) + + # Remove esphome section since it will be provided by the wrapper file + # The wrapper file includes this merged config via packages and provides + # the proper esphome: section with name, platform, etc. + if "esphome" in merged_config_data: + del merged_config_data["esphome"] + + # Write merged config + output_file.parent.mkdir(parents=True, exist_ok=True) + yaml_content = yaml_util.dump(merged_config_data) + output_file.write_text(yaml_content) + + print(f"Successfully merged {len(component_names)} components into {output_file}") + + +def main() -> None: + """Main entry point.""" + parser = argparse.ArgumentParser( + description="Merge multiple component test configs into a single file" + ) + parser.add_argument( + "--components", + "-c", + required=True, + help="Comma-separated list of component names to merge", + ) + parser.add_argument( + "--platform", + "-p", + required=True, + help="Platform to merge for (e.g., esp32-ard)", + ) + parser.add_argument( + "--output", + "-o", + required=True, + type=Path, + help="Output file path for merged config", + ) + parser.add_argument( + "--tests-dir", + type=Path, + default=Path("tests/components"), + help="Path to tests/components directory", + ) + + args = parser.parse_args() + + component_names = [c.strip() for c in args.components.split(",")] + + try: + merge_component_configs( + component_names=component_names, + platform=args.platform, + tests_dir=args.tests_dir, + output_file=args.output, + ) + except Exception as e: + print(f"Error merging configs: {e}", file=sys.stderr) + import traceback + + traceback.print_exc() + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/script/platformio_install_deps.py b/script/platformio_install_deps.py index ed133ecb47..8f7261efc3 100755 --- a/script/platformio_install_deps.py +++ b/script/platformio_install_deps.py @@ -55,4 +55,6 @@ for section in config.sections(): tools.append("-t") tools.append(tool) -subprocess.check_call(["platformio", "pkg", "install", "-g", *libs, *platforms, *tools]) +subprocess.check_call( + ["platformio", "pkg", "install", "-g", *libs, *platforms, *tools], close_fds=False +) diff --git a/script/run-in-env.py b/script/run-in-env.py index d9bd01a62f..886e65db27 100755 --- a/script/run-in-env.py +++ b/script/run-in-env.py @@ -13,7 +13,7 @@ def find_and_activate_virtualenv(): try: # Get the top-level directory of the git repository my_path = subprocess.check_output( - ["git", "rev-parse", "--show-toplevel"], text=True + ["git", "rev-parse", "--show-toplevel"], text=True, close_fds=False ).strip() except subprocess.CalledProcessError: print( @@ -44,7 +44,7 @@ def find_and_activate_virtualenv(): def run_command(): # Execute the remaining arguments in the new environment if len(sys.argv) > 1: - subprocess.run(sys.argv[1:], check=False) + subprocess.run(sys.argv[1:], check=False, close_fds=False) else: print( "No command provided to run in the virtual environment.", diff --git a/script/setup b/script/setup index 1bd7c44575..8cad7017ff 100755 --- a/script/setup +++ b/script/setup @@ -22,8 +22,6 @@ uv pip install -e ".[dev,test]" --config-settings editable_mode=compat pre-commit install -script/platformio_install_deps.py platformio.ini --libraries --tools --platforms - mkdir -p .temp echo diff --git a/script/setup.bat b/script/setup.bat index f89d5aea1a..003ea31b36 100644 --- a/script/setup.bat +++ b/script/setup.bat @@ -19,8 +19,6 @@ pip3 install -e ".[dev,test]" --config-settings editable_mode=compat pre-commit install -python script/platformio_install_deps.py platformio.ini --libraries --tools --platforms - echo . echo . echo Virtual environment created. Run 'venv/Scripts/activate' to use it. diff --git a/script/split_components_for_ci.py b/script/split_components_for_ci.py new file mode 100755 index 0000000000..65d09efb9b --- /dev/null +++ b/script/split_components_for_ci.py @@ -0,0 +1,389 @@ +#!/usr/bin/env python3 +"""Split components into batches with intelligent grouping. + +This script analyzes components to identify which ones share common bus configurations +and intelligently groups them into batches to maximize the efficiency of the +component grouping system in CI. + +Components with the same bus signature are placed in the same batch whenever possible, +allowing the test_build_components.py script to merge them into single builds. +""" + +from __future__ import annotations + +import argparse +from collections import defaultdict +import json +from pathlib import Path +import sys + +# Add esphome to path +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from script.analyze_component_buses import ( + ISOLATED_COMPONENTS, + ISOLATED_SIGNATURE_PREFIX, + NO_BUSES_SIGNATURE, + analyze_all_components, + create_grouping_signature, + merge_compatible_bus_groups, +) +from script.helpers import get_component_test_files + +# Weighting for batch creation +# Isolated components can't be grouped/merged, so they count as 10x +# Groupable components can be merged into single builds, so they count as 1x +ISOLATED_WEIGHT = 10 +GROUPABLE_WEIGHT = 1 + +# Platform used for batching (platform-agnostic batching) +# Batches are split across CI runners and each runner tests all platforms +ALL_PLATFORMS = "all" + + +def has_test_files(component_name: str, tests_dir: Path) -> bool: + """Check if a component has test files. + + Args: + component_name: Name of the component + tests_dir: Path to tests/components directory (unused, kept for compatibility) + + Returns: + True if the component has test.*.yaml or test-*.yaml files + """ + return bool(get_component_test_files(component_name, all_variants=True)) + + +def create_intelligent_batches( + components: list[str], + tests_dir: Path, + batch_size: int = 40, + directly_changed: set[str] | None = None, +) -> tuple[list[list[str]], dict[tuple[str, str], list[str]]]: + """Create batches optimized for component grouping. + + IMPORTANT: This function is called from both split_components_for_ci.py (standalone script) + and determine-jobs.py (integrated into job determination). Be careful when refactoring + to ensure changes work in both contexts. + + Args: + components: List of component names to batch + tests_dir: Path to tests/components directory + batch_size: Target size for each batch + directly_changed: Set of directly changed components (for logging only) + + Returns: + Tuple of (batches, signature_groups) where: + - batches: List of component batches (lists of component names) + - signature_groups: Dict mapping (platform, signature) to component lists + """ + # Filter out components without test files + # Platform components like 'climate' and 'climate_ir' don't have test files + components_with_tests = [ + comp for comp in components if has_test_files(comp, tests_dir) + ] + + # Log filtered components to stderr for debugging + if len(components_with_tests) < len(components): + filtered_out = set(components) - set(components_with_tests) + print( + f"Note: Filtered {len(filtered_out)} components without test files: " + f"{', '.join(sorted(filtered_out))}", + file=sys.stderr, + ) + + # Analyze all components to get their bus signatures + component_buses, non_groupable, _direct_bus_components = analyze_all_components( + tests_dir + ) + + # Group components by their bus signature ONLY (ignore platform) + # All platforms will be tested by test_build_components.py for each batch + # Key: (platform, signature), Value: list of components + # We use ALL_PLATFORMS since batching is platform-agnostic + signature_groups: dict[tuple[str, str], list[str]] = defaultdict(list) + + for component in components_with_tests: + # Components that can't be grouped get unique signatures + # This includes: + # - Manually curated ISOLATED_COMPONENTS + # - Automatically detected non_groupable components + # - Directly changed components (passed via --isolate in CI) + # These can share a batch/runner but won't be grouped/merged + is_isolated = ( + component in ISOLATED_COMPONENTS + or component in non_groupable + or (directly_changed and component in directly_changed) + ) + if is_isolated: + signature_groups[ + (ALL_PLATFORMS, f"{ISOLATED_SIGNATURE_PREFIX}{component}") + ].append(component) + continue + + # Get signature from any platform (they should all have the same buses) + # Components not in component_buses may only have variant-specific tests + comp_platforms = component_buses.get(component) + if not comp_platforms: + # Component has tests but no analyzable base config - treat as no buses + signature_groups[(ALL_PLATFORMS, NO_BUSES_SIGNATURE)].append(component) + continue + + for platform, buses in comp_platforms.items(): + if buses: + signature = create_grouping_signature({platform: buses}, platform) + # Group by signature only - platform doesn't matter for batching + # Use ALL_PLATFORMS since we're batching across all platforms + signature_groups[(ALL_PLATFORMS, signature)].append(component) + break # Only use first platform for grouping + else: + # No buses found for any platform - can be grouped together + signature_groups[(ALL_PLATFORMS, NO_BUSES_SIGNATURE)].append(component) + + # Merge compatible bus groups (cross-bus optimization) + # This allows components with different buses (ble + uart) to be batched together + # improving the efficiency of test_build_components.py grouping + signature_groups = merge_compatible_bus_groups(signature_groups) + + # Create batches by keeping signature groups together + # Components with the same signature stay in the same batches + batches = [] + + # Sort signature groups to prioritize groupable components + # 1. Put "isolated_*" signatures last (can't be grouped with others) + # 2. Sort groupable signatures by size (largest first) + # 3. "no_buses" components CAN be grouped together + def sort_key(item): + (_platform, signature), components = item + is_isolated = signature.startswith(ISOLATED_SIGNATURE_PREFIX) + # Put "isolated_*" last (1), groupable first (0) + # Within each category, sort by size (largest first) + return (is_isolated, -len(components)) + + sorted_groups = sorted(signature_groups.items(), key=sort_key) + + # Strategy: Create batches using weighted sizes + # - Isolated components count as 10x (since they can't be grouped/merged) + # - Groupable components count as 1x (can be merged into single builds) + # - This distributes isolated components across more runners + # - Ensures each runner has a good mix of groupable vs isolated components + + current_batch = [] + current_weight = 0 + + for (_platform, signature), group_components in sorted_groups: + is_isolated = signature.startswith(ISOLATED_SIGNATURE_PREFIX) + weight_per_component = ISOLATED_WEIGHT if is_isolated else GROUPABLE_WEIGHT + + for component in group_components: + # Check if adding this component would exceed the batch size + if current_weight + weight_per_component > batch_size and current_batch: + # Start a new batch + batches.append(current_batch) + current_batch = [] + current_weight = 0 + + # Add component to current batch + current_batch.append(component) + current_weight += weight_per_component + + # Don't forget the last batch + if current_batch: + batches.append(current_batch) + + return batches, signature_groups + + +def main() -> int: + """Main entry point.""" + parser = argparse.ArgumentParser( + description="Split components into intelligent batches for CI testing" + ) + parser.add_argument( + "--components", + "-c", + required=True, + help="JSON array of component names", + ) + parser.add_argument( + "--batch-size", + "-b", + type=int, + default=40, + help="Target batch size (default: 40, weighted)", + ) + parser.add_argument( + "--tests-dir", + type=Path, + default=Path("tests/components"), + help="Path to tests/components directory", + ) + parser.add_argument( + "--directly-changed", + help="JSON array of directly changed component names (for logging only)", + ) + parser.add_argument( + "--output", + "-o", + choices=["json", "github"], + default="github", + help="Output format (json or github for GitHub Actions)", + ) + + args = parser.parse_args() + + # Parse component list from JSON + try: + components = json.loads(args.components) + except json.JSONDecodeError as e: + print(f"Error parsing components JSON: {e}", file=sys.stderr) + return 1 + + if not isinstance(components, list): + print("Components must be a JSON array", file=sys.stderr) + return 1 + + # Parse directly changed components list from JSON (if provided) + directly_changed = None + if args.directly_changed: + try: + directly_changed = set(json.loads(args.directly_changed)) + except json.JSONDecodeError as e: + print(f"Error parsing directly-changed JSON: {e}", file=sys.stderr) + return 1 + + # Create intelligent batches + batches, signature_groups = create_intelligent_batches( + components=components, + tests_dir=args.tests_dir, + batch_size=args.batch_size, + directly_changed=directly_changed, + ) + + # Convert batches to space-separated strings for CI + batch_strings = [" ".join(batch) for batch in batches] + + if args.output == "json": + # Output as JSON array + print(json.dumps(batch_strings)) + else: + # Output for GitHub Actions (set output) + output_json = json.dumps(batch_strings) + print(f"components={output_json}") + + # Print summary to stderr so it shows in CI logs + # Count actual components being batched + actual_components = sum(len(batch.split()) for batch in batch_strings) + + # Re-analyze to get isolated component counts for summary + _, non_groupable, _ = analyze_all_components(args.tests_dir) + + # Show grouping details + print("\n=== Component Grouping Details ===", file=sys.stderr) + # Sort groups by signature for readability + groupable_groups = [] + isolated_groups = [] + for (platform, signature), group_comps in sorted(signature_groups.items()): + if signature.startswith(ISOLATED_SIGNATURE_PREFIX): + isolated_groups.append((signature, group_comps)) + else: + groupable_groups.append((signature, group_comps)) + + if groupable_groups: + print( + f"\nGroupable signatures ({len(groupable_groups)} merged groups after cross-bus optimization):", + file=sys.stderr, + ) + for signature, group_comps in sorted( + groupable_groups, key=lambda x: (-len(x[1]), x[0]) + ): + # Check if this is a merged signature (contains +) + is_merged = "+" in signature and signature != NO_BUSES_SIGNATURE + # Special handling for no_buses components + if signature == NO_BUSES_SIGNATURE: + print( + f" [{signature}]: {len(group_comps)} components (used as fillers across batches)", + file=sys.stderr, + ) + else: + merge_indicator = " [MERGED]" if is_merged else "" + print( + f" [{signature}]{merge_indicator}: {len(group_comps)} components", + file=sys.stderr, + ) + # Show first few components as examples + examples = ", ".join(sorted(group_comps)[:8]) + if len(group_comps) > 8: + examples += f", ... (+{len(group_comps) - 8} more)" + print(f" → {examples}", file=sys.stderr) + + if isolated_groups: + print( + f"\nIsolated components ({len(isolated_groups)} components - tested individually):", + file=sys.stderr, + ) + isolated_names = sorted( + [comp for _, comps in isolated_groups for comp in comps] + ) + # Group isolated components for compact display + for i in range(0, len(isolated_names), 10): + chunk = isolated_names[i : i + 10] + print(f" {', '.join(chunk)}", file=sys.stderr) + + # Count isolated vs groupable components + all_batched_components = [comp for batch in batches for comp in batch] + isolated_count = sum( + 1 + for comp in all_batched_components + if comp in ISOLATED_COMPONENTS + or comp in non_groupable + or (directly_changed and comp in directly_changed) + ) + groupable_count = actual_components - isolated_count + + print("\n=== Intelligent Batch Summary ===", file=sys.stderr) + print(f"Total components requested: {len(components)}", file=sys.stderr) + print(f"Components with test files: {actual_components}", file=sys.stderr) + + # Show breakdown of directly changed vs dependencies + if directly_changed: + direct_count = sum( + 1 for comp in all_batched_components if comp in directly_changed + ) + dep_count = actual_components - direct_count + direct_comps = [ + comp for comp in all_batched_components if comp in directly_changed + ] + dep_comps = [ + comp for comp in all_batched_components if comp not in directly_changed + ] + print( + f" - Direct changes: {direct_count} ({', '.join(sorted(direct_comps))})", + file=sys.stderr, + ) + print( + f" - Dependencies: {dep_count} ({', '.join(sorted(dep_comps))})", + file=sys.stderr, + ) + + print(f" - Groupable (weight=1): {groupable_count}", file=sys.stderr) + print(f" - Isolated (weight=10): {isolated_count}", file=sys.stderr) + if actual_components < len(components): + print( + f"Components skipped (no test files): {len(components) - actual_components}", + file=sys.stderr, + ) + print(f"Number of batches: {len(batches)}", file=sys.stderr) + print(f"Batch size target (weighted): {args.batch_size}", file=sys.stderr) + if len(batches) > 0: + print( + f"Average components per batch: {actual_components / len(batches):.1f}", + file=sys.stderr, + ) + print(file=sys.stderr) + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/script/templates/ci_memory_impact_comment_template.j2 b/script/templates/ci_memory_impact_comment_template.j2 new file mode 100644 index 0000000000..9fbf78e99f --- /dev/null +++ b/script/templates/ci_memory_impact_comment_template.j2 @@ -0,0 +1,27 @@ +{{ comment_marker }} +## Memory Impact Analysis + +**Components:** {{ components_str }} +**Platform:** `{{ platform }}` + +| Metric | Target Branch | This PR | Change | +|--------|--------------|---------|--------| +| **RAM** | {{ target_ram }} | {{ pr_ram }} | {{ ram_change }} | +| **Flash** | {{ target_flash }} | {{ pr_flash }} | {{ flash_change }} | +{% if component_breakdown %} +{{ component_breakdown }} +{% endif %} +{% if symbol_changes %} +{{ symbol_changes }} +{% endif %} +{%- if target_cache_hit %} + +> ⚡ Target branch analysis was loaded from cache (build skipped for faster CI). +{%- endif %} + +--- +> **Note:** This analysis measures **static RAM and Flash usage** only (compile-time allocation). +> **Dynamic memory (heap)** cannot be measured automatically. +> **⚠️ You must test this PR on a real device** to measure free heap and ensure no runtime memory issues. + +*This analysis runs automatically when components change. Memory usage is measured from {{ config_note }}.* diff --git a/script/templates/ci_memory_impact_component_breakdown.j2 b/script/templates/ci_memory_impact_component_breakdown.j2 new file mode 100644 index 0000000000..a781e5c546 --- /dev/null +++ b/script/templates/ci_memory_impact_component_breakdown.j2 @@ -0,0 +1,15 @@ + +
+📊 Component Memory Breakdown + +| Component | Target Flash | PR Flash | Change | +|-----------|--------------|----------|--------| +{% for comp, target_flash, pr_flash, delta in changed_components[:max_rows] -%} +{% set threshold = component_change_threshold if comp.startswith("[esphome]") else none -%} +| `{{ comp }}` | {{ target_flash|format_bytes }} | {{ pr_flash|format_bytes }} | {{ format_change(target_flash, pr_flash, threshold=threshold) }} | +{% endfor -%} +{% if changed_components|length > max_rows -%} +| ... | ... | ... | *({{ changed_components|length - max_rows }} more components not shown)* | +{% endif -%} + +
diff --git a/script/templates/ci_memory_impact_macros.j2 b/script/templates/ci_memory_impact_macros.j2 new file mode 100644 index 0000000000..9fb346a7c5 --- /dev/null +++ b/script/templates/ci_memory_impact_macros.j2 @@ -0,0 +1,8 @@ +{#- Macro for formatting symbol names in tables -#} +{%- macro format_symbol(symbol, max_length, truncate_length) -%} +{%- if symbol|length <= max_length -%} +`{{ symbol }}` +{%- else -%} +
{{ symbol[:truncate_length] }}...{{ symbol }}
+{%- endif -%} +{%- endmacro -%} diff --git a/script/templates/ci_memory_impact_symbol_changes.j2 b/script/templates/ci_memory_impact_symbol_changes.j2 new file mode 100644 index 0000000000..60f2f50e48 --- /dev/null +++ b/script/templates/ci_memory_impact_symbol_changes.j2 @@ -0,0 +1,51 @@ +{%- from 'ci_memory_impact_macros.j2' import format_symbol -%} + +
+🔍 Symbol-Level Changes (click to expand) + +{% if changed_symbols %} + +### Changed Symbols + +| Symbol | Target Size | PR Size | Change | +|--------|-------------|---------|--------| +{% for symbol, target_size, pr_size, delta in changed_symbols[:max_changed_rows] -%} +| {{ format_symbol(symbol, symbol_max_length, symbol_truncate_length) }} | {{ target_size|format_bytes }} | {{ pr_size|format_bytes }} | {{ format_change(target_size, pr_size) }} | +{% endfor -%} +{% if changed_symbols|length > max_changed_rows -%} +| ... | ... | ... | *({{ changed_symbols|length - max_changed_rows }} more changed symbols not shown)* | +{% endif -%} + +{% endif %} +{% if new_symbols %} + +### New Symbols (top {{ max_new_rows }}) + +| Symbol | Size | +|--------|------| +{% for symbol, size in new_symbols[:max_new_rows] -%} +| {{ format_symbol(symbol, symbol_max_length, symbol_truncate_length) }} | {{ size|format_bytes }} | +{% endfor -%} +{% if new_symbols|length > max_new_rows -%} +{% set total_new_size = new_symbols|sum(attribute=1) -%} +| *{{ new_symbols|length - max_new_rows }} more new symbols...* | *Total: {{ total_new_size|format_bytes }}* | +{% endif -%} + +{% endif %} +{% if removed_symbols %} + +### Removed Symbols (top {{ max_removed_rows }}) + +| Symbol | Size | +|--------|------| +{% for symbol, size in removed_symbols[:max_removed_rows] -%} +| {{ format_symbol(symbol, symbol_max_length, symbol_truncate_length) }} | {{ size|format_bytes }} | +{% endfor -%} +{% if removed_symbols|length > max_removed_rows -%} +{% set total_removed_size = removed_symbols|sum(attribute=1) -%} +| *{{ removed_symbols|length - max_removed_rows }} more removed symbols...* | *Total: {{ total_removed_size|format_bytes }}* | +{% endif -%} + +{% endif %} + +
diff --git a/script/test_build_components b/script/test_build_components deleted file mode 100755 index 3796280176..0000000000 --- a/script/test_build_components +++ /dev/null @@ -1,106 +0,0 @@ -#!/usr/bin/env bash - -set -e - -help() { - echo "Usage: $0 [-e ] [-c ] [-t ]" 1>&2 - echo 1>&2 - echo " - e - Parameter for esphome command. Default compile. Common alternative is config." 1>&2 - echo " - c - Component folder name to test. Default *. E.g. '-c logger'." 1>&2 - echo " - t - Target name to test. Put '-t list' to display all possibilities. E.g. '-t esp32-s2-idf-51'." 1>&2 - exit 1 -} - -# Parse parameter: -# - `e` - Parameter for `esphome` command. Default `compile`. Common alternative is `config`. -# - `c` - Component folder name to test. Default `*`. -esphome_command="compile" -target_component="*" -while getopts e:c:t: flag -do - case $flag in - e) esphome_command=${OPTARG};; - c) target_component=${OPTARG};; - t) requested_target_platform=${OPTARG};; - \?) help;; - esac -done - -cd "$(dirname "$0")/.." - -if ! [ -d "./tests/test_build_components/build" ]; then - mkdir ./tests/test_build_components/build -fi - -start_esphome() { - if [ -n "$requested_target_platform" ] && [ "$requested_target_platform" != "$target_platform_with_version" ]; then - echo "Skipping $target_platform_with_version" - return - fi - # create dynamic yaml file in `build` folder. - # `./tests/test_build_components/build/[target_component].[test_name].[target_platform_with_version].yaml` - component_test_file="./tests/test_build_components/build/$target_component.$test_name.$target_platform_with_version.yaml" - - cp $target_platform_file $component_test_file - if [[ "$OSTYPE" == "darwin"* ]]; then - # macOS sed is...different - sed -i '' "s!\$component_test_file!../../.$f!g" $component_test_file - else - sed -i "s!\$component_test_file!../../.$f!g" $component_test_file - fi - - # Start esphome process - echo "> [$target_component] [$test_name] [$target_platform_with_version]" - set -x - # TODO: Validate escape of Command line substitution value - python3 -m esphome -s component_name $target_component -s component_dir ../../components/$target_component -s test_name $test_name -s target_platform $target_platform $esphome_command $component_test_file - { set +x; } 2>/dev/null -} - -# Find all test yaml files. -# - `./tests/components/[target_component]/[test_name].[target_platform].yaml` -# - `./tests/components/[target_component]/[test_name].all.yaml` -for f in ./tests/components/$target_component/*.*.yaml; do - [ -f "$f" ] || continue - IFS='/' read -r -a folder_name <<< "$f" - target_component="${folder_name[3]}" - - IFS='.' read -r -a file_name <<< "${folder_name[4]}" - test_name="${file_name[0]}" - target_platform="${file_name[1]}" - file_name_parts=${#file_name[@]} - - if [ "$target_platform" = "all" ] || [ $file_name_parts = 2 ]; then - # Test has *not* defined a specific target platform. Need to run tests for all possible target platforms. - - for target_platform_file in ./tests/test_build_components/build_components_base.*.yaml; do - IFS='/' read -r -a folder_name <<< "$target_platform_file" - IFS='.' read -r -a file_name <<< "${folder_name[3]}" - target_platform="${file_name[1]}" - - start_esphome - done - - else - # Test has defined a specific target platform. - - # Validate we have a base test yaml for selected platform. - # The target_platform is sourced from the following location. - # 1. `./tests/test_build_components/build_components_base.[target_platform].yaml` - # 2. `./tests/test_build_components/build_components_base.[target_platform]-ard.yaml` - target_platform_file="./tests/test_build_components/build_components_base.$target_platform.yaml" - if ! [ -f "$target_platform_file" ]; then - echo "No base test file [./tests/test_build_components/build_components_base.$target_platform.yaml] for component test [$f] found." - exit 1 - fi - - for target_platform_file in ./tests/test_build_components/build_components_base.$target_platform*.yaml; do - # trim off "./tests/test_build_components/build_components_base." prefix - target_platform_with_version=${target_platform_file:52} - # ...now remove suffix starting with "." leaving just the test target hardware and software platform (possibly with version) - # For example: "esp32-s3-idf-50" - target_platform_with_version=${target_platform_with_version%.*} - start_esphome - done - fi -done diff --git a/script/test_build_components b/script/test_build_components new file mode 120000 index 0000000000..832a4a72c6 --- /dev/null +++ b/script/test_build_components @@ -0,0 +1 @@ +test_build_components.py \ No newline at end of file diff --git a/script/test_build_components.py b/script/test_build_components.py new file mode 100755 index 0000000000..e369b0364e --- /dev/null +++ b/script/test_build_components.py @@ -0,0 +1,1180 @@ +#!/usr/bin/env python3 +"""Test ESPHome component builds with intelligent grouping. + +This script replaces the bash test_build_components script with Python, +adding support for intelligent component grouping based on shared bus +configurations to reduce CI build time. + +Features: +- Analyzes components for shared common bus configs +- Groups compatible components together +- Merges configs for grouped components +- Uses --testing-mode for grouped tests +- Maintains backward compatibility with single component testing +""" + +from __future__ import annotations + +import argparse +from collections import defaultdict +from dataclasses import dataclass +import hashlib +import os +from pathlib import Path +import subprocess +import sys +import time + +# Add esphome to path +sys.path.insert(0, str(Path(__file__).parent.parent)) + +# pylint: disable=wrong-import-position +from script.analyze_component_buses import ( + BASE_BUS_COMPONENTS, + ISOLATED_COMPONENTS, + NO_BUSES_SIGNATURE, + analyze_all_components, + create_grouping_signature, + is_platform_component, + merge_compatible_bus_groups, + uses_local_file_references, +) +from script.helpers import get_component_test_files +from script.merge_component_configs import merge_component_configs + + +@dataclass +class TestResult: + """Store information about a single test run.""" + + test_id: str + components: list[str] + platform: str + success: bool + duration: float + command: str = "" + test_type: str = "compile" # "config" or "compile" + + +def show_disk_space_if_ci(esphome_command: str) -> None: + """Show disk space usage if running in CI during compile. + + Only shows output during compilation (not config validation) since + disk space is only relevant when actually building firmware. + + Args: + esphome_command: The esphome command being run (config/compile/clean) + """ + # Only show disk space during compilation in CI + # Config validation doesn't build anything so disk space isn't relevant + if not os.environ.get("GITHUB_ACTIONS"): + return + if esphome_command != "compile": + return + + print("\n" + "=" * 80) + print("Disk Space After Build:") + print("=" * 80) + # Use sys.stdout.flush() to ensure output appears immediately + sys.stdout.flush() + subprocess.run(["df", "-h"], check=False, stdout=sys.stdout, stderr=sys.stderr) + print("=" * 80 + "\n") + sys.stdout.flush() + + +def find_component_tests( + components_dir: Path, component_pattern: str = "*", base_only: bool = False +) -> dict[str, list[Path]]: + """Find all component test files. + + Args: + components_dir: Path to tests/components directory + component_pattern: Glob pattern for component names + base_only: If True, only find base test files (test.*.yaml), not variant files (test-*.yaml) + + Returns: + Dictionary mapping component name to list of test files + """ + component_tests = defaultdict(list) + + for comp_dir in components_dir.glob(component_pattern): + if not comp_dir.is_dir(): + continue + + # Get test files using helper function + test_files = get_component_test_files(comp_dir.name, all_variants=not base_only) + if test_files: + component_tests[comp_dir.name] = test_files + + return dict(component_tests) + + +def parse_test_filename(test_file: Path) -> tuple[str, str]: + """Parse test filename to extract test name and platform. + + Args: + test_file: Path to test file + + Returns: + Tuple of (test_name, platform) + """ + parts = test_file.stem.split(".") + if len(parts) == 2: + return parts[0], parts[1] # test, platform + return parts[0], "all" + + +def get_platform_base_files(base_dir: Path) -> dict[str, list[Path]]: + """Get all platform base files. + + Args: + base_dir: Path to test_build_components directory + + Returns: + Dictionary mapping platform to list of base files (for version variants) + """ + platform_files = defaultdict(list) + + for base_file in base_dir.glob("build_components_base.*.yaml"): + # Extract platform from filename + # e.g., build_components_base.esp32-idf.yaml -> esp32-idf + # or build_components_base.esp32-idf-50.yaml -> esp32-idf + filename = base_file.stem + parts = filename.replace("build_components_base.", "").split("-") + + # Platform is everything before version number (if present) + # Check if last part is a number (version) + platform = "-".join(parts[:-1]) if parts[-1].isdigit() else "-".join(parts) + + platform_files[platform].append(base_file) + + return dict(platform_files) + + +def group_components_by_platform( + failed_results: list[TestResult], +) -> dict[tuple[str, str], list[str]]: + """Group failed components by platform and test type for simplified reproduction commands. + + Args: + failed_results: List of failed test results + + Returns: + Dictionary mapping (platform, test_type) to list of component names + """ + platform_components: dict[tuple[str, str], list[str]] = {} + for result in failed_results: + key = (result.platform, result.test_type) + if key not in platform_components: + platform_components[key] = [] + platform_components[key].extend(result.components) + + # Remove duplicates and sort for each platform + return { + key: sorted(set(components)) for key, components in platform_components.items() + } + + +def format_github_summary(test_results: list[TestResult]) -> str: + """Format test results as GitHub Actions job summary markdown. + + Args: + test_results: List of all test results + + Returns: + Markdown formatted summary string + """ + # Separate results into passed and failed + passed_results = [r for r in test_results if r.success] + failed_results = [r for r in test_results if not r.success] + + lines = [] + + # Header with emoji based on success/failure + if failed_results: + lines.append("## :x: Component Tests Failed\n") + else: + lines.append("## :white_check_mark: Component Tests Passed\n") + + # Summary statistics + total_time = sum(r.duration for r in test_results) + # Determine test type from results (all should be the same) + test_type = test_results[0].test_type if test_results else "unknown" + lines.append( + f"**Results:** {len(passed_results)} passed, {len(failed_results)} failed\n" + ) + lines.append(f"**Total time:** {total_time:.1f}s\n") + lines.append(f"**Test type:** `{test_type}`\n") + + # Show failed tests if any + if failed_results: + lines.append("### Failed Tests\n") + lines.append("| Test | Components | Platform | Duration |\n") + lines.append("|------|-----------|----------|----------|\n") + for result in failed_results: + components_str = ", ".join(result.components) + lines.append( + f"| `{result.test_id}` | {components_str} | {result.platform} | {result.duration:.1f}s |\n" + ) + lines.append("\n") + + # Show simplified commands to reproduce failures + # Group all failed components by platform for a single command per platform + lines.append("
\n") + lines.append("Commands to reproduce failures\n\n") + lines.append("```bash\n") + + # Generate one command per platform and test type + platform_components = group_components_by_platform(failed_results) + for platform, test_type in sorted(platform_components.keys()): + components_csv = ",".join(platform_components[(platform, test_type)]) + lines.append( + f"script/test_build_components.py -c {components_csv} -t {platform} -e {test_type}\n" + ) + + lines.append("```\n") + lines.append("
\n") + + # Show passed tests + if passed_results: + lines.append("### Passed Tests\n\n") + lines.append(f"{len(passed_results)} tests passed successfully\n") + + # Separate grouped and individual tests + grouped_results = [r for r in passed_results if len(r.components) > 1] + individual_results = [r for r in passed_results if len(r.components) == 1] + + if grouped_results: + lines.append("#### Grouped Tests\n") + lines.append("| Components | Platform | Count | Duration |\n") + lines.append("|-----------|----------|-------|----------|\n") + for result in grouped_results: + components_str = ", ".join(result.components) + lines.append( + f"| {components_str} | {result.platform} | {len(result.components)} | {result.duration:.1f}s |\n" + ) + lines.append("\n") + + if individual_results: + lines.append("#### Individual Tests\n") + # Show first 10 individual tests with timing + if len(individual_results) <= 10: + lines.extend( + f"- `{result.test_id}` - {result.duration:.1f}s\n" + for result in individual_results + ) + else: + lines.extend( + f"- `{result.test_id}` - {result.duration:.1f}s\n" + for result in individual_results[:10] + ) + lines.append(f"\n...and {len(individual_results) - 10} more\n") + lines.append("\n") + + return "".join(lines) + + +def write_github_summary(test_results: list[TestResult]) -> None: + """Write GitHub Actions job summary with test results and timing. + + Args: + test_results: List of all test results + """ + summary_content = format_github_summary(test_results) + with open(os.environ["GITHUB_STEP_SUMMARY"], "a", encoding="utf-8") as f: + f.write(summary_content) + + +def extract_platform_with_version(base_file: Path) -> str: + """Extract platform with version from base filename. + + Args: + base_file: Path to base file + + Returns: + Platform with version (e.g., "esp32-idf-50" or "esp32-idf") + """ + # Remove "build_components_base." prefix and ".yaml" suffix + return base_file.stem.replace("build_components_base.", "") + + +def run_esphome_test( + component: str, + test_file: Path, + platform: str, + platform_with_version: str, + base_file: Path, + build_dir: Path, + esphome_command: str, + continue_on_fail: bool, + use_testing_mode: bool = False, +) -> TestResult: + """Run esphome test for a single component. + + Args: + component: Component name + test_file: Path to component test file + platform: Platform name (e.g., "esp32-idf") + platform_with_version: Platform with version (e.g., "esp32-idf-50") + base_file: Path to platform base file + build_dir: Path to build directory + esphome_command: ESPHome command (config/compile) + continue_on_fail: Whether to continue on failure + use_testing_mode: Whether to use --testing-mode flag + + Returns: + TestResult object with test details and timing + """ + test_name = test_file.stem.split(".")[0] + + # Create dynamic test file in build directory + output_file = build_dir / f"{component}.{test_name}.{platform_with_version}.yaml" + + # Copy base file and substitute component test file reference + base_content = base_file.read_text() + # Get relative path from build dir to test file + repo_root = Path(__file__).parent.parent + component_test_ref = f"../../{test_file.relative_to(repo_root / 'tests')}" + output_content = base_content.replace("$component_test_file", component_test_ref) + output_file.write_text(output_content) + + # Build esphome command + cmd = [ + sys.executable, + "-m", + "esphome", + ] + + # Add --testing-mode if needed (must be before subcommand) + if use_testing_mode: + cmd.append("--testing-mode") + + # Add substitutions + cmd.extend( + [ + "-s", + "component_name", + component, + "-s", + "component_dir", + f"../../components/{component}", + "-s", + "test_name", + test_name, + "-s", + "target_platform", + platform, + ] + ) + + # Add command and config file + cmd.extend([esphome_command, str(output_file)]) + + # Build command string for display/logging + cmd_str = " ".join(cmd) + + # Run command + print(f"> [{component}] [{test_name}] [{platform_with_version}]") + if use_testing_mode: + print(" (using --testing-mode)") + + start_time = time.time() + test_id = f"{component}.{test_name}.{platform_with_version}" + + try: + result = subprocess.run(cmd, check=False) + success = result.returncode == 0 + duration = time.time() - start_time + + # Show disk space after build in CI during compile + show_disk_space_if_ci(esphome_command) + + if not success and not continue_on_fail: + # Print command immediately for failed tests + print(f"\n{'=' * 80}") + print("FAILED - Command to reproduce:") + print(f"{'=' * 80}") + print(cmd_str) + print() + raise subprocess.CalledProcessError(result.returncode, cmd) + + return TestResult( + test_id=test_id, + components=[component], + platform=platform_with_version, + success=success, + duration=duration, + command=cmd_str, + test_type=esphome_command, + ) + except subprocess.CalledProcessError: + duration = time.time() - start_time + # Re-raise if we're not continuing on fail + if not continue_on_fail: + raise + return TestResult( + test_id=test_id, + components=[component], + platform=platform_with_version, + success=False, + duration=duration, + command=cmd_str, + test_type=esphome_command, + ) + + +def run_grouped_test( + components: list[str], + platform: str, + platform_with_version: str, + base_file: Path, + build_dir: Path, + tests_dir: Path, + esphome_command: str, + continue_on_fail: bool, +) -> TestResult: + """Run esphome test for a group of components with shared bus configs. + + Args: + components: List of component names to test together + platform: Platform name (e.g., "esp32-idf") + platform_with_version: Platform with version (e.g., "esp32-idf-50") + base_file: Path to platform base file + build_dir: Path to build directory + tests_dir: Path to tests/components directory + esphome_command: ESPHome command (config/compile) + continue_on_fail: Whether to continue on failure + + Returns: + TestResult object with test details and timing + """ + # Create merged config + group_name = "_".join(components[:3]) # Use first 3 components for name + if len(components) > 3: + group_name += f"_plus_{len(components) - 3}" + + # Create unique device name by hashing sorted component list + platform + # This prevents conflicts when different component groups are tested + sorted_components = sorted(components) + hash_input = "_".join(sorted_components) + "_" + platform + group_hash = hashlib.md5(hash_input.encode()).hexdigest()[:8] + device_name = f"comptest{platform.replace('-', '')}{group_hash}" + + merged_config_file = build_dir / f"merged_{group_name}.{platform_with_version}.yaml" + + try: + merge_component_configs( + component_names=components, + platform=platform_with_version, + tests_dir=tests_dir, + output_file=merged_config_file, + ) + except Exception as e: # pylint: disable=broad-exception-caught + print(f"Error merging configs for {components}: {e}") + if not continue_on_fail: + raise + # Return TestResult for merge failure + test_id = f"GROUPED[{','.join(components)}].{platform_with_version}" + return TestResult( + test_id=test_id, + components=components, + platform=platform_with_version, + success=False, + duration=0.0, + command=f"# Failed during config merge: {e}", + test_type=esphome_command, + ) + + # Create test file that includes merged config + output_file = build_dir / f"test_{group_name}.{platform_with_version}.yaml" + base_content = base_file.read_text() + merged_ref = merged_config_file.name + output_content = base_content.replace("$component_test_file", merged_ref) + output_file.write_text(output_content) + + # Build esphome command with --testing-mode + cmd = [ + sys.executable, + "-m", + "esphome", + "--testing-mode", # Required for grouped tests + "-s", + "component_name", + device_name, # Use unique hash-based device name + "-s", + "component_dir", + "../../components", + "-s", + "test_name", + "merged", + "-s", + "target_platform", + platform, + esphome_command, + str(output_file), + ] + + # Build command string for display/logging + cmd_str = " ".join(cmd) + + # Run command + components_str = ", ".join(components) + print(f"> [GROUPED: {components_str}] [{platform_with_version}]") + print(" (using --testing-mode)") + + start_time = time.time() + test_id = f"GROUPED[{','.join(components)}].{platform_with_version}" + + try: + result = subprocess.run(cmd, check=False) + success = result.returncode == 0 + duration = time.time() - start_time + + # Show disk space after build in CI during compile + show_disk_space_if_ci(esphome_command) + + if not success and not continue_on_fail: + # Print command immediately for failed tests + print(f"\n{'=' * 80}") + print("FAILED - Command to reproduce:") + print(f"{'=' * 80}") + print(cmd_str) + print() + raise subprocess.CalledProcessError(result.returncode, cmd) + + return TestResult( + test_id=test_id, + components=components, + platform=platform_with_version, + success=success, + duration=duration, + command=cmd_str, + test_type=esphome_command, + ) + except subprocess.CalledProcessError: + duration = time.time() - start_time + # Re-raise if we're not continuing on fail + if not continue_on_fail: + raise + return TestResult( + test_id=test_id, + components=components, + platform=platform_with_version, + success=False, + duration=duration, + command=cmd_str, + test_type=esphome_command, + ) + + +def run_grouped_component_tests( + all_tests: dict[str, list[Path]], + platform_filter: str | None, + platform_bases: dict[str, list[Path]], + tests_dir: Path, + build_dir: Path, + esphome_command: str, + continue_on_fail: bool, + additional_isolated: set[str] | None = None, +) -> tuple[set[tuple[str, str]], list[TestResult]]: + """Run grouped component tests. + + Args: + all_tests: Dictionary mapping component names to test files + platform_filter: Optional platform to filter by + platform_bases: Platform base files mapping + tests_dir: Path to tests/components directory + build_dir: Path to build directory + esphome_command: ESPHome command (config/compile) + continue_on_fail: Whether to continue on failure + additional_isolated: Additional components to treat as isolated (not grouped) + + Returns: + Tuple of (tested_components, test_results) + """ + tested_components = set() + test_results = [] + + # Group components by platform and bus signature + grouped_components: dict[tuple[str, str], list[str]] = defaultdict(list) + print("\n" + "=" * 80) + print("Analyzing components for intelligent grouping...") + print("=" * 80) + component_buses, non_groupable, direct_bus_components = analyze_all_components( + tests_dir + ) + + # Track why components can't be grouped (for detailed output) + non_groupable_reasons = {} + + # Merge additional isolated components with predefined ones + # ISOLATED COMPONENTS are tested individually WITHOUT --testing-mode + # This is critical because: + # - Grouped tests use --testing-mode which disables pin conflict checks and other validation + # - These checks are disabled to allow config merging (multiple components in one build) + # - For directly changed components (via --isolate), we need full validation to catch issues + # - Dependencies are safe to group since they weren't modified in the PR + all_isolated = set(ISOLATED_COMPONENTS.keys()) + if additional_isolated: + all_isolated.update(additional_isolated) + + # Group by (platform, bus_signature) + for component, platforms in component_buses.items(): + if component not in all_tests: + continue + + # Skip components that must be tested in isolation + # These are shown separately and should not be in non_groupable_reasons + if component in all_isolated: + continue + + # Skip base bus components (these test the bus platforms themselves) + if component in BASE_BUS_COMPONENTS: + continue + + # Skip components that use local file references or direct bus configs + if component in non_groupable: + # Track the reason (using pre-calculated results to avoid expensive re-analysis) + if component not in non_groupable_reasons: + if component in direct_bus_components: + non_groupable_reasons[component] = ( + "Defines buses directly (not via packages) - NEEDS MIGRATION" + ) + elif uses_local_file_references(tests_dir / component): + non_groupable_reasons[component] = ( + "Uses local file references ($component_dir)" + ) + elif is_platform_component(tests_dir / component): + non_groupable_reasons[component] = ( + "Platform component (abstract base class)" + ) + else: + non_groupable_reasons[component] = ( + "Uses !extend or !remove directives" + ) + continue + + for platform, buses in platforms.items(): + # Skip if platform doesn't match filter + if platform_filter and not platform.startswith(platform_filter): + continue + + # Create signature for this component's bus configuration + # Components with no buses get NO_BUSES_SIGNATURE so they can be grouped together + if buses: + signature = create_grouping_signature({platform: buses}, platform) + else: + signature = NO_BUSES_SIGNATURE + + # Add to grouped components (including those with no buses) + if signature: + grouped_components[(platform, signature)].append(component) + + # Merge groups with compatible buses (cross-bus grouping optimization) + # This allows mixing components with different buses (e.g., ble + uart) + # as long as they don't have conflicting configurations for the same bus type + grouped_components = merge_compatible_bus_groups(grouped_components) + + # Print detailed grouping plan + print("\nGrouping Plan:") + print("-" * 80) + + # Show isolated components (must test individually due to known issues or direct changes) + isolated_in_tests = [c for c in all_isolated if c in all_tests] + if isolated_in_tests: + predefined_isolated = [c for c in isolated_in_tests if c in ISOLATED_COMPONENTS] + additional_in_tests = [ + c for c in isolated_in_tests if c in (additional_isolated or set()) + ] + + if predefined_isolated: + print( + f"\n⚠ {len(predefined_isolated)} components must be tested in isolation (known build issues):" + ) + for comp in sorted(predefined_isolated): + reason = ISOLATED_COMPONENTS[comp] + print(f" - {comp}: {reason}") + + if additional_in_tests: + print( + f"\n✓ {len(additional_in_tests)} components tested in isolation (directly changed in PR):" + ) + for comp in sorted(additional_in_tests): + print(f" - {comp}") + + # Show base bus components (test the bus platform implementations) + base_bus_in_tests = [c for c in BASE_BUS_COMPONENTS if c in all_tests] + if base_bus_in_tests: + print( + f"\n○ {len(base_bus_in_tests)} base bus platform components (tested individually):" + ) + for comp in sorted(base_bus_in_tests): + print(f" - {comp}") + + # Show excluded components with detailed reasons + if non_groupable_reasons: + excluded_in_tests = [c for c in non_groupable_reasons if c in all_tests] + if excluded_in_tests: + print( + f"\n⚠ {len(excluded_in_tests)} components excluded from grouping (each needs individual build):" + ) + # Group by reason to show summary + direct_bus = [ + c + for c in excluded_in_tests + if "NEEDS MIGRATION" in non_groupable_reasons.get(c, "") + ] + if direct_bus: + print( + f"\n ⚠⚠⚠ {len(direct_bus)} DEFINE BUSES DIRECTLY - NEED MIGRATION TO PACKAGES:" + ) + for comp in sorted(direct_bus): + print(f" - {comp}") + + other_reasons = [ + c + for c in excluded_in_tests + if "NEEDS MIGRATION" not in non_groupable_reasons.get(c, "") + ] + if other_reasons and len(other_reasons) <= 10: + print("\n Other non-groupable components:") + for comp in sorted(other_reasons): + reason = non_groupable_reasons[comp] + print(f" - {comp}: {reason}") + elif other_reasons: + print( + f"\n Other non-groupable components: {len(other_reasons)} components" + ) + + # Distribute no_buses components into other groups to maximize efficiency + # Components with no buses can merge with any bus group since they have no conflicting requirements + no_buses_by_platform: dict[str, list[str]] = {} + for (platform, signature), components in list(grouped_components.items()): + if signature == NO_BUSES_SIGNATURE: + no_buses_by_platform[platform] = components + # Remove from grouped_components - we'll distribute them + del grouped_components[(platform, signature)] + + # Distribute no_buses components into existing groups for each platform + for platform, no_buses_comps in no_buses_by_platform.items(): + # Find all non-empty groups for this platform (excluding no_buses) + platform_groups = [ + (sig, comps) + for (plat, sig), comps in grouped_components.items() + if plat == platform and sig != NO_BUSES_SIGNATURE + ] + + if platform_groups: + # Distribute no_buses components round-robin across existing groups + for i, comp in enumerate(no_buses_comps): + sig, _ = platform_groups[i % len(platform_groups)] + grouped_components[(platform, sig)].append(comp) + else: + # No other groups for this platform - keep no_buses components together + grouped_components[(platform, NO_BUSES_SIGNATURE)] = no_buses_comps + + groups_to_test = [] + individual_tests = set() # Use set to avoid duplicates + + for (platform, signature), components in sorted(grouped_components.items()): + if len(components) > 1: + groups_to_test.append((platform, signature, components)) + # Note: Don't add single-component groups to individual_tests here + # They'll be added below when we check for ungrouped components + + # Add components that weren't grouped on any platform + for component in all_tests: + if component not in [c for _, _, comps in groups_to_test for c in comps]: + individual_tests.add(component) + + if groups_to_test: + print(f"\n✓ {len(groups_to_test)} groups will be tested together:") + for platform, signature, components in groups_to_test: + component_list = ", ".join(sorted(components)) + print(f" [{platform}] [{signature}]: {component_list}") + print( + f" → {len(components)} components in 1 build (saves {len(components) - 1} builds)" + ) + + if individual_tests: + print(f"\n○ {len(individual_tests)} components will be tested individually:") + sorted_individual = sorted(individual_tests) + for comp in sorted_individual[:10]: + print(f" - {comp}") + if len(individual_tests) > 10: + print(f" ... and {len(individual_tests) - 10} more") + + # Calculate actual build counts based on test files, not component counts + # Without grouping: every test file would be built separately + total_test_files = sum(len(test_files) for test_files in all_tests.values()) + + # With grouping: + # - 1 build per group (regardless of how many components) + # - Individual components still need all their platform builds + individual_test_file_count = sum( + len(all_tests[comp]) for comp in individual_tests if comp in all_tests + ) + + total_grouped_components = sum(len(comps) for _, _, comps in groups_to_test) + total_builds_with_grouping = len(groups_to_test) + individual_test_file_count + builds_saved = total_test_files - total_builds_with_grouping + + print(f"\n{'=' * 80}") + print( + f"Summary: {total_builds_with_grouping} builds total (vs {total_test_files} without grouping)" + ) + print( + f" • {len(groups_to_test)} grouped builds ({total_grouped_components} components)" + ) + print( + f" • {individual_test_file_count} individual builds ({len(individual_tests)} components)" + ) + if total_test_files > 0: + reduction_pct = (builds_saved / total_test_files) * 100 + print(f" • Saves {builds_saved} builds ({reduction_pct:.1f}% reduction)") + print("=" * 80 + "\n") + + # Execute grouped tests + for (platform, signature), components in grouped_components.items(): + # Only group if we have multiple components with same signature + if len(components) <= 1: + continue + + # Filter out components not in our test list + components_to_group = [c for c in components if c in all_tests] + if len(components_to_group) <= 1: + continue + + # Get platform base files + if platform not in platform_bases: + continue + + for base_file in platform_bases[platform]: + platform_with_version = extract_platform_with_version(base_file) + + # Skip if platform filter doesn't match + if platform_filter and platform != platform_filter: + continue + if ( + platform_filter + and platform_with_version != platform_filter + and not platform_with_version.startswith(f"{platform_filter}-") + ): + continue + + # Run grouped test + test_result = run_grouped_test( + components=components_to_group, + platform=platform, + platform_with_version=platform_with_version, + base_file=base_file, + build_dir=build_dir, + tests_dir=tests_dir, + esphome_command=esphome_command, + continue_on_fail=continue_on_fail, + ) + + # Mark all components as tested + for comp in components_to_group: + tested_components.add((comp, platform_with_version)) + + # Store test result + test_results.append(test_result) + + return tested_components, test_results + + +def run_individual_component_test( + component: str, + test_file: Path, + platform: str, + platform_with_version: str, + base_file: Path, + build_dir: Path, + esphome_command: str, + continue_on_fail: bool, + tested_components: set[tuple[str, str]], + test_results: list[TestResult], +) -> None: + """Run an individual component test if not already tested in a group. + + Args: + component: Component name + test_file: Test file path + platform: Platform name + platform_with_version: Platform with version + base_file: Base file for platform + build_dir: Build directory + esphome_command: ESPHome command + continue_on_fail: Whether to continue on failure + tested_components: Set of already tested components + test_results: List to append test results + """ + # Skip if already tested in a group + if (component, platform_with_version) in tested_components: + return + + test_result = run_esphome_test( + component=component, + test_file=test_file, + platform=platform, + platform_with_version=platform_with_version, + base_file=base_file, + build_dir=build_dir, + esphome_command=esphome_command, + continue_on_fail=continue_on_fail, + ) + test_results.append(test_result) + + +def test_components( + component_patterns: list[str], + platform_filter: str | None, + esphome_command: str, + continue_on_fail: bool, + enable_grouping: bool = True, + isolated_components: set[str] | None = None, + base_only: bool = False, +) -> int: + """Test components with optional intelligent grouping. + + Args: + component_patterns: List of component name patterns + platform_filter: Optional platform to filter by + esphome_command: ESPHome command (config/compile) + continue_on_fail: Whether to continue on failure + enable_grouping: Whether to enable component grouping + isolated_components: Set of component names to test in isolation (not grouped). + These are tested WITHOUT --testing-mode to enable full validation + (pin conflicts, etc). This is used in CI for directly changed components + to catch issues that would be missed with --testing-mode. + base_only: If True, only test base test files (test.*.yaml), not variant files (test-*.yaml) + + Returns: + Exit code (0 for success, 1 for failure) + """ + # Setup paths + repo_root = Path(__file__).parent.parent + tests_dir = repo_root / "tests" / "components" + build_components_dir = repo_root / "tests" / "test_build_components" + build_dir = build_components_dir / "build" + build_dir.mkdir(parents=True, exist_ok=True) + + # Get platform base files + platform_bases = get_platform_base_files(build_components_dir) + + # Find all component tests + all_tests = {} + for pattern in component_patterns: + # Skip empty patterns (happens when components list is empty string) + if not pattern: + continue + all_tests.update(find_component_tests(tests_dir, pattern, base_only)) + + # If no components found, build a reference configuration for baseline comparison + # Create a synthetic "empty" component test that will build just the base config + if not all_tests: + print(f"No components found matching: {component_patterns}") + print( + "Building reference configuration with no components for baseline comparison..." + ) + + # Create empty test files for each platform (or filtered platform) + reference_tests: list[Path] = [] + for platform_name, base_file in platform_bases.items(): + if platform_filter and not platform_name.startswith(platform_filter): + continue + # Create an empty test file named to match the platform + empty_test_file = build_dir / f"reference.{platform_name}.yaml" + empty_test_file.write_text( + "# Empty component test for baseline reference\n" + ) + reference_tests.append(empty_test_file) + + # Add to all_tests dict with component name "reference" + all_tests["reference"] = reference_tests + + print(f"Found {len(all_tests)} components to test") + + # Run tests + test_results = [] + tested_components = set() # Track which components were tested in groups + + # First, run grouped tests if grouping is enabled + if enable_grouping: + tested_components, grouped_results = run_grouped_component_tests( + all_tests=all_tests, + platform_filter=platform_filter, + platform_bases=platform_bases, + tests_dir=tests_dir, + build_dir=build_dir, + esphome_command=esphome_command, + continue_on_fail=continue_on_fail, + additional_isolated=isolated_components, + ) + test_results.extend(grouped_results) + + # Then run individual tests for components not in groups + for component, test_files in sorted(all_tests.items()): + for test_file in test_files: + test_name, platform = parse_test_filename(test_file) + + # Handle "all" platform tests + if platform == "all": + # Run for all platforms + for plat, base_files in platform_bases.items(): + if platform_filter and plat != platform_filter: + continue + + for base_file in base_files: + platform_with_version = extract_platform_with_version(base_file) + run_individual_component_test( + component=component, + test_file=test_file, + platform=plat, + platform_with_version=platform_with_version, + base_file=base_file, + build_dir=build_dir, + esphome_command=esphome_command, + continue_on_fail=continue_on_fail, + tested_components=tested_components, + test_results=test_results, + ) + else: + # Platform-specific test + if platform_filter and platform != platform_filter: + continue + + if platform not in platform_bases: + print(f"No base file for platform: {platform}") + continue + + for base_file in platform_bases[platform]: + platform_with_version = extract_platform_with_version(base_file) + + # Skip if requested platform doesn't match + if ( + platform_filter + and platform_with_version != platform_filter + and not platform_with_version.startswith(f"{platform_filter}-") + ): + continue + + run_individual_component_test( + component=component, + test_file=test_file, + platform=platform, + platform_with_version=platform_with_version, + base_file=base_file, + build_dir=build_dir, + esphome_command=esphome_command, + continue_on_fail=continue_on_fail, + tested_components=tested_components, + test_results=test_results, + ) + + # Separate results into passed and failed + passed_results = [r for r in test_results if r.success] + failed_results = [r for r in test_results if not r.success] + + # Print summary + print("\n" + "=" * 80) + print(f"Test Summary: {len(passed_results)} passed, {len(failed_results)} failed") + print("=" * 80) + + if failed_results: + print("\nFailed tests:") + for result in failed_results: + print(f" - {result.test_id}") + + # Print simplified commands grouped by platform and test type for easy copy-paste + print("\n" + "=" * 80) + print("Commands to reproduce failures (copy-paste to reproduce locally):") + print("=" * 80) + platform_components = group_components_by_platform(failed_results) + for platform, test_type in sorted(platform_components.keys()): + components_csv = ",".join(platform_components[(platform, test_type)]) + print( + f"script/test_build_components.py -c {components_csv} -t {platform} -e {test_type}" + ) + print() + + # Write GitHub Actions job summary if in CI + if os.environ.get("GITHUB_STEP_SUMMARY"): + write_github_summary(test_results) + + if failed_results: + return 1 + + return 0 + + +def main() -> int: + """Main entry point.""" + parser = argparse.ArgumentParser( + description="Test ESPHome component builds with intelligent grouping" + ) + parser.add_argument( + "-e", + "--esphome-command", + default="compile", + choices=["config", "compile", "clean"], + help="ESPHome command to run (default: compile)", + ) + parser.add_argument( + "-c", + "--components", + default="*", + help="Component pattern(s) to test (default: *). Comma-separated.", + ) + parser.add_argument( + "-t", + "--target", + help="Target platform to test (e.g., esp32-idf)", + ) + parser.add_argument( + "-f", + "--continue-on-fail", + action="store_true", + help="Continue testing even if a test fails", + ) + parser.add_argument( + "--no-grouping", + action="store_true", + help="Disable component grouping (test each component individually)", + ) + parser.add_argument( + "--isolate", + help="Comma-separated list of components to test in isolation (not grouped with others). " + "These are tested WITHOUT --testing-mode to enable full validation. " + "Used in CI for directly changed components to catch pin conflicts and other issues.", + ) + parser.add_argument( + "--base-only", + action="store_true", + help="Only test base test files (test.*.yaml), not variant files (test-*.yaml)", + ) + + args = parser.parse_args() + + # Parse component patterns + component_patterns = [p.strip() for p in args.components.split(",")] + + # Parse isolated components + isolated_components = None + if args.isolate: + isolated_components = {c.strip() for c in args.isolate.split(",") if c.strip()} + + return test_components( + component_patterns=component_patterns, + platform_filter=args.target, + esphome_command=args.esphome_command, + continue_on_fail=args.continue_on_fail, + enable_grouping=not args.no_grouping, + isolated_components=isolated_components, + base_only=args.base_only, + ) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/script/test_component_grouping.py b/script/test_component_grouping.py new file mode 100755 index 0000000000..a2cee6e888 --- /dev/null +++ b/script/test_component_grouping.py @@ -0,0 +1,227 @@ +#!/usr/bin/env python3 +"""Test component grouping by finding and testing groups of components. + +This script analyzes components, finds groups that can be tested together, +and runs test builds for those groups. +""" + +from __future__ import annotations + +import argparse +from pathlib import Path +import subprocess +import sys + +# Add esphome to path +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from script.analyze_component_buses import ( + analyze_all_components, + group_components_by_signature, +) + + +def test_component_group( + components: list[str], + platform: str, + esphome_command: str = "compile", + dry_run: bool = False, +) -> bool: + """Test a group of components together. + + Args: + components: List of component names to test together + platform: Platform to test on (e.g., "esp32-idf") + esphome_command: ESPHome command to run (config/compile/clean) + dry_run: If True, only print the command without running it + + Returns: + True if test passed, False otherwise + """ + components_str = ",".join(components) + cmd = [ + "./script/test_build_components", + "-c", + components_str, + "-t", + platform, + "-e", + esphome_command, + ] + + print(f"\n{'=' * 80}") + print(f"Testing {len(components)} components on {platform}:") + for comp in components: + print(f" - {comp}") + print(f"{'=' * 80}") + print(f"Command: {' '.join(cmd)}\n") + + if dry_run: + print("[DRY RUN] Skipping actual test") + return True + + try: + result = subprocess.run(cmd, check=False) + return result.returncode == 0 + except Exception as e: + print(f"Error running test: {e}") + return False + + +def main() -> None: + """Main entry point.""" + parser = argparse.ArgumentParser( + description="Test component grouping by finding and testing groups" + ) + parser.add_argument( + "--platform", + "-p", + default="esp32-idf", + help="Platform to test (default: esp32-idf)", + ) + parser.add_argument( + "-e", + "--esphome-command", + default="compile", + choices=["config", "compile", "clean"], + help="ESPHome command to run (default: compile)", + ) + parser.add_argument( + "--all", + action="store_true", + help="Test all components (sets --min-size=1, --max-size=10000, --max-groups=10000)", + ) + parser.add_argument( + "--min-size", + type=int, + default=3, + help="Minimum group size to test (default: 3)", + ) + parser.add_argument( + "--max-size", + type=int, + default=10, + help="Maximum group size to test (default: 10)", + ) + parser.add_argument( + "--max-groups", + type=int, + default=5, + help="Maximum number of groups to test (default: 5)", + ) + parser.add_argument( + "--signature", + "-s", + help="Only test groups with this bus signature (e.g., 'spi', 'i2c', 'uart')", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Print commands without running them", + ) + + args = parser.parse_args() + + # If --all is specified, test all components without grouping + if args.all: + # Get all components from tests/components directory + components_dir = Path("tests/components") + all_components = sorted( + [d.name for d in components_dir.iterdir() if d.is_dir()] + ) + + if not all_components: + print(f"\nNo components found in {components_dir}") + return + + print(f"\nTesting all {len(all_components)} components together") + + success = test_component_group( + all_components, args.platform, args.esphome_command, args.dry_run + ) + + # Print summary + print(f"\n{'=' * 80}") + print("TEST SUMMARY") + print(f"{'=' * 80}") + status = "✅ PASS" if success else "❌ FAIL" + print(f"{status} All components: {len(all_components)} components") + + if not args.dry_run and not success: + sys.exit(1) + return + + print("Analyzing all components...") + components, non_groupable, _ = analyze_all_components(Path("tests/components")) + + print(f"Found {len(components)} components, {len(non_groupable)} non-groupable") + + # Group components by signature for the platform + groups = group_components_by_signature(components, args.platform) + + # Filter and sort groups + filtered_groups = [] + for signature, comp_list in groups.items(): + # Filter by signature if specified + if args.signature and signature != args.signature: + continue + + # Remove non-groupable components + comp_list = [c for c in comp_list if c not in non_groupable] + + # Filter by minimum size + if len(comp_list) < args.min_size: + continue + + # If group is larger than max_size, we'll take a subset later + filtered_groups.append((signature, comp_list)) + + # Sort by group size (largest first) + filtered_groups.sort(key=lambda x: len(x[1]), reverse=True) + + # Limit number of groups + filtered_groups = filtered_groups[: args.max_groups] + + if not filtered_groups: + print("\nNo groups found matching criteria:") + print(f" - Platform: {args.platform}") + print(f" - Size: {args.min_size}-{args.max_size}") + if args.signature: + print(f" - Signature: {args.signature}") + return + + print(f"\nFound {len(filtered_groups)} groups to test:") + for signature, comp_list in filtered_groups: + print(f" [{signature}]: {len(comp_list)} components") + + # Test each group + results = [] + for signature, comp_list in filtered_groups: + # Limit to max_size if group is larger + if len(comp_list) > args.max_size: + comp_list = comp_list[: args.max_size] + + success = test_component_group( + comp_list, args.platform, args.esphome_command, args.dry_run + ) + results.append((signature, comp_list, success)) + + if not args.dry_run and not success: + print(f"\n❌ FAILED: {signature} group") + break + + # Print summary + print(f"\n{'=' * 80}") + print("TEST SUMMARY") + print(f"{'=' * 80}") + for signature, comp_list, success in results: + status = "✅ PASS" if success else "❌ FAIL" + print(f"{status} [{signature}]: {len(comp_list)} components") + + # Exit with error if any tests failed + if not args.dry_run and any(not success for _, _, success in results): + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/sdkconfig.defaults b/sdkconfig.defaults index 72ca3f6e9c..322efb701a 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -13,6 +13,7 @@ CONFIG_ESP_TASK_WDT=y CONFIG_ESP_TASK_WDT_PANIC=y CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0=n CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1=n +CONFIG_AUTOSTART_ARDUINO=y # esp32_ble CONFIG_BT_ENABLED=y diff --git a/tests/component_tests/binary_sensor/test_binary_sensor.py b/tests/component_tests/binary_sensor/test_binary_sensor.py index 32d74027ba..86e0705023 100644 --- a/tests/component_tests/binary_sensor/test_binary_sensor.py +++ b/tests/component_tests/binary_sensor/test_binary_sensor.py @@ -29,7 +29,7 @@ def test_binary_sensor_sets_mandatory_fields(generate_main): ) # Then - assert 'bs_1->set_name("test bs1");' in main_cpp + assert 'bs_1->set_name_and_object_id("test bs1", "test_bs1");' in main_cpp assert "bs_1->set_pin(" in main_cpp diff --git a/tests/component_tests/button/test_button.py b/tests/component_tests/button/test_button.py index 512ef42b44..b21665288c 100644 --- a/tests/component_tests/button/test_button.py +++ b/tests/component_tests/button/test_button.py @@ -26,7 +26,7 @@ def test_button_sets_mandatory_fields(generate_main): main_cpp = generate_main("tests/component_tests/button/test_button.yaml") # Then - assert 'wol_1->set_name("wol_test_1");' in main_cpp + assert 'wol_1->set_name_and_object_id("wol_test_1", "wol_test_1");' in main_cpp assert "wol_2->set_macaddr(18, 52, 86, 120, 144, 171);" in main_cpp diff --git a/tests/component_tests/conftest.py b/tests/component_tests/conftest.py index 2045b03502..0641e698e9 100644 --- a/tests/component_tests/conftest.py +++ b/tests/component_tests/conftest.py @@ -6,6 +6,7 @@ from collections.abc import Callable, Generator from pathlib import Path import sys from typing import Any +from unittest import mock import pytest @@ -17,6 +18,7 @@ from esphome.const import ( PlatformFramework, ) from esphome.types import ConfigType +from esphome.util import OrderedDict # Add package root to python path here = Path(__file__).parent @@ -40,9 +42,9 @@ def config_path(request: pytest.FixtureRequest) -> Generator[None]: if config_dir.exists(): # Set config_path to a dummy yaml file in the config directory # This ensures CORE.config_dir points to the config directory - CORE.config_path = str(config_dir / "dummy.yaml") + CORE.config_path = config_dir / "dummy.yaml" else: - CORE.config_path = str(Path(request.fspath).parent / "dummy.yaml") + CORE.config_path = Path(request.fspath).parent / "dummy.yaml" yield CORE.config_path = original_path @@ -129,9 +131,35 @@ def generate_main() -> Generator[Callable[[str | Path], str]]: """Generates the C++ main.cpp from a given yaml file and returns it in string form.""" def generator(path: str | Path) -> str: - CORE.config_path = str(path) + CORE.config_path = Path(path) CORE.config = read_config({}) generate_cpp_contents(CORE.config) return CORE.cpp_main_section yield generator + + +@pytest.fixture +def mock_clone_or_update() -> Generator[Any]: + """Mock git.clone_or_update for testing.""" + with mock.patch("esphome.git.clone_or_update") as mock_func: + # Default return value + mock_func.return_value = (Path("/tmp/test"), None) + yield mock_func + + +@pytest.fixture +def mock_load_yaml() -> Generator[Any]: + """Mock yaml_util.load_yaml for testing.""" + + with mock.patch("esphome.yaml_util.load_yaml") as mock_func: + # Default return value + mock_func.return_value = OrderedDict({"sensor": []}) + yield mock_func + + +@pytest.fixture +def mock_install_meta_finder() -> Generator[Any]: + """Mock loader.install_meta_finder for testing.""" + with mock.patch("esphome.loader.install_meta_finder") as mock_func: + yield mock_func diff --git a/tests/component_tests/external_components/test_init.py b/tests/component_tests/external_components/test_init.py new file mode 100644 index 0000000000..905c0afa8b --- /dev/null +++ b/tests/component_tests/external_components/test_init.py @@ -0,0 +1,134 @@ +"""Tests for the external_components skip_update functionality.""" + +from pathlib import Path +from typing import Any +from unittest.mock import MagicMock + +from esphome.components.external_components import do_external_components_pass +from esphome.const import ( + CONF_EXTERNAL_COMPONENTS, + CONF_REFRESH, + CONF_SOURCE, + CONF_URL, + TYPE_GIT, +) + + +def test_external_components_skip_update_true( + tmp_path: Path, mock_clone_or_update: MagicMock, mock_install_meta_finder: MagicMock +) -> None: + """Test that external components don't update when skip_update=True.""" + # Create a components directory structure + components_dir = tmp_path / "components" + components_dir.mkdir() + + # Create a test component + test_component_dir = components_dir / "test_component" + test_component_dir.mkdir() + (test_component_dir / "__init__.py").write_text("# Test component") + + # Set up mock to return our tmp_path + mock_clone_or_update.return_value = (tmp_path, None) + + config: dict[str, Any] = { + CONF_EXTERNAL_COMPONENTS: [ + { + CONF_SOURCE: { + "type": TYPE_GIT, + CONF_URL: "https://github.com/test/components", + }, + CONF_REFRESH: "1d", + "components": "all", + } + ] + } + + # Call with skip_update=True + do_external_components_pass(config, skip_update=True) + + # Verify clone_or_update was called with NEVER_REFRESH + mock_clone_or_update.assert_called_once() + call_args = mock_clone_or_update.call_args + from esphome import git + + assert call_args.kwargs["refresh"] == git.NEVER_REFRESH + + +def test_external_components_skip_update_false( + tmp_path: Path, mock_clone_or_update: MagicMock, mock_install_meta_finder: MagicMock +) -> None: + """Test that external components update when skip_update=False.""" + # Create a components directory structure + components_dir = tmp_path / "components" + components_dir.mkdir() + + # Create a test component + test_component_dir = components_dir / "test_component" + test_component_dir.mkdir() + (test_component_dir / "__init__.py").write_text("# Test component") + + # Set up mock to return our tmp_path + mock_clone_or_update.return_value = (tmp_path, None) + + config: dict[str, Any] = { + CONF_EXTERNAL_COMPONENTS: [ + { + CONF_SOURCE: { + "type": TYPE_GIT, + CONF_URL: "https://github.com/test/components", + }, + CONF_REFRESH: "1d", + "components": "all", + } + ] + } + + # Call with skip_update=False + do_external_components_pass(config, skip_update=False) + + # Verify clone_or_update was called with actual refresh value + mock_clone_or_update.assert_called_once() + call_args = mock_clone_or_update.call_args + from esphome.core import TimePeriodSeconds + + assert call_args.kwargs["refresh"] == TimePeriodSeconds(days=1) + + +def test_external_components_default_no_skip( + tmp_path: Path, mock_clone_or_update: MagicMock, mock_install_meta_finder: MagicMock +) -> None: + """Test that external components update by default when skip_update not specified.""" + # Create a components directory structure + components_dir = tmp_path / "components" + components_dir.mkdir() + + # Create a test component + test_component_dir = components_dir / "test_component" + test_component_dir.mkdir() + (test_component_dir / "__init__.py").write_text("# Test component") + + # Set up mock to return our tmp_path + mock_clone_or_update.return_value = (tmp_path, None) + + config: dict[str, Any] = { + CONF_EXTERNAL_COMPONENTS: [ + { + CONF_SOURCE: { + "type": TYPE_GIT, + CONF_URL: "https://github.com/test/components", + }, + CONF_REFRESH: "1d", + "components": "all", + } + ] + } + + # Call without skip_update parameter + do_external_components_pass(config) + + # Verify clone_or_update was called with actual refresh value + mock_clone_or_update.assert_called_once() + call_args = mock_clone_or_update.call_args + from esphome.core import TimePeriodSeconds + + assert call_args.kwargs["refresh"] == TimePeriodSeconds(days=1) diff --git a/tests/component_tests/gpio/test_gpio_binary_sensor.py b/tests/component_tests/gpio/test_gpio_binary_sensor.py index 74fa2ab1c1..73665dc45d 100644 --- a/tests/component_tests/gpio/test_gpio_binary_sensor.py +++ b/tests/component_tests/gpio/test_gpio_binary_sensor.py @@ -18,7 +18,8 @@ def test_gpio_binary_sensor_basic_setup( assert "new gpio::GPIOBinarySensor();" in main_cpp assert "App.register_binary_sensor" in main_cpp - assert "bs_gpio->set_use_interrupt(true);" in main_cpp + # set_use_interrupt(true) should NOT be generated (uses C++ default) + assert "bs_gpio->set_use_interrupt(true);" not in main_cpp assert "bs_gpio->set_interrupt_type(gpio::INTERRUPT_ANY_EDGE);" in main_cpp @@ -51,8 +52,8 @@ def test_gpio_binary_sensor_esp8266_other_pins_use_interrupt( "tests/component_tests/gpio/test_gpio_binary_sensor_esp8266.yaml" ) - # GPIO5 should still use interrupts - assert "bs_gpio5->set_use_interrupt(true);" in main_cpp + # GPIO5 should still use interrupts (default, so no setter call) + assert "bs_gpio5->set_use_interrupt(true);" not in main_cpp assert "bs_gpio5->set_interrupt_type(gpio::INTERRUPT_ANY_EDGE);" in main_cpp diff --git a/tests/component_tests/mipi_spi/test_init.py b/tests/component_tests/mipi_spi/test_init.py index fbb3222812..56a52df2ab 100644 --- a/tests/component_tests/mipi_spi/test_init.py +++ b/tests/component_tests/mipi_spi/test_init.py @@ -69,7 +69,7 @@ def run_schema_validation(config: ConfigType) -> None: { "id": "display_id", "model": "custom", - "dimensions": {"width": 320, "height": 240}, + "dimensions": {"width": 260, "height": 260}, "draw_rounding": 13, "init_sequence": [[0xA0, 0x01]], }, @@ -220,7 +220,7 @@ def test_esp32s3_specific_errors( set_core_config( PlatformFramework.ESP32_IDF, - platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32S3}, + platform_data={KEY_BOARD: "esp32-s3-devkitc-1", KEY_VARIANT: VARIANT_ESP32S3}, ) with pytest.raises(cv.Invalid, match=error_match): @@ -250,7 +250,7 @@ def test_custom_model_with_all_options( """Test custom model configuration with all available options.""" set_core_config( PlatformFramework.ESP32_IDF, - platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32S3}, + platform_data={KEY_BOARD: "esp32-s3-devkitc-1", KEY_VARIANT: VARIANT_ESP32S3}, ) run_schema_validation( @@ -293,7 +293,7 @@ def test_all_predefined_models( """Test all predefined display models validate successfully with appropriate defaults.""" set_core_config( PlatformFramework.ESP32_IDF, - platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32S3}, + platform_data={KEY_BOARD: "esp32-s3-devkitc-1", KEY_VARIANT: VARIANT_ESP32S3}, ) # Enable PSRAM which is required for some models @@ -336,7 +336,7 @@ def test_native_generation( main_cpp = generate_main(component_fixture_path("native.yaml")) assert ( - "mipi_spi::MipiSpiBuffer()" + "mipi_spi::MipiSpiBuffer()" in main_cpp ) assert "set_init_sequence({240, 1, 8, 242" in main_cpp diff --git a/tests/component_tests/ota/test_web_server_ota.py b/tests/component_tests/ota/test_web_server_ota.py index 0d8ff6f134..794eaac9be 100644 --- a/tests/component_tests/ota/test_web_server_ota.py +++ b/tests/component_tests/ota/test_web_server_ota.py @@ -1,6 +1,18 @@ """Tests for the web_server OTA platform.""" +from __future__ import annotations + from collections.abc import Callable +import logging +from typing import Any + +import pytest + +from esphome import config_validation as cv +from esphome.components.web_server.ota import _web_server_ota_final_validate +from esphome.const import CONF_ID, CONF_OTA, CONF_PLATFORM, CONF_WEB_SERVER +from esphome.core import ID +import esphome.final_validate as fv def test_web_server_ota_generated(generate_main: Callable[[str], str]) -> None: @@ -100,3 +112,144 @@ def test_web_server_ota_esp8266(generate_main: Callable[[str], str]) -> None: # Check web server OTA component is present assert "WebServerOTAComponent" in main_cpp assert "web_server::WebServerOTAComponent" in main_cpp + + +@pytest.mark.parametrize( + ("ota_configs", "expected_count", "warning_expected"), + [ + pytest.param( + [ + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web", is_manual=False), + } + ], + 1, + False, + id="single_instance_no_merge", + ), + pytest.param( + [ + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web_1", is_manual=False), + }, + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web_2", is_manual=False), + }, + ], + 1, + True, + id="two_instances_merged", + ), + pytest.param( + [ + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web_1", is_manual=False), + }, + { + CONF_PLATFORM: "esphome", + CONF_ID: ID("ota_esphome", is_manual=False), + }, + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web_2", is_manual=False), + }, + ], + 2, + True, + id="mixed_platforms_web_server_merged", + ), + ], +) +def test_web_server_ota_instance_merging( + ota_configs: list[dict[str, Any]], + expected_count: int, + warning_expected: bool, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test web_server OTA instance merging behavior.""" + full_conf = {CONF_OTA: ota_configs.copy()} + + token = fv.full_config.set(full_conf) + try: + with caplog.at_level(logging.WARNING): + _web_server_ota_final_validate({}) + + updated_conf = fv.full_config.get() + + # Verify total number of OTA platforms + assert len(updated_conf[CONF_OTA]) == expected_count + + # Verify warning + if warning_expected: + assert any( + "Found and merged" in record.message + and "web_server OTA" in record.message + for record in caplog.records + ), "Expected merge warning not found in log" + else: + assert len(caplog.records) == 0, "Unexpected warnings logged" + finally: + fv.full_config.reset(token) + + +def test_web_server_ota_consistent_manual_ids( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test that consistent manual IDs can be merged successfully.""" + ota_configs = [ + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web", is_manual=True), + }, + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web", is_manual=True), + }, + ] + + full_conf = {CONF_OTA: ota_configs} + + token = fv.full_config.set(full_conf) + try: + with caplog.at_level(logging.WARNING): + _web_server_ota_final_validate({}) + + updated_conf = fv.full_config.get() + assert len(updated_conf[CONF_OTA]) == 1 + assert updated_conf[CONF_OTA][0][CONF_ID].id == "ota_web" + assert any( + "Found and merged" in record.message and "web_server OTA" in record.message + for record in caplog.records + ) + finally: + fv.full_config.reset(token) + + +def test_web_server_ota_inconsistent_manual_ids() -> None: + """Test that inconsistent manual IDs raise an error.""" + ota_configs = [ + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web_1", is_manual=True), + }, + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web_2", is_manual=True), + }, + ] + + full_conf = {CONF_OTA: ota_configs} + + token = fv.full_config.set(full_conf) + try: + with pytest.raises( + cv.Invalid, + match="Found multiple web_server OTA configurations but id is inconsistent", + ): + _web_server_ota_final_validate({}) + finally: + fv.full_config.reset(token) diff --git a/tests/component_tests/packages/test_init.py b/tests/component_tests/packages/test_init.py new file mode 100644 index 0000000000..779244e2ed --- /dev/null +++ b/tests/component_tests/packages/test_init.py @@ -0,0 +1,114 @@ +"""Tests for the packages component skip_update functionality.""" + +from pathlib import Path +from typing import Any +from unittest.mock import MagicMock + +from esphome.components.packages import do_packages_pass +from esphome.const import CONF_FILES, CONF_PACKAGES, CONF_REFRESH, CONF_URL +from esphome.util import OrderedDict + + +def test_packages_skip_update_true( + tmp_path: Path, mock_clone_or_update: MagicMock, mock_load_yaml: MagicMock +) -> None: + """Test that packages don't update when skip_update=True.""" + # Set up mock to return our tmp_path + mock_clone_or_update.return_value = (tmp_path, None) + + # Create the test yaml file + test_file = tmp_path / "test.yaml" + test_file.write_text("sensor: []") + + # Set mock_load_yaml to return some valid config + mock_load_yaml.return_value = OrderedDict({"sensor": []}) + + config: dict[str, Any] = { + CONF_PACKAGES: { + "test_package": { + CONF_URL: "https://github.com/test/repo", + CONF_FILES: ["test.yaml"], + CONF_REFRESH: "1d", + } + } + } + + # Call with skip_update=True + do_packages_pass(config, skip_update=True) + + # Verify clone_or_update was called with NEVER_REFRESH + mock_clone_or_update.assert_called_once() + call_args = mock_clone_or_update.call_args + from esphome import git + + assert call_args.kwargs["refresh"] == git.NEVER_REFRESH + + +def test_packages_skip_update_false( + tmp_path: Path, mock_clone_or_update: MagicMock, mock_load_yaml: MagicMock +) -> None: + """Test that packages update when skip_update=False.""" + # Set up mock to return our tmp_path + mock_clone_or_update.return_value = (tmp_path, None) + + # Create the test yaml file + test_file = tmp_path / "test.yaml" + test_file.write_text("sensor: []") + + # Set mock_load_yaml to return some valid config + mock_load_yaml.return_value = OrderedDict({"sensor": []}) + + config: dict[str, Any] = { + CONF_PACKAGES: { + "test_package": { + CONF_URL: "https://github.com/test/repo", + CONF_FILES: ["test.yaml"], + CONF_REFRESH: "1d", + } + } + } + + # Call with skip_update=False (default) + do_packages_pass(config, skip_update=False) + + # Verify clone_or_update was called with actual refresh value + mock_clone_or_update.assert_called_once() + call_args = mock_clone_or_update.call_args + from esphome.core import TimePeriodSeconds + + assert call_args.kwargs["refresh"] == TimePeriodSeconds(days=1) + + +def test_packages_default_no_skip( + tmp_path: Path, mock_clone_or_update: MagicMock, mock_load_yaml: MagicMock +) -> None: + """Test that packages update by default when skip_update not specified.""" + # Set up mock to return our tmp_path + mock_clone_or_update.return_value = (tmp_path, None) + + # Create the test yaml file + test_file = tmp_path / "test.yaml" + test_file.write_text("sensor: []") + + # Set mock_load_yaml to return some valid config + mock_load_yaml.return_value = OrderedDict({"sensor": []}) + + config: dict[str, Any] = { + CONF_PACKAGES: { + "test_package": { + CONF_URL: "https://github.com/test/repo", + CONF_FILES: ["test.yaml"], + CONF_REFRESH: "1d", + } + } + } + + # Call without skip_update parameter + do_packages_pass(config) + + # Verify clone_or_update was called with actual refresh value + mock_clone_or_update.assert_called_once() + call_args = mock_clone_or_update.call_args + from esphome.core import TimePeriodSeconds + + assert call_args.kwargs["refresh"] == TimePeriodSeconds(days=1) diff --git a/tests/component_tests/packages/test_packages.py b/tests/component_tests/packages/test_packages.py index 4712daad0d..ac4e211fe6 100644 --- a/tests/component_tests/packages/test_packages.py +++ b/tests/component_tests/packages/test_packages.py @@ -5,7 +5,8 @@ from unittest.mock import MagicMock, patch import pytest -from esphome.components.packages import do_packages_pass +from esphome.components.packages import CONFIG_SCHEMA, do_packages_pass +from esphome.config import resolve_extend_remove from esphome.config_helpers import Extend, Remove import esphome.config_validation as cv from esphome.const import ( @@ -64,13 +65,20 @@ def fixture_basic_esphome(): return {CONF_NAME: TEST_DEVICE_NAME, CONF_PLATFORM: TEST_PLATFORM} +def packages_pass(config): + """Wrapper around packages_pass that also resolves Extend and Remove.""" + config = do_packages_pass(config) + resolve_extend_remove(config) + return config + + def test_package_unused(basic_esphome, basic_wifi): """ Ensures do_package_pass does not change a config if packages aren't used. """ config = {CONF_ESPHOME: basic_esphome, CONF_WIFI: basic_wifi} - actual = do_packages_pass(config) + actual = packages_pass(config) assert actual == config @@ -83,7 +91,51 @@ def test_package_invalid_dict(basic_esphome, basic_wifi): config = {CONF_ESPHOME: basic_esphome, CONF_PACKAGES: basic_wifi | {CONF_URL: ""}} with pytest.raises(cv.Invalid): - do_packages_pass(config) + packages_pass(config) + + +@pytest.mark.parametrize( + "packages", + [ + {"package1": "github://esphome/non-existant-repo/file1.yml@main"}, + {"package2": "github://esphome/non-existant-repo/file1.yml"}, + {"package3": "github://esphome/non-existant-repo/other-folder/file1.yml"}, + [ + "github://esphome/non-existant-repo/file1.yml@main", + "github://esphome/non-existant-repo/file1.yml", + "github://esphome/non-existant-repo/other-folder/file1.yml", + ], + ], +) +def test_package_shorthand(packages): + CONFIG_SCHEMA(packages) + + +@pytest.mark.parametrize( + "packages", + [ + # not github + {"package1": "someplace://esphome/non-existant-repo/file1.yml@main"}, + # missing repo + {"package2": "github://esphome/file1.yml"}, + # missing file + {"package3": "github://esphome/non-existant-repo/@main"}, + {"a": "invalid string, not shorthand"}, + "some string", + 3, + False, + {"a": 8}, + ["someplace://esphome/non-existant-repo/file1.yml@main"], + ["github://esphome/file1.yml"], + ["github://esphome/non-existant-repo/@main"], + ["some string"], + [True], + [3], + ], +) +def test_package_invalid(packages): + with pytest.raises(cv.Invalid): + CONFIG_SCHEMA(packages) def test_package_include(basic_wifi, basic_esphome): @@ -99,10 +151,37 @@ def test_package_include(basic_wifi, basic_esphome): expected = {CONF_ESPHOME: basic_esphome, CONF_WIFI: basic_wifi} - actual = do_packages_pass(config) + actual = packages_pass(config) assert actual == expected +def test_single_package( + basic_esphome, + basic_wifi, + caplog: pytest.LogCaptureFixture, +): + """ + Tests the simple case where a single package is added to the top-level config as is. + In this test, the CONF_WIFI config is expected to be simply added to the top-level config. + This tests the case where the user just put packages: !include package.yaml, not + part of a list or mapping of packages. + This behavior is deprecated, the test also checks if a warning is issued. + """ + config = {CONF_ESPHOME: basic_esphome, CONF_PACKAGES: {CONF_WIFI: basic_wifi}} + + expected = {CONF_ESPHOME: basic_esphome, CONF_WIFI: basic_wifi} + + with caplog.at_level("WARNING"): + actual = packages_pass(config) + + assert actual == expected + + assert ( + "Including a single package under `packages:` is deprecated. Use a list instead." + in caplog.text + ) + + def test_package_append(basic_wifi, basic_esphome): """ Tests the case where a key is present in both a package and top-level config. @@ -124,7 +203,7 @@ def test_package_append(basic_wifi, basic_esphome): }, } - actual = do_packages_pass(config) + actual = packages_pass(config) assert actual == expected @@ -148,7 +227,7 @@ def test_package_override(basic_wifi, basic_esphome): }, } - actual = do_packages_pass(config) + actual = packages_pass(config) assert actual == expected @@ -177,7 +256,7 @@ def test_multiple_package_order(): }, } - actual = do_packages_pass(config) + actual = packages_pass(config) assert actual == expected @@ -233,7 +312,7 @@ def test_package_list_merge(): ] } - actual = do_packages_pass(config) + actual = packages_pass(config) assert actual == expected @@ -311,7 +390,7 @@ def test_package_list_merge_by_id(): ] } - actual = do_packages_pass(config) + actual = packages_pass(config) assert actual == expected @@ -350,13 +429,13 @@ def test_package_merge_by_id_with_list(): ] } - actual = do_packages_pass(config) + actual = packages_pass(config) assert actual == expected def test_package_merge_by_missing_id(): """ - Ensures that components with missing IDs are not merged. + Ensures that a validation error is thrown when trying to extend a missing ID. """ config = { @@ -379,25 +458,15 @@ def test_package_merge_by_missing_id(): ], } - expected = { - CONF_SENSOR: [ - { - CONF_ID: TEST_SENSOR_ID_1, - CONF_FILTERS: [{CONF_MULTIPLY: 42.0}], - }, - { - CONF_ID: TEST_SENSOR_ID_1, - CONF_FILTERS: [{CONF_MULTIPLY: 10.0}], - }, - { - CONF_ID: Extend(TEST_SENSOR_ID_2), - CONF_FILTERS: [{CONF_OFFSET: 146.0}], - }, - ] - } + error_raised = False + try: + packages_pass(config) + assert False, "Expected validation error for missing ID" + except cv.Invalid as err: + error_raised = True + assert err.path == [CONF_SENSOR, 2] - actual = do_packages_pass(config) - assert actual == expected + assert error_raised def test_package_list_remove_by_id(): @@ -447,7 +516,7 @@ def test_package_list_remove_by_id(): ] } - actual = do_packages_pass(config) + actual = packages_pass(config) assert actual == expected @@ -493,7 +562,7 @@ def test_multiple_package_list_remove_by_id(): ] } - actual = do_packages_pass(config) + actual = packages_pass(config) assert actual == expected @@ -514,7 +583,7 @@ def test_package_dict_remove_by_id(basic_wifi, basic_esphome): CONF_ESPHOME: basic_esphome, } - actual = do_packages_pass(config) + actual = packages_pass(config) assert actual == expected @@ -545,7 +614,6 @@ def test_package_remove_by_missing_id(): } expected = { - "missing_key": Remove(), CONF_SENSOR: [ { CONF_ID: TEST_SENSOR_ID_1, @@ -555,14 +623,10 @@ def test_package_remove_by_missing_id(): CONF_ID: TEST_SENSOR_ID_1, CONF_FILTERS: [{CONF_MULTIPLY: 10.0}], }, - { - CONF_ID: Remove(TEST_SENSOR_ID_2), - CONF_FILTERS: [{CONF_OFFSET: 146.0}], - }, ], } - actual = do_packages_pass(config) + actual = packages_pass(config) assert actual == expected @@ -634,7 +698,7 @@ def test_remote_packages_with_files_list( ] } - actual = do_packages_pass(config) + actual = packages_pass(config) assert actual == expected @@ -730,5 +794,5 @@ def test_remote_packages_with_files_and_vars( ] } - actual = do_packages_pass(config) + actual = packages_pass(config) assert actual == expected diff --git a/tests/component_tests/psram/test_psram.py b/tests/component_tests/psram/test_psram.py new file mode 100644 index 0000000000..f8ad013689 --- /dev/null +++ b/tests/component_tests/psram/test_psram.py @@ -0,0 +1,200 @@ +"""Tests for PSRAM component.""" + +from typing import Any + +import pytest + +from esphome.components.esp32.const import ( + KEY_VARIANT, + VARIANT_ESP32, + VARIANT_ESP32C2, + VARIANT_ESP32C3, + VARIANT_ESP32C5, + VARIANT_ESP32C6, + VARIANT_ESP32H2, + VARIANT_ESP32P4, + VARIANT_ESP32S2, + VARIANT_ESP32S3, +) +import esphome.config_validation as cv +from esphome.const import CONF_ESPHOME, PlatformFramework +from tests.component_tests.types import SetCoreConfigCallable + +UNSUPPORTED_PSRAM_VARIANTS = [ + VARIANT_ESP32C2, + VARIANT_ESP32C3, + VARIANT_ESP32C5, + VARIANT_ESP32C6, + VARIANT_ESP32H2, +] + +SUPPORTED_PSRAM_VARIANTS = [ + VARIANT_ESP32, + VARIANT_ESP32S2, + VARIANT_ESP32S3, + VARIANT_ESP32P4, +] +SUPPORTED_PSRAM_MODES = { + VARIANT_ESP32: ["quad"], + VARIANT_ESP32S2: ["quad"], + VARIANT_ESP32S3: ["quad", "octal"], + VARIANT_ESP32P4: ["hex"], +} + + +@pytest.mark.parametrize( + ("config", "error_match"), + [ + pytest.param( + {}, + r"PSRAM is not supported on this chip", + id="psram_not_supported", + ), + ], +) +@pytest.mark.parametrize("variant", UNSUPPORTED_PSRAM_VARIANTS) +def test_psram_configuration_errors_unsupported_variants( + config: Any, + error_match: str, + variant: str, + set_core_config: SetCoreConfigCallable, +) -> None: + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_VARIANT: variant}, + full_config={CONF_ESPHOME: {}}, + ) + """Test detection of invalid PSRAM configuration on unsupported variants.""" + from esphome.components.psram import CONFIG_SCHEMA + + with pytest.raises(cv.Invalid, match=error_match): + CONFIG_SCHEMA(config) + + +@pytest.mark.parametrize("variant", SUPPORTED_PSRAM_VARIANTS) +def test_psram_configuration_valid_supported_variants( + variant: str, + set_core_config: SetCoreConfigCallable, +) -> None: + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_VARIANT: variant}, + full_config={ + CONF_ESPHOME: {}, + "esp32": { + "variant": variant, + "cpu_frequency": "160MHz", + "framework": {"type": "esp-idf"}, + }, + }, + ) + """Test that PSRAM configuration is valid on supported variants.""" + from esphome.components.psram import CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA + + # This should not raise an exception + config = CONFIG_SCHEMA({"mode": SUPPORTED_PSRAM_MODES[variant][0]}) + FINAL_VALIDATE_SCHEMA(config) + + +def _setup_psram_final_validation_test( + esp32_config: dict, + set_core_config: SetCoreConfigCallable, + set_component_config: Any, +) -> str: + """Helper function to set up ESP32 configuration for PSRAM final validation tests.""" + # Use ESP32S3 for schema validation to allow all options, then override for final validation + schema_variant = "ESP32S3" + final_variant = esp32_config.get("variant", "ESP32S3") + full_esp32_config = { + "variant": final_variant, + "cpu_frequency": esp32_config.get("cpu_frequency", "240MHz"), + "framework": {"type": "esp-idf"}, + } + + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_VARIANT: schema_variant}, + full_config={ + CONF_ESPHOME: {}, + "esp32": full_esp32_config, + }, + ) + set_component_config("esp32", full_esp32_config) + + return final_variant + + +@pytest.mark.parametrize( + ("config", "esp32_config", "expect_error", "error_match"), + [ + pytest.param( + {"mode": "quad", "speed": "120MHz"}, + {"cpu_frequency": "160MHz"}, + True, + r"PSRAM 120MHz requires 240MHz CPU frequency", + id="120mhz_requires_240mhz_cpu", + ), + pytest.param( + {"mode": "octal"}, + {"variant": "ESP32"}, + True, + r"Octal PSRAM is only supported on ESP32-S3", + id="octal_mode_only_esp32s3", + ), + pytest.param( + {"mode": "quad", "enable_ecc": True}, + {}, + True, + r"ECC is only available in octal mode", + id="ecc_only_in_octal_mode", + ), + pytest.param( + {"mode": "quad", "speed": "120MHZ"}, + {"cpu_frequency": "240MHZ"}, + False, + None, + id="120mhz_with_240mhz_cpu", + ), + pytest.param( + {"mode": "octal"}, + {"variant": "ESP32S3"}, + False, + None, + id="octal_mode_on_esp32s3", + ), + pytest.param( + {"mode": "octal", "enable_ecc": True}, + {"variant": "ESP32S3"}, + False, + None, + id="ecc_in_octal_mode", + ), + ], +) +def test_psram_final_validation( + config: Any, + esp32_config: dict, + expect_error: bool, + error_match: str | None, + set_core_config: SetCoreConfigCallable, + set_component_config: Any, +) -> None: + """Test PSRAM final validation for both error and valid cases.""" + from esphome.components.psram import CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA + from esphome.core import CORE + + final_variant = _setup_psram_final_validation_test( + esp32_config, set_core_config, set_component_config + ) + + validated_config = CONFIG_SCHEMA(config) + + # Update CORE variant for final validation + CORE.data["esp32"][KEY_VARIANT] = final_variant + + if expect_error: + with pytest.raises(cv.Invalid, match=error_match): + FINAL_VALIDATE_SCHEMA(validated_config) + else: + # This should not raise an exception + FINAL_VALIDATE_SCHEMA(validated_config) diff --git a/tests/component_tests/sntp/__init__.py b/tests/component_tests/sntp/__init__.py new file mode 100644 index 0000000000..7d323a4980 --- /dev/null +++ b/tests/component_tests/sntp/__init__.py @@ -0,0 +1 @@ +"""Tests for SNTP component.""" diff --git a/tests/component_tests/sntp/config/sntp_test.yaml b/tests/component_tests/sntp/config/sntp_test.yaml new file mode 100644 index 0000000000..3942c9606b --- /dev/null +++ b/tests/component_tests/sntp/config/sntp_test.yaml @@ -0,0 +1,22 @@ +esphome: + name: sntp-test + +esp32: + board: esp32dev + framework: + type: esp-idf + +wifi: + ssid: "testssid" + password: "testpassword" + +# Test multiple SNTP instances that should be merged +time: + - platform: sntp + servers: + - 192.168.1.1 + - pool.ntp.org + - platform: sntp + servers: + - pool.ntp.org + - 192.168.1.2 diff --git a/tests/component_tests/sntp/test_init.py b/tests/component_tests/sntp/test_init.py new file mode 100644 index 0000000000..9197ff55d0 --- /dev/null +++ b/tests/component_tests/sntp/test_init.py @@ -0,0 +1,238 @@ +"""Tests for SNTP time configuration validation.""" + +from __future__ import annotations + +import logging +from typing import Any + +import pytest + +from esphome import config_validation as cv +from esphome.components.sntp.time import CONF_SNTP, _sntp_final_validate +from esphome.const import CONF_ID, CONF_PLATFORM, CONF_SERVERS, CONF_TIME +from esphome.core import ID +import esphome.final_validate as fv + + +@pytest.mark.parametrize( + ("time_configs", "expected_count", "expected_servers", "warning_messages"), + [ + pytest.param( + [ + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time", is_manual=False), + CONF_SERVERS: ["192.168.1.1", "pool.ntp.org"], + } + ], + 1, + ["192.168.1.1", "pool.ntp.org"], + [], + id="single_instance_no_merge", + ), + pytest.param( + [ + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_1", is_manual=False), + CONF_SERVERS: ["192.168.1.1", "pool.ntp.org"], + }, + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_2", is_manual=False), + CONF_SERVERS: ["192.168.1.2"], + }, + ], + 1, + ["192.168.1.1", "pool.ntp.org", "192.168.1.2"], + ["Found and merged 2 SNTP time configurations into one instance"], + id="two_instances_merged", + ), + pytest.param( + [ + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_1", is_manual=False), + CONF_SERVERS: ["192.168.1.1", "pool.ntp.org"], + }, + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_2", is_manual=False), + CONF_SERVERS: ["pool.ntp.org", "192.168.1.2"], + }, + ], + 1, + ["192.168.1.1", "pool.ntp.org", "192.168.1.2"], + ["Found and merged 2 SNTP time configurations into one instance"], + id="deduplication_preserves_order", + ), + pytest.param( + [ + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_1", is_manual=False), + CONF_SERVERS: ["192.168.1.1", "pool.ntp.org"], + }, + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_2", is_manual=False), + CONF_SERVERS: ["192.168.1.2", "pool2.ntp.org"], + }, + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_3", is_manual=False), + CONF_SERVERS: ["pool3.ntp.org"], + }, + ], + 1, + ["192.168.1.1", "pool.ntp.org", "192.168.1.2"], + [ + "SNTP supports maximum 3 servers. Dropped excess server(s): ['pool2.ntp.org', 'pool3.ntp.org']", + "Found and merged 3 SNTP time configurations into one instance", + ], + id="three_instances_drops_excess_servers", + ), + pytest.param( + [ + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_1", is_manual=False), + CONF_SERVERS: [ + "192.168.1.1", + "pool.ntp.org", + "pool.ntp.org", + "192.168.1.1", + ], + }, + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_2", is_manual=False), + CONF_SERVERS: ["pool.ntp.org", "192.168.1.2"], + }, + ], + 1, + ["192.168.1.1", "pool.ntp.org", "192.168.1.2"], + ["Found and merged 2 SNTP time configurations into one instance"], + id="deduplication_multiple_duplicates", + ), + ], +) +def test_sntp_instance_merging( + time_configs: list[dict[str, Any]], + expected_count: int, + expected_servers: list[str], + warning_messages: list[str], + caplog: pytest.LogCaptureFixture, +) -> None: + """Test SNTP instance merging behavior.""" + # Create a mock full config with time configs + full_conf = {CONF_TIME: time_configs.copy()} + + # Set the context var + token = fv.full_config.set(full_conf) + try: + with caplog.at_level(logging.WARNING): + _sntp_final_validate({}) + + # Get the updated config + updated_conf = fv.full_config.get() + + # Check if merging occurred + if len(time_configs) > 1: + # Verify only one SNTP instance remains + sntp_instances = [ + tc + for tc in updated_conf[CONF_TIME] + if tc.get(CONF_PLATFORM) == CONF_SNTP + ] + assert len(sntp_instances) == expected_count + + # Verify server list + assert sntp_instances[0][CONF_SERVERS] == expected_servers + + # Verify warnings + for expected_msg in warning_messages: + assert any( + expected_msg in record.message for record in caplog.records + ), f"Expected warning message '{expected_msg}' not found in log" + else: + # Single instance should not trigger merging or warnings + assert len(caplog.records) == 0 + # Config should be unchanged + assert updated_conf[CONF_TIME] == time_configs + finally: + fv.full_config.reset(token) + + +def test_sntp_inconsistent_manual_ids() -> None: + """Test that inconsistent manual IDs raise an error.""" + # Create configs with manual IDs that are inconsistent + time_configs = [ + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_1", is_manual=True), + CONF_SERVERS: ["192.168.1.1"], + }, + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_2", is_manual=True), + CONF_SERVERS: ["192.168.1.2"], + }, + ] + + full_conf = {CONF_TIME: time_configs} + + token = fv.full_config.set(full_conf) + try: + with pytest.raises( + cv.Invalid, + match="Found multiple SNTP configurations but id is inconsistent", + ): + _sntp_final_validate({}) + finally: + fv.full_config.reset(token) + + +def test_sntp_with_other_time_platforms(caplog: pytest.LogCaptureFixture) -> None: + """Test that SNTP merging doesn't affect other time platforms.""" + time_configs = [ + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_1", is_manual=False), + CONF_SERVERS: ["192.168.1.1"], + }, + { + CONF_PLATFORM: "homeassistant", + CONF_ID: ID("homeassistant_time", is_manual=False), + }, + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_2", is_manual=False), + CONF_SERVERS: ["192.168.1.2"], + }, + ] + + full_conf = {CONF_TIME: time_configs.copy()} + + token = fv.full_config.set(full_conf) + try: + with caplog.at_level(logging.WARNING): + _sntp_final_validate({}) + + updated_conf = fv.full_config.get() + + # Should have 2 time platforms: 1 merged SNTP + 1 homeassistant + assert len(updated_conf[CONF_TIME]) == 2 + + # Find the platforms + platforms = {tc[CONF_PLATFORM] for tc in updated_conf[CONF_TIME]} + assert platforms == {CONF_SNTP, "homeassistant"} + + # Verify SNTP was merged + sntp_instances = [ + tc for tc in updated_conf[CONF_TIME] if tc[CONF_PLATFORM] == CONF_SNTP + ] + assert len(sntp_instances) == 1 + assert sntp_instances[0][CONF_SERVERS] == ["192.168.1.1", "192.168.1.2"] + finally: + fv.full_config.reset(token) diff --git a/tests/component_tests/text/test_text.py b/tests/component_tests/text/test_text.py index 75f1c4b88b..bfc3131f6d 100644 --- a/tests/component_tests/text/test_text.py +++ b/tests/component_tests/text/test_text.py @@ -25,7 +25,7 @@ def test_text_sets_mandatory_fields(generate_main): main_cpp = generate_main("tests/component_tests/text/test_text.yaml") # Then - assert 'it_1->set_name("test 1 text");' in main_cpp + assert 'it_1->set_name_and_object_id("test 1 text", "test_1_text");' in main_cpp def test_text_config_value_internal_set(generate_main): @@ -58,7 +58,7 @@ def test_text_config_value_mode_set(generate_main): def test_text_config_lamda_is_set(generate_main): """ - Test if lambda is set for lambda mode + Test if lambda is set for lambda mode (optimized with stateless lambda) """ # Given @@ -66,5 +66,5 @@ def test_text_config_lamda_is_set(generate_main): main_cpp = generate_main("tests/component_tests/text/test_text.yaml") # Then - assert "it_4->set_template([=]() -> esphome::optional {" in main_cpp + assert "it_4->set_template([]() -> esphome::optional {" in main_cpp assert 'return std::string{"Hello"};' in main_cpp diff --git a/tests/component_tests/text_sensor/test_text_sensor.py b/tests/component_tests/text_sensor/test_text_sensor.py index 1c4ef6633d..934ee67cef 100644 --- a/tests/component_tests/text_sensor/test_text_sensor.py +++ b/tests/component_tests/text_sensor/test_text_sensor.py @@ -25,9 +25,18 @@ def test_text_sensor_sets_mandatory_fields(generate_main): main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml") # Then - assert 'ts_1->set_name("Template Text Sensor 1");' in main_cpp - assert 'ts_2->set_name("Template Text Sensor 2");' in main_cpp - assert 'ts_3->set_name("Template Text Sensor 3");' in main_cpp + assert ( + 'ts_1->set_name_and_object_id("Template Text Sensor 1", "template_text_sensor_1");' + in main_cpp + ) + assert ( + 'ts_2->set_name_and_object_id("Template Text Sensor 2", "template_text_sensor_2");' + in main_cpp + ) + assert ( + 'ts_3->set_name_and_object_id("Template Text Sensor 3", "template_text_sensor_3");' + in main_cpp + ) def test_text_sensor_config_value_internal_set(generate_main): diff --git a/tests/components/.gitignore b/tests/components/.gitignore new file mode 100644 index 0000000000..d8b4157aef --- /dev/null +++ b/tests/components/.gitignore @@ -0,0 +1,5 @@ +# Gitignore settings for ESPHome +# This is an example and may include too much for your use-case. +# You can modify this file to suit your needs. +/.esphome/ +/secrets.yaml diff --git a/tests/components/README.md b/tests/components/README.md new file mode 100644 index 0000000000..0901f2ef17 --- /dev/null +++ b/tests/components/README.md @@ -0,0 +1,32 @@ +# How to write C++ ESPHome unit tests + +1. Locate the folder with your component or create a new one with the same name as the component. +2. Write the tests. You can add as many `.cpp` and `.h` files as you need to organize your tests. + +**IMPORTANT**: wrap all your testing code in a unique namespace to avoid linker collisions when compiling +testing binaries that combine many components. By convention, this unique namespace is `esphome::component::testing` +(where "component" is the component under test), for example: `esphome::uart::testing`. + + +## Running component unit tests + +(from the repository root) +```bash +./script/cpp_unit_test.py component1 component2 ... +``` + +The above will compile and run the provided components and their tests. + +To run all tests, you can invoke `cpp_unit_test.py` with the special `--all` flag: + +```bash +./script/cpp_unit_test.py --all +``` + +To run a specific test suite, you can provide a Google Test filter: + +```bash +GTEST_FILTER='UART*' ./script/cpp_unit_test.py uart modbus +``` + +The process will return `0` for success or nonzero for failure. In case of failure, the errors will be printed out to the console. diff --git a/tests/components/a01nyub/common.yaml b/tests/components/a01nyub/common.yaml index 0717acfff7..fc0b2d3bfb 100644 --- a/tests/components/a01nyub/common.yaml +++ b/tests/components/a01nyub/common.yaml @@ -1,11 +1,4 @@ -uart: - - id: uart_a01nyub - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 9600 - sensor: - platform: a01nyub id: a01nyub_sensor name: a01nyub Distance - uart_id: uart_a01nyub diff --git a/tests/components/a01nyub/test.esp32-ard.yaml b/tests/components/a01nyub/test.esp32-ard.yaml deleted file mode 100644 index f486544afa..0000000000 --- a/tests/components/a01nyub/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 - -<<: !include common.yaml diff --git a/tests/components/a01nyub/test.esp32-c3-ard.yaml b/tests/components/a01nyub/test.esp32-c3-ard.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/a01nyub/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/a01nyub/test.esp32-c3-idf.yaml b/tests/components/a01nyub/test.esp32-c3-idf.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/a01nyub/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/a01nyub/test.esp32-idf.yaml b/tests/components/a01nyub/test.esp32-idf.yaml index f486544afa..b415125e84 100644 --- a/tests/components/a01nyub/test.esp32-idf.yaml +++ b/tests/components/a01nyub/test.esp32-idf.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 + tx_pin: GPIO4 + rx_pin: GPIO5 + +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/a01nyub/test.esp8266-ard.yaml b/tests/components/a01nyub/test.esp8266-ard.yaml index b516342f3b..5a05efa259 100644 --- a/tests/components/a01nyub/test.esp8266-ard.yaml +++ b/tests/components/a01nyub/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/a01nyub/test.rp2040-ard.yaml b/tests/components/a01nyub/test.rp2040-ard.yaml index b516342f3b..f1df2daf83 100644 --- a/tests/components/a01nyub/test.rp2040-ard.yaml +++ b/tests/components/a01nyub/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/a02yyuw/common.yaml b/tests/components/a02yyuw/common.yaml index b2e5927ff4..4de8a6eb67 100644 --- a/tests/components/a02yyuw/common.yaml +++ b/tests/components/a02yyuw/common.yaml @@ -1,11 +1,4 @@ -uart: - - id: uart_a02yyuw - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 9600 - sensor: - platform: a02yyuw id: a02yyuw_sensor name: a02yyuw Distance - uart_id: uart_a02yyuw diff --git a/tests/components/a02yyuw/test.esp32-ard.yaml b/tests/components/a02yyuw/test.esp32-ard.yaml deleted file mode 100644 index f486544afa..0000000000 --- a/tests/components/a02yyuw/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 - -<<: !include common.yaml diff --git a/tests/components/a02yyuw/test.esp32-c3-ard.yaml b/tests/components/a02yyuw/test.esp32-c3-ard.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/a02yyuw/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/a02yyuw/test.esp32-c3-idf.yaml b/tests/components/a02yyuw/test.esp32-c3-idf.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/a02yyuw/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/a02yyuw/test.esp32-idf.yaml b/tests/components/a02yyuw/test.esp32-idf.yaml index f486544afa..b415125e84 100644 --- a/tests/components/a02yyuw/test.esp32-idf.yaml +++ b/tests/components/a02yyuw/test.esp32-idf.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 + tx_pin: GPIO4 + rx_pin: GPIO5 + +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/a02yyuw/test.esp8266-ard.yaml b/tests/components/a02yyuw/test.esp8266-ard.yaml index b516342f3b..5a05efa259 100644 --- a/tests/components/a02yyuw/test.esp8266-ard.yaml +++ b/tests/components/a02yyuw/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/a02yyuw/test.rp2040-ard.yaml b/tests/components/a02yyuw/test.rp2040-ard.yaml index b516342f3b..f1df2daf83 100644 --- a/tests/components/a02yyuw/test.rp2040-ard.yaml +++ b/tests/components/a02yyuw/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/a4988/test.esp32-ard.yaml b/tests/components/a4988/test.esp32-ard.yaml deleted file mode 100644 index 1ca8c0c084..0000000000 --- a/tests/components/a4988/test.esp32-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - step_pin: GPIO22 - dir_pin: GPIO23 - sleep_pin: GPIO25 - -<<: !include common.yaml diff --git a/tests/components/a4988/test.esp32-c3-ard.yaml b/tests/components/a4988/test.esp32-c3-ard.yaml deleted file mode 100644 index 25caba75b5..0000000000 --- a/tests/components/a4988/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - step_pin: GPIO2 - dir_pin: GPIO3 - sleep_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/a4988/test.esp32-c3-idf.yaml b/tests/components/a4988/test.esp32-c3-idf.yaml deleted file mode 100644 index 25caba75b5..0000000000 --- a/tests/components/a4988/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - step_pin: GPIO2 - dir_pin: GPIO3 - sleep_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/a4988/test.esp32-idf.yaml b/tests/components/a4988/test.esp32-idf.yaml index 1ca8c0c084..7d46b048e1 100644 --- a/tests/components/a4988/test.esp32-idf.yaml +++ b/tests/components/a4988/test.esp32-idf.yaml @@ -1,6 +1,6 @@ substitutions: step_pin: GPIO22 - dir_pin: GPIO23 + dir_pin: GPIO4 sleep_pin: GPIO25 <<: !include common.yaml diff --git a/tests/components/a4988/test.esp8266-ard.yaml b/tests/components/a4988/test.esp8266-ard.yaml index 22b5677d27..5b1b1293be 100644 --- a/tests/components/a4988/test.esp8266-ard.yaml +++ b/tests/components/a4988/test.esp8266-ard.yaml @@ -1,6 +1,6 @@ substitutions: step_pin: GPIO1 dir_pin: GPIO2 - sleep_pin: GPIO5 + sleep_pin: GPIO0 <<: !include common.yaml diff --git a/tests/components/absolute_humidity/common.yaml b/tests/components/absolute_humidity/common.yaml index 87a99f5206..026f88654f 100644 --- a/tests/components/absolute_humidity/common.yaml +++ b/tests/components/absolute_humidity/common.yaml @@ -8,14 +8,12 @@ sensor: lambda: |- if (millis() > 10000) { return 0.6; - } else { - return 0.0; } + return 0.0; - platform: template id: template_temperature lambda: |- if (millis() > 10000) { return 42.0; - } else { - return 0.0; } + return 0.0; diff --git a/tests/components/ac_dimmer/test.esp32-ard.yaml b/tests/components/ac_dimmer/test.esp32-ard.yaml index 3ec069f430..eaa4901f03 100644 --- a/tests/components/ac_dimmer/test.esp32-ard.yaml +++ b/tests/components/ac_dimmer/test.esp32-ard.yaml @@ -1,5 +1,5 @@ substitutions: - gate_pin: GPIO18 - zero_cross_pin: GPIO19 + gate_pin: GPIO4 + zero_cross_pin: GPIO5 <<: !include common.yaml diff --git a/tests/components/ac_dimmer/test.esp32-c3-ard.yaml b/tests/components/ac_dimmer/test.esp32-c3-ard.yaml deleted file mode 100644 index 5d2d42b713..0000000000 --- a/tests/components/ac_dimmer/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - gate_pin: GPIO5 - zero_cross_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ac_dimmer/test.esp8266-ard.yaml b/tests/components/ac_dimmer/test.esp8266-ard.yaml index 5d2d42b713..2f50b04956 100644 --- a/tests/components/ac_dimmer/test.esp8266-ard.yaml +++ b/tests/components/ac_dimmer/test.esp8266-ard.yaml @@ -1,5 +1,5 @@ substitutions: - gate_pin: GPIO5 - zero_cross_pin: GPIO4 + gate_pin: GPIO0 + zero_cross_pin: GPIO2 <<: !include common.yaml diff --git a/tests/components/adc/common.yaml b/tests/components/adc/common.yaml deleted file mode 100644 index ebdd1aece5..0000000000 --- a/tests/components/adc/common.yaml +++ /dev/null @@ -1,11 +0,0 @@ -sensor: - - id: my_sensor - platform: adc - name: ADC Test sensor - update_interval: "1:01" - attenuation: 2.5db - unit_of_measurement: "°C" - icon: "mdi:water-percent" - accuracy_decimals: 5 - setup_priority: -100 - force_update: true diff --git a/tests/components/adc/test.bk72xx-ard.yaml b/tests/components/adc/test.bk72xx-ard.yaml index 0a3d5d1fdc..0645333a81 100644 --- a/tests/components/adc/test.bk72xx-ard.yaml +++ b/tests/components/adc/test.bk72xx-ard.yaml @@ -1,7 +1,11 @@ -packages: - base: !include common.yaml - sensor: - - id: !extend my_sensor + - id: my_sensor + platform: adc pin: P23 - attenuation: !remove + name: ADC Test sensor + update_interval: "1:01" + unit_of_measurement: "°C" + icon: "mdi:water-percent" + accuracy_decimals: 5 + setup_priority: -100 + force_update: true diff --git a/tests/components/adc/test.esp32-ard.yaml b/tests/components/adc/test.esp32-ard.yaml deleted file mode 100644 index e6a1fd3bd9..0000000000 --- a/tests/components/adc/test.esp32-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -packages: - base: !include common.yaml - -sensor: - - id: !extend my_sensor - pin: A0 diff --git a/tests/components/adc/test.esp32-c3-ard.yaml b/tests/components/adc/test.esp32-c3-ard.yaml deleted file mode 100644 index ea3b00a85f..0000000000 --- a/tests/components/adc/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -packages: - base: !include common.yaml - -sensor: - - id: !extend my_sensor - pin: 4 diff --git a/tests/components/adc/test.esp32-c3-idf.yaml b/tests/components/adc/test.esp32-c3-idf.yaml index ea3b00a85f..e764f0fe21 100644 --- a/tests/components/adc/test.esp32-c3-idf.yaml +++ b/tests/components/adc/test.esp32-c3-idf.yaml @@ -1,6 +1,12 @@ -packages: - base: !include common.yaml - sensor: - - id: !extend my_sensor - pin: 4 + - id: my_sensor + platform: adc + pin: GPIO1 + name: ADC Test sensor + update_interval: "1:01" + attenuation: 2.5db + unit_of_measurement: "°C" + icon: "mdi:water-percent" + accuracy_decimals: 5 + setup_priority: -100 + force_update: true diff --git a/tests/components/adc/test.esp32-idf.yaml b/tests/components/adc/test.esp32-idf.yaml index e6a1fd3bd9..ff1e3bb919 100644 --- a/tests/components/adc/test.esp32-idf.yaml +++ b/tests/components/adc/test.esp32-idf.yaml @@ -1,6 +1,12 @@ -packages: - base: !include common.yaml - sensor: - - id: !extend my_sensor + - id: my_sensor + platform: adc pin: A0 + name: ADC Test sensor + update_interval: "1:01" + attenuation: 2.5db + unit_of_measurement: "°C" + icon: "mdi:water-percent" + accuracy_decimals: 5 + setup_priority: -100 + force_update: true diff --git a/tests/components/adc/test.esp32-p4-idf.yaml b/tests/components/adc/test.esp32-p4-idf.yaml index 97844cf398..b77dc299c2 100644 --- a/tests/components/adc/test.esp32-p4-idf.yaml +++ b/tests/components/adc/test.esp32-p4-idf.yaml @@ -1,6 +1,12 @@ -packages: - base: !include common.yaml - sensor: - - id: !extend my_sensor - pin: GPIO50 + - id: my_sensor + platform: adc + pin: GPIO16 + name: ADC Test sensor + update_interval: "1:01" + attenuation: 2.5db + unit_of_measurement: "°C" + icon: "mdi:water-percent" + accuracy_decimals: 5 + setup_priority: -100 + force_update: true diff --git a/tests/components/adc/test.esp32-s2-ard.yaml b/tests/components/adc/test.esp32-s2-ard.yaml deleted file mode 100644 index bbd91c5e5a..0000000000 --- a/tests/components/adc/test.esp32-s2-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -packages: - base: !include common.yaml - -sensor: - - id: !extend my_sensor - pin: 1 diff --git a/tests/components/adc/test.esp32-s2-idf.yaml b/tests/components/adc/test.esp32-s2-idf.yaml index bbd91c5e5a..e764f0fe21 100644 --- a/tests/components/adc/test.esp32-s2-idf.yaml +++ b/tests/components/adc/test.esp32-s2-idf.yaml @@ -1,6 +1,12 @@ -packages: - base: !include common.yaml - sensor: - - id: !extend my_sensor - pin: 1 + - id: my_sensor + platform: adc + pin: GPIO1 + name: ADC Test sensor + update_interval: "1:01" + attenuation: 2.5db + unit_of_measurement: "°C" + icon: "mdi:water-percent" + accuracy_decimals: 5 + setup_priority: -100 + force_update: true diff --git a/tests/components/adc/test.esp32-s3-ard.yaml b/tests/components/adc/test.esp32-s3-ard.yaml deleted file mode 100644 index bbd91c5e5a..0000000000 --- a/tests/components/adc/test.esp32-s3-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -packages: - base: !include common.yaml - -sensor: - - id: !extend my_sensor - pin: 1 diff --git a/tests/components/adc/test.esp32-s3-idf.yaml b/tests/components/adc/test.esp32-s3-idf.yaml index bbd91c5e5a..e764f0fe21 100644 --- a/tests/components/adc/test.esp32-s3-idf.yaml +++ b/tests/components/adc/test.esp32-s3-idf.yaml @@ -1,6 +1,12 @@ -packages: - base: !include common.yaml - sensor: - - id: !extend my_sensor - pin: 1 + - id: my_sensor + platform: adc + pin: GPIO1 + name: ADC Test sensor + update_interval: "1:01" + attenuation: 2.5db + unit_of_measurement: "°C" + icon: "mdi:water-percent" + accuracy_decimals: 5 + setup_priority: -100 + force_update: true diff --git a/tests/components/adc/test.esp8266-ard.yaml b/tests/components/adc/test.esp8266-ard.yaml index bcb3620cfc..4cc865bb5d 100644 --- a/tests/components/adc/test.esp8266-ard.yaml +++ b/tests/components/adc/test.esp8266-ard.yaml @@ -1,7 +1,11 @@ -packages: - base: !include common.yaml - sensor: - - id: !extend my_sensor + - id: my_sensor + platform: adc pin: VCC - attenuation: !remove + name: ADC Test sensor + update_interval: "1:01" + unit_of_measurement: "°C" + icon: "mdi:water-percent" + accuracy_decimals: 5 + setup_priority: -100 + force_update: true diff --git a/tests/components/adc/test.ln882x-ard.yaml b/tests/components/adc/test.ln882x-ard.yaml index 0622cd7b27..face38b647 100644 --- a/tests/components/adc/test.ln882x-ard.yaml +++ b/tests/components/adc/test.ln882x-ard.yaml @@ -1,7 +1,11 @@ -packages: - base: !include common.yaml - sensor: - - id: !extend my_sensor - pin: PA0 - attenuation: !remove + - id: my_sensor + platform: adc + pin: A5 + name: ADC Test sensor + update_interval: "1:01" + unit_of_measurement: "°C" + icon: "mdi:water-percent" + accuracy_decimals: 5 + setup_priority: -100 + force_update: true diff --git a/tests/components/adc/test.rp2040-ard.yaml b/tests/components/adc/test.rp2040-ard.yaml index bcb3620cfc..4cc865bb5d 100644 --- a/tests/components/adc/test.rp2040-ard.yaml +++ b/tests/components/adc/test.rp2040-ard.yaml @@ -1,7 +1,11 @@ -packages: - base: !include common.yaml - sensor: - - id: !extend my_sensor + - id: my_sensor + platform: adc pin: VCC - attenuation: !remove + name: ADC Test sensor + update_interval: "1:01" + unit_of_measurement: "°C" + icon: "mdi:water-percent" + accuracy_decimals: 5 + setup_priority: -100 + force_update: true diff --git a/tests/components/adc128s102/common.yaml b/tests/components/adc128s102/common.yaml index 5f1638a7e2..b909310bdf 100644 --- a/tests/components/adc128s102/common.yaml +++ b/tests/components/adc128s102/common.yaml @@ -1,9 +1,3 @@ -spi: - - id: spi_adc128s102 - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - miso_pin: ${miso_pin} - adc128s102: cs_pin: ${cs_pin} id: adc128s102_adc diff --git a/tests/components/adc128s102/test.esp32-ard.yaml b/tests/components/adc128s102/test.esp32-ard.yaml deleted file mode 100644 index aba72f0614..0000000000 --- a/tests/components/adc128s102/test.esp32-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 - cs_pin: GPIO12 - -<<: !include common.yaml diff --git a/tests/components/adc128s102/test.esp32-c3-ard.yaml b/tests/components/adc128s102/test.esp32-c3-ard.yaml deleted file mode 100644 index 24da4b5452..0000000000 --- a/tests/components/adc128s102/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/adc128s102/test.esp32-c3-idf.yaml b/tests/components/adc128s102/test.esp32-c3-idf.yaml deleted file mode 100644 index 24da4b5452..0000000000 --- a/tests/components/adc128s102/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/adc128s102/test.esp32-idf.yaml b/tests/components/adc128s102/test.esp32-idf.yaml index aba72f0614..9bb524aa65 100644 --- a/tests/components/adc128s102/test.esp32-idf.yaml +++ b/tests/components/adc128s102/test.esp32-idf.yaml @@ -1,7 +1,7 @@ substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 cs_pin: GPIO12 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/adc128s102/test.esp8266-ard.yaml b/tests/components/adc128s102/test.esp8266-ard.yaml index dbd158d030..b4673ba8b7 100644 --- a/tests/components/adc128s102/test.esp8266-ard.yaml +++ b/tests/components/adc128s102/test.esp8266-ard.yaml @@ -1,7 +1,10 @@ substitutions: - clk_pin: GPIO14 - mosi_pin: GPIO13 - miso_pin: GPIO12 + clk_pin: GPIO0 + mosi_pin: GPIO2 + miso_pin: GPIO16 cs_pin: GPIO15 +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/adc128s102/test.rp2040-ard.yaml b/tests/components/adc128s102/test.rp2040-ard.yaml index f6c3f1eeca..1ded24de1c 100644 --- a/tests/components/adc128s102/test.rp2040-ard.yaml +++ b/tests/components/adc128s102/test.rp2040-ard.yaml @@ -4,4 +4,7 @@ substitutions: miso_pin: GPIO4 cs_pin: GPIO5 +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/addressable_light/esp32_rmt_led_strip.esp32-ard.yaml b/tests/components/addressable_light/esp32_rmt_led_strip.esp32-ard.yaml deleted file mode 100644 index d93c554dae..0000000000 --- a/tests/components/addressable_light/esp32_rmt_led_strip.esp32-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO2 - -<<: !include common-ard-esp32_rmt_led_strip.yaml diff --git a/tests/components/addressable_light/esp32_rmt_led_strip.esp32-c3-ard.yaml b/tests/components/addressable_light/esp32_rmt_led_strip.esp32-c3-ard.yaml deleted file mode 100644 index d93c554dae..0000000000 --- a/tests/components/addressable_light/esp32_rmt_led_strip.esp32-c3-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO2 - -<<: !include common-ard-esp32_rmt_led_strip.yaml diff --git a/tests/components/addressable_light/fastled_clockless.esp32-ard.yaml b/tests/components/addressable_light/fastled_clockless.esp32-ard.yaml deleted file mode 100644 index 78eb5d7fdb..0000000000 --- a/tests/components/addressable_light/fastled_clockless.esp32-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO2 - -<<: !include common-ard-fastled.yaml diff --git a/tests/components/ade7880/common.yaml b/tests/components/ade7880/common.yaml index 48c22c8485..0b0b560282 100644 --- a/tests/components/ade7880/common.yaml +++ b/tests/components/ade7880/common.yaml @@ -1,23 +1,18 @@ -i2c: - - id: i2c_ade7880 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: ade7880 - i2c_id: i2c_ade7880 + i2c_id: i2c_bus irq0_pin: ${irq0_pin} irq1_pin: ${irq1_pin} reset_pin: ${reset_pin} frequency: 60Hz phase_a: name: Channel A - voltage: Channel A Voltage - current: Channel A Current - active_power: Channel A Active Power - power_factor: Channel A Power Factor - forward_active_energy: Channel A Forward Active Energy - reverse_active_energy: Channel A Reverse Active Energy + voltage: Voltage + current: Current + active_power: Active Power + power_factor: Power Factor + forward_active_energy: Forward Active Energy + reverse_active_energy: Reverse Active Energy calibration: current_gain: 3116628 voltage_gain: -757178 @@ -25,12 +20,12 @@ sensor: phase_angle: 188 phase_b: name: Channel B - voltage: Channel B Voltage - current: Channel B Current - active_power: Channel B Active Power - power_factor: Channel B Power Factor - forward_active_energy: Channel B Forward Active Energy - reverse_active_energy: Channel B Reverse Active Energy + voltage: Voltage + current: Current + active_power: Active Power + power_factor: Power Factor + forward_active_energy: Forward Active Energy + reverse_active_energy: Reverse Active Energy calibration: current_gain: 3133655 voltage_gain: -755235 @@ -38,12 +33,12 @@ sensor: phase_angle: 188 phase_c: name: Channel C - voltage: Channel C Voltage - current: Channel C Current - active_power: Channel C Active Power - power_factor: Channel C Power Factor - forward_active_energy: Channel C Forward Active Energy - reverse_active_energy: Channel C Reverse Active Energy + voltage: Voltage + current: Current + active_power: Active Power + power_factor: Power Factor + forward_active_energy: Forward Active Energy + reverse_active_energy: Reverse Active Energy calibration: current_gain: 3111158 voltage_gain: -743813 @@ -51,6 +46,6 @@ sensor: phase_angle: 180 neutral: name: Neutral - current: Neutral Current + current: Current calibration: current_gain: 3189 diff --git a/tests/components/ade7880/test.esp32-ard.yaml b/tests/components/ade7880/test.esp32-ard.yaml deleted file mode 100644 index 685b49ff32..0000000000 --- a/tests/components/ade7880/test.esp32-ard.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - irq0_pin: GPIO13 - irq1_pin: GPIO15 - reset_pin: GPIO16 - -<<: !include common.yaml diff --git a/tests/components/ade7880/test.esp32-c3-ard.yaml b/tests/components/ade7880/test.esp32-c3-ard.yaml deleted file mode 100644 index 87db3e9427..0000000000 --- a/tests/components/ade7880/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - irq0_pin: GPIO6 - irq1_pin: GPIO7 - reset_pin: GPIO10 - -<<: !include common.yaml diff --git a/tests/components/ade7880/test.esp32-c3-idf.yaml b/tests/components/ade7880/test.esp32-c3-idf.yaml deleted file mode 100644 index 87db3e9427..0000000000 --- a/tests/components/ade7880/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - irq0_pin: GPIO6 - irq1_pin: GPIO7 - reset_pin: GPIO10 - -<<: !include common.yaml diff --git a/tests/components/ade7880/test.esp32-idf.yaml b/tests/components/ade7880/test.esp32-idf.yaml index 685b49ff32..9db2e50049 100644 --- a/tests/components/ade7880/test.esp32-idf.yaml +++ b/tests/components/ade7880/test.esp32-idf.yaml @@ -1,8 +1,9 @@ substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 irq0_pin: GPIO13 irq1_pin: GPIO15 - reset_pin: GPIO16 + reset_pin: GPIO12 + +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/ade7880/test.esp8266-ard.yaml b/tests/components/ade7880/test.esp8266-ard.yaml index 685b49ff32..8b5e47f0b5 100644 --- a/tests/components/ade7880/test.esp8266-ard.yaml +++ b/tests/components/ade7880/test.esp8266-ard.yaml @@ -1,8 +1,9 @@ substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - irq0_pin: GPIO13 + irq0_pin: GPIO0 irq1_pin: GPIO15 reset_pin: GPIO16 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/ade7880/test.rp2040-ard.yaml b/tests/components/ade7880/test.rp2040-ard.yaml index 685b49ff32..f531f852ae 100644 --- a/tests/components/ade7880/test.rp2040-ard.yaml +++ b/tests/components/ade7880/test.rp2040-ard.yaml @@ -1,8 +1,9 @@ substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 irq0_pin: GPIO13 irq1_pin: GPIO15 reset_pin: GPIO16 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/ade7953_i2c/common.yaml b/tests/components/ade7953_i2c/common.yaml index a2d163567d..5253759888 100644 --- a/tests/components/ade7953_i2c/common.yaml +++ b/tests/components/ade7953_i2c/common.yaml @@ -1,20 +1,16 @@ -i2c: - - id: i2c_ade7953 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: ade7953_i2c + i2c_id: i2c_bus irq_pin: ${irq_pin} voltage: name: ADE7953 Voltage - id: ade7953_voltage + id: ade7953_i2c_voltage current_a: name: ADE7953 Current A - id: ade7953_current_a + id: ade7953_i2c_current_a current_b: name: ADE7953 Current B - id: ade7953_current_b + id: ade7953_i2c_current_b power_factor_a: name: ADE7953 Power Factor A power_factor_b: diff --git a/tests/components/ade7953_i2c/test.esp32-ard.yaml b/tests/components/ade7953_i2c/test.esp32-ard.yaml deleted file mode 100644 index 2c57d412f6..0000000000 --- a/tests/components/ade7953_i2c/test.esp32-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - irq_pin: GPIO15 - -<<: !include common.yaml diff --git a/tests/components/ade7953_i2c/test.esp32-c3-ard.yaml b/tests/components/ade7953_i2c/test.esp32-c3-ard.yaml deleted file mode 100644 index 799acabd5a..0000000000 --- a/tests/components/ade7953_i2c/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - irq_pin: GPIO6 - -<<: !include common.yaml diff --git a/tests/components/ade7953_i2c/test.esp32-c3-idf.yaml b/tests/components/ade7953_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index 799acabd5a..0000000000 --- a/tests/components/ade7953_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - irq_pin: GPIO6 - -<<: !include common.yaml diff --git a/tests/components/ade7953_i2c/test.esp32-idf.yaml b/tests/components/ade7953_i2c/test.esp32-idf.yaml index 2c57d412f6..49629536e7 100644 --- a/tests/components/ade7953_i2c/test.esp32-idf.yaml +++ b/tests/components/ade7953_i2c/test.esp32-idf.yaml @@ -1,6 +1,7 @@ substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 irq_pin: GPIO15 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/ade7953_i2c/test.esp8266-ard.yaml b/tests/components/ade7953_i2c/test.esp8266-ard.yaml index c8e6a43f44..dc7609ab37 100644 --- a/tests/components/ade7953_i2c/test.esp8266-ard.yaml +++ b/tests/components/ade7953_i2c/test.esp8266-ard.yaml @@ -1,6 +1,7 @@ substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 irq_pin: GPIO15 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/ade7953_i2c/test.rp2040-ard.yaml b/tests/components/ade7953_i2c/test.rp2040-ard.yaml index 799acabd5a..b80562ad22 100644 --- a/tests/components/ade7953_i2c/test.rp2040-ard.yaml +++ b/tests/components/ade7953_i2c/test.rp2040-ard.yaml @@ -1,6 +1,7 @@ substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 irq_pin: GPIO6 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/ade7953_spi/common.yaml b/tests/components/ade7953_spi/common.yaml index 706f31f22c..f66ab697a1 100644 --- a/tests/components/ade7953_spi/common.yaml +++ b/tests/components/ade7953_spi/common.yaml @@ -1,22 +1,16 @@ -spi: - - id: spi_ade7953 - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - miso_pin: ${miso_pin} - sensor: - platform: ade7953_spi cs_pin: ${cs_pin} irq_pin: ${irq_pin} voltage: name: ADE7953 Voltage - id: ade7953_voltage + id: ade7953_spi_voltage current_a: name: ADE7953 Current A - id: ade7953_current_a + id: ade7953_spi_current_a current_b: name: ADE7953 Current B - id: ade7953_current_b + id: ade7953_spi_current_b power_factor_a: name: ADE7953 Power Factor A power_factor_b: diff --git a/tests/components/ade7953_spi/test.esp32-ard.yaml b/tests/components/ade7953_spi/test.esp32-ard.yaml deleted file mode 100644 index e00f522dd4..0000000000 --- a/tests/components/ade7953_spi/test.esp32-ard.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 - irq_pin: GPIO13 - cs_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/ade7953_spi/test.esp32-c3-ard.yaml b/tests/components/ade7953_spi/test.esp32-c3-ard.yaml deleted file mode 100644 index fcf35f528e..0000000000 --- a/tests/components/ade7953_spi/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - irq_pin: GPIO9 - cs_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/ade7953_spi/test.esp32-c3-idf.yaml b/tests/components/ade7953_spi/test.esp32-c3-idf.yaml deleted file mode 100644 index fcf35f528e..0000000000 --- a/tests/components/ade7953_spi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - irq_pin: GPIO9 - cs_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/ade7953_spi/test.esp32-idf.yaml b/tests/components/ade7953_spi/test.esp32-idf.yaml index e00f522dd4..19791e24b7 100644 --- a/tests/components/ade7953_spi/test.esp32-idf.yaml +++ b/tests/components/ade7953_spi/test.esp32-idf.yaml @@ -1,8 +1,8 @@ substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 irq_pin: GPIO13 cs_pin: GPIO5 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/ade7953_spi/test.esp8266-ard.yaml b/tests/components/ade7953_spi/test.esp8266-ard.yaml index b90e661ec0..8475dc4c50 100644 --- a/tests/components/ade7953_spi/test.esp8266-ard.yaml +++ b/tests/components/ade7953_spi/test.esp8266-ard.yaml @@ -1,8 +1,11 @@ substitutions: - clk_pin: GPIO14 - mosi_pin: GPIO13 - miso_pin: GPIO12 + clk_pin: GPIO0 + mosi_pin: GPIO2 + miso_pin: GPIO16 irq_pin: GPIO5 cs_pin: GPIO15 +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/ade7953_spi/test.rp2040-ard.yaml b/tests/components/ade7953_spi/test.rp2040-ard.yaml index 8f5941e1b2..7c4a74a236 100644 --- a/tests/components/ade7953_spi/test.rp2040-ard.yaml +++ b/tests/components/ade7953_spi/test.rp2040-ard.yaml @@ -5,4 +5,7 @@ substitutions: irq_pin: GPIO5 cs_pin: GPIO6 +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/ads1115/common.yaml b/tests/components/ads1115/common.yaml index 297877d2d8..4724dc5a14 100644 --- a/tests/components/ads1115/common.yaml +++ b/tests/components/ads1115/common.yaml @@ -1,9 +1,5 @@ -i2c: - - id: i2c_ads1115 - scl: ${scl_pin} - sda: ${sda_pin} - ads1115: + i2c_id: i2c_bus address: 0x48 sensor: diff --git a/tests/components/ads1115/test.esp32-ard.yaml b/tests/components/ads1115/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/ads1115/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/ads1115/test.esp32-c3-ard.yaml b/tests/components/ads1115/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/ads1115/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ads1115/test.esp32-c3-idf.yaml b/tests/components/ads1115/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/ads1115/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ads1115/test.esp32-idf.yaml b/tests/components/ads1115/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/ads1115/test.esp32-idf.yaml +++ b/tests/components/ads1115/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/ads1115/test.esp8266-ard.yaml b/tests/components/ads1115/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/ads1115/test.esp8266-ard.yaml +++ b/tests/components/ads1115/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/ads1115/test.nrf52-adafruit.yaml b/tests/components/ads1115/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..2a0de6241c --- /dev/null +++ b/tests/components/ads1115/test.nrf52-adafruit.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/nrf52.yaml + +<<: !include common.yaml diff --git a/tests/components/ads1115/test.nrf52-mcumgr.yaml b/tests/components/ads1115/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..2a0de6241c --- /dev/null +++ b/tests/components/ads1115/test.nrf52-mcumgr.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/nrf52.yaml + +<<: !include common.yaml diff --git a/tests/components/ads1115/test.rp2040-ard.yaml b/tests/components/ads1115/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/ads1115/test.rp2040-ard.yaml +++ b/tests/components/ads1115/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/ags10/common.yaml b/tests/components/ags10/common.yaml index 0c4c3513cf..0551871e59 100644 --- a/tests/components/ags10/common.yaml +++ b/tests/components/ags10/common.yaml @@ -1,9 +1,3 @@ -i2c: - - id: i2c_ags10 - scl: ${scl_pin} - sda: ${sda_pin} - frequency: 10kHz - sensor: - platform: ags10 id: ags10_1 diff --git a/tests/components/ags10/test.esp32-ard.yaml b/tests/components/ags10/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/ags10/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/ags10/test.esp32-c3-ard.yaml b/tests/components/ags10/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/ags10/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ags10/test.esp32-c3-idf.yaml b/tests/components/ags10/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/ags10/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ags10/test.esp32-idf.yaml b/tests/components/ags10/test.esp32-idf.yaml index 63c3bd6afd..7a5d01898a 100644 --- a/tests/components/ags10/test.esp32-idf.yaml +++ b/tests/components/ags10/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c_low_freq: !include ../../test_build_components/common/i2c_low_freq/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/ags10/test.esp8266-ard.yaml b/tests/components/ags10/test.esp8266-ard.yaml index ee2c29ca4e..9e23bb3778 100644 --- a/tests/components/ags10/test.esp8266-ard.yaml +++ b/tests/components/ags10/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c_low_freq: !include ../../test_build_components/common/i2c_low_freq/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/aht10/common.yaml b/tests/components/aht10/common.yaml index 721af09bb4..d7c3f9364f 100644 --- a/tests/components/aht10/common.yaml +++ b/tests/components/aht10/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_aht10 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: aht10 + i2c_id: i2c_bus temperature: name: Temperature humidity: diff --git a/tests/components/aht10/test.esp32-ard.yaml b/tests/components/aht10/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/aht10/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/aht10/test.esp32-c3-ard.yaml b/tests/components/aht10/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/aht10/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/aht10/test.esp32-c3-idf.yaml b/tests/components/aht10/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/aht10/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/aht10/test.esp32-idf.yaml b/tests/components/aht10/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/aht10/test.esp32-idf.yaml +++ b/tests/components/aht10/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/aht10/test.esp8266-ard.yaml b/tests/components/aht10/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/aht10/test.esp8266-ard.yaml +++ b/tests/components/aht10/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/aht10/test.nrf52-adafruit.yaml b/tests/components/aht10/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..2a0de6241c --- /dev/null +++ b/tests/components/aht10/test.nrf52-adafruit.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/nrf52.yaml + +<<: !include common.yaml diff --git a/tests/components/aht10/test.rp2040-ard.yaml b/tests/components/aht10/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/aht10/test.rp2040-ard.yaml +++ b/tests/components/aht10/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/aic3204/common.yaml b/tests/components/aic3204/common.yaml index 6e939bd260..5f175faee3 100644 --- a/tests/components/aic3204/common.yaml +++ b/tests/components/aic3204/common.yaml @@ -6,10 +6,6 @@ esphome: - audio_dac.set_volume: volume: 50% -i2c: - - id: i2c_aic3204 - scl: ${scl_pin} - sda: ${sda_pin} - audio_dac: - platform: aic3204 + i2c_id: i2c_bus diff --git a/tests/components/aic3204/test.esp32-ard.yaml b/tests/components/aic3204/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/aic3204/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/aic3204/test.esp32-c3-ard.yaml b/tests/components/aic3204/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/aic3204/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/aic3204/test.esp32-c3-idf.yaml b/tests/components/aic3204/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/aic3204/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/aic3204/test.esp32-idf.yaml b/tests/components/aic3204/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/aic3204/test.esp32-idf.yaml +++ b/tests/components/aic3204/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/aic3204/test.esp8266-ard.yaml b/tests/components/aic3204/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/aic3204/test.esp8266-ard.yaml +++ b/tests/components/aic3204/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/airthings_wave_mini/test.esp32-idf.yaml b/tests/components/airthings_wave_mini/test.esp32-idf.yaml index dade44d145..7a6541ae76 100644 --- a/tests/components/airthings_wave_mini/test.esp32-idf.yaml +++ b/tests/components/airthings_wave_mini/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/airthings_wave_plus/test.esp32-idf.yaml b/tests/components/airthings_wave_plus/test.esp32-idf.yaml index dade44d145..7a6541ae76 100644 --- a/tests/components/airthings_wave_plus/test.esp32-idf.yaml +++ b/tests/components/airthings_wave_plus/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/absolute_humidity/test.esp32-ard.yaml b/tests/components/alarm_control_panel/test.nrf52-adafruit.yaml similarity index 100% rename from tests/components/absolute_humidity/test.esp32-ard.yaml rename to tests/components/alarm_control_panel/test.nrf52-adafruit.yaml diff --git a/tests/components/absolute_humidity/test.esp32-c3-ard.yaml b/tests/components/alarm_control_panel/test.nrf52-mcumgr.yaml similarity index 100% rename from tests/components/absolute_humidity/test.esp32-c3-ard.yaml rename to tests/components/alarm_control_panel/test.nrf52-mcumgr.yaml diff --git a/tests/components/alpha3/test.esp32-idf.yaml b/tests/components/alpha3/test.esp32-idf.yaml index dade44d145..7a6541ae76 100644 --- a/tests/components/alpha3/test.esp32-idf.yaml +++ b/tests/components/alpha3/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/am2315c/common.yaml b/tests/components/am2315c/common.yaml index ab4656c17d..362fe19e4d 100644 --- a/tests/components/am2315c/common.yaml +++ b/tests/components/am2315c/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_am2315c - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: am2315c + i2c_id: i2c_bus temperature: name: Temperature humidity: diff --git a/tests/components/am2315c/test.esp32-ard.yaml b/tests/components/am2315c/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/am2315c/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/am2315c/test.esp32-c3-ard.yaml b/tests/components/am2315c/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/am2315c/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/am2315c/test.esp32-c3-idf.yaml b/tests/components/am2315c/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/am2315c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/am2315c/test.esp32-idf.yaml b/tests/components/am2315c/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/am2315c/test.esp32-idf.yaml +++ b/tests/components/am2315c/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/am2315c/test.esp8266-ard.yaml b/tests/components/am2315c/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/am2315c/test.esp8266-ard.yaml +++ b/tests/components/am2315c/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/am2315c/test.rp2040-ard.yaml b/tests/components/am2315c/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/am2315c/test.rp2040-ard.yaml +++ b/tests/components/am2315c/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/am2320/common.yaml b/tests/components/am2320/common.yaml index c0982b8818..d67ca3e564 100644 --- a/tests/components/am2320/common.yaml +++ b/tests/components/am2320/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_am2320 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: am2320 + i2c_id: i2c_bus temperature: name: Temperature humidity: diff --git a/tests/components/am2320/test.esp32-ard.yaml b/tests/components/am2320/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/am2320/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/am2320/test.esp32-c3-ard.yaml b/tests/components/am2320/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/am2320/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/am2320/test.esp32-c3-idf.yaml b/tests/components/am2320/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/am2320/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/am2320/test.esp32-idf.yaml b/tests/components/am2320/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/am2320/test.esp32-idf.yaml +++ b/tests/components/am2320/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/am2320/test.esp8266-ard.yaml b/tests/components/am2320/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/am2320/test.esp8266-ard.yaml +++ b/tests/components/am2320/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/am2320/test.rp2040-ard.yaml b/tests/components/am2320/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/am2320/test.rp2040-ard.yaml +++ b/tests/components/am2320/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/am43/test.esp32-idf.yaml b/tests/components/am43/test.esp32-idf.yaml index dade44d145..7a6541ae76 100644 --- a/tests/components/am43/test.esp32-idf.yaml +++ b/tests/components/am43/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/analog_threshold/common.yaml b/tests/components/analog_threshold/common.yaml index 44d79756b5..26c401b92a 100644 --- a/tests/components/analog_threshold/common.yaml +++ b/tests/components/analog_threshold/common.yaml @@ -5,9 +5,8 @@ sensor: lambda: |- if (millis() > 10000) { return 42.0; - } else { - return 0.0; } + return 0.0; update_interval: 15s binary_sensor: diff --git a/tests/components/absolute_humidity/test.esp32-c3-idf.yaml b/tests/components/analog_threshold/test.nrf52-adafruit.yaml similarity index 100% rename from tests/components/absolute_humidity/test.esp32-c3-idf.yaml rename to tests/components/analog_threshold/test.nrf52-adafruit.yaml diff --git a/tests/components/airthings_wave_mini/test.esp32-ard.yaml b/tests/components/analog_threshold/test.nrf52-mcumgr.yaml similarity index 100% rename from tests/components/airthings_wave_mini/test.esp32-ard.yaml rename to tests/components/analog_threshold/test.nrf52-mcumgr.yaml diff --git a/tests/components/animation/test.esp32-ard.yaml b/tests/components/animation/test.esp32-ard.yaml deleted file mode 100644 index 7d9fe45bff..0000000000 --- a/tests/components/animation/test.esp32-ard.yaml +++ /dev/null @@ -1,17 +0,0 @@ -spi: - - id: spi_main_lcd - clk_pin: 16 - mosi_pin: 17 - miso_pin: 15 - -display: - - platform: ili9xxx - id: main_lcd - model: ili9342 - cs_pin: 12 - dc_pin: 13 - reset_pin: 21 - invert_colors: false - -packages: - animation: !include common.yaml diff --git a/tests/components/animation/test.esp32-c3-ard.yaml b/tests/components/animation/test.esp32-c3-ard.yaml deleted file mode 100644 index 18aa2a5b06..0000000000 --- a/tests/components/animation/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,17 +0,0 @@ -spi: - - id: spi_main_lcd - clk_pin: 6 - mosi_pin: 7 - miso_pin: 5 - -display: - - platform: ili9xxx - id: main_lcd - model: ili9342 - cs_pin: 8 - dc_pin: 9 - reset_pin: 10 - invert_colors: false - -packages: - animation: !include common.yaml diff --git a/tests/components/animation/test.esp32-c3-idf.yaml b/tests/components/animation/test.esp32-c3-idf.yaml deleted file mode 100644 index 18aa2a5b06..0000000000 --- a/tests/components/animation/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,17 +0,0 @@ -spi: - - id: spi_main_lcd - clk_pin: 6 - mosi_pin: 7 - miso_pin: 5 - -display: - - platform: ili9xxx - id: main_lcd - model: ili9342 - cs_pin: 8 - dc_pin: 9 - reset_pin: 10 - invert_colors: false - -packages: - animation: !include common.yaml diff --git a/tests/components/animation/test.esp32-idf.yaml b/tests/components/animation/test.esp32-idf.yaml index 7d9fe45bff..c28e9584dd 100644 --- a/tests/components/animation/test.esp32-idf.yaml +++ b/tests/components/animation/test.esp32-idf.yaml @@ -1,17 +1,13 @@ -spi: - - id: spi_main_lcd - clk_pin: 16 - mosi_pin: 17 - miso_pin: 15 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + animation: !include common.yaml display: - platform: ili9xxx id: main_lcd + spi_id: spi_bus model: ili9342 cs_pin: 12 dc_pin: 13 reset_pin: 21 invert_colors: false - -packages: - animation: !include common.yaml diff --git a/tests/components/animation/test.esp8266-ard.yaml b/tests/components/animation/test.esp8266-ard.yaml index 9548c7fbeb..11a7117d91 100644 --- a/tests/components/animation/test.esp8266-ard.yaml +++ b/tests/components/animation/test.esp8266-ard.yaml @@ -1,17 +1,13 @@ -spi: - - id: spi_main_lcd - clk_pin: 14 - mosi_pin: 13 - miso_pin: 12 +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + animation: !include common.yaml display: - platform: ili9xxx id: main_lcd + spi_id: spi_bus model: ili9342 cs_pin: 5 dc_pin: 15 reset_pin: 16 invert_colors: false - -packages: - animation: !include common.yaml diff --git a/tests/components/animation/test.rp2040-ard.yaml b/tests/components/animation/test.rp2040-ard.yaml index efb3f2907c..32fb4efb04 100644 --- a/tests/components/animation/test.rp2040-ard.yaml +++ b/tests/components/animation/test.rp2040-ard.yaml @@ -1,17 +1,13 @@ -spi: - - id: spi_main_lcd - clk_pin: 2 - mosi_pin: 3 - miso_pin: 4 +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml + animation: !include common.yaml display: - platform: ili9xxx id: main_lcd + spi_id: spi_bus model: ili9342 cs_pin: 20 dc_pin: 21 reset_pin: 22 invert_colors: false - -packages: - animation: !include common.yaml diff --git a/tests/components/anova/test.esp32-idf.yaml b/tests/components/anova/test.esp32-idf.yaml index dade44d145..7a6541ae76 100644 --- a/tests/components/anova/test.esp32-idf.yaml +++ b/tests/components/anova/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/apds9306/common.yaml b/tests/components/apds9306/common.yaml index b3828e62ff..dc34f47645 100644 --- a/tests/components/apds9306/common.yaml +++ b/tests/components/apds9306/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_apds9306 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: apds9306 + i2c_id: i2c_bus name: "APDS9306 Light Level" gain: 3 bit_width: 16 diff --git a/tests/components/apds9306/test.esp32-ard.yaml b/tests/components/apds9306/test.esp32-ard.yaml deleted file mode 100644 index 3b761d3fc1..0000000000 --- a/tests/components/apds9306/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO22 - sda_pin: GPIO21 - -<<: !include common.yaml diff --git a/tests/components/apds9306/test.esp32-c3-ard.yaml b/tests/components/apds9306/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/apds9306/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/apds9306/test.esp32-c3-idf.yaml b/tests/components/apds9306/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/apds9306/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/apds9306/test.esp32-idf.yaml b/tests/components/apds9306/test.esp32-idf.yaml index 3b761d3fc1..b47e39c389 100644 --- a/tests/components/apds9306/test.esp32-idf.yaml +++ b/tests/components/apds9306/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO22 - sda_pin: GPIO21 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/apds9306/test.esp8266-ard.yaml b/tests/components/apds9306/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/apds9306/test.esp8266-ard.yaml +++ b/tests/components/apds9306/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/apds9306/test.rp2040-ard.yaml b/tests/components/apds9306/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/apds9306/test.rp2040-ard.yaml +++ b/tests/components/apds9306/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/apds9960/common.yaml b/tests/components/apds9960/common.yaml index de7706648a..c14212d263 100644 --- a/tests/components/apds9960/common.yaml +++ b/tests/components/apds9960/common.yaml @@ -1,9 +1,5 @@ -i2c: - - id: i2c_apds9960 - scl: ${scl_pin} - sda: ${sda_pin} - apds9960: + i2c_id: i2c_bus address: 0x20 update_interval: 60s diff --git a/tests/components/apds9960/test.esp32-ard.yaml b/tests/components/apds9960/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/apds9960/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/apds9960/test.esp32-c3-ard.yaml b/tests/components/apds9960/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/apds9960/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/apds9960/test.esp32-c3-idf.yaml b/tests/components/apds9960/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/apds9960/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/apds9960/test.esp32-idf.yaml b/tests/components/apds9960/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/apds9960/test.esp32-idf.yaml +++ b/tests/components/apds9960/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/apds9960/test.esp8266-ard.yaml b/tests/components/apds9960/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/apds9960/test.esp8266-ard.yaml +++ b/tests/components/apds9960/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/apds9960/test.rp2040-ard.yaml b/tests/components/apds9960/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/apds9960/test.rp2040-ard.yaml +++ b/tests/components/apds9960/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/api/common-base.yaml b/tests/components/api/common-base.yaml new file mode 100644 index 0000000000..0416cebf9b --- /dev/null +++ b/tests/components/api/common-base.yaml @@ -0,0 +1,197 @@ +esphome: + on_boot: + then: + - wait_until: + condition: + api.connected: + state_subscription_only: true + - homeassistant.event: + event: esphome.button_pressed + data: + message: Button was pressed + - homeassistant.action: + action: notify.html5 + data: + message: Button was pressed + - homeassistant.tag_scanned: pulse + - homeassistant.action: + action: weather.get_forecasts + data: + entity_id: weather.forecast_home + type: hourly + capture_response: true + on_success: + - lambda: |- + JsonObjectConst next_hour = response["response"]["weather.forecast_home"]["forecast"][0]; + float next_temperature = next_hour["temperature"].as(); + ESP_LOGD("main", "Next hour temperature: %f", next_temperature); + on_error: + - lambda: |- + ESP_LOGE("main", "Action failed with error: %s", error.c_str()); + - homeassistant.action: + action: weather.get_forecasts + data: + entity_id: weather.forecast_home + type: hourly + capture_response: true + response_template: "{{ response['weather.forecast_home']['forecast'][0]['temperature'] }}" + on_success: + - lambda: |- + float temperature = response["response"].as(); + ESP_LOGD("main", "Next hour temperature: %f", temperature); + - homeassistant.action: + action: light.toggle + data: + entity_id: light.demo_light + on_success: + - logger.log: "Toggled demo light" + on_error: + - logger.log: "Failed to toggle demo light" + +api: + port: 8000 + reboot_timeout: 0min + actions: + - action: hello_world + variables: + name: string + then: + - logger.log: + format: Hello World %s! + args: + - name.c_str() + - action: empty_action + then: + - logger.log: Action Called + - action: all_types + variables: + bool_: bool + int_: int + float_: float + string_: string + then: + - logger.log: Something happened + - action: array_types + variables: + bool_arr: bool[] + int_arr: int[] + float_arr: float[] + string_arr: string[] + then: + - logger.log: + # yamllint disable rule:line-length + format: "Bool: %s (%u), Int: %ld (%u), Float: %f (%u), String: %s (%u)" + # yamllint enable rule:line-length + args: + - YESNO(bool_arr[0]) + - bool_arr.size() + - (long) int_arr[0] + - int_arr.size() + - float_arr[0] + - float_arr.size() + - string_arr[0].c_str() + - string_arr.size() + # Test ContinuationAction (IfAction with then/else branches) + - action: test_if_action + variables: + condition: bool + value: int + then: + - if: + condition: + lambda: 'return condition;' + then: + - logger.log: + format: "Condition true, value: %d" + args: ['value'] + else: + - logger.log: + format: "Condition false, value: %d" + args: ['value'] + - logger.log: "After if/else" + # Test nested IfAction (multiple ContinuationAction instances) + - action: test_nested_if + variables: + outer: bool + inner: bool + then: + - if: + condition: + lambda: 'return outer;' + then: + - if: + condition: + lambda: 'return inner;' + then: + - logger.log: "Both true" + else: + - logger.log: "Outer true, inner false" + else: + - logger.log: "Outer false" + - logger.log: "After nested if" + # Test WhileLoopContinuation (WhileAction) + - action: test_while_action + variables: + max_count: int + then: + - lambda: 'id(api_continuation_test_counter) = 0;' + - while: + condition: + lambda: 'return id(api_continuation_test_counter) < max_count;' + then: + - logger.log: + format: "While loop iteration: %d" + args: ['id(api_continuation_test_counter)'] + - lambda: 'id(api_continuation_test_counter)++;' + - logger.log: "After while loop" + # Test RepeatLoopContinuation (RepeatAction) + - action: test_repeat_action + variables: + count: int + then: + - repeat: + count: !lambda 'return count;' + then: + - logger.log: + format: "Repeat iteration: %d" + args: ['iteration'] + - logger.log: "After repeat" + # Test combined continuations (if + while + repeat) + - action: test_combined_continuations + variables: + do_loop: bool + loop_count: int + then: + - if: + condition: + lambda: 'return do_loop;' + then: + - repeat: + count: !lambda 'return loop_count;' + then: + - lambda: 'id(api_continuation_test_counter) = iteration;' + - while: + condition: + lambda: 'return id(api_continuation_test_counter) > 0;' + then: + - logger.log: + format: "Combined: repeat=%d, while=%d" + args: ['iteration', 'id(api_continuation_test_counter)'] + - lambda: 'id(api_continuation_test_counter)--;' + else: + - logger.log: "Skipped loops" + - logger.log: "After combined test" + +event: + - platform: template + name: Test Event + id: test_event + event_types: + - single_click + - double_click + +globals: + - id: api_continuation_test_counter + type: int + restore_value: false + initial_value: '0' diff --git a/tests/components/api/common.yaml b/tests/components/api/common.yaml index 7ac11e4da6..6115838b6d 100644 --- a/tests/components/api/common.yaml +++ b/tests/components/api/common.yaml @@ -1,59 +1,5 @@ -esphome: - on_boot: - then: - - homeassistant.event: - event: esphome.button_pressed - data: - message: Button was pressed - - homeassistant.action: - action: notify.html5 - data: - message: Button was pressed - - homeassistant.tag_scanned: pulse +<<: !include common-base.yaml api: - port: 8000 - password: pwd - reboot_timeout: 0min encryption: key: bOFFzzvfpg5DB94DuBGLXD/hMnhpDKgP9UQyBulwWVU= - actions: - - action: hello_world - variables: - name: string - then: - - logger.log: - format: Hello World %s! - args: - - name.c_str() - - action: empty_action - then: - - logger.log: Action Called - - action: all_types - variables: - bool_: bool - int_: int - float_: float - string_: string - then: - - logger.log: Something happened - - action: array_types - variables: - bool_arr: bool[] - int_arr: int[] - float_arr: float[] - string_arr: string[] - then: - - logger.log: - # yamllint disable rule:line-length - format: "Bool: %s (%u), Int: %ld (%u), Float: %f (%u), String: %s (%u)" - # yamllint enable rule:line-length - args: - - YESNO(bool_arr[0]) - - bool_arr.size() - - (long) int_arr[0] - - int_arr.size() - - float_arr[0] - - float_arr.size() - - string_arr[0].c_str() - - string_arr.size() diff --git a/tests/components/api/test-dynamic-encryption.esp32-idf.yaml b/tests/components/api/test-dynamic-encryption.esp32-idf.yaml index d8f8c247f4..504871716b 100644 --- a/tests/components/api/test-dynamic-encryption.esp32-idf.yaml +++ b/tests/components/api/test-dynamic-encryption.esp32-idf.yaml @@ -1,10 +1,5 @@ -packages: - common: !include common.yaml +<<: !include common-base.yaml wifi: ssid: MySSID password: password1 - -api: - encryption: - key: !remove diff --git a/tests/components/api/test.esp32-ard.yaml b/tests/components/api/test.esp32-ard.yaml deleted file mode 100644 index 46c01d926f..0000000000 --- a/tests/components/api/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -<<: !include common.yaml - -wifi: - ssid: MySSID - password: password1 diff --git a/tests/components/api/test.esp32-c3-ard.yaml b/tests/components/api/test.esp32-c3-ard.yaml deleted file mode 100644 index 46c01d926f..0000000000 --- a/tests/components/api/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -<<: !include common.yaml - -wifi: - ssid: MySSID - password: password1 diff --git a/tests/components/api/test.esp32-c3-idf.yaml b/tests/components/api/test.esp32-c3-idf.yaml deleted file mode 100644 index 46c01d926f..0000000000 --- a/tests/components/api/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -<<: !include common.yaml - -wifi: - ssid: MySSID - password: password1 diff --git a/tests/components/as3935_i2c/common.yaml b/tests/components/as3935_i2c/common.yaml index d76cc37fc1..d659486e83 100644 --- a/tests/components/as3935_i2c/common.yaml +++ b/tests/components/as3935_i2c/common.yaml @@ -1,17 +1,16 @@ -i2c: - - id: i2c_as3935 - scl: ${scl_pin} - sda: ${sda_pin} - as3935_i2c: + id: as3935_i2c_id + i2c_id: i2c_bus irq_pin: ${irq_pin} binary_sensor: - platform: as3935 + as3935_id: as3935_i2c_id name: Storm Alert sensor: - platform: as3935 + as3935_id: as3935_i2c_id lightning_energy: name: Lightning Energy distance: diff --git a/tests/components/as3935_i2c/test.esp32-ard.yaml b/tests/components/as3935_i2c/test.esp32-ard.yaml deleted file mode 100644 index 52d5a045cb..0000000000 --- a/tests/components/as3935_i2c/test.esp32-ard.yaml +++ /dev/null @@ -1,11 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - irq_pin: GPIO15 - -packages: - as3935: !include common.yaml - -# Trigger issue: https://github.com/esphome/issues/issues/6990 -# Compile with no binary sensor results in error -binary_sensor: !remove diff --git a/tests/components/as3935_i2c/test.esp32-c3-ard.yaml b/tests/components/as3935_i2c/test.esp32-c3-ard.yaml deleted file mode 100644 index 799acabd5a..0000000000 --- a/tests/components/as3935_i2c/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - irq_pin: GPIO6 - -<<: !include common.yaml diff --git a/tests/components/as3935_i2c/test.esp32-c3-idf.yaml b/tests/components/as3935_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index 799acabd5a..0000000000 --- a/tests/components/as3935_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - irq_pin: GPIO6 - -<<: !include common.yaml diff --git a/tests/components/as3935_i2c/test.esp32-idf.yaml b/tests/components/as3935_i2c/test.esp32-idf.yaml index 2c57d412f6..49629536e7 100644 --- a/tests/components/as3935_i2c/test.esp32-idf.yaml +++ b/tests/components/as3935_i2c/test.esp32-idf.yaml @@ -1,6 +1,7 @@ substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 irq_pin: GPIO15 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/as3935_i2c/test.esp8266-ard.yaml b/tests/components/as3935_i2c/test.esp8266-ard.yaml index c8e6a43f44..dc7609ab37 100644 --- a/tests/components/as3935_i2c/test.esp8266-ard.yaml +++ b/tests/components/as3935_i2c/test.esp8266-ard.yaml @@ -1,6 +1,7 @@ substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 irq_pin: GPIO15 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/as3935_i2c/test.rp2040-ard.yaml b/tests/components/as3935_i2c/test.rp2040-ard.yaml index 799acabd5a..b80562ad22 100644 --- a/tests/components/as3935_i2c/test.rp2040-ard.yaml +++ b/tests/components/as3935_i2c/test.rp2040-ard.yaml @@ -1,6 +1,7 @@ substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 irq_pin: GPIO6 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/as3935_spi/common.yaml b/tests/components/as3935_spi/common.yaml index c3fb93dff1..d2942dc01d 100644 --- a/tests/components/as3935_spi/common.yaml +++ b/tests/components/as3935_spi/common.yaml @@ -1,19 +1,16 @@ -spi: - - id: spi_as3935 - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - miso_pin: ${miso_pin} - as3935_spi: + id: as3935_spi_id cs_pin: ${cs_pin} irq_pin: ${irq_pin} binary_sensor: - platform: as3935 + as3935_id: as3935_spi_id name: Storm Alert sensor: - platform: as3935 + as3935_id: as3935_spi_id lightning_energy: name: Lightning Energy distance: diff --git a/tests/components/as3935_spi/test.esp32-ard.yaml b/tests/components/as3935_spi/test.esp32-ard.yaml deleted file mode 100644 index e00f522dd4..0000000000 --- a/tests/components/as3935_spi/test.esp32-ard.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 - irq_pin: GPIO13 - cs_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/as3935_spi/test.esp32-c3-ard.yaml b/tests/components/as3935_spi/test.esp32-c3-ard.yaml deleted file mode 100644 index fcf35f528e..0000000000 --- a/tests/components/as3935_spi/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - irq_pin: GPIO9 - cs_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/as3935_spi/test.esp32-c3-idf.yaml b/tests/components/as3935_spi/test.esp32-c3-idf.yaml deleted file mode 100644 index fcf35f528e..0000000000 --- a/tests/components/as3935_spi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - irq_pin: GPIO9 - cs_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/as3935_spi/test.esp32-idf.yaml b/tests/components/as3935_spi/test.esp32-idf.yaml index e00f522dd4..19791e24b7 100644 --- a/tests/components/as3935_spi/test.esp32-idf.yaml +++ b/tests/components/as3935_spi/test.esp32-idf.yaml @@ -1,8 +1,8 @@ substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 irq_pin: GPIO13 cs_pin: GPIO5 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/as3935_spi/test.esp8266-ard.yaml b/tests/components/as3935_spi/test.esp8266-ard.yaml index b90e661ec0..8475dc4c50 100644 --- a/tests/components/as3935_spi/test.esp8266-ard.yaml +++ b/tests/components/as3935_spi/test.esp8266-ard.yaml @@ -1,8 +1,11 @@ substitutions: - clk_pin: GPIO14 - mosi_pin: GPIO13 - miso_pin: GPIO12 + clk_pin: GPIO0 + mosi_pin: GPIO2 + miso_pin: GPIO16 irq_pin: GPIO5 cs_pin: GPIO15 +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/as3935_spi/test.rp2040-ard.yaml b/tests/components/as3935_spi/test.rp2040-ard.yaml index 8f5941e1b2..7c4a74a236 100644 --- a/tests/components/as3935_spi/test.rp2040-ard.yaml +++ b/tests/components/as3935_spi/test.rp2040-ard.yaml @@ -5,4 +5,7 @@ substitutions: irq_pin: GPIO5 cs_pin: GPIO6 +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/as5600/common.yaml b/tests/components/as5600/common.yaml index 860f5bf803..d867c66a21 100644 --- a/tests/components/as5600/common.yaml +++ b/tests/components/as5600/common.yaml @@ -1,9 +1,5 @@ -i2c: - - id: i2c_as5600 - scl: ${scl_pin} - sda: ${sda_pin} - as5600: + i2c_id: i2c_bus dir_pin: ${dir_pin} direction: clockwise start_position: 90deg diff --git a/tests/components/as5600/test.esp32-ard.yaml b/tests/components/as5600/test.esp32-ard.yaml deleted file mode 100644 index fa08763501..0000000000 --- a/tests/components/as5600/test.esp32-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - dir_pin: GPIO15 - -<<: !include common.yaml diff --git a/tests/components/as5600/test.esp32-c3-ard.yaml b/tests/components/as5600/test.esp32-c3-ard.yaml deleted file mode 100644 index a0623c91e5..0000000000 --- a/tests/components/as5600/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - dir_pin: GPIO6 - -<<: !include common.yaml diff --git a/tests/components/as5600/test.esp32-c3-idf.yaml b/tests/components/as5600/test.esp32-c3-idf.yaml deleted file mode 100644 index a0623c91e5..0000000000 --- a/tests/components/as5600/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - dir_pin: GPIO6 - -<<: !include common.yaml diff --git a/tests/components/as5600/test.esp32-idf.yaml b/tests/components/as5600/test.esp32-idf.yaml index fa08763501..9d25a7f09a 100644 --- a/tests/components/as5600/test.esp32-idf.yaml +++ b/tests/components/as5600/test.esp32-idf.yaml @@ -1,6 +1,7 @@ substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 dir_pin: GPIO15 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/as5600/test.esp8266-ard.yaml b/tests/components/as5600/test.esp8266-ard.yaml index 5e27f8c134..8d18740b95 100644 --- a/tests/components/as5600/test.esp8266-ard.yaml +++ b/tests/components/as5600/test.esp8266-ard.yaml @@ -1,6 +1,7 @@ substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 dir_pin: GPIO15 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/as5600/test.rp2040-ard.yaml b/tests/components/as5600/test.rp2040-ard.yaml index a0623c91e5..4bcbb99c81 100644 --- a/tests/components/as5600/test.rp2040-ard.yaml +++ b/tests/components/as5600/test.rp2040-ard.yaml @@ -1,6 +1,7 @@ substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 dir_pin: GPIO6 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/as7341/common.yaml b/tests/components/as7341/common.yaml index 0351b344c6..3f94656c74 100644 --- a/tests/components/as7341/common.yaml +++ b/tests/components/as7341/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_as7341 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: as7341 + i2c_id: i2c_bus update_interval: 15s gain: X8 atime: 120 diff --git a/tests/components/as7341/test.esp32-ard.yaml b/tests/components/as7341/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/as7341/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/as7341/test.esp32-c3-ard.yaml b/tests/components/as7341/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/as7341/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/as7341/test.esp32-c3-idf.yaml b/tests/components/as7341/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/as7341/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/as7341/test.esp32-idf.yaml b/tests/components/as7341/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/as7341/test.esp32-idf.yaml +++ b/tests/components/as7341/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/as7341/test.esp8266-ard.yaml b/tests/components/as7341/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/as7341/test.esp8266-ard.yaml +++ b/tests/components/as7341/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/as7341/test.rp2040-ard.yaml b/tests/components/as7341/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/as7341/test.rp2040-ard.yaml +++ b/tests/components/as7341/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/at581x/common.yaml b/tests/components/at581x/common.yaml index 018a0fded1..425be47c42 100644 --- a/tests/components/at581x/common.yaml +++ b/tests/components/at581x/common.yaml @@ -16,13 +16,9 @@ esphome: id: waveradar at581x: + i2c_id: i2c_bus id: waveradar -i2c: - - id: i2c_at581x - scl: ${scl_pin} - sda: ${sda_pin} - switch: - platform: at581x name: Enable Radar diff --git a/tests/components/at581x/test.esp32-ard.yaml b/tests/components/at581x/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/at581x/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/at581x/test.esp32-c3-ard.yaml b/tests/components/at581x/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/at581x/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/at581x/test.esp32-c3-idf.yaml b/tests/components/at581x/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/at581x/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/at581x/test.esp32-idf.yaml b/tests/components/at581x/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/at581x/test.esp32-idf.yaml +++ b/tests/components/at581x/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/at581x/test.esp8266-ard.yaml b/tests/components/at581x/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/at581x/test.esp8266-ard.yaml +++ b/tests/components/at581x/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/at581x/test.rp2040-ard.yaml b/tests/components/at581x/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/at581x/test.rp2040-ard.yaml +++ b/tests/components/at581x/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/atc_mithermometer/test.esp32-idf.yaml b/tests/components/atc_mithermometer/test.esp32-idf.yaml index dade44d145..7a6541ae76 100644 --- a/tests/components/atc_mithermometer/test.esp32-idf.yaml +++ b/tests/components/atc_mithermometer/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/atm90e26/common.yaml b/tests/components/atm90e26/common.yaml index 49c3a73ec8..478be7b8b3 100644 --- a/tests/components/atm90e26/common.yaml +++ b/tests/components/atm90e26/common.yaml @@ -1,9 +1,3 @@ -spi: - - id: spi_atm90e26 - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - miso_pin: ${miso_pin} - sensor: - platform: atm90e26 cs_pin: ${cs_pin} diff --git a/tests/components/atm90e26/test.esp32-ard.yaml b/tests/components/atm90e26/test.esp32-ard.yaml deleted file mode 100644 index 54e027a614..0000000000 --- a/tests/components/atm90e26/test.esp32-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 - cs_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/atm90e26/test.esp32-c3-ard.yaml b/tests/components/atm90e26/test.esp32-c3-ard.yaml deleted file mode 100644 index 2415ba5dc6..0000000000 --- a/tests/components/atm90e26/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/atm90e26/test.esp32-c3-idf.yaml b/tests/components/atm90e26/test.esp32-c3-idf.yaml deleted file mode 100644 index 2415ba5dc6..0000000000 --- a/tests/components/atm90e26/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/atm90e26/test.esp32-idf.yaml b/tests/components/atm90e26/test.esp32-idf.yaml index 54e027a614..a3352cf880 100644 --- a/tests/components/atm90e26/test.esp32-idf.yaml +++ b/tests/components/atm90e26/test.esp32-idf.yaml @@ -1,7 +1,7 @@ substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 cs_pin: GPIO5 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/atm90e26/test.esp8266-ard.yaml b/tests/components/atm90e26/test.esp8266-ard.yaml index dbd158d030..b4673ba8b7 100644 --- a/tests/components/atm90e26/test.esp8266-ard.yaml +++ b/tests/components/atm90e26/test.esp8266-ard.yaml @@ -1,7 +1,10 @@ substitutions: - clk_pin: GPIO14 - mosi_pin: GPIO13 - miso_pin: GPIO12 + clk_pin: GPIO0 + mosi_pin: GPIO2 + miso_pin: GPIO16 cs_pin: GPIO15 +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/atm90e26/test.rp2040-ard.yaml b/tests/components/atm90e26/test.rp2040-ard.yaml index c8bfab0023..5d0c35c2d2 100644 --- a/tests/components/atm90e26/test.rp2040-ard.yaml +++ b/tests/components/atm90e26/test.rp2040-ard.yaml @@ -4,4 +4,7 @@ substitutions: miso_pin: GPIO4 cs_pin: GPIO6 +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/atm90e32/common.yaml b/tests/components/atm90e32/common.yaml index 3eeed8395f..b8b480ab62 100644 --- a/tests/components/atm90e32/common.yaml +++ b/tests/components/atm90e32/common.yaml @@ -1,9 +1,3 @@ -spi: - - id: spi_atm90e32 - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - miso_pin: ${miso_pin} - sensor: - platform: atm90e32 cs_pin: ${cs_pin} diff --git a/tests/components/atm90e32/test.esp32-ard.yaml b/tests/components/atm90e32/test.esp32-ard.yaml deleted file mode 100644 index 54e027a614..0000000000 --- a/tests/components/atm90e32/test.esp32-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 - cs_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/atm90e32/test.esp32-c3-ard.yaml b/tests/components/atm90e32/test.esp32-c3-ard.yaml deleted file mode 100644 index 2415ba5dc6..0000000000 --- a/tests/components/atm90e32/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/atm90e32/test.esp32-c3-idf.yaml b/tests/components/atm90e32/test.esp32-c3-idf.yaml deleted file mode 100644 index 2415ba5dc6..0000000000 --- a/tests/components/atm90e32/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/atm90e32/test.esp32-idf.yaml b/tests/components/atm90e32/test.esp32-idf.yaml index 54e027a614..a3352cf880 100644 --- a/tests/components/atm90e32/test.esp32-idf.yaml +++ b/tests/components/atm90e32/test.esp32-idf.yaml @@ -1,7 +1,7 @@ substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 cs_pin: GPIO5 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/atm90e32/test.esp8266-ard.yaml b/tests/components/atm90e32/test.esp8266-ard.yaml index dbd158d030..b4673ba8b7 100644 --- a/tests/components/atm90e32/test.esp8266-ard.yaml +++ b/tests/components/atm90e32/test.esp8266-ard.yaml @@ -1,7 +1,10 @@ substitutions: - clk_pin: GPIO14 - mosi_pin: GPIO13 - miso_pin: GPIO12 + clk_pin: GPIO0 + mosi_pin: GPIO2 + miso_pin: GPIO16 cs_pin: GPIO15 +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/atm90e32/test.rp2040-ard.yaml b/tests/components/atm90e32/test.rp2040-ard.yaml index c8bfab0023..5d0c35c2d2 100644 --- a/tests/components/atm90e32/test.rp2040-ard.yaml +++ b/tests/components/atm90e32/test.rp2040-ard.yaml @@ -4,4 +4,7 @@ substitutions: miso_pin: GPIO4 cs_pin: GPIO6 +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/axs15231/common.yaml b/tests/components/axs15231/common.yaml index 1c0c79975f..d4fd3becbb 100644 --- a/tests/components/axs15231/common.yaml +++ b/tests/components/axs15231/common.yaml @@ -1,20 +1,18 @@ -i2c: - - id: i2c_axs15231 - scl: 3 - sda: 21 - display: - platform: ssd1306_i2c - id: ssd1306_display + i2c_id: i2c_bus + id: ssd1306_i2c_display model: SSD1306_128X64 reset_pin: 19 pages: - - id: page1 + - id: axs15231_page1 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); touchscreen: - platform: axs15231 - display: ssd1306_display + i2c_id: i2c_bus + id: axs15231_touchscreen + display: ssd1306_i2c_display interrupt_pin: 20 reset_pin: 18 diff --git a/tests/components/axs15231/test.esp32-idf.yaml b/tests/components/axs15231/test.esp32-idf.yaml index dade44d145..b47e39c389 100644 --- a/tests/components/axs15231/test.esp32-idf.yaml +++ b/tests/components/axs15231/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/axs15231/test.esp8266-ard.yaml b/tests/components/axs15231/test.esp8266-ard.yaml index c09d139574..eb599da773 100644 --- a/tests/components/axs15231/test.esp8266-ard.yaml +++ b/tests/components/axs15231/test.esp8266-ard.yaml @@ -1,10 +1,9 @@ -i2c: - - id: i2c_axs15231 - scl: 5 - sda: 4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml display: - platform: ssd1306_i2c + i2c_id: i2c_bus id: ssd1306_display model: SSD1306_128X64 reset_pin: 13 @@ -15,5 +14,6 @@ display: touchscreen: - platform: axs15231 + i2c_id: i2c_bus display: ssd1306_display interrupt_pin: 12 diff --git a/tests/components/axs15231/test.rp2040-ard.yaml b/tests/components/axs15231/test.rp2040-ard.yaml index dade44d145..319a7c71a6 100644 --- a/tests/components/axs15231/test.rp2040-ard.yaml +++ b/tests/components/axs15231/test.rp2040-ard.yaml @@ -1 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/b_parasite/test.esp32-idf.yaml b/tests/components/b_parasite/test.esp32-idf.yaml index dade44d145..7a6541ae76 100644 --- a/tests/components/b_parasite/test.esp32-idf.yaml +++ b/tests/components/b_parasite/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/ballu/common.yaml b/tests/components/ballu/common.yaml index 52f86aa26a..178c39b8ce 100644 --- a/tests/components/ballu/common.yaml +++ b/tests/components/ballu/common.yaml @@ -1,7 +1,3 @@ -remote_transmitter: - pin: ${pin} - carrier_duty_percent: 50% - climate: - platform: heatpumpir protocol: ballu @@ -10,3 +6,4 @@ climate: name: HeatpumpIR Climate min_temperature: 18 max_temperature: 30 + transmitter_id: xmitr diff --git a/tests/components/ballu/test.esp32-ard.yaml b/tests/components/ballu/test.esp32-ard.yaml deleted file mode 100644 index 7b012aa64c..0000000000 --- a/tests/components/ballu/test.esp32-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/ballu/test.esp8266-ard.yaml b/tests/components/ballu/test.esp8266-ard.yaml index f5097fcf5f..4bed2f03e5 100644 --- a/tests/components/ballu/test.esp8266-ard.yaml +++ b/tests/components/ballu/test.esp8266-ard.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO5 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/airthings_wave_mini/test.esp32-c3-ard.yaml b/tests/components/bang_bang/test.nrf52-adafruit.yaml similarity index 100% rename from tests/components/airthings_wave_mini/test.esp32-c3-ard.yaml rename to tests/components/bang_bang/test.nrf52-adafruit.yaml diff --git a/tests/components/airthings_wave_mini/test.esp32-c3-idf.yaml b/tests/components/bang_bang/test.nrf52-mcumgr.yaml similarity index 100% rename from tests/components/airthings_wave_mini/test.esp32-c3-idf.yaml rename to tests/components/bang_bang/test.nrf52-mcumgr.yaml diff --git a/tests/components/bedjet/test.esp32-idf.yaml b/tests/components/bedjet/test.esp32-idf.yaml index dade44d145..7a6541ae76 100644 --- a/tests/components/bedjet/test.esp32-idf.yaml +++ b/tests/components/bedjet/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/bh1750/common.yaml b/tests/components/bh1750/common.yaml index c0e0bc1c59..46ea99b7e3 100644 --- a/tests/components/bh1750/common.yaml +++ b/tests/components/bh1750/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_bh1750 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: bh1750 + i2c_id: i2c_bus name: Living Room Brightness address: 0x23 update_interval: 30s diff --git a/tests/components/bh1750/test.esp32-ard.yaml b/tests/components/bh1750/test.esp32-ard.yaml deleted file mode 100644 index 3b761d3fc1..0000000000 --- a/tests/components/bh1750/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO22 - sda_pin: GPIO21 - -<<: !include common.yaml diff --git a/tests/components/bh1750/test.esp32-c3-ard.yaml b/tests/components/bh1750/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/bh1750/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/bh1750/test.esp32-c3-idf.yaml b/tests/components/bh1750/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/bh1750/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/bh1750/test.esp32-idf.yaml b/tests/components/bh1750/test.esp32-idf.yaml index 3b761d3fc1..b47e39c389 100644 --- a/tests/components/bh1750/test.esp32-idf.yaml +++ b/tests/components/bh1750/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO22 - sda_pin: GPIO21 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/bh1750/test.esp8266-ard.yaml b/tests/components/bh1750/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/bh1750/test.esp8266-ard.yaml +++ b/tests/components/bh1750/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/bh1750/test.rp2040-ard.yaml b/tests/components/bh1750/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/bh1750/test.rp2040-ard.yaml +++ b/tests/components/bh1750/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/bh1900nux/common.yaml b/tests/components/bh1900nux/common.yaml new file mode 100644 index 0000000000..3438418702 --- /dev/null +++ b/tests/components/bh1900nux/common.yaml @@ -0,0 +1,6 @@ +sensor: + - platform: bh1900nux + i2c_id: i2c_bus + name: Temperature Living Room + address: 0x48 + update_interval: 30s diff --git a/tests/components/bh1900nux/test.esp32-idf.yaml b/tests/components/bh1900nux/test.esp32-idf.yaml new file mode 100644 index 0000000000..b47e39c389 --- /dev/null +++ b/tests/components/bh1900nux/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/bh1900nux/test.esp8266-ard.yaml b/tests/components/bh1900nux/test.esp8266-ard.yaml new file mode 100644 index 0000000000..4a98b9388a --- /dev/null +++ b/tests/components/bh1900nux/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/bh1900nux/test.rp2040-ard.yaml b/tests/components/bh1900nux/test.rp2040-ard.yaml new file mode 100644 index 0000000000..319a7c71a6 --- /dev/null +++ b/tests/components/bh1900nux/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/binary_sensor/common.yaml b/tests/components/binary_sensor/common.yaml index 2b4a006352..e3fd159b08 100644 --- a/tests/components/binary_sensor/common.yaml +++ b/tests/components/binary_sensor/common.yaml @@ -23,9 +23,8 @@ binary_sensor: - lambda: |- if (id(some_binary_sensor).state) { return x; - } else { - return {}; } + return {}; - settle: 100ms - timeout: 10s @@ -38,3 +37,102 @@ binary_sensor: format: "New state is %s" args: ['x.has_value() ? ONOFF(x) : "Unknown"'] - binary_sensor.invalidate_state: some_binary_sensor + + # Test autorepeat with default configuration (no timings) + - platform: template + id: autorepeat_default + name: "Autorepeat Default" + filters: + - autorepeat: + + # Test autorepeat with single timing entry + - platform: template + id: autorepeat_single + name: "Autorepeat Single" + filters: + - autorepeat: + - delay: 2s + time_off: 200ms + time_on: 800ms + + # Test autorepeat with three timing entries + - platform: template + id: autorepeat_multiple + name: "Autorepeat Multiple" + filters: + - autorepeat: + - delay: 500ms + time_off: 50ms + time_on: 950ms + - delay: 2s + time_off: 100ms + time_on: 900ms + - delay: 10s + time_off: 200ms + time_on: 800ms + + # Test on_multi_click with single click + - platform: template + id: multi_click_single + name: "Multi Click Single" + on_multi_click: + - timing: + - state: true + min_length: 50ms + max_length: 350ms + then: + - logger.log: "Single click detected" + + # Test on_multi_click with double click + - platform: template + id: multi_click_double + name: "Multi Click Double" + on_multi_click: + - timing: + - state: true + min_length: 50ms + max_length: 350ms + - state: false + min_length: 50ms + max_length: 350ms + - state: true + min_length: 50ms + max_length: 350ms + then: + - logger.log: "Double click detected" + + # Test on_multi_click with complex pattern (5 events) + - platform: template + id: multi_click_complex + name: "Multi Click Complex" + on_multi_click: + - timing: + - state: true + min_length: 50ms + max_length: 350ms + - state: false + min_length: 50ms + max_length: 350ms + - state: true + min_length: 50ms + max_length: 350ms + - state: false + min_length: 50ms + max_length: 350ms + - state: true + min_length: 50ms + then: + - logger.log: "Complex pattern detected" + + # Test on_multi_click with custom invalid_cooldown + - platform: template + id: multi_click_cooldown + name: "Multi Click Cooldown" + on_multi_click: + - timing: + - state: true + min_length: 100ms + max_length: 500ms + invalid_cooldown: 2s + then: + - logger.log: "Click with custom cooldown" diff --git a/tests/components/binary_sensor/test.esp32-ard.yaml b/tests/components/binary_sensor/test.esp32-ard.yaml deleted file mode 100644 index 25cb37a0b4..0000000000 --- a/tests/components/binary_sensor/test.esp32-ard.yaml +++ /dev/null @@ -1,2 +0,0 @@ -packages: - common: !include common.yaml diff --git a/tests/components/binary_sensor/test.esp32-c3-ard.yaml b/tests/components/binary_sensor/test.esp32-c3-ard.yaml deleted file mode 100644 index 25cb37a0b4..0000000000 --- a/tests/components/binary_sensor/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,2 +0,0 @@ -packages: - common: !include common.yaml diff --git a/tests/components/binary_sensor/test.esp32-c3-idf.yaml b/tests/components/binary_sensor/test.esp32-c3-idf.yaml deleted file mode 100644 index 25cb37a0b4..0000000000 --- a/tests/components/binary_sensor/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,2 +0,0 @@ -packages: - common: !include common.yaml diff --git a/tests/components/binary_sensor/test.esp32-s3-idf.yaml b/tests/components/binary_sensor/test.esp32-s3-idf.yaml deleted file mode 100644 index 25cb37a0b4..0000000000 --- a/tests/components/binary_sensor/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,2 +0,0 @@ -packages: - common: !include common.yaml diff --git a/tests/components/airthings_wave_plus/test.esp32-ard.yaml b/tests/components/binary_sensor/test.nrf52-adafruit.yaml similarity index 100% rename from tests/components/airthings_wave_plus/test.esp32-ard.yaml rename to tests/components/binary_sensor/test.nrf52-adafruit.yaml diff --git a/tests/components/airthings_wave_plus/test.esp32-c3-ard.yaml b/tests/components/binary_sensor/test.nrf52-mcumgr.yaml similarity index 100% rename from tests/components/airthings_wave_plus/test.esp32-c3-ard.yaml rename to tests/components/binary_sensor/test.nrf52-mcumgr.yaml diff --git a/tests/components/binary_sensor_map/common.yaml b/tests/components/binary_sensor_map/common.yaml index 2fed5ae515..c054022583 100644 --- a/tests/components/binary_sensor_map/common.yaml +++ b/tests/components/binary_sensor_map/common.yaml @@ -4,25 +4,22 @@ binary_sensor: lambda: |- if (millis() > 10000) { return true; - } else { - return false; } + return false; - platform: template id: bin2 lambda: |- if (millis() > 20000) { return true; - } else { - return false; } + return false; - platform: template id: bin3 lambda: |- if (millis() > 30000) { return true; - } else { - return false; } + return false; sensor: - platform: binary_sensor_map diff --git a/tests/components/bl0906/common.yaml b/tests/components/bl0906/common.yaml index 29b82a5958..006aa682f1 100644 --- a/tests/components/bl0906/common.yaml +++ b/tests/components/bl0906/common.yaml @@ -1,9 +1,3 @@ -uart: - - id: uart_bl0906 - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 19200 - sensor: - platform: bl0906 id: bl diff --git a/tests/components/bl0906/test.esp32-ard.yaml b/tests/components/bl0906/test.esp32-ard.yaml deleted file mode 100644 index 811f6b72a6..0000000000 --- a/tests/components/bl0906/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO12 - rx_pin: GPIO14 - -<<: !include common.yaml diff --git a/tests/components/bl0906/test.esp32-c3-ard.yaml b/tests/components/bl0906/test.esp32-c3-ard.yaml deleted file mode 100644 index c79d14c740..0000000000 --- a/tests/components/bl0906/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO7 - rx_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/bl0906/test.esp32-c3-idf.yaml b/tests/components/bl0906/test.esp32-c3-idf.yaml deleted file mode 100644 index c79d14c740..0000000000 --- a/tests/components/bl0906/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO7 - rx_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/bl0906/test.esp32-idf.yaml b/tests/components/bl0906/test.esp32-idf.yaml index 811f6b72a6..76222997a8 100644 --- a/tests/components/bl0906/test.esp32-idf.yaml +++ b/tests/components/bl0906/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - tx_pin: GPIO12 - rx_pin: GPIO14 +packages: + uart_19200: !include ../../test_build_components/common/uart_19200/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/bl0906/test.esp8266-ard.yaml b/tests/components/bl0906/test.esp8266-ard.yaml index 3b44f9c9c3..ac781ea834 100644 --- a/tests/components/bl0906/test.esp8266-ard.yaml +++ b/tests/components/bl0906/test.esp8266-ard.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO1 rx_pin: GPIO3 +packages: + uart_19200: !include ../../test_build_components/common/uart_19200/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/bl0906/test.rp2040-ard.yaml b/tests/components/bl0906/test.rp2040-ard.yaml index b516342f3b..f4dada6605 100644 --- a/tests/components/bl0906/test.rp2040-ard.yaml +++ b/tests/components/bl0906/test.rp2040-ard.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO4 rx_pin: GPIO5 +packages: + uart_19200: !include ../../test_build_components/common/uart_19200/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/bl0939/common.yaml b/tests/components/bl0939/common.yaml index 7a6b635b70..a47aa05606 100644 --- a/tests/components/bl0939/common.yaml +++ b/tests/components/bl0939/common.yaml @@ -1,9 +1,3 @@ -uart: - - id: uart_bl0939 - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 9600 - sensor: - platform: bl0939 voltage: diff --git a/tests/components/bl0939/test.esp32-ard.yaml b/tests/components/bl0939/test.esp32-ard.yaml deleted file mode 100644 index 811f6b72a6..0000000000 --- a/tests/components/bl0939/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO12 - rx_pin: GPIO14 - -<<: !include common.yaml diff --git a/tests/components/bl0939/test.esp32-c3-ard.yaml b/tests/components/bl0939/test.esp32-c3-ard.yaml deleted file mode 100644 index c79d14c740..0000000000 --- a/tests/components/bl0939/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO7 - rx_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/bl0939/test.esp32-c3-idf.yaml b/tests/components/bl0939/test.esp32-c3-idf.yaml deleted file mode 100644 index c79d14c740..0000000000 --- a/tests/components/bl0939/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO7 - rx_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/bl0939/test.esp32-idf.yaml b/tests/components/bl0939/test.esp32-idf.yaml index 811f6b72a6..64baa4ec9d 100644 --- a/tests/components/bl0939/test.esp32-idf.yaml +++ b/tests/components/bl0939/test.esp32-idf.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO12 rx_pin: GPIO14 +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/bl0939/test.esp8266-ard.yaml b/tests/components/bl0939/test.esp8266-ard.yaml index 3b44f9c9c3..89ca3ab5ae 100644 --- a/tests/components/bl0939/test.esp8266-ard.yaml +++ b/tests/components/bl0939/test.esp8266-ard.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO1 rx_pin: GPIO3 +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/bl0939/test.rp2040-ard.yaml b/tests/components/bl0939/test.rp2040-ard.yaml index b516342f3b..b28f2b5e05 100644 --- a/tests/components/bl0939/test.rp2040-ard.yaml +++ b/tests/components/bl0939/test.rp2040-ard.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO4 rx_pin: GPIO5 +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/bl0940/common.yaml b/tests/components/bl0940/common.yaml index 97a997d2b4..e476ba10c0 100644 --- a/tests/components/bl0940/common.yaml +++ b/tests/components/bl0940/common.yaml @@ -1,11 +1,11 @@ -uart: - - id: uart_bl0939 - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 9600 +button: + - platform: bl0940 + bl0940_id: bl0940_test_id + name: Cal Reset sensor: - platform: bl0940 + id: bl0940_test_id voltage: name: BL0940 Voltage current: @@ -18,3 +18,18 @@ sensor: name: BL0940 Internal temperature external_temperature: name: BL0940 External temperature + +number: + - platform: bl0940 + id: bl0940_number_id + bl0940_id: bl0940_test_id + current_calibration: + name: Cal Current + min_value: -5 + max_value: 5 + voltage_calibration: + name: Cal Voltage + step: 0.01 + power_calibration: + name: Cal Power + disabled_by_default: true diff --git a/tests/components/bl0940/test.esp32-ard.yaml b/tests/components/bl0940/test.esp32-ard.yaml deleted file mode 100644 index 811f6b72a6..0000000000 --- a/tests/components/bl0940/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO12 - rx_pin: GPIO14 - -<<: !include common.yaml diff --git a/tests/components/bl0940/test.esp32-c3-ard.yaml b/tests/components/bl0940/test.esp32-c3-ard.yaml deleted file mode 100644 index c79d14c740..0000000000 --- a/tests/components/bl0940/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO7 - rx_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/bl0940/test.esp32-c3-idf.yaml b/tests/components/bl0940/test.esp32-c3-idf.yaml deleted file mode 100644 index c79d14c740..0000000000 --- a/tests/components/bl0940/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO7 - rx_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/bl0940/test.esp32-idf.yaml b/tests/components/bl0940/test.esp32-idf.yaml index 811f6b72a6..64baa4ec9d 100644 --- a/tests/components/bl0940/test.esp32-idf.yaml +++ b/tests/components/bl0940/test.esp32-idf.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO12 rx_pin: GPIO14 +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/bl0940/test.esp8266-ard.yaml b/tests/components/bl0940/test.esp8266-ard.yaml index 3b44f9c9c3..89ca3ab5ae 100644 --- a/tests/components/bl0940/test.esp8266-ard.yaml +++ b/tests/components/bl0940/test.esp8266-ard.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO1 rx_pin: GPIO3 +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/bl0940/test.rp2040-ard.yaml b/tests/components/bl0940/test.rp2040-ard.yaml index b516342f3b..b28f2b5e05 100644 --- a/tests/components/bl0940/test.rp2040-ard.yaml +++ b/tests/components/bl0940/test.rp2040-ard.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO4 rx_pin: GPIO5 +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/bl0942/common.yaml b/tests/components/bl0942/common.yaml index 32da24885f..1aaab8bb86 100644 --- a/tests/components/bl0942/common.yaml +++ b/tests/components/bl0942/common.yaml @@ -1,9 +1,3 @@ -uart: - - id: uart_bl0939 - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 9600 - sensor: - platform: bl0942 reset: true diff --git a/tests/components/bl0942/test.bk72xx-ard.yaml b/tests/components/bl0942/test.bk72xx-ard.yaml index 96e13c83a9..0caf71ba1f 100644 --- a/tests/components/bl0942/test.bk72xx-ard.yaml +++ b/tests/components/bl0942/test.bk72xx-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - tx_pin: TX1 - rx_pin: RX1 +packages: + uart: !include ../../test_build_components/common/uart/bk72xx-ard.yaml <<: !include common.yaml diff --git a/tests/components/bl0942/test.esp32-ard.yaml b/tests/components/bl0942/test.esp32-ard.yaml deleted file mode 100644 index 811f6b72a6..0000000000 --- a/tests/components/bl0942/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO12 - rx_pin: GPIO14 - -<<: !include common.yaml diff --git a/tests/components/bl0942/test.esp32-c3-ard.yaml b/tests/components/bl0942/test.esp32-c3-ard.yaml deleted file mode 100644 index c79d14c740..0000000000 --- a/tests/components/bl0942/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO7 - rx_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/bl0942/test.esp32-c3-idf.yaml b/tests/components/bl0942/test.esp32-c3-idf.yaml deleted file mode 100644 index c79d14c740..0000000000 --- a/tests/components/bl0942/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO7 - rx_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/bl0942/test.esp32-idf.yaml b/tests/components/bl0942/test.esp32-idf.yaml index 811f6b72a6..64baa4ec9d 100644 --- a/tests/components/bl0942/test.esp32-idf.yaml +++ b/tests/components/bl0942/test.esp32-idf.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO12 rx_pin: GPIO14 +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/bl0942/test.esp8266-ard.yaml b/tests/components/bl0942/test.esp8266-ard.yaml index 3b44f9c9c3..89ca3ab5ae 100644 --- a/tests/components/bl0942/test.esp8266-ard.yaml +++ b/tests/components/bl0942/test.esp8266-ard.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO1 rx_pin: GPIO3 +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/bl0942/test.rp2040-ard.yaml b/tests/components/bl0942/test.rp2040-ard.yaml index b516342f3b..b28f2b5e05 100644 --- a/tests/components/bl0942/test.rp2040-ard.yaml +++ b/tests/components/bl0942/test.rp2040-ard.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO4 rx_pin: GPIO5 +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/ble_client/common.yaml b/tests/components/ble_client/common.yaml index b5272d01f0..4ea1dd60f3 100644 --- a/tests/components/ble_client/common.yaml +++ b/tests/components/ble_client/common.yaml @@ -3,3 +3,74 @@ esp32_ble_tracker: ble_client: - mac_address: 01:02:03:04:05:06 id: test_blec + on_connect: + - ble_client.ble_write: + id: test_blec + service_uuid: "abcd1234-abcd-1234-abcd-abcd12345678" + characteristic_uuid: "abcd1235-abcd-1234-abcd-abcd12345678" + value: !lambda |- + return std::vector{0x01, 0x02, 0x03}; + - ble_client.ble_write: + id: test_blec + service_uuid: "abcd1234-abcd-1234-abcd-abcd12345678" + characteristic_uuid: "abcd1235-abcd-1234-abcd-abcd12345678" + value: [0x04, 0x05, 0x06] + on_passkey_request: + - ble_client.passkey_reply: + id: test_blec + passkey: !lambda |- + return 123456; + - ble_client.passkey_reply: + id: test_blec + passkey: 654321 + on_numeric_comparison_request: + - ble_client.numeric_comparison_reply: + id: test_blec + accept: !lambda |- + return true; + - ble_client.numeric_comparison_reply: + id: test_blec + accept: false + +sensor: + - platform: ble_client + ble_client_id: test_blec + type: characteristic + id: test_sensor_lambda + name: "BLE Sensor with Lambda" + service_uuid: "abcd1234-abcd-1234-abcd-abcd12345678" + characteristic_uuid: "abcd1236-abcd-1234-abcd-abcd12345678" + lambda: |- + if (x.size() >= 2) { + return (float)(x[0] | (x[1] << 8)) / 100.0; + } + return NAN; + - platform: ble_client + ble_client_id: test_blec + type: characteristic + id: test_sensor_no_lambda + name: "BLE Sensor without Lambda" + service_uuid: "abcd1234-abcd-1234-abcd-abcd12345678" + characteristic_uuid: "abcd1237-abcd-1234-abcd-abcd12345678" + +number: + - platform: template + name: "Test Number" + id: test_number + optimistic: true + min_value: 0 + max_value: 255 + step: 1 + +button: + # Test ble_write with lambda that references a component (function pointer) + - platform: template + name: "BLE Write Lambda Test" + on_press: + - ble_client.ble_write: + id: test_blec + service_uuid: "abcd1234-abcd-1234-abcd-abcd12345678" + characteristic_uuid: "abcd1235-abcd-1234-abcd-abcd12345678" + value: !lambda |- + uint8_t val = (uint8_t)id(test_number).state; + return std::vector{0xAA, val, 0xBB}; diff --git a/tests/components/ble_client/test.esp32-idf.yaml b/tests/components/ble_client/test.esp32-idf.yaml index dade44d145..7a6541ae76 100644 --- a/tests/components/ble_client/test.esp32-idf.yaml +++ b/tests/components/ble_client/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/ble_nus/test.nrf52-adafruit.yaml b/tests/components/ble_nus/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..20eec16956 --- /dev/null +++ b/tests/components/ble_nus/test.nrf52-adafruit.yaml @@ -0,0 +1,2 @@ +ble_nus: + type: logs diff --git a/tests/components/ble_nus/test.nrf52-mcumgr.yaml b/tests/components/ble_nus/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..20eec16956 --- /dev/null +++ b/tests/components/ble_nus/test.nrf52-mcumgr.yaml @@ -0,0 +1,2 @@ +ble_nus: + type: logs diff --git a/tests/components/ble_presence/test.esp32-idf.yaml b/tests/components/ble_presence/test.esp32-idf.yaml index dade44d145..7a6541ae76 100644 --- a/tests/components/ble_presence/test.esp32-idf.yaml +++ b/tests/components/ble_presence/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/ble_rssi/test.esp32-idf.yaml b/tests/components/ble_rssi/test.esp32-idf.yaml index dade44d145..7a6541ae76 100644 --- a/tests/components/ble_rssi/test.esp32-idf.yaml +++ b/tests/components/ble_rssi/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/ble_scanner/test.esp32-idf.yaml b/tests/components/ble_scanner/test.esp32-idf.yaml index dade44d145..7a6541ae76 100644 --- a/tests/components/ble_scanner/test.esp32-idf.yaml +++ b/tests/components/ble_scanner/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/bluetooth_proxy/test.esp32-p4-idf.yaml b/tests/components/bluetooth_proxy/test.esp32-p4-idf.yaml new file mode 100644 index 0000000000..edb18c1ada --- /dev/null +++ b/tests/components/bluetooth_proxy/test.esp32-p4-idf.yaml @@ -0,0 +1,19 @@ +<<: !include common.yaml + +esp32_ble_tracker: + max_connections: 9 + +bluetooth_proxy: + active: true + connection_slots: 9 + +esp32_hosted: + active_high: true + variant: ESP32C6 + reset_pin: GPIO54 + cmd_pin: GPIO19 + clk_pin: GPIO18 + d0_pin: GPIO14 + d1_pin: GPIO15 + d2_pin: GPIO16 + d3_pin: GPIO17 diff --git a/tests/components/bluetooth_proxy/test.esp32-s3-ard.yaml b/tests/components/bluetooth_proxy/test.esp32-s3-ard.yaml deleted file mode 100644 index bf01b65b6f..0000000000 --- a/tests/components/bluetooth_proxy/test.esp32-s3-ard.yaml +++ /dev/null @@ -1,8 +0,0 @@ -<<: !include common.yaml - -esp32_ble_tracker: - max_connections: 3 - -bluetooth_proxy: - active: true - connection_slots: 2 diff --git a/tests/components/bm8563/common.yaml b/tests/components/bm8563/common.yaml new file mode 100644 index 0000000000..ec3fdd1518 --- /dev/null +++ b/tests/components/bm8563/common.yaml @@ -0,0 +1,10 @@ +esphome: + on_boot: + - bm8563.read_time + - bm8563.write_time + - bm8563.start_timer: + duration: 300s + +time: + - platform: bm8563 + i2c_id: i2c_bus diff --git a/tests/components/bm8563/test.esp32-ard.yaml b/tests/components/bm8563/test.esp32-ard.yaml new file mode 100644 index 0000000000..7c503b0ccb --- /dev/null +++ b/tests/components/bm8563/test.esp32-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/bm8563/test.esp32-idf.yaml b/tests/components/bm8563/test.esp32-idf.yaml new file mode 100644 index 0000000000..b47e39c389 --- /dev/null +++ b/tests/components/bm8563/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/bm8563/test.esp8266-ard.yaml b/tests/components/bm8563/test.esp8266-ard.yaml new file mode 100644 index 0000000000..4a98b9388a --- /dev/null +++ b/tests/components/bm8563/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/bm8563/test.rp2040-ard.yaml b/tests/components/bm8563/test.rp2040-ard.yaml new file mode 100644 index 0000000000..319a7c71a6 --- /dev/null +++ b/tests/components/bm8563/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/bme280_i2c/common.yaml b/tests/components/bme280_i2c/common.yaml index e74ce9bf6d..a31a6f9a6c 100644 --- a/tests/components/bme280_i2c/common.yaml +++ b/tests/components/bme280_i2c/common.yaml @@ -1,19 +1,14 @@ -i2c: - - id: i2c_bme280 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: bme280_i2c - i2c_id: i2c_bme280 + i2c_id: i2c_bus address: 0x76 temperature: - id: bme280_temperature + id: bme280_i2c_temperature name: BME280 Temperature humidity: - id: bme280_humidity + id: bme280_i2c_humidity name: BME280 Humidity pressure: - id: bme280_pressure + id: bme280_i2c_pressure name: BME280 Pressure update_interval: 15s diff --git a/tests/components/bme280_i2c/test.esp32-ard.yaml b/tests/components/bme280_i2c/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/bme280_i2c/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/bme280_i2c/test.esp32-c3-ard.yaml b/tests/components/bme280_i2c/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/bme280_i2c/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/bme280_i2c/test.esp32-c3-idf.yaml b/tests/components/bme280_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/bme280_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/bme280_i2c/test.esp32-idf.yaml b/tests/components/bme280_i2c/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/bme280_i2c/test.esp32-idf.yaml +++ b/tests/components/bme280_i2c/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/bme280_i2c/test.esp8266-ard.yaml b/tests/components/bme280_i2c/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/bme280_i2c/test.esp8266-ard.yaml +++ b/tests/components/bme280_i2c/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/bme280_i2c/test.rp2040-ard.yaml b/tests/components/bme280_i2c/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/bme280_i2c/test.rp2040-ard.yaml +++ b/tests/components/bme280_i2c/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/bme280_spi/common.yaml b/tests/components/bme280_spi/common.yaml index 303ecf9f73..d97b475f0e 100644 --- a/tests/components/bme280_spi/common.yaml +++ b/tests/components/bme280_spi/common.yaml @@ -1,20 +1,13 @@ -spi: - - id: spi_bme280 - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - miso_pin: ${miso_pin} - sensor: - platform: bme280_spi - spi_id: spi_bme280 cs_pin: ${cs_pin} temperature: - id: bme280_temperature + id: bme280_spi_temperature name: BME280 Temperature humidity: - id: bme280_humidity + id: bme280_spi_humidity name: BME280 Humidity pressure: - id: bme280_pressure + id: bme280_spi_pressure name: BME280 Pressure update_interval: 15s diff --git a/tests/components/bme280_spi/test.esp32-ard.yaml b/tests/components/bme280_spi/test.esp32-ard.yaml deleted file mode 100644 index 54e027a614..0000000000 --- a/tests/components/bme280_spi/test.esp32-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 - cs_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/bme280_spi/test.esp32-c3-ard.yaml b/tests/components/bme280_spi/test.esp32-c3-ard.yaml deleted file mode 100644 index 2415ba5dc6..0000000000 --- a/tests/components/bme280_spi/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/bme280_spi/test.esp32-c3-idf.yaml b/tests/components/bme280_spi/test.esp32-c3-idf.yaml deleted file mode 100644 index 2415ba5dc6..0000000000 --- a/tests/components/bme280_spi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/bme280_spi/test.esp32-idf.yaml b/tests/components/bme280_spi/test.esp32-idf.yaml index 54e027a614..a3352cf880 100644 --- a/tests/components/bme280_spi/test.esp32-idf.yaml +++ b/tests/components/bme280_spi/test.esp32-idf.yaml @@ -1,7 +1,7 @@ substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 cs_pin: GPIO5 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/bme280_spi/test.esp8266-ard.yaml b/tests/components/bme280_spi/test.esp8266-ard.yaml index dbd158d030..b4673ba8b7 100644 --- a/tests/components/bme280_spi/test.esp8266-ard.yaml +++ b/tests/components/bme280_spi/test.esp8266-ard.yaml @@ -1,7 +1,10 @@ substitutions: - clk_pin: GPIO14 - mosi_pin: GPIO13 - miso_pin: GPIO12 + clk_pin: GPIO0 + mosi_pin: GPIO2 + miso_pin: GPIO16 cs_pin: GPIO15 +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/bme280_spi/test.rp2040-ard.yaml b/tests/components/bme280_spi/test.rp2040-ard.yaml index f6c3f1eeca..1ded24de1c 100644 --- a/tests/components/bme280_spi/test.rp2040-ard.yaml +++ b/tests/components/bme280_spi/test.rp2040-ard.yaml @@ -4,4 +4,7 @@ substitutions: miso_pin: GPIO4 cs_pin: GPIO5 +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/bme680/common.yaml b/tests/components/bme680/common.yaml index 13a42488f2..d5a7267060 100644 --- a/tests/components/bme680/common.yaml +++ b/tests/components/bme680/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_bme680 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: bme680 + i2c_id: i2c_bus temperature: name: BME680 Temperature oversampling: 16x diff --git a/tests/components/bme680/test.esp32-ard.yaml b/tests/components/bme680/test.esp32-ard.yaml deleted file mode 100644 index 3b761d3fc1..0000000000 --- a/tests/components/bme680/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO22 - sda_pin: GPIO21 - -<<: !include common.yaml diff --git a/tests/components/bme680/test.esp32-c3-ard.yaml b/tests/components/bme680/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/bme680/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/bme680/test.esp32-c3-idf.yaml b/tests/components/bme680/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/bme680/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/bme680/test.esp32-idf.yaml b/tests/components/bme680/test.esp32-idf.yaml index 3b761d3fc1..b47e39c389 100644 --- a/tests/components/bme680/test.esp32-idf.yaml +++ b/tests/components/bme680/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO22 - sda_pin: GPIO21 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/bme680/test.esp8266-ard.yaml b/tests/components/bme680/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/bme680/test.esp8266-ard.yaml +++ b/tests/components/bme680/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/bme680/test.rp2040-ard.yaml b/tests/components/bme680/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/bme680/test.rp2040-ard.yaml +++ b/tests/components/bme680/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/bme680_bsec/common.yaml b/tests/components/bme680_bsec/common.yaml index 7d2e9e210b..1a78ab2ae0 100644 --- a/tests/components/bme680_bsec/common.yaml +++ b/tests/components/bme680_bsec/common.yaml @@ -1,9 +1,5 @@ -i2c: - - id: i2c_bme680 - scl: ${scl_pin} - sda: ${sda_pin} - bme680_bsec: + i2c_id: i2c_bus address: 0x77 sensor: diff --git a/tests/components/bme680_bsec/test.esp32-ard.yaml b/tests/components/bme680_bsec/test.esp32-ard.yaml index 3b761d3fc1..7c503b0ccb 100644 --- a/tests/components/bme680_bsec/test.esp32-ard.yaml +++ b/tests/components/bme680_bsec/test.esp32-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO22 - sda_pin: GPIO21 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-ard.yaml <<: !include common.yaml diff --git a/tests/components/bme680_bsec/test.esp8266-ard.yaml b/tests/components/bme680_bsec/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/bme680_bsec/test.esp8266-ard.yaml +++ b/tests/components/bme680_bsec/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/bme68x_bsec2_i2c/common.yaml b/tests/components/bme68x_bsec2_i2c/common.yaml index b8a16ee7bb..bee964f433 100644 --- a/tests/components/bme68x_bsec2_i2c/common.yaml +++ b/tests/components/bme68x_bsec2_i2c/common.yaml @@ -1,9 +1,5 @@ -i2c: - - id: i2c_bme68x - scl: ${scl_pin} - sda: ${sda_pin} - bme68x_bsec2_i2c: + i2c_id: i2c_bus address: 0x76 model: bme688 algorithm_output: classification diff --git a/tests/components/bme68x_bsec2_i2c/test.esp32-ard.yaml b/tests/components/bme68x_bsec2_i2c/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/bme68x_bsec2_i2c/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/bme68x_bsec2_i2c/test.esp32-c3-ard.yaml b/tests/components/bme68x_bsec2_i2c/test.esp32-c3-ard.yaml deleted file mode 100644 index 84a9dd4bb4..0000000000 --- a/tests/components/bme68x_bsec2_i2c/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO6 - sda_pin: GPIO7 - -<<: !include common.yaml diff --git a/tests/components/bme68x_bsec2_i2c/test.esp32-c3-idf.yaml b/tests/components/bme68x_bsec2_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index 84a9dd4bb4..0000000000 --- a/tests/components/bme68x_bsec2_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO6 - sda_pin: GPIO7 - -<<: !include common.yaml diff --git a/tests/components/bme68x_bsec2_i2c/test.esp32-idf.yaml b/tests/components/bme68x_bsec2_i2c/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/bme68x_bsec2_i2c/test.esp32-idf.yaml +++ b/tests/components/bme68x_bsec2_i2c/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/bme68x_bsec2_i2c/test.esp32-s2-ard.yaml b/tests/components/bme68x_bsec2_i2c/test.esp32-s2-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/bme68x_bsec2_i2c/test.esp32-s2-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/bme68x_bsec2_i2c/test.esp32-s2-idf.yaml b/tests/components/bme68x_bsec2_i2c/test.esp32-s2-idf.yaml index 63c3bd6afd..54f59a59fc 100644 --- a/tests/components/bme68x_bsec2_i2c/test.esp32-s2-idf.yaml +++ b/tests/components/bme68x_bsec2_i2c/test.esp32-s2-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-s2-idf.yaml <<: !include common.yaml diff --git a/tests/components/bme68x_bsec2_i2c/test.esp32-s3-ard.yaml b/tests/components/bme68x_bsec2_i2c/test.esp32-s3-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/bme68x_bsec2_i2c/test.esp32-s3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/bme68x_bsec2_i2c/test.esp32-s3-idf.yaml b/tests/components/bme68x_bsec2_i2c/test.esp32-s3-idf.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/bme68x_bsec2_i2c/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/bme68x_bsec2_i2c/test.esp8266-ard.yaml b/tests/components/bme68x_bsec2_i2c/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/bme68x_bsec2_i2c/test.esp8266-ard.yaml +++ b/tests/components/bme68x_bsec2_i2c/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/bme68x_bsec2_i2c/test.rp2040-ard.yaml b/tests/components/bme68x_bsec2_i2c/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/bme68x_bsec2_i2c/test.rp2040-ard.yaml +++ b/tests/components/bme68x_bsec2_i2c/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/bmi160/common.yaml b/tests/components/bmi160/common.yaml index 6aa9aa6ed0..7375732db2 100644 --- a/tests/components/bmi160/common.yaml +++ b/tests/components/bmi160/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_bmi160 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: bmi160 + i2c_id: i2c_bus address: 0x68 acceleration_x: name: BMI160 Accel X diff --git a/tests/components/bmi160/test.esp32-ard.yaml b/tests/components/bmi160/test.esp32-ard.yaml deleted file mode 100644 index 3b761d3fc1..0000000000 --- a/tests/components/bmi160/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO22 - sda_pin: GPIO21 - -<<: !include common.yaml diff --git a/tests/components/bmi160/test.esp32-c3-ard.yaml b/tests/components/bmi160/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/bmi160/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/bmi160/test.esp32-c3-idf.yaml b/tests/components/bmi160/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/bmi160/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/bmi160/test.esp32-idf.yaml b/tests/components/bmi160/test.esp32-idf.yaml index 3b761d3fc1..b47e39c389 100644 --- a/tests/components/bmi160/test.esp32-idf.yaml +++ b/tests/components/bmi160/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO22 - sda_pin: GPIO21 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/bmi160/test.esp8266-ard.yaml b/tests/components/bmi160/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/bmi160/test.esp8266-ard.yaml +++ b/tests/components/bmi160/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/bmi160/test.rp2040-ard.yaml b/tests/components/bmi160/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/bmi160/test.rp2040-ard.yaml +++ b/tests/components/bmi160/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/bmp085/common.yaml b/tests/components/bmp085/common.yaml index 219bc51fbb..ad358f4409 100644 --- a/tests/components/bmp085/common.yaml +++ b/tests/components/bmp085/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_bmp085 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: bmp085 + i2c_id: i2c_bus temperature: name: Outside Temperature pressure: diff --git a/tests/components/bmp085/test.esp32-ard.yaml b/tests/components/bmp085/test.esp32-ard.yaml deleted file mode 100644 index 3b761d3fc1..0000000000 --- a/tests/components/bmp085/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO22 - sda_pin: GPIO21 - -<<: !include common.yaml diff --git a/tests/components/bmp085/test.esp32-c3-ard.yaml b/tests/components/bmp085/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/bmp085/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/bmp085/test.esp32-c3-idf.yaml b/tests/components/bmp085/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/bmp085/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/bmp085/test.esp32-idf.yaml b/tests/components/bmp085/test.esp32-idf.yaml index 3b761d3fc1..b47e39c389 100644 --- a/tests/components/bmp085/test.esp32-idf.yaml +++ b/tests/components/bmp085/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO22 - sda_pin: GPIO21 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/bmp085/test.esp8266-ard.yaml b/tests/components/bmp085/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/bmp085/test.esp8266-ard.yaml +++ b/tests/components/bmp085/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/bmp085/test.rp2040-ard.yaml b/tests/components/bmp085/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/bmp085/test.rp2040-ard.yaml +++ b/tests/components/bmp085/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/bmp280_i2c/common.yaml b/tests/components/bmp280_i2c/common.yaml index edf52b2cd4..77a9db7fc5 100644 --- a/tests/components/bmp280_i2c/common.yaml +++ b/tests/components/bmp280_i2c/common.yaml @@ -1,17 +1,12 @@ -i2c: - - id: i2c_bmp280 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: bmp280_i2c - i2c_id: i2c_bmp280 + i2c_id: i2c_bus address: 0x77 temperature: - id: bmp280_temperature + id: bmp280_i2c_temperature name: Outside Temperature pressure: name: Outside Pressure - id: bmp280_pressure + id: bmp280_i2c_pressure iir_filter: 16x update_interval: 15s diff --git a/tests/components/bmp280_i2c/test.esp32-ard.yaml b/tests/components/bmp280_i2c/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/bmp280_i2c/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/bmp280_i2c/test.esp32-c3-ard.yaml b/tests/components/bmp280_i2c/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/bmp280_i2c/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/bmp280_i2c/test.esp32-c3-idf.yaml b/tests/components/bmp280_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/bmp280_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/bmp280_i2c/test.esp32-idf.yaml b/tests/components/bmp280_i2c/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/bmp280_i2c/test.esp32-idf.yaml +++ b/tests/components/bmp280_i2c/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/bmp280_i2c/test.esp8266-ard.yaml b/tests/components/bmp280_i2c/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/bmp280_i2c/test.esp8266-ard.yaml +++ b/tests/components/bmp280_i2c/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/bmp280_i2c/test.rp2040-ard.yaml b/tests/components/bmp280_i2c/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/bmp280_i2c/test.rp2040-ard.yaml +++ b/tests/components/bmp280_i2c/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/bmp280_spi/common.yaml b/tests/components/bmp280_spi/common.yaml index 798804de5b..1be54f6b74 100644 --- a/tests/components/bmp280_spi/common.yaml +++ b/tests/components/bmp280_spi/common.yaml @@ -1,18 +1,11 @@ -spi: - - id: spi_bmp280 - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - miso_pin: ${miso_pin} - sensor: - platform: bmp280_spi - spi_id: spi_bmp280 cs_pin: ${cs_pin} temperature: - id: bmp280_temperature + id: bmp280_spi_temperature name: Outside Temperature pressure: name: Outside Pressure - id: bmp280_pressure + id: bmp280_spi_pressure iir_filter: 16x update_interval: 15s diff --git a/tests/components/bmp280_spi/test.esp32-ard.yaml b/tests/components/bmp280_spi/test.esp32-ard.yaml deleted file mode 100644 index 54e027a614..0000000000 --- a/tests/components/bmp280_spi/test.esp32-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 - cs_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/bmp280_spi/test.esp32-c3-ard.yaml b/tests/components/bmp280_spi/test.esp32-c3-ard.yaml deleted file mode 100644 index 2415ba5dc6..0000000000 --- a/tests/components/bmp280_spi/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/bmp280_spi/test.esp32-c3-idf.yaml b/tests/components/bmp280_spi/test.esp32-c3-idf.yaml deleted file mode 100644 index 2415ba5dc6..0000000000 --- a/tests/components/bmp280_spi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/bmp280_spi/test.esp32-idf.yaml b/tests/components/bmp280_spi/test.esp32-idf.yaml index 54e027a614..a3352cf880 100644 --- a/tests/components/bmp280_spi/test.esp32-idf.yaml +++ b/tests/components/bmp280_spi/test.esp32-idf.yaml @@ -1,7 +1,7 @@ substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 cs_pin: GPIO5 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/bmp280_spi/test.esp8266-ard.yaml b/tests/components/bmp280_spi/test.esp8266-ard.yaml index dbd158d030..b4673ba8b7 100644 --- a/tests/components/bmp280_spi/test.esp8266-ard.yaml +++ b/tests/components/bmp280_spi/test.esp8266-ard.yaml @@ -1,7 +1,10 @@ substitutions: - clk_pin: GPIO14 - mosi_pin: GPIO13 - miso_pin: GPIO12 + clk_pin: GPIO0 + mosi_pin: GPIO2 + miso_pin: GPIO16 cs_pin: GPIO15 +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/bmp280_spi/test.rp2040-ard.yaml b/tests/components/bmp280_spi/test.rp2040-ard.yaml index f6c3f1eeca..1ded24de1c 100644 --- a/tests/components/bmp280_spi/test.rp2040-ard.yaml +++ b/tests/components/bmp280_spi/test.rp2040-ard.yaml @@ -4,4 +4,7 @@ substitutions: miso_pin: GPIO4 cs_pin: GPIO5 +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/bmp3xx_i2c/common.yaml b/tests/components/bmp3xx_i2c/common.yaml index 6641b7a1b8..e651072f25 100644 --- a/tests/components/bmp3xx_i2c/common.yaml +++ b/tests/components/bmp3xx_i2c/common.yaml @@ -1,15 +1,12 @@ -i2c: - - id: i2c_bmp3xx - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: bmp3xx_i2c - i2c_id: i2c_bmp3xx + i2c_id: i2c_bus address: 0x77 temperature: + id: bmp3xx_i2c_temperature name: BMP Temperature oversampling: 16x pressure: + id: bmp3xx_i2c_pressure name: BMP Pressure iir_filter: 2X diff --git a/tests/components/bmp3xx_i2c/test.esp32-ard.yaml b/tests/components/bmp3xx_i2c/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/bmp3xx_i2c/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/bmp3xx_i2c/test.esp32-c3-ard.yaml b/tests/components/bmp3xx_i2c/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/bmp3xx_i2c/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/bmp3xx_i2c/test.esp32-c3-idf.yaml b/tests/components/bmp3xx_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/bmp3xx_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/bmp3xx_i2c/test.esp32-idf.yaml b/tests/components/bmp3xx_i2c/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/bmp3xx_i2c/test.esp32-idf.yaml +++ b/tests/components/bmp3xx_i2c/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/bmp3xx_i2c/test.esp8266-ard.yaml b/tests/components/bmp3xx_i2c/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/bmp3xx_i2c/test.esp8266-ard.yaml +++ b/tests/components/bmp3xx_i2c/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/bmp3xx_i2c/test.rp2040-ard.yaml b/tests/components/bmp3xx_i2c/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/bmp3xx_i2c/test.rp2040-ard.yaml +++ b/tests/components/bmp3xx_i2c/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/bmp3xx_spi/common.yaml b/tests/components/bmp3xx_spi/common.yaml index 8d5f897661..b59d46c967 100644 --- a/tests/components/bmp3xx_spi/common.yaml +++ b/tests/components/bmp3xx_spi/common.yaml @@ -1,16 +1,11 @@ -spi: - - id: spi_bmp3xx - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - miso_pin: ${miso_pin} - sensor: - platform: bmp3xx_spi - spi_id: spi_bmp3xx cs_pin: ${cs_pin} temperature: + id: bmp3xx_spi_temperature name: BMP Temperature oversampling: 16x pressure: + id: bmp3xx_spi_pressure name: BMP Pressure iir_filter: 2X diff --git a/tests/components/bmp3xx_spi/test.esp32-ard.yaml b/tests/components/bmp3xx_spi/test.esp32-ard.yaml deleted file mode 100644 index 54e027a614..0000000000 --- a/tests/components/bmp3xx_spi/test.esp32-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 - cs_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/bmp3xx_spi/test.esp32-c3-ard.yaml b/tests/components/bmp3xx_spi/test.esp32-c3-ard.yaml deleted file mode 100644 index 2415ba5dc6..0000000000 --- a/tests/components/bmp3xx_spi/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/bmp3xx_spi/test.esp32-c3-idf.yaml b/tests/components/bmp3xx_spi/test.esp32-c3-idf.yaml deleted file mode 100644 index 2415ba5dc6..0000000000 --- a/tests/components/bmp3xx_spi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/bmp3xx_spi/test.esp32-idf.yaml b/tests/components/bmp3xx_spi/test.esp32-idf.yaml index 54e027a614..a3352cf880 100644 --- a/tests/components/bmp3xx_spi/test.esp32-idf.yaml +++ b/tests/components/bmp3xx_spi/test.esp32-idf.yaml @@ -1,7 +1,7 @@ substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 cs_pin: GPIO5 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/bmp3xx_spi/test.esp8266-ard.yaml b/tests/components/bmp3xx_spi/test.esp8266-ard.yaml index dbd158d030..b4673ba8b7 100644 --- a/tests/components/bmp3xx_spi/test.esp8266-ard.yaml +++ b/tests/components/bmp3xx_spi/test.esp8266-ard.yaml @@ -1,7 +1,10 @@ substitutions: - clk_pin: GPIO14 - mosi_pin: GPIO13 - miso_pin: GPIO12 + clk_pin: GPIO0 + mosi_pin: GPIO2 + miso_pin: GPIO16 cs_pin: GPIO15 +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/bmp3xx_spi/test.rp2040-ard.yaml b/tests/components/bmp3xx_spi/test.rp2040-ard.yaml index f6c3f1eeca..1ded24de1c 100644 --- a/tests/components/bmp3xx_spi/test.rp2040-ard.yaml +++ b/tests/components/bmp3xx_spi/test.rp2040-ard.yaml @@ -4,4 +4,7 @@ substitutions: miso_pin: GPIO4 cs_pin: GPIO5 +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/bmp581/common.yaml b/tests/components/bmp581/common.yaml index 71ad4bfb1a..250b1f5857 100644 --- a/tests/components/bmp581/common.yaml +++ b/tests/components/bmp581/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_bmp581 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: bmp581 + i2c_id: i2c_bus temperature: name: BMP581 Temperature iir_filter: 2x diff --git a/tests/components/bmp581/test.esp32-ard.yaml b/tests/components/bmp581/test.esp32-ard.yaml deleted file mode 100644 index 3b761d3fc1..0000000000 --- a/tests/components/bmp581/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO22 - sda_pin: GPIO21 - -<<: !include common.yaml diff --git a/tests/components/bmp581/test.esp32-c3-ard.yaml b/tests/components/bmp581/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/bmp581/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/bmp581/test.esp32-c3-idf.yaml b/tests/components/bmp581/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/bmp581/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/bmp581/test.esp32-idf.yaml b/tests/components/bmp581/test.esp32-idf.yaml index 3b761d3fc1..b47e39c389 100644 --- a/tests/components/bmp581/test.esp32-idf.yaml +++ b/tests/components/bmp581/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO22 - sda_pin: GPIO21 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/bmp581/test.esp8266-ard.yaml b/tests/components/bmp581/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/bmp581/test.esp8266-ard.yaml +++ b/tests/components/bmp581/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/bmp581/test.rp2040-ard.yaml b/tests/components/bmp581/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/bmp581/test.rp2040-ard.yaml +++ b/tests/components/bmp581/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/bp1658cj/test.esp32-ard.yaml b/tests/components/bp1658cj/test.esp32-ard.yaml deleted file mode 100644 index d295973e3f..0000000000 --- a/tests/components/bp1658cj/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - clock_pin: GPIO16 - data_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/bp1658cj/test.esp32-c3-ard.yaml b/tests/components/bp1658cj/test.esp32-c3-ard.yaml deleted file mode 100644 index 7808481215..0000000000 --- a/tests/components/bp1658cj/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - clock_pin: GPIO5 - data_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/bp1658cj/test.esp32-c3-idf.yaml b/tests/components/bp1658cj/test.esp32-c3-idf.yaml deleted file mode 100644 index 7808481215..0000000000 --- a/tests/components/bp1658cj/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - clock_pin: GPIO5 - data_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/bp1658cj/test.esp32-idf.yaml b/tests/components/bp1658cj/test.esp32-idf.yaml index d295973e3f..a4ecdb6c49 100644 --- a/tests/components/bp1658cj/test.esp32-idf.yaml +++ b/tests/components/bp1658cj/test.esp32-idf.yaml @@ -1,5 +1,5 @@ substitutions: - clock_pin: GPIO16 - data_pin: GPIO17 + clock_pin: GPIO4 + data_pin: GPIO5 <<: !include common.yaml diff --git a/tests/components/bp1658cj/test.esp8266-ard.yaml b/tests/components/bp1658cj/test.esp8266-ard.yaml index 7808481215..7c7f1e1a11 100644 --- a/tests/components/bp1658cj/test.esp8266-ard.yaml +++ b/tests/components/bp1658cj/test.esp8266-ard.yaml @@ -1,5 +1,5 @@ substitutions: - clock_pin: GPIO5 - data_pin: GPIO4 + clock_pin: GPIO0 + data_pin: GPIO2 <<: !include common.yaml diff --git a/tests/components/bp5758d/test.esp32-ard.yaml b/tests/components/bp5758d/test.esp32-ard.yaml deleted file mode 100644 index d295973e3f..0000000000 --- a/tests/components/bp5758d/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - clock_pin: GPIO16 - data_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/bp5758d/test.esp32-c3-ard.yaml b/tests/components/bp5758d/test.esp32-c3-ard.yaml deleted file mode 100644 index 7808481215..0000000000 --- a/tests/components/bp5758d/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - clock_pin: GPIO5 - data_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/bp5758d/test.esp32-c3-idf.yaml b/tests/components/bp5758d/test.esp32-c3-idf.yaml deleted file mode 100644 index 7808481215..0000000000 --- a/tests/components/bp5758d/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - clock_pin: GPIO5 - data_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/bp5758d/test.esp32-idf.yaml b/tests/components/bp5758d/test.esp32-idf.yaml index d295973e3f..a4ecdb6c49 100644 --- a/tests/components/bp5758d/test.esp32-idf.yaml +++ b/tests/components/bp5758d/test.esp32-idf.yaml @@ -1,5 +1,5 @@ substitutions: - clock_pin: GPIO16 - data_pin: GPIO17 + clock_pin: GPIO4 + data_pin: GPIO5 <<: !include common.yaml diff --git a/tests/components/bp5758d/test.esp8266-ard.yaml b/tests/components/bp5758d/test.esp8266-ard.yaml index 7808481215..7c7f1e1a11 100644 --- a/tests/components/bp5758d/test.esp8266-ard.yaml +++ b/tests/components/bp5758d/test.esp8266-ard.yaml @@ -1,5 +1,5 @@ substitutions: - clock_pin: GPIO5 - data_pin: GPIO4 + clock_pin: GPIO0 + data_pin: GPIO2 <<: !include common.yaml diff --git a/tests/components/airthings_wave_plus/test.esp32-c3-idf.yaml b/tests/components/button/test.nrf52-adafruit.yaml similarity index 100% rename from tests/components/airthings_wave_plus/test.esp32-c3-idf.yaml rename to tests/components/button/test.nrf52-adafruit.yaml diff --git a/tests/components/alarm_control_panel/test.esp32-ard.yaml b/tests/components/button/test.nrf52-mcumgr.yaml similarity index 100% rename from tests/components/alarm_control_panel/test.esp32-ard.yaml rename to tests/components/button/test.nrf52-mcumgr.yaml diff --git a/tests/components/bytebuffer/test.esp32-ard.yaml b/tests/components/bytebuffer/test.esp32-ard.yaml deleted file mode 100644 index 380ca87628..0000000000 --- a/tests/components/bytebuffer/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -!include common.yaml diff --git a/tests/components/bytebuffer/test.esp32-c3-ard.yaml b/tests/components/bytebuffer/test.esp32-c3-ard.yaml deleted file mode 100644 index 380ca87628..0000000000 --- a/tests/components/bytebuffer/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -!include common.yaml diff --git a/tests/components/bytebuffer/test.esp32-c3-idf.yaml b/tests/components/bytebuffer/test.esp32-c3-idf.yaml deleted file mode 100644 index 380ca87628..0000000000 --- a/tests/components/bytebuffer/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -!include common.yaml diff --git a/tests/components/camera/common.yaml b/tests/components/camera/common.yaml index 3daf1e8565..76cca4cf94 100644 --- a/tests/components/camera/common.yaml +++ b/tests/components/camera/common.yaml @@ -1,18 +1,2 @@ -esphome: - includes: - - ../../../esphome/components/camera/ - -script: - - id: interface_compile_check - then: - - lambda: |- - using namespace esphome::camera; - class MockCamera : public Camera { - public: - void add_image_callback(std::function)> &&callback) override {} - CameraImageReader *create_image_reader() override { return 0; } - void request_image(CameraRequester requester) override {} - void start_stream(CameraRequester requester) override {} - void stop_stream(CameraRequester requester) override {} - }; - MockCamera* camera = new MockCamera(); +# Camera is a base component auto-loaded by esp32_camera +# The hardware configuration comes from the camera package diff --git a/tests/components/camera/test.esp32-idf.yaml b/tests/components/camera/test.esp32-idf.yaml index dade44d145..ef8f74d4eb 100644 --- a/tests/components/camera/test.esp32-idf.yaml +++ b/tests/components/camera/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + i2c_camera: !include ../../test_build_components/common/i2c_camera/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/camera_encoder/common.yaml b/tests/components/camera_encoder/common.yaml new file mode 100644 index 0000000000..8fd7a8ce47 --- /dev/null +++ b/tests/components/camera_encoder/common.yaml @@ -0,0 +1,5 @@ +camera_encoder: + id: jpeg_encoder + quality: 80 + buffer_size: 4096 + buffer_expand_size: 1024 diff --git a/tests/components/camera_encoder/test.esp32-idf.yaml b/tests/components/camera_encoder/test.esp32-idf.yaml new file mode 100644 index 0000000000..ef8f74d4eb --- /dev/null +++ b/tests/components/camera_encoder/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + i2c_camera: !include ../../test_build_components/common/i2c_camera/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/canbus/common.yaml b/tests/components/canbus/common.yaml index fd146cc3a3..8bddeb7409 100644 --- a/tests/components/canbus/common.yaml +++ b/tests/components/canbus/common.yaml @@ -37,6 +37,15 @@ canbus: break; } +number: + - platform: template + name: "Test Number" + id: test_number + optimistic: true + min_value: 0 + max_value: 255 + step: 1 + button: - platform: template name: Canbus Actions @@ -44,3 +53,7 @@ button: - canbus.send: "abc" - canbus.send: [0, 1, 2] - canbus.send: !lambda return {0, 1, 2}; + # Test canbus.send with lambda that references a component (function pointer) + - canbus.send: !lambda |- + uint8_t val = (uint8_t)id(test_number).state; + return std::vector{0xAA, val, 0xBB}; diff --git a/tests/components/canbus/test.esp32-idf.yaml b/tests/components/canbus/test.esp32-idf.yaml index dade44d145..2d29656c94 100644 --- a/tests/components/canbus/test.esp32-idf.yaml +++ b/tests/components/canbus/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/cap1188/common.yaml b/tests/components/cap1188/common.yaml index e83bf5d5d2..3e4ed972ff 100644 --- a/tests/components/cap1188/common.yaml +++ b/tests/components/cap1188/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_cap1188 - scl: ${scl_pin} - sda: ${sda_pin} - cap1188: id: cap1188_component + i2c_id: i2c_bus address: 0x29 reset_pin: ${reset_pin} touch_threshold: 0x20 diff --git a/tests/components/cap1188/test.esp32-ard.yaml b/tests/components/cap1188/test.esp32-ard.yaml deleted file mode 100644 index 1ca773e06c..0000000000 --- a/tests/components/cap1188/test.esp32-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - reset_pin: GPIO15 - -<<: !include common.yaml diff --git a/tests/components/cap1188/test.esp32-c3-ard.yaml b/tests/components/cap1188/test.esp32-c3-ard.yaml deleted file mode 100644 index 1e6670c196..0000000000 --- a/tests/components/cap1188/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - reset_pin: GPIO6 - -<<: !include common.yaml diff --git a/tests/components/cap1188/test.esp32-c3-idf.yaml b/tests/components/cap1188/test.esp32-c3-idf.yaml deleted file mode 100644 index 1e6670c196..0000000000 --- a/tests/components/cap1188/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - reset_pin: GPIO6 - -<<: !include common.yaml diff --git a/tests/components/cap1188/test.esp32-idf.yaml b/tests/components/cap1188/test.esp32-idf.yaml index 1ca773e06c..4ff2241ec9 100644 --- a/tests/components/cap1188/test.esp32-idf.yaml +++ b/tests/components/cap1188/test.esp32-idf.yaml @@ -1,6 +1,7 @@ substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 reset_pin: GPIO15 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/cap1188/test.esp8266-ard.yaml b/tests/components/cap1188/test.esp8266-ard.yaml index dfdc12a3d1..b8bb94edde 100644 --- a/tests/components/cap1188/test.esp8266-ard.yaml +++ b/tests/components/cap1188/test.esp8266-ard.yaml @@ -1,6 +1,7 @@ substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 reset_pin: GPIO15 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/cap1188/test.rp2040-ard.yaml b/tests/components/cap1188/test.rp2040-ard.yaml index 1e6670c196..1bf10642c5 100644 --- a/tests/components/cap1188/test.rp2040-ard.yaml +++ b/tests/components/cap1188/test.rp2040-ard.yaml @@ -1,6 +1,7 @@ substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 reset_pin: GPIO6 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/captive_portal/test.esp32-c3-idf.yaml b/tests/components/captive_portal/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/captive_portal/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/ccs811/common.yaml b/tests/components/ccs811/common.yaml index a781996c66..0d912fd3ac 100644 --- a/tests/components/ccs811/common.yaml +++ b/tests/components/ccs811/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_ccs811 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: ccs811 + i2c_id: i2c_bus eco2: name: CCS811 eCO2 tvoc: diff --git a/tests/components/ccs811/test.esp32-ard.yaml b/tests/components/ccs811/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/ccs811/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/ccs811/test.esp32-c3-ard.yaml b/tests/components/ccs811/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/ccs811/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ccs811/test.esp32-c3-idf.yaml b/tests/components/ccs811/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/ccs811/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ccs811/test.esp32-idf.yaml b/tests/components/ccs811/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/ccs811/test.esp32-idf.yaml +++ b/tests/components/ccs811/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/ccs811/test.esp8266-ard.yaml b/tests/components/ccs811/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/ccs811/test.esp8266-ard.yaml +++ b/tests/components/ccs811/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/ccs811/test.rp2040-ard.yaml b/tests/components/ccs811/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/ccs811/test.rp2040-ard.yaml +++ b/tests/components/ccs811/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/cd74hc4067/test.esp32-ard.yaml b/tests/components/cd74hc4067/test.esp32-ard.yaml deleted file mode 100644 index c4dd280943..0000000000 --- a/tests/components/cd74hc4067/test.esp32-ard.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - pin_s0: GPIO12 - pin_s1: GPIO13 - pin_s2: GPIO14 - pin_s3: GPIO15 - pin: GPIO39 - -<<: !include common.yaml diff --git a/tests/components/cd74hc4067/test.esp32-c3-ard.yaml b/tests/components/cd74hc4067/test.esp32-c3-ard.yaml deleted file mode 100644 index 5e8784c1fc..0000000000 --- a/tests/components/cd74hc4067/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - pin_s0: GPIO2 - pin_s1: GPIO3 - pin_s2: GPIO4 - pin_s3: GPIO5 - pin: GPIO0 - -<<: !include common.yaml diff --git a/tests/components/cd74hc4067/test.esp32-c3-idf.yaml b/tests/components/cd74hc4067/test.esp32-c3-idf.yaml deleted file mode 100644 index 5e8784c1fc..0000000000 --- a/tests/components/cd74hc4067/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - pin_s0: GPIO2 - pin_s1: GPIO3 - pin_s2: GPIO4 - pin_s3: GPIO5 - pin: GPIO0 - -<<: !include common.yaml diff --git a/tests/components/ch422g/common.yaml b/tests/components/ch422g/common.yaml index d65956ecac..ad3707eee0 100644 --- a/tests/components/ch422g/common.yaml +++ b/tests/components/ch422g/common.yaml @@ -1,5 +1,6 @@ ch422g: - id: ch422g_hub + i2c_id: i2c_bus binary_sensor: - platform: gpio diff --git a/tests/components/ch422g/test.esp32-ard.yaml b/tests/components/ch422g/test.esp32-ard.yaml deleted file mode 100644 index cd3f1bbeef..0000000000 --- a/tests/components/ch422g/test.esp32-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -i2c: - - id: i2c_ch422g - scl: 16 - sda: 17 - -<<: !include common.yaml diff --git a/tests/components/ch422g/test.esp32-c3-ard.yaml b/tests/components/ch422g/test.esp32-c3-ard.yaml deleted file mode 100644 index cd822cb308..0000000000 --- a/tests/components/ch422g/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -i2c: - - id: i2c_ch422g - scl: 5 - sda: 4 - -<<: !include common.yaml diff --git a/tests/components/ch422g/test.esp32-c3-idf.yaml b/tests/components/ch422g/test.esp32-c3-idf.yaml deleted file mode 100644 index cd822cb308..0000000000 --- a/tests/components/ch422g/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -i2c: - - id: i2c_ch422g - scl: 5 - sda: 4 - -<<: !include common.yaml diff --git a/tests/components/ch422g/test.esp32-idf.yaml b/tests/components/ch422g/test.esp32-idf.yaml index cd3f1bbeef..b47e39c389 100644 --- a/tests/components/ch422g/test.esp32-idf.yaml +++ b/tests/components/ch422g/test.esp32-idf.yaml @@ -1,6 +1,4 @@ -i2c: - - id: i2c_ch422g - scl: 16 - sda: 17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/ch422g/test.esp8266-ard.yaml b/tests/components/ch422g/test.esp8266-ard.yaml index cd822cb308..4a98b9388a 100644 --- a/tests/components/ch422g/test.esp8266-ard.yaml +++ b/tests/components/ch422g/test.esp8266-ard.yaml @@ -1,6 +1,4 @@ -i2c: - - id: i2c_ch422g - scl: 5 - sda: 4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/ch422g/test.rp2040-ard.yaml b/tests/components/ch422g/test.rp2040-ard.yaml index cd822cb308..319a7c71a6 100644 --- a/tests/components/ch422g/test.rp2040-ard.yaml +++ b/tests/components/ch422g/test.rp2040-ard.yaml @@ -1,6 +1,4 @@ -i2c: - - id: i2c_ch422g - scl: 5 - sda: 4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/chsc6x/test.esp32-ard.yaml b/tests/components/chsc6x/test.esp32-ard.yaml deleted file mode 100644 index 9bc58b66f6..0000000000 --- a/tests/components/chsc6x/test.esp32-ard.yaml +++ /dev/null @@ -1,25 +0,0 @@ -i2c: - - id: i2c_chsc6x - scl: 3 - sda: 21 - -spi: - clk_pin: 16 - mosi_pin: 17 - -display: - - platform: ili9xxx - id: ili9xxx_display - model: GC9A01A - invert_colors: True - cs_pin: 18 - dc_pin: 19 - pages: - - id: page1 - lambda: |- - it.rectangle(0, 0, it.get_width(), it.get_height()); - -touchscreen: - - platform: chsc6x - display: ili9xxx_display - interrupt_pin: 20 diff --git a/tests/components/chsc6x/test.esp32-c3-ard.yaml b/tests/components/chsc6x/test.esp32-c3-ard.yaml deleted file mode 100644 index b0f55eb2e6..0000000000 --- a/tests/components/chsc6x/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,25 +0,0 @@ -i2c: - - id: i2c_chsc6x - scl: 3 - sda: 9 - -spi: - clk_pin: 5 - mosi_pin: 4 - -display: - - platform: ili9xxx - id: ili9xxx_display - model: GC9A01A - invert_colors: True - cs_pin: 18 - dc_pin: 19 - pages: - - id: page1 - lambda: |- - it.rectangle(0, 0, it.get_width(), it.get_height()); - -touchscreen: - - platform: chsc6x - display: ili9xxx_display - interrupt_pin: 20 diff --git a/tests/components/chsc6x/test.esp32-c3-idf.yaml b/tests/components/chsc6x/test.esp32-c3-idf.yaml deleted file mode 100644 index b0f55eb2e6..0000000000 --- a/tests/components/chsc6x/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,25 +0,0 @@ -i2c: - - id: i2c_chsc6x - scl: 3 - sda: 9 - -spi: - clk_pin: 5 - mosi_pin: 4 - -display: - - platform: ili9xxx - id: ili9xxx_display - model: GC9A01A - invert_colors: True - cs_pin: 18 - dc_pin: 19 - pages: - - id: page1 - lambda: |- - it.rectangle(0, 0, it.get_width(), it.get_height()); - -touchscreen: - - platform: chsc6x - display: ili9xxx_display - interrupt_pin: 20 diff --git a/tests/components/chsc6x/test.esp32-idf.yaml b/tests/components/chsc6x/test.esp32-idf.yaml index 9bc58b66f6..fa7c72150e 100644 --- a/tests/components/chsc6x/test.esp32-idf.yaml +++ b/tests/components/chsc6x/test.esp32-idf.yaml @@ -1,19 +1,15 @@ -i2c: - - id: i2c_chsc6x - scl: 3 - sda: 21 - -spi: - clk_pin: 16 - mosi_pin: 17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml display: - platform: ili9xxx + spi_id: spi_bus id: ili9xxx_display model: GC9A01A invert_colors: True - cs_pin: 18 - dc_pin: 19 + cs_pin: 22 + dc_pin: 21 pages: - id: page1 lambda: |- @@ -21,5 +17,6 @@ display: touchscreen: - platform: chsc6x + i2c_id: i2c_bus display: ili9xxx_display interrupt_pin: 20 diff --git a/tests/components/chsc6x/test.rp2040-ard.yaml b/tests/components/chsc6x/test.rp2040-ard.yaml index dbd0d59fc4..2e3613a4a3 100644 --- a/tests/components/chsc6x/test.rp2040-ard.yaml +++ b/tests/components/chsc6x/test.rp2040-ard.yaml @@ -1,19 +1,14 @@ -i2c: - - id: i2c_chsc6x - scl: 1 - sda: 0 - -spi: - clk_pin: 2 - mosi_pin: 3 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml display: - platform: ili9xxx id: ili9xxx_display model: GC9A01A invert_colors: True - cs_pin: 18 - dc_pin: 19 + cs_pin: 20 + dc_pin: 21 pages: - id: page1 lambda: |- @@ -21,5 +16,6 @@ display: touchscreen: - platform: chsc6x + i2c_id: i2c_bus display: ili9xxx_display - interrupt_pin: 20 + interrupt_pin: 22 diff --git a/tests/components/climate/common.yaml b/tests/components/climate/common.yaml new file mode 100644 index 0000000000..ff405b68e2 --- /dev/null +++ b/tests/components/climate/common.yaml @@ -0,0 +1,31 @@ +switch: + - platform: template + id: climate_heater_switch + optimistic: true + - platform: template + id: climate_cooler_switch + optimistic: true + +sensor: + - platform: template + id: climate_temperature_sensor + lambda: |- + return 21.5; + update_interval: 60s + +climate: + - platform: bang_bang + id: climate_test_climate + name: Test Climate + sensor: climate_temperature_sensor + default_target_temperature_low: 18°C + default_target_temperature_high: 24°C + idle_action: + - switch.turn_off: climate_heater_switch + - switch.turn_off: climate_cooler_switch + cool_action: + - switch.turn_on: climate_cooler_switch + - switch.turn_off: climate_heater_switch + heat_action: + - switch.turn_on: climate_heater_switch + - switch.turn_off: climate_cooler_switch diff --git a/tests/components/alarm_control_panel/test.esp32-c3-ard.yaml b/tests/components/climate/test.esp8266-ard.yaml similarity index 100% rename from tests/components/alarm_control_panel/test.esp32-c3-ard.yaml rename to tests/components/climate/test.esp8266-ard.yaml diff --git a/tests/components/climate_ir_lg/common.yaml b/tests/components/climate_ir_lg/common.yaml index c8f84411c0..37011b16ee 100644 --- a/tests/components/climate_ir_lg/common.yaml +++ b/tests/components/climate_ir_lg/common.yaml @@ -1,7 +1,16 @@ -remote_transmitter: - pin: ${pin} - carrier_duty_percent: 50% +sensor: + - platform: template + id: temp_sensor + lambda: return 22.0; + update_interval: 60s + - platform: template + id: humidity_sensor + lambda: return 50.0; + update_interval: 60s climate: - platform: climate_ir_lg name: LG Climate + transmitter_id: xmitr + sensor: temp_sensor + humidity_sensor: humidity_sensor diff --git a/tests/components/climate_ir_lg/test.esp32-ard.yaml b/tests/components/climate_ir_lg/test.esp32-ard.yaml deleted file mode 100644 index 7b012aa64c..0000000000 --- a/tests/components/climate_ir_lg/test.esp32-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/climate_ir_lg/test.esp32-c3-ard.yaml b/tests/components/climate_ir_lg/test.esp32-c3-ard.yaml deleted file mode 100644 index 7b012aa64c..0000000000 --- a/tests/components/climate_ir_lg/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/climate_ir_lg/test.esp32-c3-idf.yaml b/tests/components/climate_ir_lg/test.esp32-c3-idf.yaml deleted file mode 100644 index 7b012aa64c..0000000000 --- a/tests/components/climate_ir_lg/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/climate_ir_lg/test.esp32-idf.yaml b/tests/components/climate_ir_lg/test.esp32-idf.yaml index 7b012aa64c..e891f9dc85 100644 --- a/tests/components/climate_ir_lg/test.esp32-idf.yaml +++ b/tests/components/climate_ir_lg/test.esp32-idf.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO2 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/climate_ir_lg/test.esp8266-ard.yaml b/tests/components/climate_ir_lg/test.esp8266-ard.yaml index f5097fcf5f..4bed2f03e5 100644 --- a/tests/components/climate_ir_lg/test.esp8266-ard.yaml +++ b/tests/components/climate_ir_lg/test.esp8266-ard.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO5 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/cm1106/common.yaml b/tests/components/cm1106/common.yaml index a01e78024e..ed2fd67007 100644 --- a/tests/components/cm1106/common.yaml +++ b/tests/components/cm1106/common.yaml @@ -1,9 +1,3 @@ -uart: - - id: uart_cm1106 - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 9600 - sensor: - platform: cm1106 co2: diff --git a/tests/components/cm1106/test.esp32-ard.yaml b/tests/components/cm1106/test.esp32-ard.yaml deleted file mode 100644 index f486544afa..0000000000 --- a/tests/components/cm1106/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 - -<<: !include common.yaml diff --git a/tests/components/cm1106/test.esp32-c3-ard.yaml b/tests/components/cm1106/test.esp32-c3-ard.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/cm1106/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/cm1106/test.esp32-c3-idf.yaml b/tests/components/cm1106/test.esp32-c3-idf.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/cm1106/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/cm1106/test.esp32-idf.yaml b/tests/components/cm1106/test.esp32-idf.yaml index f486544afa..b415125e84 100644 --- a/tests/components/cm1106/test.esp32-idf.yaml +++ b/tests/components/cm1106/test.esp32-idf.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 + tx_pin: GPIO4 + rx_pin: GPIO5 + +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/cm1106/test.esp8266-ard.yaml b/tests/components/cm1106/test.esp8266-ard.yaml index b516342f3b..96ab4ef6ac 100644 --- a/tests/components/cm1106/test.esp8266-ard.yaml +++ b/tests/components/cm1106/test.esp8266-ard.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: GPIO0 + rx_pin: GPIO2 + +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/cm1106/test.rp2040-ard.yaml b/tests/components/cm1106/test.rp2040-ard.yaml index b516342f3b..b28f2b5e05 100644 --- a/tests/components/cm1106/test.rp2040-ard.yaml +++ b/tests/components/cm1106/test.rp2040-ard.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO4 rx_pin: GPIO5 +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/color/test.esp32-ard.yaml b/tests/components/color/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/color/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/color/test.esp32-c3-ard.yaml b/tests/components/color/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/color/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/color/test.esp32-c3-idf.yaml b/tests/components/color/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/color/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/color_temperature/test.esp32-ard.yaml b/tests/components/color_temperature/test.esp32-ard.yaml deleted file mode 100644 index 1831adda6e..0000000000 --- a/tests/components/color_temperature/test.esp32-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - light_platform: ledc - pin_o1: GPIO16 - pin_o2: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/color_temperature/test.esp32-c3-ard.yaml b/tests/components/color_temperature/test.esp32-c3-ard.yaml deleted file mode 100644 index 016f315d9f..0000000000 --- a/tests/components/color_temperature/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - light_platform: ledc - pin_o1: GPIO6 - pin_o2: GPIO7 - -<<: !include common.yaml diff --git a/tests/components/color_temperature/test.esp32-c3-idf.yaml b/tests/components/color_temperature/test.esp32-c3-idf.yaml deleted file mode 100644 index 016f315d9f..0000000000 --- a/tests/components/color_temperature/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - light_platform: ledc - pin_o1: GPIO6 - pin_o2: GPIO7 - -<<: !include common.yaml diff --git a/tests/components/combination/common.yaml b/tests/components/combination/common.yaml index 62246190af..0e5d512d08 100644 --- a/tests/components/combination/common.yaml +++ b/tests/components/combination/common.yaml @@ -4,17 +4,15 @@ sensor: lambda: |- if (millis() > 10000) { return 0.6; - } else { - return 0.0; } + return 0.0; - platform: template id: template_temperature2 lambda: |- if (millis() > 20000) { return 0.8; - } else { - return 0.0; } + return 0.0; - platform: combination type: kalman name: Kalman-filtered temperature diff --git a/tests/components/combination/test.esp32-ard.yaml b/tests/components/combination/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/combination/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/combination/test.esp32-c3-ard.yaml b/tests/components/combination/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/combination/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/combination/test.esp32-c3-idf.yaml b/tests/components/combination/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/combination/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/const/common.yaml b/tests/components/const/common.yaml index f4b15f2b90..109db65b63 100644 --- a/tests/components/const/common.yaml +++ b/tests/components/const/common.yaml @@ -1,9 +1,3 @@ -spi: - id: quad_spi - clk_pin: 15 - type: quad - data_pins: [14, 10, 16, 12] - display: - platform: qspi_dbi model: RM690B0 diff --git a/tests/components/const/test.esp32-s3-idf.yaml b/tests/components/const/test.esp32-s3-idf.yaml index dade44d145..c335dee1f3 100644 --- a/tests/components/const/test.esp32-s3-idf.yaml +++ b/tests/components/const/test.esp32-s3-idf.yaml @@ -1 +1,4 @@ +packages: + qspi: !include ../../test_build_components/common/qspi/esp32-s3-idf.yaml + <<: !include common.yaml diff --git a/tests/components/coolix/common.yaml b/tests/components/coolix/common.yaml index abe609c3ea..a1f68b8be0 100644 --- a/tests/components/coolix/common.yaml +++ b/tests/components/coolix/common.yaml @@ -1,7 +1,4 @@ -remote_transmitter: - pin: ${pin} - carrier_duty_percent: 50% - climate: - platform: coolix name: Coolix Climate + transmitter_id: xmitr diff --git a/tests/components/coolix/test.esp32-ard.yaml b/tests/components/coolix/test.esp32-ard.yaml deleted file mode 100644 index 7b012aa64c..0000000000 --- a/tests/components/coolix/test.esp32-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/coolix/test.esp32-c3-ard.yaml b/tests/components/coolix/test.esp32-c3-ard.yaml deleted file mode 100644 index 7b012aa64c..0000000000 --- a/tests/components/coolix/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/coolix/test.esp32-c3-idf.yaml b/tests/components/coolix/test.esp32-c3-idf.yaml deleted file mode 100644 index 7b012aa64c..0000000000 --- a/tests/components/coolix/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/coolix/test.esp32-idf.yaml b/tests/components/coolix/test.esp32-idf.yaml index 7b012aa64c..e891f9dc85 100644 --- a/tests/components/coolix/test.esp32-idf.yaml +++ b/tests/components/coolix/test.esp32-idf.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO2 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/coolix/test.esp8266-ard.yaml b/tests/components/coolix/test.esp8266-ard.yaml index f5097fcf5f..4bed2f03e5 100644 --- a/tests/components/coolix/test.esp8266-ard.yaml +++ b/tests/components/coolix/test.esp8266-ard.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO5 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/copy/test.esp32-ard.yaml b/tests/components/copy/test.esp32-ard.yaml deleted file mode 100644 index e5337726dc..0000000000 --- a/tests/components/copy/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - pwm_platform: ledc - pin: GPIO12 - -<<: !include common.yaml diff --git a/tests/components/copy/test.esp32-c3-ard.yaml b/tests/components/copy/test.esp32-c3-ard.yaml deleted file mode 100644 index 76272beb77..0000000000 --- a/tests/components/copy/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - pwm_platform: ledc - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/copy/test.esp32-c3-idf.yaml b/tests/components/copy/test.esp32-c3-idf.yaml deleted file mode 100644 index 76272beb77..0000000000 --- a/tests/components/copy/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - pwm_platform: ledc - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/cs5460a/common.yaml b/tests/components/cs5460a/common.yaml index d97b01716b..9ecd934eda 100644 --- a/tests/components/cs5460a/common.yaml +++ b/tests/components/cs5460a/common.yaml @@ -1,9 +1,3 @@ -spi: - - id: spi_cs5460a - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - miso_pin: ${miso_pin} - sensor: - platform: cs5460a id: cs5460a1 diff --git a/tests/components/cs5460a/test.esp32-ard.yaml b/tests/components/cs5460a/test.esp32-ard.yaml deleted file mode 100644 index 54e027a614..0000000000 --- a/tests/components/cs5460a/test.esp32-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 - cs_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/cs5460a/test.esp32-c3-ard.yaml b/tests/components/cs5460a/test.esp32-c3-ard.yaml deleted file mode 100644 index 2415ba5dc6..0000000000 --- a/tests/components/cs5460a/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/cs5460a/test.esp32-c3-idf.yaml b/tests/components/cs5460a/test.esp32-c3-idf.yaml deleted file mode 100644 index 2415ba5dc6..0000000000 --- a/tests/components/cs5460a/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/cs5460a/test.esp32-idf.yaml b/tests/components/cs5460a/test.esp32-idf.yaml index 54e027a614..a3352cf880 100644 --- a/tests/components/cs5460a/test.esp32-idf.yaml +++ b/tests/components/cs5460a/test.esp32-idf.yaml @@ -1,7 +1,7 @@ substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 cs_pin: GPIO5 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/cs5460a/test.esp8266-ard.yaml b/tests/components/cs5460a/test.esp8266-ard.yaml index dbd158d030..b4673ba8b7 100644 --- a/tests/components/cs5460a/test.esp8266-ard.yaml +++ b/tests/components/cs5460a/test.esp8266-ard.yaml @@ -1,7 +1,10 @@ substitutions: - clk_pin: GPIO14 - mosi_pin: GPIO13 - miso_pin: GPIO12 + clk_pin: GPIO0 + mosi_pin: GPIO2 + miso_pin: GPIO16 cs_pin: GPIO15 +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/cs5460a/test.rp2040-ard.yaml b/tests/components/cs5460a/test.rp2040-ard.yaml index f6c3f1eeca..1ded24de1c 100644 --- a/tests/components/cs5460a/test.rp2040-ard.yaml +++ b/tests/components/cs5460a/test.rp2040-ard.yaml @@ -4,4 +4,7 @@ substitutions: miso_pin: GPIO4 cs_pin: GPIO5 +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/cse7761/common.yaml b/tests/components/cse7761/common.yaml index 60cce3864a..77b19957b4 100644 --- a/tests/components/cse7761/common.yaml +++ b/tests/components/cse7761/common.yaml @@ -1,9 +1,3 @@ -uart: - - id: uart_cse7761 - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 38400 - sensor: - platform: cse7761 voltage: diff --git a/tests/components/cse7761/test.esp32-ard.yaml b/tests/components/cse7761/test.esp32-ard.yaml deleted file mode 100644 index 811f6b72a6..0000000000 --- a/tests/components/cse7761/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO12 - rx_pin: GPIO14 - -<<: !include common.yaml diff --git a/tests/components/cse7761/test.esp32-c3-ard.yaml b/tests/components/cse7761/test.esp32-c3-ard.yaml deleted file mode 100644 index c79d14c740..0000000000 --- a/tests/components/cse7761/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO7 - rx_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/cse7761/test.esp32-c3-idf.yaml b/tests/components/cse7761/test.esp32-c3-idf.yaml deleted file mode 100644 index c79d14c740..0000000000 --- a/tests/components/cse7761/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO7 - rx_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/cse7761/test.esp32-idf.yaml b/tests/components/cse7761/test.esp32-idf.yaml index 811f6b72a6..a6a8fee7e9 100644 --- a/tests/components/cse7761/test.esp32-idf.yaml +++ b/tests/components/cse7761/test.esp32-idf.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO12 rx_pin: GPIO14 +packages: + uart_38400: !include ../../test_build_components/common/uart_38400/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/cse7761/test.esp8266-ard.yaml b/tests/components/cse7761/test.esp8266-ard.yaml index 3b44f9c9c3..134274ffb8 100644 --- a/tests/components/cse7761/test.esp8266-ard.yaml +++ b/tests/components/cse7761/test.esp8266-ard.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO1 rx_pin: GPIO3 +packages: + uart_38400: !include ../../test_build_components/common/uart_38400/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/cse7761/test.rp2040-ard.yaml b/tests/components/cse7761/test.rp2040-ard.yaml index b516342f3b..b813e0f7f1 100644 --- a/tests/components/cse7761/test.rp2040-ard.yaml +++ b/tests/components/cse7761/test.rp2040-ard.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO4 rx_pin: GPIO5 +packages: + uart_38400: !include ../../test_build_components/common/uart_38400/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/cse7766/common.yaml b/tests/components/cse7766/common.yaml index f12b135a77..6db19691f1 100644 --- a/tests/components/cse7766/common.yaml +++ b/tests/components/cse7766/common.yaml @@ -1,10 +1,3 @@ -uart: - - id: uart_cse7766 - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 4800 - parity: EVEN - sensor: - platform: cse7766 voltage: diff --git a/tests/components/cse7766/test.esp32-ard.yaml b/tests/components/cse7766/test.esp32-ard.yaml deleted file mode 100644 index 811f6b72a6..0000000000 --- a/tests/components/cse7766/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO12 - rx_pin: GPIO14 - -<<: !include common.yaml diff --git a/tests/components/cse7766/test.esp32-c3-ard.yaml b/tests/components/cse7766/test.esp32-c3-ard.yaml deleted file mode 100644 index c79d14c740..0000000000 --- a/tests/components/cse7766/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO7 - rx_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/cse7766/test.esp32-c3-idf.yaml b/tests/components/cse7766/test.esp32-c3-idf.yaml deleted file mode 100644 index c79d14c740..0000000000 --- a/tests/components/cse7766/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO7 - rx_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/cse7766/test.esp32-idf.yaml b/tests/components/cse7766/test.esp32-idf.yaml index 811f6b72a6..911b867708 100644 --- a/tests/components/cse7766/test.esp32-idf.yaml +++ b/tests/components/cse7766/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - tx_pin: GPIO12 - rx_pin: GPIO14 +packages: + uart_4800_even: !include ../../test_build_components/common/uart_4800_even/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/cse7766/test.esp8266-ard.yaml b/tests/components/cse7766/test.esp8266-ard.yaml index 3b44f9c9c3..77e529ca48 100644 --- a/tests/components/cse7766/test.esp8266-ard.yaml +++ b/tests/components/cse7766/test.esp8266-ard.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO1 rx_pin: GPIO3 +packages: + uart_4800_even: !include ../../test_build_components/common/uart_4800_even/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/cse7766/test.rp2040-ard.yaml b/tests/components/cse7766/test.rp2040-ard.yaml index b516342f3b..b7056670ef 100644 --- a/tests/components/cse7766/test.rp2040-ard.yaml +++ b/tests/components/cse7766/test.rp2040-ard.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO4 rx_pin: GPIO5 +packages: + uart_4800_even: !include ../../test_build_components/common/uart_4800_even/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/cst226/common.yaml b/tests/components/cst226/common.yaml index d0b8ea3a86..79d7e7fd53 100644 --- a/tests/components/cst226/common.yaml +++ b/tests/components/cst226/common.yaml @@ -1,15 +1,5 @@ -i2c: - - id: i2c_cst226 - scl: ${scl_pin} - sda: ${sda_pin} - -spi: - - id: spi_ili9xxx - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - display: - - id: my_display + - id: cst226_display platform: ili9xxx model: ili9342 cs_pin: ${cs_pin} @@ -19,7 +9,9 @@ display: touchscreen: - id: ts_cst226 + i2c_id: i2c_bus platform: cst226 + display: cst226_display interrupt_pin: ${interrupt_pin} reset_pin: ${reset_pin} diff --git a/tests/components/cst226/test.esp32-ard.yaml b/tests/components/cst226/test.esp32-ard.yaml deleted file mode 100644 index 11e2c4fd43..0000000000 --- a/tests/components/cst226/test.esp32-ard.yaml +++ /dev/null @@ -1,12 +0,0 @@ -substitutions: - clk_pin: GPIO0 - mosi_pin: GPIO2 - cs_pin: GPIO4 - dc_pin: GPIO5 - disp_reset_pin: GPIO12 - scl_pin: GPIO13 - sda_pin: GPIO14 - interrupt_pin: GPIO15 - reset_pin: GPIO16 - -<<: !include common.yaml diff --git a/tests/components/cst226/test.esp32-c3-ard.yaml b/tests/components/cst226/test.esp32-c3-ard.yaml deleted file mode 100644 index 2f9bd72882..0000000000 --- a/tests/components/cst226/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,12 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - cs_pin: GPIO8 - dc_pin: GPIO9 - disp_reset_pin: GPIO10 - scl_pin: GPIO0 - sda_pin: GPIO1 - interrupt_pin: GPIO2 - reset_pin: GPIO3 - -<<: !include common.yaml diff --git a/tests/components/cst226/test.esp32-c3-idf.yaml b/tests/components/cst226/test.esp32-c3-idf.yaml deleted file mode 100644 index 2f9bd72882..0000000000 --- a/tests/components/cst226/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,12 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - cs_pin: GPIO8 - dc_pin: GPIO9 - disp_reset_pin: GPIO10 - scl_pin: GPIO0 - sda_pin: GPIO1 - interrupt_pin: GPIO2 - reset_pin: GPIO3 - -<<: !include common.yaml diff --git a/tests/components/cst226/test.esp32-idf.yaml b/tests/components/cst226/test.esp32-idf.yaml index 11e2c4fd43..984f08db47 100644 --- a/tests/components/cst226/test.esp32-idf.yaml +++ b/tests/components/cst226/test.esp32-idf.yaml @@ -1,12 +1,12 @@ substitutions: - clk_pin: GPIO0 - mosi_pin: GPIO2 cs_pin: GPIO4 dc_pin: GPIO5 disp_reset_pin: GPIO12 - scl_pin: GPIO13 - sda_pin: GPIO14 interrupt_pin: GPIO15 - reset_pin: GPIO16 + reset_pin: GPIO25 + +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/cst816/common.yaml b/tests/components/cst816/common.yaml index 9400e4eef0..889a477dd2 100644 --- a/tests/components/cst816/common.yaml +++ b/tests/components/cst816/common.yaml @@ -1,15 +1,5 @@ -i2c: - - id: i2c_cst816 - scl: ${scl_pin} - sda: ${sda_pin} - -spi: - - id: spi_ili9xxx - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - display: - - id: my_display + - id: cst816_display platform: ili9xxx dimensions: 480x320 model: ST7796 @@ -25,7 +15,9 @@ display: touchscreen: - id: ts_cst816 + i2c_id: i2c_bus platform: cst816 + display: cst816_display interrupt_pin: ${interrupt_pin} reset_pin: ${reset_pin} skip_probe: false @@ -36,6 +28,7 @@ touchscreen: binary_sensor: - platform: touchscreen + touchscreen_id: ts_cst816 name: Home Button use_raw: true x_min: 0 diff --git a/tests/components/cst816/test.esp32-ard.yaml b/tests/components/cst816/test.esp32-ard.yaml deleted file mode 100644 index 11e2c4fd43..0000000000 --- a/tests/components/cst816/test.esp32-ard.yaml +++ /dev/null @@ -1,12 +0,0 @@ -substitutions: - clk_pin: GPIO0 - mosi_pin: GPIO2 - cs_pin: GPIO4 - dc_pin: GPIO5 - disp_reset_pin: GPIO12 - scl_pin: GPIO13 - sda_pin: GPIO14 - interrupt_pin: GPIO15 - reset_pin: GPIO16 - -<<: !include common.yaml diff --git a/tests/components/cst816/test.esp32-c3-ard.yaml b/tests/components/cst816/test.esp32-c3-ard.yaml deleted file mode 100644 index 2f9bd72882..0000000000 --- a/tests/components/cst816/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,12 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - cs_pin: GPIO8 - dc_pin: GPIO9 - disp_reset_pin: GPIO10 - scl_pin: GPIO0 - sda_pin: GPIO1 - interrupt_pin: GPIO2 - reset_pin: GPIO3 - -<<: !include common.yaml diff --git a/tests/components/cst816/test.esp32-c3-idf.yaml b/tests/components/cst816/test.esp32-c3-idf.yaml deleted file mode 100644 index 2f9bd72882..0000000000 --- a/tests/components/cst816/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,12 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - cs_pin: GPIO8 - dc_pin: GPIO9 - disp_reset_pin: GPIO10 - scl_pin: GPIO0 - sda_pin: GPIO1 - interrupt_pin: GPIO2 - reset_pin: GPIO3 - -<<: !include common.yaml diff --git a/tests/components/cst816/test.esp32-idf.yaml b/tests/components/cst816/test.esp32-idf.yaml index 11e2c4fd43..984f08db47 100644 --- a/tests/components/cst816/test.esp32-idf.yaml +++ b/tests/components/cst816/test.esp32-idf.yaml @@ -1,12 +1,12 @@ substitutions: - clk_pin: GPIO0 - mosi_pin: GPIO2 cs_pin: GPIO4 dc_pin: GPIO5 disp_reset_pin: GPIO12 - scl_pin: GPIO13 - sda_pin: GPIO14 interrupt_pin: GPIO15 - reset_pin: GPIO16 + reset_pin: GPIO25 + +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/ct_clamp/test.esp32-ard.yaml b/tests/components/ct_clamp/test.esp32-ard.yaml deleted file mode 100644 index 0a70e3f733..0000000000 --- a/tests/components/ct_clamp/test.esp32-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO39 - -<<: !include common.yaml diff --git a/tests/components/ct_clamp/test.esp32-c3-ard.yaml b/tests/components/ct_clamp/test.esp32-c3-ard.yaml deleted file mode 100644 index a8f29c98ae..0000000000 --- a/tests/components/ct_clamp/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO0 - -<<: !include common.yaml diff --git a/tests/components/ct_clamp/test.esp32-c3-idf.yaml b/tests/components/ct_clamp/test.esp32-c3-idf.yaml deleted file mode 100644 index a8f29c98ae..0000000000 --- a/tests/components/ct_clamp/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO0 - -<<: !include common.yaml diff --git a/tests/components/current_based/common.yaml b/tests/components/current_based/common.yaml index 25dc9671b7..503c4596e9 100644 --- a/tests/components/current_based/common.yaml +++ b/tests/components/current_based/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_ade7953 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: ade7953_i2c + i2c_id: i2c_bus irq_pin: ${irq_pin} voltage: name: ADE7953 Voltage diff --git a/tests/components/current_based/test.esp32-ard.yaml b/tests/components/current_based/test.esp32-ard.yaml deleted file mode 100644 index 2c57d412f6..0000000000 --- a/tests/components/current_based/test.esp32-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - irq_pin: GPIO15 - -<<: !include common.yaml diff --git a/tests/components/current_based/test.esp32-c3-ard.yaml b/tests/components/current_based/test.esp32-c3-ard.yaml deleted file mode 100644 index 799acabd5a..0000000000 --- a/tests/components/current_based/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - irq_pin: GPIO6 - -<<: !include common.yaml diff --git a/tests/components/current_based/test.esp32-c3-idf.yaml b/tests/components/current_based/test.esp32-c3-idf.yaml deleted file mode 100644 index 799acabd5a..0000000000 --- a/tests/components/current_based/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - irq_pin: GPIO6 - -<<: !include common.yaml diff --git a/tests/components/current_based/test.esp32-idf.yaml b/tests/components/current_based/test.esp32-idf.yaml index 2c57d412f6..49629536e7 100644 --- a/tests/components/current_based/test.esp32-idf.yaml +++ b/tests/components/current_based/test.esp32-idf.yaml @@ -1,6 +1,7 @@ substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 irq_pin: GPIO15 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/current_based/test.esp8266-ard.yaml b/tests/components/current_based/test.esp8266-ard.yaml index c8e6a43f44..dc7609ab37 100644 --- a/tests/components/current_based/test.esp8266-ard.yaml +++ b/tests/components/current_based/test.esp8266-ard.yaml @@ -1,6 +1,7 @@ substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 irq_pin: GPIO15 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/current_based/test.rp2040-ard.yaml b/tests/components/current_based/test.rp2040-ard.yaml index 799acabd5a..b80562ad22 100644 --- a/tests/components/current_based/test.rp2040-ard.yaml +++ b/tests/components/current_based/test.rp2040-ard.yaml @@ -1,6 +1,7 @@ substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 irq_pin: GPIO6 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/cwww/common.yaml b/tests/components/cwww/common.yaml index 0ad5beeaae..7fa5ab668c 100644 --- a/tests/components/cwww/common.yaml +++ b/tests/components/cwww/common.yaml @@ -1,11 +1,3 @@ -output: - - platform: ${light_platform} - id: light_output_1 - pin: ${pin_o1} - - platform: ${light_platform} - id: light_output_2 - pin: ${pin_o2} - light: - platform: cwww name: CWWW Light diff --git a/tests/components/cwww/test.esp32-ard.yaml b/tests/components/cwww/test.esp32-ard.yaml deleted file mode 100644 index 1831adda6e..0000000000 --- a/tests/components/cwww/test.esp32-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - light_platform: ledc - pin_o1: GPIO16 - pin_o2: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/cwww/test.esp32-c3-ard.yaml b/tests/components/cwww/test.esp32-c3-ard.yaml deleted file mode 100644 index 016f315d9f..0000000000 --- a/tests/components/cwww/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - light_platform: ledc - pin_o1: GPIO6 - pin_o2: GPIO7 - -<<: !include common.yaml diff --git a/tests/components/cwww/test.esp32-c3-idf.yaml b/tests/components/cwww/test.esp32-c3-idf.yaml deleted file mode 100644 index 982394ded6..0000000000 --- a/tests/components/cwww/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,14 +0,0 @@ -substitutions: - light_platform: ledc - pin_o1: GPIO6 - pin_o2: GPIO7 - -packages: - device_base: !include common.yaml - -output: - - id: !extend light_output_1 - channel: 0 - - id: !extend light_output_2 - channel: 1 - phase_angle: 180° diff --git a/tests/components/cwww/test.esp32-idf.yaml b/tests/components/cwww/test.esp32-idf.yaml index d2a998e6b3..01edf0b0b5 100644 --- a/tests/components/cwww/test.esp32-idf.yaml +++ b/tests/components/cwww/test.esp32-idf.yaml @@ -3,12 +3,15 @@ substitutions: pin_o1: GPIO16 pin_o2: GPIO17 -packages: - device_base: !include common.yaml - output: - - id: !extend light_output_1 + - platform: ${light_platform} + id: light_output_1 + pin: ${pin_o1} channel: 0 - - id: !extend light_output_2 + - platform: ${light_platform} + id: light_output_2 + pin: ${pin_o2} channel: 1 phase_angle: 180° + +<<: !include common.yaml diff --git a/tests/components/cwww/test.esp8266-ard.yaml b/tests/components/cwww/test.esp8266-ard.yaml index 75a5b9d64d..49d73b7d3d 100644 --- a/tests/components/cwww/test.esp8266-ard.yaml +++ b/tests/components/cwww/test.esp8266-ard.yaml @@ -3,4 +3,12 @@ substitutions: pin_o1: GPIO12 pin_o2: GPIO13 +output: + - platform: ${light_platform} + id: light_output_1 + pin: ${pin_o1} + - platform: ${light_platform} + id: light_output_2 + pin: ${pin_o2} + <<: !include common.yaml diff --git a/tests/components/cwww/test.rp2040-ard.yaml b/tests/components/cwww/test.rp2040-ard.yaml index 537177aca1..ba8e0ad071 100644 --- a/tests/components/cwww/test.rp2040-ard.yaml +++ b/tests/components/cwww/test.rp2040-ard.yaml @@ -3,4 +3,12 @@ substitutions: pin_o1: GPIO12 pin_o2: GPIO13 +output: + - platform: ${light_platform} + id: light_output_1 + pin: ${pin_o1} + - platform: ${light_platform} + id: light_output_2 + pin: ${pin_o2} + <<: !include common.yaml diff --git a/tests/components/dac7678/common.yaml b/tests/components/dac7678/common.yaml index efad81a5ff..6c9c032686 100644 --- a/tests/components/dac7678/common.yaml +++ b/tests/components/dac7678/common.yaml @@ -1,9 +1,5 @@ -i2c: - - id: i2c_dac7678 - scl: ${scl_pin} - sda: ${sda_pin} - dac7678: + i2c_id: i2c_bus address: 0x4A id: dac7678_hub internal_reference: true diff --git a/tests/components/dac7678/test.esp32-ard.yaml b/tests/components/dac7678/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/dac7678/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/dac7678/test.esp32-c3-ard.yaml b/tests/components/dac7678/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/dac7678/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/dac7678/test.esp32-c3-idf.yaml b/tests/components/dac7678/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/dac7678/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/dac7678/test.esp32-idf.yaml b/tests/components/dac7678/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/dac7678/test.esp32-idf.yaml +++ b/tests/components/dac7678/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/dac7678/test.esp8266-ard.yaml b/tests/components/dac7678/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/dac7678/test.esp8266-ard.yaml +++ b/tests/components/dac7678/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/dac7678/test.rp2040-ard.yaml b/tests/components/dac7678/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/dac7678/test.rp2040-ard.yaml +++ b/tests/components/dac7678/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/daikin/common.yaml b/tests/components/daikin/common.yaml index 27f381b422..fd73841686 100644 --- a/tests/components/daikin/common.yaml +++ b/tests/components/daikin/common.yaml @@ -1,7 +1,3 @@ -remote_transmitter: - pin: ${pin} - carrier_duty_percent: 50% - climate: - platform: heatpumpir protocol: daikin @@ -10,3 +6,4 @@ climate: name: HeatpumpIR Climate min_temperature: 18 max_temperature: 30 + transmitter_id: xmitr diff --git a/tests/components/daikin/test.esp32-ard.yaml b/tests/components/daikin/test.esp32-ard.yaml deleted file mode 100644 index 7b012aa64c..0000000000 --- a/tests/components/daikin/test.esp32-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/daikin/test.esp8266-ard.yaml b/tests/components/daikin/test.esp8266-ard.yaml index f5097fcf5f..4bed2f03e5 100644 --- a/tests/components/daikin/test.esp8266-ard.yaml +++ b/tests/components/daikin/test.esp8266-ard.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO5 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/daikin_arc/common.yaml b/tests/components/daikin_arc/common.yaml index 5c0510f6df..53df3cf911 100644 --- a/tests/components/daikin_arc/common.yaml +++ b/tests/components/daikin_arc/common.yaml @@ -1,18 +1,3 @@ -remote_transmitter: - pin: ${tx_pin} - carrier_duty_percent: 50% - id: tsvr - -remote_receiver: - id: rcvr - pin: - number: ${rx_pin} - inverted: true - mode: - input: true - pullup: true - tolerance: 40% - climate: - platform: daikin_arc name: Daikin AC diff --git a/tests/components/daikin_arc/test.esp32-ard.yaml b/tests/components/daikin_arc/test.esp32-ard.yaml deleted file mode 100644 index cd59eb0832..0000000000 --- a/tests/components/daikin_arc/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO2 - rx_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/daikin_arc/test.esp8266-ard.yaml b/tests/components/daikin_arc/test.esp8266-ard.yaml index 8e08490d0c..aa8651e556 100644 --- a/tests/components/daikin_arc/test.esp8266-ard.yaml +++ b/tests/components/daikin_arc/test.esp8266-ard.yaml @@ -1,5 +1,5 @@ -substitutions: - tx_pin: GPIO5 - rx_pin: GPIO4 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml + remote_receiver: !include ../../test_build_components/common/remote_receiver/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/daikin_brc/common.yaml b/tests/components/daikin_brc/common.yaml index c9d7baa989..89786954ba 100644 --- a/tests/components/daikin_brc/common.yaml +++ b/tests/components/daikin_brc/common.yaml @@ -1,7 +1,4 @@ -remote_transmitter: - pin: ${pin} - carrier_duty_percent: 50% - climate: - platform: daikin_brc name: Daikin_brc Climate + transmitter_id: xmitr diff --git a/tests/components/daikin_brc/test.esp32-ard.yaml b/tests/components/daikin_brc/test.esp32-ard.yaml deleted file mode 100644 index 7b012aa64c..0000000000 --- a/tests/components/daikin_brc/test.esp32-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/daikin_brc/test.esp32-c3-ard.yaml b/tests/components/daikin_brc/test.esp32-c3-ard.yaml deleted file mode 100644 index 7b012aa64c..0000000000 --- a/tests/components/daikin_brc/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/daikin_brc/test.esp32-c3-idf.yaml b/tests/components/daikin_brc/test.esp32-c3-idf.yaml deleted file mode 100644 index 7b012aa64c..0000000000 --- a/tests/components/daikin_brc/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/daikin_brc/test.esp32-idf.yaml b/tests/components/daikin_brc/test.esp32-idf.yaml index 7b012aa64c..e891f9dc85 100644 --- a/tests/components/daikin_brc/test.esp32-idf.yaml +++ b/tests/components/daikin_brc/test.esp32-idf.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO2 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/daikin_brc/test.esp8266-ard.yaml b/tests/components/daikin_brc/test.esp8266-ard.yaml index f5097fcf5f..4bed2f03e5 100644 --- a/tests/components/daikin_brc/test.esp8266-ard.yaml +++ b/tests/components/daikin_brc/test.esp8266-ard.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO5 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/dallas_temp/common.yaml b/tests/components/dallas_temp/common.yaml index fb51f4818e..abd8e0cfa3 100644 --- a/tests/components/dallas_temp/common.yaml +++ b/tests/components/dallas_temp/common.yaml @@ -1,6 +1,6 @@ one_wire: - platform: gpio - pin: 4 + pin: ${one_wire_pin} sensor: - platform: dallas_temp @@ -9,3 +9,6 @@ sensor: resolution: 9 - platform: dallas_temp name: Dallas Temperature 2 + - platform: dallas_temp + name: Dallas Temperature 3 + index: 2 diff --git a/tests/components/dallas_temp/test.esp32-ard.yaml b/tests/components/dallas_temp/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/dallas_temp/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/dallas_temp/test.esp32-c3-ard.yaml b/tests/components/dallas_temp/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/dallas_temp/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/dallas_temp/test.esp32-c3-idf.yaml b/tests/components/dallas_temp/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/dallas_temp/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/dallas_temp/test.esp32-idf.yaml b/tests/components/dallas_temp/test.esp32-idf.yaml index dade44d145..7f9a7d4c2c 100644 --- a/tests/components/dallas_temp/test.esp32-idf.yaml +++ b/tests/components/dallas_temp/test.esp32-idf.yaml @@ -1 +1,7 @@ +substitutions: + one_wire_pin: "4" + +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/dallas_temp/test.esp8266-ard.yaml b/tests/components/dallas_temp/test.esp8266-ard.yaml index dade44d145..f58b3e0ff3 100644 --- a/tests/components/dallas_temp/test.esp8266-ard.yaml +++ b/tests/components/dallas_temp/test.esp8266-ard.yaml @@ -1 +1,7 @@ +substitutions: + one_wire_pin: "13" + +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/dallas_temp/test.rp2040-ard.yaml b/tests/components/dallas_temp/test.rp2040-ard.yaml index dade44d145..d86645aa64 100644 --- a/tests/components/dallas_temp/test.rp2040-ard.yaml +++ b/tests/components/dallas_temp/test.rp2040-ard.yaml @@ -1 +1,7 @@ +substitutions: + one_wire_pin: "10" + +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/daly_bms/common.yaml b/tests/components/daly_bms/common.yaml index a4cb849f9f..222999e25e 100644 --- a/tests/components/daly_bms/common.yaml +++ b/tests/components/daly_bms/common.yaml @@ -1,9 +1,3 @@ -uart: - - id: uart_daly_bms - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 4800 - daly_bms: update_interval: 20s diff --git a/tests/components/daly_bms/test.esp32-ard.yaml b/tests/components/daly_bms/test.esp32-ard.yaml deleted file mode 100644 index 811f6b72a6..0000000000 --- a/tests/components/daly_bms/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO12 - rx_pin: GPIO14 - -<<: !include common.yaml diff --git a/tests/components/daly_bms/test.esp32-c3-ard.yaml b/tests/components/daly_bms/test.esp32-c3-ard.yaml deleted file mode 100644 index c79d14c740..0000000000 --- a/tests/components/daly_bms/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO7 - rx_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/daly_bms/test.esp32-c3-idf.yaml b/tests/components/daly_bms/test.esp32-c3-idf.yaml deleted file mode 100644 index c79d14c740..0000000000 --- a/tests/components/daly_bms/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO7 - rx_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/daly_bms/test.esp32-idf.yaml b/tests/components/daly_bms/test.esp32-idf.yaml index 811f6b72a6..64baa4ec9d 100644 --- a/tests/components/daly_bms/test.esp32-idf.yaml +++ b/tests/components/daly_bms/test.esp32-idf.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO12 rx_pin: GPIO14 +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/daly_bms/test.esp8266-ard.yaml b/tests/components/daly_bms/test.esp8266-ard.yaml index 3b44f9c9c3..89ca3ab5ae 100644 --- a/tests/components/daly_bms/test.esp8266-ard.yaml +++ b/tests/components/daly_bms/test.esp8266-ard.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO1 rx_pin: GPIO3 +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/daly_bms/test.rp2040-ard.yaml b/tests/components/daly_bms/test.rp2040-ard.yaml index b516342f3b..b28f2b5e05 100644 --- a/tests/components/daly_bms/test.rp2040-ard.yaml +++ b/tests/components/daly_bms/test.rp2040-ard.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO4 rx_pin: GPIO5 +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/datetime/test.all.yaml b/tests/components/datetime/test.all.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/datetime/test.all.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/alarm_control_panel/test.esp32-c3-idf.yaml b/tests/components/datetime/test.bk72xx-ard.yaml similarity index 100% rename from tests/components/alarm_control_panel/test.esp32-c3-idf.yaml rename to tests/components/datetime/test.bk72xx-ard.yaml diff --git a/tests/components/alpha3/test.esp32-ard.yaml b/tests/components/datetime/test.esp32-idf.yaml similarity index 100% rename from tests/components/alpha3/test.esp32-ard.yaml rename to tests/components/datetime/test.esp32-idf.yaml diff --git a/tests/components/alpha3/test.esp32-c3-ard.yaml b/tests/components/datetime/test.esp8266-ard.yaml similarity index 100% rename from tests/components/alpha3/test.esp32-c3-ard.yaml rename to tests/components/datetime/test.esp8266-ard.yaml diff --git a/tests/components/alpha3/test.esp32-c3-idf.yaml b/tests/components/datetime/test.ln882x-ard.yaml similarity index 100% rename from tests/components/alpha3/test.esp32-c3-idf.yaml rename to tests/components/datetime/test.ln882x-ard.yaml diff --git a/tests/components/am43/test.esp32-ard.yaml b/tests/components/datetime/test.rp2040-ard.yaml similarity index 100% rename from tests/components/am43/test.esp32-ard.yaml rename to tests/components/datetime/test.rp2040-ard.yaml diff --git a/tests/components/debug/test.esp32-c3-ard.yaml b/tests/components/debug/test.esp32-c3-ard.yaml deleted file mode 100644 index 7d43491862..0000000000 --- a/tests/components/debug/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -<<: !include common.yaml - -esp32: - cpu_frequency: 80MHz diff --git a/tests/components/debug/test.esp32-c3-idf.yaml b/tests/components/debug/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/debug/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/debug/test.esp32-s2-ard.yaml b/tests/components/debug/test.esp32-s2-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/debug/test.esp32-s2-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/debug/test.esp32-s3-ard.yaml b/tests/components/debug/test.esp32-s3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/debug/test.esp32-s3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/am43/test.esp32-c3-ard.yaml b/tests/components/debug/test.nrf52-xiao-ble.yaml similarity index 100% rename from tests/components/am43/test.esp32-c3-ard.yaml rename to tests/components/debug/test.nrf52-xiao-ble.yaml diff --git a/tests/components/deep_sleep/common-esp32-all.yaml b/tests/components/deep_sleep/common-esp32-all.yaml new file mode 100644 index 0000000000..b97eec76b9 --- /dev/null +++ b/tests/components/deep_sleep/common-esp32-all.yaml @@ -0,0 +1,14 @@ +deep_sleep: + run_duration: + default: 10s + gpio_wakeup_reason: 30s + touch_wakeup_reason: 15s + sleep_duration: 50s + wakeup_pin: ${wakeup_pin} + wakeup_pin_mode: INVERT_WAKEUP + esp32_ext1_wakeup: + pins: + - number: GPIO2 + - number: GPIO13 + mode: ANY_HIGH + touch_wakeup: true diff --git a/tests/components/deep_sleep/common-esp32-ext1.yaml b/tests/components/deep_sleep/common-esp32-ext1.yaml new file mode 100644 index 0000000000..9ed4279a33 --- /dev/null +++ b/tests/components/deep_sleep/common-esp32-ext1.yaml @@ -0,0 +1,12 @@ +deep_sleep: + run_duration: + default: 10s + gpio_wakeup_reason: 30s + sleep_duration: 50s + wakeup_pin: ${wakeup_pin} + wakeup_pin_mode: INVERT_WAKEUP + esp32_ext1_wakeup: + pins: + - number: GPIO2 + - number: GPIO5 + mode: ANY_HIGH diff --git a/tests/components/deep_sleep/test.esp32-c6-idf.yaml b/tests/components/deep_sleep/test.esp32-c6-idf.yaml new file mode 100644 index 0000000000..11abe70711 --- /dev/null +++ b/tests/components/deep_sleep/test.esp32-c6-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + wakeup_pin: GPIO4 + +<<: !include common.yaml +<<: !include common-esp32-ext1.yaml diff --git a/tests/components/deep_sleep/test.esp32-idf.yaml b/tests/components/deep_sleep/test.esp32-idf.yaml index 10c17af0f5..e45eb08349 100644 --- a/tests/components/deep_sleep/test.esp32-idf.yaml +++ b/tests/components/deep_sleep/test.esp32-idf.yaml @@ -2,4 +2,4 @@ substitutions: wakeup_pin: GPIO4 <<: !include common.yaml -<<: !include common-esp32.yaml +<<: !include common-esp32-all.yaml diff --git a/tests/components/deep_sleep/test.esp32-ard.yaml b/tests/components/deep_sleep/test.esp32-s2-idf.yaml similarity index 63% rename from tests/components/deep_sleep/test.esp32-ard.yaml rename to tests/components/deep_sleep/test.esp32-s2-idf.yaml index 10c17af0f5..e45eb08349 100644 --- a/tests/components/deep_sleep/test.esp32-ard.yaml +++ b/tests/components/deep_sleep/test.esp32-s2-idf.yaml @@ -2,4 +2,4 @@ substitutions: wakeup_pin: GPIO4 <<: !include common.yaml -<<: !include common-esp32.yaml +<<: !include common-esp32-all.yaml diff --git a/tests/components/deep_sleep/test.esp32-c3-ard.yaml b/tests/components/deep_sleep/test.esp32-s3-idf.yaml similarity index 63% rename from tests/components/deep_sleep/test.esp32-c3-ard.yaml rename to tests/components/deep_sleep/test.esp32-s3-idf.yaml index 10c17af0f5..e45eb08349 100644 --- a/tests/components/deep_sleep/test.esp32-c3-ard.yaml +++ b/tests/components/deep_sleep/test.esp32-s3-idf.yaml @@ -2,4 +2,4 @@ substitutions: wakeup_pin: GPIO4 <<: !include common.yaml -<<: !include common-esp32.yaml +<<: !include common-esp32-all.yaml diff --git a/tests/components/delonghi/common.yaml b/tests/components/delonghi/common.yaml index 8e9a1293d7..c3935adcbd 100644 --- a/tests/components/delonghi/common.yaml +++ b/tests/components/delonghi/common.yaml @@ -1,7 +1,4 @@ -remote_transmitter: - pin: ${pin} - carrier_duty_percent: 50% - climate: - platform: delonghi name: Delonghi Climate + transmitter_id: xmitr diff --git a/tests/components/delonghi/test.esp32-ard.yaml b/tests/components/delonghi/test.esp32-ard.yaml deleted file mode 100644 index 7b012aa64c..0000000000 --- a/tests/components/delonghi/test.esp32-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/delonghi/test.esp32-c3-ard.yaml b/tests/components/delonghi/test.esp32-c3-ard.yaml deleted file mode 100644 index 7b012aa64c..0000000000 --- a/tests/components/delonghi/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/delonghi/test.esp32-c3-idf.yaml b/tests/components/delonghi/test.esp32-c3-idf.yaml deleted file mode 100644 index 7b012aa64c..0000000000 --- a/tests/components/delonghi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/delonghi/test.esp32-idf.yaml b/tests/components/delonghi/test.esp32-idf.yaml index 7b012aa64c..e891f9dc85 100644 --- a/tests/components/delonghi/test.esp32-idf.yaml +++ b/tests/components/delonghi/test.esp32-idf.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO2 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/delonghi/test.esp8266-ard.yaml b/tests/components/delonghi/test.esp8266-ard.yaml index f5097fcf5f..4bed2f03e5 100644 --- a/tests/components/delonghi/test.esp8266-ard.yaml +++ b/tests/components/delonghi/test.esp8266-ard.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO5 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/dfplayer/common.yaml b/tests/components/dfplayer/common.yaml index d7446141c3..5d2540c275 100644 --- a/tests/components/dfplayer/common.yaml +++ b/tests/components/dfplayer/common.yaml @@ -26,12 +26,6 @@ esphome: - dfplayer.volume_down - dfplayer.sleep -uart: - - id: uart_dfplayer - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 9600 - dfplayer: on_finished_playback: then: diff --git a/tests/components/dfplayer/test.esp32-ard.yaml b/tests/components/dfplayer/test.esp32-ard.yaml deleted file mode 100644 index 811f6b72a6..0000000000 --- a/tests/components/dfplayer/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO12 - rx_pin: GPIO14 - -<<: !include common.yaml diff --git a/tests/components/dfplayer/test.esp32-c3-ard.yaml b/tests/components/dfplayer/test.esp32-c3-ard.yaml deleted file mode 100644 index c79d14c740..0000000000 --- a/tests/components/dfplayer/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO7 - rx_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/dfplayer/test.esp32-c3-idf.yaml b/tests/components/dfplayer/test.esp32-c3-idf.yaml deleted file mode 100644 index c79d14c740..0000000000 --- a/tests/components/dfplayer/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO7 - rx_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/dfplayer/test.esp32-idf.yaml b/tests/components/dfplayer/test.esp32-idf.yaml index 811f6b72a6..64baa4ec9d 100644 --- a/tests/components/dfplayer/test.esp32-idf.yaml +++ b/tests/components/dfplayer/test.esp32-idf.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO12 rx_pin: GPIO14 +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/dfplayer/test.esp8266-ard.yaml b/tests/components/dfplayer/test.esp8266-ard.yaml index 3b44f9c9c3..89ca3ab5ae 100644 --- a/tests/components/dfplayer/test.esp8266-ard.yaml +++ b/tests/components/dfplayer/test.esp8266-ard.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO1 rx_pin: GPIO3 +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/dfplayer/test.rp2040-ard.yaml b/tests/components/dfplayer/test.rp2040-ard.yaml index b516342f3b..b28f2b5e05 100644 --- a/tests/components/dfplayer/test.rp2040-ard.yaml +++ b/tests/components/dfplayer/test.rp2040-ard.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO4 rx_pin: GPIO5 +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/dfrobot_sen0395/common.yaml b/tests/components/dfrobot_sen0395/common.yaml index 8c349911d3..7f980574ed 100644 --- a/tests/components/dfrobot_sen0395/common.yaml +++ b/tests/components/dfrobot_sen0395/common.yaml @@ -14,12 +14,6 @@ esphome: sensitivity: 6 - dfrobot_sen0395.reset -uart: - - id: uart_dfrobot_sen0395 - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 9600 - dfrobot_sen0395: - id: mmwave diff --git a/tests/components/dfrobot_sen0395/test.esp32-ard.yaml b/tests/components/dfrobot_sen0395/test.esp32-ard.yaml deleted file mode 100644 index 811f6b72a6..0000000000 --- a/tests/components/dfrobot_sen0395/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO12 - rx_pin: GPIO14 - -<<: !include common.yaml diff --git a/tests/components/dfrobot_sen0395/test.esp32-c3-ard.yaml b/tests/components/dfrobot_sen0395/test.esp32-c3-ard.yaml deleted file mode 100644 index c79d14c740..0000000000 --- a/tests/components/dfrobot_sen0395/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO7 - rx_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/dfrobot_sen0395/test.esp32-c3-idf.yaml b/tests/components/dfrobot_sen0395/test.esp32-c3-idf.yaml deleted file mode 100644 index c79d14c740..0000000000 --- a/tests/components/dfrobot_sen0395/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO7 - rx_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/dfrobot_sen0395/test.esp32-idf.yaml b/tests/components/dfrobot_sen0395/test.esp32-idf.yaml index 811f6b72a6..64baa4ec9d 100644 --- a/tests/components/dfrobot_sen0395/test.esp32-idf.yaml +++ b/tests/components/dfrobot_sen0395/test.esp32-idf.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO12 rx_pin: GPIO14 +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/dfrobot_sen0395/test.esp8266-ard.yaml b/tests/components/dfrobot_sen0395/test.esp8266-ard.yaml index 3b44f9c9c3..89ca3ab5ae 100644 --- a/tests/components/dfrobot_sen0395/test.esp8266-ard.yaml +++ b/tests/components/dfrobot_sen0395/test.esp8266-ard.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO1 rx_pin: GPIO3 +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/dfrobot_sen0395/test.rp2040-ard.yaml b/tests/components/dfrobot_sen0395/test.rp2040-ard.yaml index b516342f3b..b28f2b5e05 100644 --- a/tests/components/dfrobot_sen0395/test.rp2040-ard.yaml +++ b/tests/components/dfrobot_sen0395/test.rp2040-ard.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO4 rx_pin: GPIO5 +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/dht/test.esp32-ard.yaml b/tests/components/dht/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/dht/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/dht/test.esp32-c3-ard.yaml b/tests/components/dht/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/dht/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/dht/test.esp32-c3-idf.yaml b/tests/components/dht/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/dht/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/dht12/common.yaml b/tests/components/dht12/common.yaml index 91346e0e27..2602ae13cf 100644 --- a/tests/components/dht12/common.yaml +++ b/tests/components/dht12/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_dht12 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: dht12 + i2c_id: i2c_bus temperature: name: DHT12 Temperature humidity: diff --git a/tests/components/dht12/test.esp32-ard.yaml b/tests/components/dht12/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/dht12/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/dht12/test.esp32-c3-ard.yaml b/tests/components/dht12/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/dht12/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/dht12/test.esp32-c3-idf.yaml b/tests/components/dht12/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/dht12/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/dht12/test.esp32-idf.yaml b/tests/components/dht12/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/dht12/test.esp32-idf.yaml +++ b/tests/components/dht12/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/dht12/test.esp8266-ard.yaml b/tests/components/dht12/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/dht12/test.esp8266-ard.yaml +++ b/tests/components/dht12/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/dht12/test.rp2040-ard.yaml b/tests/components/dht12/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/dht12/test.rp2040-ard.yaml +++ b/tests/components/dht12/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/display/common.yaml b/tests/components/display/common.yaml index 4fc4fafa25..27abb23e03 100644 --- a/tests/components/display/common.yaml +++ b/tests/components/display/common.yaml @@ -1,9 +1,3 @@ -spi: - - id: spi_main_lcd - clk_pin: 16 - mosi_pin: 17 - miso_pin: 15 - display: - platform: ili9xxx id: main_lcd diff --git a/tests/components/display/test.esp32-ard.yaml b/tests/components/display/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/display/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/dps310/common.yaml b/tests/components/dps310/common.yaml index e6c0c9fab8..847a79f2e6 100644 --- a/tests/components/dps310/common.yaml +++ b/tests/components/dps310/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_dps310 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: dps310 + i2c_id: i2c_bus temperature: name: DPS310 Temperature pressure: diff --git a/tests/components/dps310/test.esp32-ard.yaml b/tests/components/dps310/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/dps310/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/dps310/test.esp32-c3-ard.yaml b/tests/components/dps310/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/dps310/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/dps310/test.esp32-c3-idf.yaml b/tests/components/dps310/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/dps310/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/dps310/test.esp32-idf.yaml b/tests/components/dps310/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/dps310/test.esp32-idf.yaml +++ b/tests/components/dps310/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/dps310/test.esp8266-ard.yaml b/tests/components/dps310/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/dps310/test.esp8266-ard.yaml +++ b/tests/components/dps310/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/dps310/test.rp2040-ard.yaml b/tests/components/dps310/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/dps310/test.rp2040-ard.yaml +++ b/tests/components/dps310/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/ds1307/common.yaml b/tests/components/ds1307/common.yaml index 3466a9eec8..cdd2b9cf56 100644 --- a/tests/components/ds1307/common.yaml +++ b/tests/components/ds1307/common.yaml @@ -1,9 +1,5 @@ -i2c: - - id: i2c_ds1307 - scl: ${scl_pin} - sda: ${sda_pin} - time: - platform: ds1307 + i2c_id: i2c_bus id: ds1307_time update_interval: never diff --git a/tests/components/ds1307/test.esp32-ard.yaml b/tests/components/ds1307/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/ds1307/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/ds1307/test.esp32-c3-ard.yaml b/tests/components/ds1307/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/ds1307/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ds1307/test.esp32-c3-idf.yaml b/tests/components/ds1307/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/ds1307/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ds1307/test.esp32-idf.yaml b/tests/components/ds1307/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/ds1307/test.esp32-idf.yaml +++ b/tests/components/ds1307/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/ds1307/test.esp8266-ard.yaml b/tests/components/ds1307/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/ds1307/test.esp8266-ard.yaml +++ b/tests/components/ds1307/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/ds1307/test.nrf52-adafruit.yaml b/tests/components/ds1307/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..2a0de6241c --- /dev/null +++ b/tests/components/ds1307/test.nrf52-adafruit.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/nrf52.yaml + +<<: !include common.yaml diff --git a/tests/components/ds1307/test.rp2040-ard.yaml b/tests/components/ds1307/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/ds1307/test.rp2040-ard.yaml +++ b/tests/components/ds1307/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/ds2484/common.yaml b/tests/components/ds2484/common.yaml index 9d2882a3c0..1e5dcd7dba 100644 --- a/tests/components/ds2484/common.yaml +++ b/tests/components/ds2484/common.yaml @@ -1,11 +1,6 @@ -i2c: - - id: i2c_ds2484 - scl: ${scl_pin} - sda: ${sda_pin} - one_wire: platform: ds2484 - i2c_id: i2c_ds2484 + i2c_id: i2c_bus address: 0x18 active_pullup: true strong_pullup: false diff --git a/tests/components/ds2484/test.esp32-ard.yaml b/tests/components/ds2484/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/ds2484/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/ds2484/test.esp32-c3-ard.yaml b/tests/components/ds2484/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/ds2484/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ds2484/test.esp32-c3-idf.yaml b/tests/components/ds2484/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/ds2484/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ds2484/test.esp32-idf.yaml b/tests/components/ds2484/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/ds2484/test.esp32-idf.yaml +++ b/tests/components/ds2484/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/ds2484/test.esp8266-ard.yaml b/tests/components/ds2484/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/ds2484/test.esp8266-ard.yaml +++ b/tests/components/ds2484/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/ds2484/test.rp2040-ard.yaml b/tests/components/ds2484/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/ds2484/test.rp2040-ard.yaml +++ b/tests/components/ds2484/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/dsmr/common.yaml b/tests/components/dsmr/common.yaml index 2901b811fe..038bf2806b 100644 --- a/tests/components/dsmr/common.yaml +++ b/tests/components/dsmr/common.yaml @@ -1,9 +1,3 @@ -uart: - - id: uart_dsmr - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 9600 - dsmr: decryption_key: 00112233445566778899aabbccddeeff max_telegram_length: 1000 diff --git a/tests/components/dsmr/test.esp32-ard.yaml b/tests/components/dsmr/test.esp32-ard.yaml index 7a65a48ec0..f218b297aa 100644 --- a/tests/components/dsmr/test.esp32-ard.yaml +++ b/tests/components/dsmr/test.esp32-ard.yaml @@ -1,6 +1,7 @@ substitutions: - tx_pin: GPIO12 - rx_pin: GPIO14 request_pin: GPIO15 +packages: + uart: !include ../../test_build_components/common/uart/esp32-ard.yaml + <<: !include common.yaml diff --git a/tests/components/dsmr/test.esp32-c3-ard.yaml b/tests/components/dsmr/test.esp32-c3-ard.yaml deleted file mode 100644 index 72998506ac..0000000000 --- a/tests/components/dsmr/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - request_pin: GPIO6 - -<<: !include common.yaml diff --git a/tests/components/dsmr/test.esp8266-ard.yaml b/tests/components/dsmr/test.esp8266-ard.yaml index a47bd58806..08bcf16fc9 100644 --- a/tests/components/dsmr/test.esp8266-ard.yaml +++ b/tests/components/dsmr/test.esp8266-ard.yaml @@ -1,6 +1,7 @@ substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 request_pin: GPIO15 +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/dsmr/test.rp2040-ard.yaml b/tests/components/dsmr/test.rp2040-ard.yaml index 72998506ac..8684cb76aa 100644 --- a/tests/components/dsmr/test.rp2040-ard.yaml +++ b/tests/components/dsmr/test.rp2040-ard.yaml @@ -1,6 +1,7 @@ substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 request_pin: GPIO6 +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/duty_cycle/test.esp32-ard.yaml b/tests/components/duty_cycle/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/duty_cycle/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/duty_cycle/test.esp32-c3-ard.yaml b/tests/components/duty_cycle/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/duty_cycle/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/duty_cycle/test.esp32-c3-idf.yaml b/tests/components/duty_cycle/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/duty_cycle/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/am43/test.esp32-c3-idf.yaml b/tests/components/duty_cycle/test.nrf52-adafruit.yaml similarity index 100% rename from tests/components/am43/test.esp32-c3-idf.yaml rename to tests/components/duty_cycle/test.nrf52-adafruit.yaml diff --git a/tests/components/analog_threshold/test.esp32-ard.yaml b/tests/components/duty_cycle/test.nrf52-mcumgr.yaml similarity index 100% rename from tests/components/analog_threshold/test.esp32-ard.yaml rename to tests/components/duty_cycle/test.nrf52-mcumgr.yaml diff --git a/tests/components/duty_time/common.yaml b/tests/components/duty_time/common.yaml index 28fa4afd1c..761d10f16a 100644 --- a/tests/components/duty_time/common.yaml +++ b/tests/components/duty_time/common.yaml @@ -4,9 +4,8 @@ binary_sensor: lambda: |- if (millis() > 10000) { return true; - } else { - return false; } + return false; sensor: - platform: duty_time diff --git a/tests/components/duty_time/test.esp32-ard.yaml b/tests/components/duty_time/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/duty_time/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/duty_time/test.esp32-c3-ard.yaml b/tests/components/duty_time/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/duty_time/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/duty_time/test.esp32-c3-idf.yaml b/tests/components/duty_time/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/duty_time/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/analog_threshold/test.esp32-c3-ard.yaml b/tests/components/duty_time/test.nrf52-adafruit.yaml similarity index 100% rename from tests/components/analog_threshold/test.esp32-c3-ard.yaml rename to tests/components/duty_time/test.nrf52-adafruit.yaml diff --git a/tests/components/analog_threshold/test.esp32-c3-idf.yaml b/tests/components/duty_time/test.nrf52-mcumgr.yaml similarity index 100% rename from tests/components/analog_threshold/test.esp32-c3-idf.yaml rename to tests/components/duty_time/test.nrf52-mcumgr.yaml diff --git a/tests/components/e131/test.esp32-ard.yaml b/tests/components/e131/test.esp32-ard.yaml deleted file mode 100644 index 32cf2a64ba..0000000000 --- a/tests/components/e131/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - light_platform: esp32_rmt_led_strip - pin: GPIO2 - -<<: !include common-ard.yaml diff --git a/tests/components/e131/test.esp32-c3-ard.yaml b/tests/components/e131/test.esp32-c3-ard.yaml deleted file mode 100644 index 32cf2a64ba..0000000000 --- a/tests/components/e131/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - light_platform: esp32_rmt_led_strip - pin: GPIO2 - -<<: !include common-ard.yaml diff --git a/tests/components/e131/test.esp32-c3-idf.yaml b/tests/components/e131/test.esp32-c3-idf.yaml deleted file mode 100644 index d9dc4f6804..0000000000 --- a/tests/components/e131/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - light_platform: esp32_rmt_led_strip - pin: GPIO2 - -<<: !include common-idf.yaml diff --git a/tests/components/ee895/common.yaml b/tests/components/ee895/common.yaml index 63d77abcaf..f1b17ca9d9 100644 --- a/tests/components/ee895/common.yaml +++ b/tests/components/ee895/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_ee895 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: ee895 + i2c_id: i2c_bus address: 0x5F co2: name: EE895 CO2 diff --git a/tests/components/ee895/test.esp32-ard.yaml b/tests/components/ee895/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/ee895/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/ee895/test.esp32-c3-ard.yaml b/tests/components/ee895/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/ee895/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ee895/test.esp32-c3-idf.yaml b/tests/components/ee895/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/ee895/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ee895/test.esp32-idf.yaml b/tests/components/ee895/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/ee895/test.esp32-idf.yaml +++ b/tests/components/ee895/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/ee895/test.esp8266-ard.yaml b/tests/components/ee895/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/ee895/test.esp8266-ard.yaml +++ b/tests/components/ee895/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/ee895/test.rp2040-ard.yaml b/tests/components/ee895/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/ee895/test.rp2040-ard.yaml +++ b/tests/components/ee895/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/ektf2232/common.yaml b/tests/components/ektf2232/common.yaml index 3271839fd4..1c4d768b08 100644 --- a/tests/components/ektf2232/common.yaml +++ b/tests/components/ektf2232/common.yaml @@ -1,23 +1,21 @@ -i2c: - - id: i2c_ektf2232 - scl: ${scl_pin} - sda: ${sda_pin} - display: - platform: ssd1306_i2c - id: ssd1306_display + i2c_id: i2c_bus + id: ssd1306_i2c_display model: SSD1306_128X64 - reset_pin: ${reset_pin} + reset_pin: ${display_reset_pin} pages: - - id: page1 + - id: ektf2232_page1 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); touchscreen: - platform: ektf2232 + i2c_id: i2c_bus + id: ektf2232_touchscreen interrupt_pin: ${interrupt_pin} - rts_pin: ${rts_pin} - display: ssd1306_display + reset_pin: ${touch_reset_pin} + display: ssd1306_i2c_display on_touch: - logger.log: format: Touch at (%d, %d) diff --git a/tests/components/ektf2232/test.esp32-ard.yaml b/tests/components/ektf2232/test.esp32-ard.yaml deleted file mode 100644 index b8f491c0c3..0000000000 --- a/tests/components/ektf2232/test.esp32-ard.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - reset_pin: GPIO13 - interrupt_pin: GPIO14 - rts_pin: GPIO15 - -<<: !include common.yaml diff --git a/tests/components/ektf2232/test.esp32-c3-ard.yaml b/tests/components/ektf2232/test.esp32-c3-ard.yaml deleted file mode 100644 index 9f2149b9d7..0000000000 --- a/tests/components/ektf2232/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - reset_pin: GPIO3 - interrupt_pin: GPIO6 - rts_pin: GPIO7 - -<<: !include common.yaml diff --git a/tests/components/ektf2232/test.esp32-c3-idf.yaml b/tests/components/ektf2232/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2149b9d7..0000000000 --- a/tests/components/ektf2232/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - reset_pin: GPIO3 - interrupt_pin: GPIO6 - rts_pin: GPIO7 - -<<: !include common.yaml diff --git a/tests/components/ektf2232/test.esp32-idf.yaml b/tests/components/ektf2232/test.esp32-idf.yaml index b8f491c0c3..3fab081bed 100644 --- a/tests/components/ektf2232/test.esp32-idf.yaml +++ b/tests/components/ektf2232/test.esp32-idf.yaml @@ -1,8 +1,9 @@ substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - reset_pin: GPIO13 + display_reset_pin: GPIO13 interrupt_pin: GPIO14 - rts_pin: GPIO15 + touch_reset_pin: GPIO15 + +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/ektf2232/test.esp8266-ard.yaml b/tests/components/ektf2232/test.esp8266-ard.yaml index 6d91a6533f..38a3894deb 100644 --- a/tests/components/ektf2232/test.esp8266-ard.yaml +++ b/tests/components/ektf2232/test.esp8266-ard.yaml @@ -1,8 +1,9 @@ substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - reset_pin: GPIO3 - interrupt_pin: GPIO12 - rts_pin: GPIO13 + display_reset_pin: GPIO3 + interrupt_pin: GPIO15 + touch_reset_pin: GPIO16 + +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/ektf2232/test.rp2040-ard.yaml b/tests/components/ektf2232/test.rp2040-ard.yaml index 9f2149b9d7..cda1b67715 100644 --- a/tests/components/ektf2232/test.rp2040-ard.yaml +++ b/tests/components/ektf2232/test.rp2040-ard.yaml @@ -1,8 +1,9 @@ substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - reset_pin: GPIO3 + display_reset_pin: GPIO3 interrupt_pin: GPIO6 - rts_pin: GPIO7 + touch_reset_pin: GPIO7 + +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/emc2101/common.yaml b/tests/components/emc2101/common.yaml index 795b02c6d3..d9e6fe2b96 100644 --- a/tests/components/emc2101/common.yaml +++ b/tests/components/emc2101/common.yaml @@ -1,9 +1,5 @@ -i2c: - - id: i2c_emc2101 - scl: ${scl_pin} - sda: ${sda_pin} - emc2101: + i2c_id: i2c_bus pwm: resolution: 8 diff --git a/tests/components/emc2101/test.esp32-ard.yaml b/tests/components/emc2101/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/emc2101/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/emc2101/test.esp32-c3-ard.yaml b/tests/components/emc2101/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/emc2101/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/emc2101/test.esp32-c3-idf.yaml b/tests/components/emc2101/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/emc2101/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/emc2101/test.esp32-idf.yaml b/tests/components/emc2101/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/emc2101/test.esp32-idf.yaml +++ b/tests/components/emc2101/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/emc2101/test.esp8266-ard.yaml b/tests/components/emc2101/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/emc2101/test.esp8266-ard.yaml +++ b/tests/components/emc2101/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/emc2101/test.rp2040-ard.yaml b/tests/components/emc2101/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/emc2101/test.rp2040-ard.yaml +++ b/tests/components/emc2101/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/emmeti/common.yaml b/tests/components/emmeti/common.yaml index ac4201e19b..77f381ecf9 100644 --- a/tests/components/emmeti/common.yaml +++ b/tests/components/emmeti/common.yaml @@ -1,14 +1,5 @@ -remote_transmitter: - id: tx - pin: ${remote_transmitter_pin} - carrier_duty_percent: 100% - -remote_receiver: - id: rcvr - pin: ${remote_receiver_pin} - climate: - platform: emmeti name: Emmeti receiver_id: rcvr - transmitter_id: tx + transmitter_id: xmitr diff --git a/tests/components/emmeti/test.esp32-ard.yaml b/tests/components/emmeti/test.esp32-ard.yaml deleted file mode 100644 index 2689ff279e..0000000000 --- a/tests/components/emmeti/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - remote_transmitter_pin: GPIO33 - remote_receiver_pin: GPIO32 - -<<: !include common.yaml diff --git a/tests/components/emmeti/test.esp32-idf.yaml b/tests/components/emmeti/test.esp32-idf.yaml index 2689ff279e..b241dbd159 100644 --- a/tests/components/emmeti/test.esp32-idf.yaml +++ b/tests/components/emmeti/test.esp32-idf.yaml @@ -1,5 +1,5 @@ -substitutions: - remote_transmitter_pin: GPIO33 - remote_receiver_pin: GPIO32 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml + remote_receiver: !include ../../test_build_components/common/remote_receiver/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/emmeti/test.esp8266-ard.yaml b/tests/components/emmeti/test.esp8266-ard.yaml index 2fb00aea61..aa8651e556 100644 --- a/tests/components/emmeti/test.esp8266-ard.yaml +++ b/tests/components/emmeti/test.esp8266-ard.yaml @@ -1,5 +1,5 @@ -substitutions: - remote_transmitter_pin: GPIO4 - remote_receiver_pin: GPIO5 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml + remote_receiver: !include ../../test_build_components/common/remote_receiver/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/endstop/common.yaml b/tests/components/endstop/common.yaml index 341fbf7260..b92b1e13b9 100644 --- a/tests/components/endstop/common.yaml +++ b/tests/components/endstop/common.yaml @@ -4,9 +4,8 @@ binary_sensor: lambda: |- if (millis() > 10000) { return true; - } else { - return false; } + return false; switch: - platform: template diff --git a/tests/components/endstop/test.esp32-ard.yaml b/tests/components/endstop/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/endstop/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/endstop/test.esp32-c3-ard.yaml b/tests/components/endstop/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/endstop/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/endstop/test.esp32-c3-idf.yaml b/tests/components/endstop/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/endstop/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/ens160_i2c/common.yaml b/tests/components/ens160_i2c/common.yaml index 39a5b35067..1da2bacec2 100644 --- a/tests/components/ens160_i2c/common.yaml +++ b/tests/components/ens160_i2c/common.yaml @@ -1,15 +1,13 @@ -i2c: - - id: i2c_ens160 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: ens160_i2c - i2c_id: i2c_ens160 + i2c_id: i2c_bus address: 0x53 eco2: + id: ens160_i2c_eco2 name: "ENS160 eCO2" tvoc: + id: ens160_i2c_tvoc name: "ENS160 Total Volatile Organic Compounds" aqi: + id: ens160_i2c_aqi name: "ENS160 Air Quality Index" diff --git a/tests/components/ens160_i2c/test.esp32-ard.yaml b/tests/components/ens160_i2c/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/ens160_i2c/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/ens160_i2c/test.esp32-c3-ard.yaml b/tests/components/ens160_i2c/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/ens160_i2c/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ens160_i2c/test.esp32-c3-idf.yaml b/tests/components/ens160_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/ens160_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ens160_i2c/test.esp32-idf.yaml b/tests/components/ens160_i2c/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/ens160_i2c/test.esp32-idf.yaml +++ b/tests/components/ens160_i2c/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/ens160_i2c/test.esp8266-ard.yaml b/tests/components/ens160_i2c/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/ens160_i2c/test.esp8266-ard.yaml +++ b/tests/components/ens160_i2c/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/ens160_i2c/test.rp2040-ard.yaml b/tests/components/ens160_i2c/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/ens160_i2c/test.rp2040-ard.yaml +++ b/tests/components/ens160_i2c/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/ens160_spi/common.yaml b/tests/components/ens160_spi/common.yaml index c8b663272f..46a3f40b40 100644 --- a/tests/components/ens160_spi/common.yaml +++ b/tests/components/ens160_spi/common.yaml @@ -1,16 +1,12 @@ -spi: - - id: spi_ens160 - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - miso_pin: ${miso_pin} - sensor: - platform: ens160_spi - spi_id: spi_ens160 cs_pin: ${cs_pin} eco2: + id: ens160_spi_eco2 name: "ENS160 eCO2" tvoc: + id: ens160_spi_tvoc name: "ENS160 Total Volatile Organic Compounds" aqi: + id: ens160_spi_aqi name: "ENS160 Air Quality Index" diff --git a/tests/components/ens160_spi/test.esp32-ard.yaml b/tests/components/ens160_spi/test.esp32-ard.yaml deleted file mode 100644 index 54e027a614..0000000000 --- a/tests/components/ens160_spi/test.esp32-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 - cs_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/ens160_spi/test.esp32-c3-ard.yaml b/tests/components/ens160_spi/test.esp32-c3-ard.yaml deleted file mode 100644 index 2415ba5dc6..0000000000 --- a/tests/components/ens160_spi/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/ens160_spi/test.esp32-c3-idf.yaml b/tests/components/ens160_spi/test.esp32-c3-idf.yaml deleted file mode 100644 index 2415ba5dc6..0000000000 --- a/tests/components/ens160_spi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/ens160_spi/test.esp32-idf.yaml b/tests/components/ens160_spi/test.esp32-idf.yaml index 54e027a614..a3352cf880 100644 --- a/tests/components/ens160_spi/test.esp32-idf.yaml +++ b/tests/components/ens160_spi/test.esp32-idf.yaml @@ -1,7 +1,7 @@ substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 cs_pin: GPIO5 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/ens160_spi/test.esp8266-ard.yaml b/tests/components/ens160_spi/test.esp8266-ard.yaml index dbd158d030..b4673ba8b7 100644 --- a/tests/components/ens160_spi/test.esp8266-ard.yaml +++ b/tests/components/ens160_spi/test.esp8266-ard.yaml @@ -1,7 +1,10 @@ substitutions: - clk_pin: GPIO14 - mosi_pin: GPIO13 - miso_pin: GPIO12 + clk_pin: GPIO0 + mosi_pin: GPIO2 + miso_pin: GPIO16 cs_pin: GPIO15 +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/ens160_spi/test.rp2040-ard.yaml b/tests/components/ens160_spi/test.rp2040-ard.yaml index f6c3f1eeca..1ded24de1c 100644 --- a/tests/components/ens160_spi/test.rp2040-ard.yaml +++ b/tests/components/ens160_spi/test.rp2040-ard.yaml @@ -4,4 +4,7 @@ substitutions: miso_pin: GPIO4 cs_pin: GPIO5 +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/ens210/common.yaml b/tests/components/ens210/common.yaml index b276154449..6bc8f824cc 100644 --- a/tests/components/ens210/common.yaml +++ b/tests/components/ens210/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_ens210 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: ens210 + i2c_id: i2c_bus temperature: name: ENS210 Temperature humidity: diff --git a/tests/components/ens210/test.esp32-ard.yaml b/tests/components/ens210/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/ens210/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/ens210/test.esp32-c3-ard.yaml b/tests/components/ens210/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/ens210/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ens210/test.esp32-c3-idf.yaml b/tests/components/ens210/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/ens210/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ens210/test.esp32-idf.yaml b/tests/components/ens210/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/ens210/test.esp32-idf.yaml +++ b/tests/components/ens210/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/ens210/test.esp8266-ard.yaml b/tests/components/ens210/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/ens210/test.esp8266-ard.yaml +++ b/tests/components/ens210/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/ens210/test.rp2040-ard.yaml b/tests/components/ens210/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/ens210/test.rp2040-ard.yaml +++ b/tests/components/ens210/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/epaper_spi/test.esp32-s3-idf.yaml b/tests/components/epaper_spi/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..d330b4127d --- /dev/null +++ b/tests/components/epaper_spi/test.esp32-s3-idf.yaml @@ -0,0 +1,26 @@ +packages: + spi: !include ../../test_build_components/common/spi/esp32-s3-idf.yaml + +display: + - platform: epaper_spi + spi_id: spi_bus + model: spectra-e6 + dimensions: + width: 800 + height: 480 + cs_pin: GPIO5 + dc_pin: GPIO17 + reset_pin: GPIO16 + busy_pin: GPIO4 + rotation: 0 + update_interval: 60s + lambda: |- + it.circle(64, 64, 50, Color::BLACK); + + - platform: epaper_spi + model: seeed-reterminal-e1002 + - platform: epaper_spi + model: seeed-ee04-mono-4.26 + # Override pins to avoid conflict with other display configs + busy_pin: 43 + dc_pin: 42 diff --git a/tests/components/es7210/common.yaml b/tests/components/es7210/common.yaml index 3fab177cb3..b9f0b0c3e9 100644 --- a/tests/components/es7210/common.yaml +++ b/tests/components/es7210/common.yaml @@ -4,13 +4,9 @@ esphome: - audio_adc.set_mic_gain: 0db - audio_adc.set_mic_gain: !lambda 'return 4;' -i2c: - - id: i2c_aic3204 - scl: ${scl_pin} - sda: ${sda_pin} - audio_adc: - platform: es7210 + i2c_id: i2c_bus id: es7210_adc bits_per_sample: 16bit sample_rate: 16000 diff --git a/tests/components/es7210/test.esp32-ard.yaml b/tests/components/es7210/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/es7210/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/es7210/test.esp32-c3-ard.yaml b/tests/components/es7210/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/es7210/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/es7210/test.esp32-c3-idf.yaml b/tests/components/es7210/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/es7210/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/es7210/test.esp32-idf.yaml b/tests/components/es7210/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/es7210/test.esp32-idf.yaml +++ b/tests/components/es7210/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/es7243e/common.yaml b/tests/components/es7243e/common.yaml index 3de76909e9..0fcfb97273 100644 --- a/tests/components/es7243e/common.yaml +++ b/tests/components/es7243e/common.yaml @@ -4,11 +4,7 @@ esphome: - audio_adc.set_mic_gain: 0db - audio_adc.set_mic_gain: !lambda 'return 4;' -i2c: - - id: i2c_es7243e - scl: ${scl_pin} - sda: ${sda_pin} - audio_adc: - platform: es7243e + i2c_id: i2c_bus id: es7243e_adc diff --git a/tests/components/es7243e/test.esp32-ard.yaml b/tests/components/es7243e/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/es7243e/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/es7243e/test.esp32-c3-ard.yaml b/tests/components/es7243e/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/es7243e/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/es7243e/test.esp32-c3-idf.yaml b/tests/components/es7243e/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/es7243e/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/es7243e/test.esp32-idf.yaml b/tests/components/es7243e/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/es7243e/test.esp32-idf.yaml +++ b/tests/components/es7243e/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/es8156/common.yaml b/tests/components/es8156/common.yaml index addaa0b70a..c41a332bd7 100644 --- a/tests/components/es8156/common.yaml +++ b/tests/components/es8156/common.yaml @@ -6,10 +6,6 @@ esphome: - audio_dac.set_volume: volume: 50% -i2c: - - id: i2c_es8156 - scl: ${scl_pin} - sda: ${sda_pin} - audio_dac: - platform: es8156 + i2c_id: i2c_bus diff --git a/tests/components/es8156/test.esp32-ard.yaml b/tests/components/es8156/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/es8156/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/es8156/test.esp32-c3-ard.yaml b/tests/components/es8156/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/es8156/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/es8156/test.esp32-c3-idf.yaml b/tests/components/es8156/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/es8156/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/es8156/test.esp32-idf.yaml b/tests/components/es8156/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/es8156/test.esp32-idf.yaml +++ b/tests/components/es8156/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/es8156/test.esp8266-ard.yaml b/tests/components/es8156/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/es8156/test.esp8266-ard.yaml +++ b/tests/components/es8156/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/es8311/common.yaml b/tests/components/es8311/common.yaml index d833d1c043..e855761ef4 100644 --- a/tests/components/es8311/common.yaml +++ b/tests/components/es8311/common.yaml @@ -6,10 +6,6 @@ esphome: - audio_dac.set_volume: volume: 50% -i2c: - - id: i2c_aic3204 - scl: ${scl_pin} - sda: ${sda_pin} - audio_dac: - platform: es8311 + i2c_id: i2c_bus diff --git a/tests/components/es8311/test.esp32-ard.yaml b/tests/components/es8311/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/es8311/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/es8311/test.esp32-c3-ard.yaml b/tests/components/es8311/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/es8311/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/es8311/test.esp32-c3-idf.yaml b/tests/components/es8311/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/es8311/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/es8311/test.esp32-idf.yaml b/tests/components/es8311/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/es8311/test.esp32-idf.yaml +++ b/tests/components/es8311/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/es8311/test.esp8266-ard.yaml b/tests/components/es8311/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/es8311/test.esp8266-ard.yaml +++ b/tests/components/es8311/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/es8388/common.yaml b/tests/components/es8388/common.yaml index 6a63de5aa1..cc7465ab93 100644 --- a/tests/components/es8388/common.yaml +++ b/tests/components/es8388/common.yaml @@ -7,13 +7,9 @@ esphome: - audio_dac.set_volume: volume: 50% -i2c: - - id: i2c_es8388 - scl: ${scl_pin} - sda: ${sda_pin} - audio_dac: - platform: es8388 + i2c_id: i2c_bus id: es8388_parent select: diff --git a/tests/components/es8388/test.esp32-ard.yaml b/tests/components/es8388/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/es8388/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/es8388/test.esp32-c3-ard.yaml b/tests/components/es8388/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/es8388/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/es8388/test.esp32-c3-idf.yaml b/tests/components/es8388/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/es8388/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/es8388/test.esp32-idf.yaml b/tests/components/es8388/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/es8388/test.esp32-idf.yaml +++ b/tests/components/es8388/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/es8388/test.esp8266-ard.yaml b/tests/components/es8388/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/es8388/test.esp8266-ard.yaml +++ b/tests/components/es8388/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/esp32/common.yaml b/tests/components/esp32/common.yaml new file mode 100644 index 0000000000..039a261016 --- /dev/null +++ b/tests/components/esp32/common.yaml @@ -0,0 +1,13 @@ +logger: + level: VERBOSE + +esphome: + on_boot: + - lambda: |- + int x = 100; + x = clamp(x, 50, 90); + assert(x == 90); + x = clamp_at_least(x, 95); + assert(x == 95); + x = clamp_at_most(x, 40); + assert(x == 40); diff --git a/tests/components/esp32/test-stack_size.esp32-idf.yaml b/tests/components/esp32/test-stack_size.esp32-idf.yaml new file mode 100644 index 0000000000..4953588035 --- /dev/null +++ b/tests/components/esp32/test-stack_size.esp32-idf.yaml @@ -0,0 +1,6 @@ +esp32: + board: esp32dev + framework: + type: esp-idf + advanced: + loop_task_stack_size: 16384 diff --git a/tests/components/esp32/test.esp32-idf.yaml b/tests/components/esp32/test.esp32-idf.yaml index ccf0b7cbd5..6338fe98dd 100644 --- a/tests/components/esp32/test.esp32-idf.yaml +++ b/tests/components/esp32/test.esp32-idf.yaml @@ -5,6 +5,7 @@ esp32: advanced: enable_lwip_mdns_queries: true enable_lwip_bridge_interface: true + disable_libc_locks_in_iram: false # Test explicit opt-out of RAM optimization wifi: ssid: MySSID diff --git a/tests/components/esp32/test.esp32-p4-idf.yaml b/tests/components/esp32/test.esp32-p4-idf.yaml new file mode 100644 index 0000000000..a4c930f236 --- /dev/null +++ b/tests/components/esp32/test.esp32-p4-idf.yaml @@ -0,0 +1,27 @@ +esp32: + variant: esp32p4 + flash_size: 32MB + cpu_frequency: 400MHz + framework: + type: esp-idf + advanced: + enable_idf_experimental_features: yes + +ota: + platform: esphome + +wifi: + ssid: MySSID + password: password1 + +esp32_hosted: + variant: ESP32C6 + slot: 1 + active_high: true + reset_pin: GPIO15 + cmd_pin: GPIO13 + clk_pin: GPIO12 + d0_pin: GPIO11 + d1_pin: GPIO10 + d2_pin: GPIO9 + d3_pin: GPIO8 diff --git a/tests/components/esp32/test.esp32-s3-idf.yaml b/tests/components/esp32/test.esp32-s3-idf.yaml index 1d5a5e52a4..4ae5e6b999 100644 --- a/tests/components/esp32/test.esp32-s3-idf.yaml +++ b/tests/components/esp32/test.esp32-s3-idf.yaml @@ -4,6 +4,7 @@ esp32: type: esp-idf advanced: execute_from_psram: true + disable_libc_locks_in_iram: true # Test default RAM optimization enabled psram: mode: octal diff --git a/tests/components/esp32_ble/test.esp32-c3-ard.yaml b/tests/components/esp32_ble/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/esp32_ble/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/esp32_ble_beacon/test.esp32-ard.yaml b/tests/components/esp32_ble_beacon/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/esp32_ble_beacon/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/esp32_ble_beacon/test.esp32-c3-ard.yaml b/tests/components/esp32_ble_beacon/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/esp32_ble_beacon/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/esp32_ble_client/test.esp32-ard.yaml b/tests/components/esp32_ble_client/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/esp32_ble_client/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/esp32_ble_client/test.esp32-c3-ard.yaml b/tests/components/esp32_ble_client/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/esp32_ble_client/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/esp32_ble_client/test.esp32-c3-idf.yaml b/tests/components/esp32_ble_client/test.esp32-c3-idf.yaml index dade44d145..9f2634f967 100644 --- a/tests/components/esp32_ble_client/test.esp32-c3-idf.yaml +++ b/tests/components/esp32_ble_client/test.esp32-c3-idf.yaml @@ -1 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml + <<: !include common.yaml diff --git a/tests/components/esp32_ble_client/test.esp32-idf.yaml b/tests/components/esp32_ble_client/test.esp32-idf.yaml index dade44d145..7a6541ae76 100644 --- a/tests/components/esp32_ble_client/test.esp32-idf.yaml +++ b/tests/components/esp32_ble_client/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/esp32_ble_server/test.esp32-ard.yaml b/tests/components/esp32_ble_server/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/esp32_ble_server/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/esp32_ble_server/test.esp32-c3-ard.yaml b/tests/components/esp32_ble_server/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/esp32_ble_server/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/esp32_ble_tracker/test-on-scan-end.esp32-idf.yaml b/tests/components/esp32_ble_tracker/test-on-scan-end.esp32-idf.yaml new file mode 100644 index 0000000000..4e9849a540 --- /dev/null +++ b/tests/components/esp32_ble_tracker/test-on-scan-end.esp32-idf.yaml @@ -0,0 +1,3 @@ +esp32_ble_tracker: + on_scan_end: + - logger.log: "Scan ended!" diff --git a/tests/components/esp32_ble_tracker/test.esp32-ard.yaml b/tests/components/esp32_ble_tracker/test.esp32-ard.yaml deleted file mode 100644 index 3bfdb8773f..0000000000 --- a/tests/components/esp32_ble_tracker/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -<<: !include common.yaml - -esp32_ble_tracker: - software_coexistence: true - max_connections: 3 diff --git a/tests/components/esp32_ble_tracker/test.esp32-c3-ard.yaml b/tests/components/esp32_ble_tracker/test.esp32-c3-ard.yaml deleted file mode 100644 index 2e3c48117a..0000000000 --- a/tests/components/esp32_ble_tracker/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -<<: !include common.yaml - -esp32_ble_tracker: - max_connections: 3 - software_coexistence: false diff --git a/tests/components/esp32_ble_tracker/test.esp32-c3-idf.yaml b/tests/components/esp32_ble_tracker/test.esp32-c3-idf.yaml index b71896bad5..ea6f0d4022 100644 --- a/tests/components/esp32_ble_tracker/test.esp32-c3-idf.yaml +++ b/tests/components/esp32_ble_tracker/test.esp32-c3-idf.yaml @@ -1,3 +1,6 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml + <<: !include common.yaml esp32_ble_tracker: diff --git a/tests/components/esp32_ble_tracker/test.esp32-idf.yaml b/tests/components/esp32_ble_tracker/test.esp32-idf.yaml index 1ffcfb9988..8f6e3c5731 100644 --- a/tests/components/esp32_ble_tracker/test.esp32-idf.yaml +++ b/tests/components/esp32_ble_tracker/test.esp32-idf.yaml @@ -1,3 +1,6 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-idf.yaml + <<: !include common.yaml esp32_ble_tracker: diff --git a/tests/components/esp32_camera/common.yaml b/tests/components/esp32_camera/common.yaml index 64f75c699a..eac5109bc8 100644 --- a/tests/components/esp32_camera/common.yaml +++ b/tests/components/esp32_camera/common.yaml @@ -1,29 +1 @@ -esp32_camera: - name: ESP32 Camera - data_pins: - - number: 17 - - number: 35 - - number: 34 - - number: 5 - - number: 39 - - number: 18 - - number: 36 - - number: 19 - vsync_pin: 22 - href_pin: 26 - pixel_clock_pin: 21 - external_clock: - pin: 27 - frequency: 20MHz - i2c_pins: - sda: 25 - scl: 23 - reset_pin: 15 - power_down_pin: 1 - resolution: 640x480 - jpeg_quality: 10 - frame_buffer_location: PSRAM - on_image: - then: - - lambda: |- - ESP_LOGD("main", "image len=%d, data=%c", image.length, image.data[0]); +# ESP32 camera hardware configuration comes from the camera package diff --git a/tests/components/esp32_camera/test.esp32-ard.yaml b/tests/components/esp32_camera/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/esp32_camera/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/esp32_camera/test.esp32-idf.yaml b/tests/components/esp32_camera/test.esp32-idf.yaml index dade44d145..ef8f74d4eb 100644 --- a/tests/components/esp32_camera/test.esp32-idf.yaml +++ b/tests/components/esp32_camera/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + i2c_camera: !include ../../test_build_components/common/i2c_camera/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/esp32_camera_web_server/common.yaml b/tests/components/esp32_camera_web_server/common.yaml index fe2a6a2739..2a1858f681 100644 --- a/tests/components/esp32_camera_web_server/common.yaml +++ b/tests/components/esp32_camera_web_server/common.yaml @@ -1,32 +1,3 @@ -esp32_camera: - name: ESP32 Camera - data_pins: - - number: 17 - - number: 35 - - number: 34 - - number: 5 - - number: 39 - - number: 18 - - number: 36 - - number: 19 - vsync_pin: 22 - href_pin: 26 - pixel_clock_pin: 21 - external_clock: - pin: 27 - frequency: 20MHz - i2c_pins: - sda: 25 - scl: 23 - reset_pin: 15 - power_down_pin: 1 - resolution: 640x480 - jpeg_quality: 10 - on_image: - then: - - lambda: |- - ESP_LOGD("main", "image len=%d, data=%c", image.length, image.data[0]); - esp32_camera_web_server: - port: 8080 mode: stream diff --git a/tests/components/esp32_camera_web_server/test.esp32-ard.yaml b/tests/components/esp32_camera_web_server/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/esp32_camera_web_server/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/esp32_camera_web_server/test.esp32-idf.yaml b/tests/components/esp32_camera_web_server/test.esp32-idf.yaml index dade44d145..ef8f74d4eb 100644 --- a/tests/components/esp32_camera_web_server/test.esp32-idf.yaml +++ b/tests/components/esp32_camera_web_server/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + i2c_camera: !include ../../test_build_components/common/i2c_camera/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/esp32_can/test.esp32-ard.yaml b/tests/components/esp32_can/test.esp32-ard.yaml deleted file mode 100644 index 811f6b72a6..0000000000 --- a/tests/components/esp32_can/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO12 - rx_pin: GPIO14 - -<<: !include common.yaml diff --git a/tests/components/esp32_can/test.esp32-c3-ard.yaml b/tests/components/esp32_can/test.esp32-c3-ard.yaml deleted file mode 100644 index c79d14c740..0000000000 --- a/tests/components/esp32_can/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO7 - rx_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/esp32_can/test.esp32-c3-idf.yaml b/tests/components/esp32_can/test.esp32-c3-idf.yaml index c79d14c740..22effb1bd0 100644 --- a/tests/components/esp32_can/test.esp32-c3-idf.yaml +++ b/tests/components/esp32_can/test.esp32-c3-idf.yaml @@ -1,5 +1,5 @@ substitutions: tx_pin: GPIO7 - rx_pin: GPIO8 + rx_pin: GPIO9 <<: !include common.yaml diff --git a/tests/components/esp32_can/test.esp32-c6-idf.yaml b/tests/components/esp32_can/test.esp32-c6-idf.yaml new file mode 100644 index 0000000000..6ef730c378 --- /dev/null +++ b/tests/components/esp32_can/test.esp32-c6-idf.yaml @@ -0,0 +1,89 @@ +esphome: + on_boot: + then: + - canbus.send: + # Extended ID explicit + canbus_id: esp32_internal_can + use_extended_id: true + can_id: 0x100 + data: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08] + - canbus.send: + # Standard ID by default + canbus_id: esp32_internal_can + can_id: 0x100 + data: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08] + - canbus.send: + # Extended ID explicit + canbus_id: esp32_internal_can_2 + use_extended_id: true + can_id: 0x100 + data: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08] + - canbus.send: + # Standard ID by default + canbus_id: esp32_internal_can_2 + can_id: 0x100 + data: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08] + +canbus: + - platform: esp32_can + id: esp32_internal_can + rx_pin: GPIO8 + tx_pin: GPIO7 + can_id: 4 + bit_rate: 50kbps + on_frame: + - can_id: 500 + then: + - lambda: |- + std::string b(x.begin(), x.end()); + ESP_LOGD("canbus1", "canid 500 %s", b.c_str() ); + - can_id: 0b00000000000000000000001000000 + can_id_mask: 0b11111000000000011111111000000 + use_extended_id: true + then: + - lambda: |- + auto pdo_id = can_id >> 14; + switch (pdo_id) + { + case 117: + ESP_LOGD("canbus1", "exhaust_fan_duty"); + break; + case 118: + ESP_LOGD("canbus1", "supply_fan_duty"); + break; + case 119: + ESP_LOGD("canbus1", "supply_fan_flow"); + break; + // to be continued... + } + - platform: esp32_can + id: esp32_internal_can_2 + rx_pin: GPIO10 + tx_pin: GPIO9 + can_id: 4 + bit_rate: 50kbps + on_frame: + - can_id: 500 + then: + - lambda: |- + std::string b(x.begin(), x.end()); + ESP_LOGD("canbus2", "canid 500 %s", b.c_str() ); + - can_id: 0b00000000000000000000001000000 + can_id_mask: 0b11111000000000011111111000000 + use_extended_id: true + then: + - lambda: |- + auto pdo_id = can_id >> 14; + switch (pdo_id) + { + case 117: + ESP_LOGD("canbus2", "exhaust_fan_duty"); + break; + case 118: + ESP_LOGD("canbus2", "supply_fan_duty"); + break; + case 119: + ESP_LOGD("canbus2", "supply_fan_flow"); + break; + // to be continued... + } diff --git a/tests/components/esp32_dac/test.esp32-ard.yaml b/tests/components/esp32_dac/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/esp32_dac/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/esp32_hosted/.gitattributes b/tests/components/esp32_hosted/.gitattributes new file mode 100644 index 0000000000..6abdc56117 --- /dev/null +++ b/tests/components/esp32_hosted/.gitattributes @@ -0,0 +1 @@ +*.bin -text diff --git a/tests/components/esp32_hosted/test.esp32-p4-idf.yaml b/tests/components/esp32_hosted/test.esp32-p4-idf.yaml index dade44d145..2a76f17e2f 100644 --- a/tests/components/esp32_hosted/test.esp32-p4-idf.yaml +++ b/tests/components/esp32_hosted/test.esp32-p4-idf.yaml @@ -1 +1,7 @@ <<: !include common.yaml + +update: + - platform: esp32_hosted + name: "Coprocessor Firmware Update" + path: $component_dir/test_firmware.bin + sha256: de2f256064a0af797747c2b97505dc0b9f3df0de4f489eac731c23ae9ca9cc31 diff --git a/tests/components/esp32_hosted/test_firmware.bin b/tests/components/esp32_hosted/test_firmware.bin new file mode 100644 index 0000000000..c97c12f9b0 Binary files /dev/null and b/tests/components/esp32_hosted/test_firmware.bin differ diff --git a/tests/components/esp32_improv/common.yaml b/tests/components/esp32_improv/common.yaml index 7eb3f9c0be..7dc2f7b6c7 100644 --- a/tests/components/esp32_improv/common.yaml +++ b/tests/components/esp32_improv/common.yaml @@ -16,3 +16,4 @@ esp32_improv: authorizer: io0_button authorized_duration: 1min status_indicator: built_in_led + next_url: "https://example.com/setup?device={{device_name}}&ip={{ip_address}}&version={{esphome_version}}" diff --git a/tests/components/esp32_improv/test.esp32-ard.yaml b/tests/components/esp32_improv/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/esp32_improv/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/esp32_improv/test.esp32-c3-ard.yaml b/tests/components/esp32_improv/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/esp32_improv/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/esp32_rmt_led_strip/test.esp32-ard.yaml b/tests/components/esp32_rmt_led_strip/test.esp32-ard.yaml deleted file mode 100644 index 0949b676d5..0000000000 --- a/tests/components/esp32_rmt_led_strip/test.esp32-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - pin1: GPIO13 - pin2: GPIO14 - -packages: - common: !include common.yaml diff --git a/tests/components/esp32_rmt_led_strip/test.esp32-c3-ard.yaml b/tests/components/esp32_rmt_led_strip/test.esp32-c3-ard.yaml deleted file mode 100644 index 6cc0667e77..0000000000 --- a/tests/components/esp32_rmt_led_strip/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - pin1: GPIO3 - pin2: GPIO4 - -packages: - common: !include common.yaml diff --git a/tests/components/esp32_rmt_led_strip/test.esp32-s3-idf.yaml b/tests/components/esp32_rmt_led_strip/test.esp32-s3-idf.yaml index ad273903b2..6bf0639a52 100644 --- a/tests/components/esp32_rmt_led_strip/test.esp32-s3-idf.yaml +++ b/tests/components/esp32_rmt_led_strip/test.esp32-s3-idf.yaml @@ -2,11 +2,22 @@ substitutions: pin1: GPIO3 pin2: GPIO4 -packages: - common: !include common.yaml - +# WARNING: Using !extend or !remove prevents automatic component grouping in CI, making builds slower. light: - - id: !extend led_strip1 + - platform: esp32_rmt_led_strip + id: led_strip1 + pin: ${pin1} + num_leds: 60 + rgb_order: GRB + chipset: ws2812 use_dma: "true" - - id: !extend led_strip2 + - platform: esp32_rmt_led_strip + id: led_strip2 + pin: ${pin2} + num_leds: 60 + rgb_order: RGB + bit0_high: 100us + bit0_low: 100us + bit1_high: 100us + bit1_low: 100us use_dma: "false" diff --git a/tests/components/esp32_touch/common-get-value.yaml b/tests/components/esp32_touch/common-get-value.yaml new file mode 100644 index 0000000000..4066303797 --- /dev/null +++ b/tests/components/esp32_touch/common-get-value.yaml @@ -0,0 +1,18 @@ +binary_sensor: + - platform: esp32_touch + name: ESP32 Touch Pad Get Value + pin: ${pin} + threshold: 1000 + id: esp32_touch_pad_get_value + on_press: + then: + - lambda: |- + // Test that get_value() compiles and works + uint32_t value = id(esp32_touch_pad_get_value).get_value(); + ESP_LOGD("test", "Touch value on press: %u", value); + on_release: + then: + - lambda: |- + // Test get_value() on release + uint32_t value = id(esp32_touch_pad_get_value).get_value(); + ESP_LOGD("test", "Touch value on release: %u", value); diff --git a/tests/components/esp32_touch/test.esp32-ard.yaml b/tests/components/esp32_touch/test.esp32-ard.yaml deleted file mode 100644 index 25316b8646..0000000000 --- a/tests/components/esp32_touch/test.esp32-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO27 - -<<: !include common.yaml diff --git a/tests/components/esp32_touch/test.esp32-idf.yaml b/tests/components/esp32_touch/test.esp32-idf.yaml index 25316b8646..5158613eb1 100644 --- a/tests/components/esp32_touch/test.esp32-idf.yaml +++ b/tests/components/esp32_touch/test.esp32-idf.yaml @@ -2,3 +2,4 @@ substitutions: pin: GPIO27 <<: !include common.yaml +<<: !include common-get-value.yaml diff --git a/tests/components/esp32_touch/test.esp32-s2-ard.yaml b/tests/components/esp32_touch/test.esp32-s2-ard.yaml deleted file mode 100644 index 575d758fae..0000000000 --- a/tests/components/esp32_touch/test.esp32-s2-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO12 - -<<: !include common-variants.yaml diff --git a/tests/components/esp32_touch/test.esp32-s2-idf.yaml b/tests/components/esp32_touch/test.esp32-s2-idf.yaml index 575d758fae..b9f5671969 100644 --- a/tests/components/esp32_touch/test.esp32-s2-idf.yaml +++ b/tests/components/esp32_touch/test.esp32-s2-idf.yaml @@ -2,3 +2,4 @@ substitutions: pin: GPIO12 <<: !include common-variants.yaml +<<: !include common-get-value.yaml diff --git a/tests/components/esp32_touch/test.esp32-s3-ard.yaml b/tests/components/esp32_touch/test.esp32-s3-ard.yaml deleted file mode 100644 index 575d758fae..0000000000 --- a/tests/components/esp32_touch/test.esp32-s3-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO12 - -<<: !include common-variants.yaml diff --git a/tests/components/esp32_touch/test.esp32-s3-idf.yaml b/tests/components/esp32_touch/test.esp32-s3-idf.yaml index 575d758fae..b9f5671969 100644 --- a/tests/components/esp32_touch/test.esp32-s3-idf.yaml +++ b/tests/components/esp32_touch/test.esp32-s3-idf.yaml @@ -2,3 +2,4 @@ substitutions: pin: GPIO12 <<: !include common-variants.yaml +<<: !include common-get-value.yaml diff --git a/tests/components/esp8266/test.esp8266-ard.yaml b/tests/components/esp8266/test.esp8266-ard.yaml new file mode 100644 index 0000000000..039a261016 --- /dev/null +++ b/tests/components/esp8266/test.esp8266-ard.yaml @@ -0,0 +1,13 @@ +logger: + level: VERBOSE + +esphome: + on_boot: + - lambda: |- + int x = 100; + x = clamp(x, 50, 90); + assert(x == 90); + x = clamp_at_least(x, 95); + assert(x == 95); + x = clamp_at_most(x, 40); + assert(x == 40); diff --git a/tests/components/esphome/common.yaml b/tests/components/esphome/common.yaml index a4b309b69d..db75b08b38 100644 --- a/tests/components/esphome/common.yaml +++ b/tests/components/esphome/common.yaml @@ -2,6 +2,9 @@ esphome: debug_scheduler: true platformio_options: board_build.flash_mode: dio + environment_variables: + TEST_ENV_VAR: "test_value" + BUILD_NUMBER: "12345" area: id: testing_area name: Testing Area @@ -10,7 +13,11 @@ esphome: on_shutdown: logger.log: on_shutdown on_loop: - logger.log: on_loop + if: + condition: + component.is_idle: binary_sensor_id + then: + logger.log: on_loop - sensor idle compile_process_limit: 1 min_version: "2025.1" name_add_mac_suffix: true @@ -34,5 +41,6 @@ esphome: binary_sensor: - platform: template + id: binary_sensor_id name: Other device sensor device_id: other_device diff --git a/tests/components/esphome/test.esp32-ard.yaml b/tests/components/esphome/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/esphome/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/esphome/test.esp32-c3-ard.yaml b/tests/components/esphome/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/esphome/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/esphome/test.esp32-c3-idf.yaml b/tests/components/esphome/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/esphome/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/anova/test.esp32-ard.yaml b/tests/components/esphome/test.nrf52-adafruit.yaml similarity index 100% rename from tests/components/anova/test.esp32-ard.yaml rename to tests/components/esphome/test.nrf52-adafruit.yaml diff --git a/tests/components/anova/test.esp32-c3-ard.yaml b/tests/components/esphome/test.nrf52-mcumgr.yaml similarity index 100% rename from tests/components/anova/test.esp32-c3-ard.yaml rename to tests/components/esphome/test.nrf52-mcumgr.yaml diff --git a/tests/components/espnow/common.yaml b/tests/components/espnow/common.yaml index abb31c12b8..895ffb9d15 100644 --- a/tests/components/espnow/common.yaml +++ b/tests/components/espnow/common.yaml @@ -1,4 +1,5 @@ espnow: + id: espnow_component auto_add_peer: false channel: 1 peers: @@ -50,3 +51,26 @@ espnow: - format_mac_address_pretty(info.src_addr).c_str() - format_hex_pretty(data, size).c_str() - info.rx_ctrl->rssi + +packet_transport: + - platform: espnow + id: transport1 + espnow_id: espnow_component + peer_address: "FF:FF:FF:FF:FF:FF" + encryption: + key: "0123456789abcdef0123456789abcdef" + sensors: + - temp_sensor + providers: + - name: test_provider + encryption: + key: "0123456789abcdef0123456789abcdef" + +sensor: + - platform: internal_temperature + id: temp_sensor + + - platform: packet_transport + provider: test_provider + remote_id: temp_sensor + id: remote_temp diff --git a/tests/components/espnow/test.esp32-idf.yaml b/tests/components/espnow/test.esp32-idf.yaml index dade44d145..b47e39c389 100644 --- a/tests/components/espnow/test.esp32-idf.yaml +++ b/tests/components/espnow/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/ethernet/common-dm9051.yaml b/tests/components/ethernet/common-dm9051.yaml index c878ca6e59..4526e7732d 100644 --- a/tests/components/ethernet/common-dm9051.yaml +++ b/tests/components/ethernet/common-dm9051.yaml @@ -12,3 +12,4 @@ ethernet: gateway: 192.168.178.1 subnet: 255.255.255.0 domain: .local + mac_address: "02:AA:BB:CC:DD:01" diff --git a/tests/components/ethernet/common-dp83848.yaml b/tests/components/ethernet/common-dp83848.yaml index 140c7d0d1b..f9069c5fb9 100644 --- a/tests/components/ethernet/common-dp83848.yaml +++ b/tests/components/ethernet/common-dp83848.yaml @@ -1,14 +1,15 @@ ethernet: type: DP83848 mdc_pin: 23 - mdio_pin: 25 + mdio_pin: 32 clk: pin: 0 mode: CLK_EXT_IN phy_addr: 0 - power_pin: 26 + power_pin: 33 manual_ip: static_ip: 192.168.178.56 gateway: 192.168.178.1 subnet: 255.255.255.0 domain: .local + mac_address: "02:AA:BB:CC:DD:01" diff --git a/tests/components/ethernet/common-ip101.yaml b/tests/components/ethernet/common-ip101.yaml index b5589220de..cea7a5cc35 100644 --- a/tests/components/ethernet/common-ip101.yaml +++ b/tests/components/ethernet/common-ip101.yaml @@ -1,14 +1,15 @@ ethernet: type: IP101 mdc_pin: 23 - mdio_pin: 25 + mdio_pin: 32 clk: pin: 0 mode: CLK_EXT_IN phy_addr: 0 - power_pin: 26 + power_pin: 33 manual_ip: static_ip: 192.168.178.56 gateway: 192.168.178.1 subnet: 255.255.255.0 domain: .local + mac_address: "02:AA:BB:CC:DD:01" diff --git a/tests/components/ethernet/common-jl1101.yaml b/tests/components/ethernet/common-jl1101.yaml index 2ada9495a0..7b0a2dfdc4 100644 --- a/tests/components/ethernet/common-jl1101.yaml +++ b/tests/components/ethernet/common-jl1101.yaml @@ -1,14 +1,15 @@ ethernet: type: JL1101 mdc_pin: 23 - mdio_pin: 25 + mdio_pin: 32 clk: pin: 0 mode: CLK_EXT_IN phy_addr: 0 - power_pin: 26 + power_pin: 33 manual_ip: static_ip: 192.168.178.56 gateway: 192.168.178.1 subnet: 255.255.255.0 domain: .local + mac_address: "02:AA:BB:CC:DD:01" diff --git a/tests/components/ethernet/common-ksz8081.yaml b/tests/components/ethernet/common-ksz8081.yaml index 7da8adb09a..65541832c2 100644 --- a/tests/components/ethernet/common-ksz8081.yaml +++ b/tests/components/ethernet/common-ksz8081.yaml @@ -1,14 +1,15 @@ ethernet: type: KSZ8081 mdc_pin: 23 - mdio_pin: 25 + mdio_pin: 32 clk: pin: 0 mode: CLK_EXT_IN phy_addr: 0 - power_pin: 26 + power_pin: 33 manual_ip: static_ip: 192.168.178.56 gateway: 192.168.178.1 subnet: 255.255.255.0 domain: .local + mac_address: "02:AA:BB:CC:DD:01" diff --git a/tests/components/ethernet/common-ksz8081rna.yaml b/tests/components/ethernet/common-ksz8081rna.yaml index df04f06132..f04cba15b2 100644 --- a/tests/components/ethernet/common-ksz8081rna.yaml +++ b/tests/components/ethernet/common-ksz8081rna.yaml @@ -1,14 +1,15 @@ ethernet: type: KSZ8081RNA mdc_pin: 23 - mdio_pin: 25 + mdio_pin: 32 clk: pin: 0 mode: CLK_EXT_IN phy_addr: 0 - power_pin: 26 + power_pin: 33 manual_ip: static_ip: 192.168.178.56 gateway: 192.168.178.1 subnet: 255.255.255.0 domain: .local + mac_address: "02:AA:BB:CC:DD:01" diff --git a/tests/components/ethernet/common-lan8670.yaml b/tests/components/ethernet/common-lan8670.yaml new file mode 100644 index 0000000000..fb751ebd23 --- /dev/null +++ b/tests/components/ethernet/common-lan8670.yaml @@ -0,0 +1,14 @@ +ethernet: + type: LAN8670 + mdc_pin: 23 + mdio_pin: 32 + clk: + pin: 0 + mode: CLK_EXT_IN + phy_addr: 0 + power_pin: 33 + manual_ip: + static_ip: 192.168.178.56 + gateway: 192.168.178.1 + subnet: 255.255.255.0 + domain: .local diff --git a/tests/components/ethernet/common-lan8720.yaml b/tests/components/ethernet/common-lan8720.yaml index f227752f42..838d57df28 100644 --- a/tests/components/ethernet/common-lan8720.yaml +++ b/tests/components/ethernet/common-lan8720.yaml @@ -1,14 +1,15 @@ ethernet: type: LAN8720 mdc_pin: 23 - mdio_pin: 25 + mdio_pin: 32 clk: pin: 0 mode: CLK_EXT_IN phy_addr: 0 - power_pin: 26 + power_pin: 33 manual_ip: static_ip: 192.168.178.56 gateway: 192.168.178.1 subnet: 255.255.255.0 domain: .local + mac_address: "02:AA:BB:CC:DD:01" diff --git a/tests/components/ethernet/common-rtl8201.yaml b/tests/components/ethernet/common-rtl8201.yaml index 7c9c9d913c..0e7cbe73c6 100644 --- a/tests/components/ethernet/common-rtl8201.yaml +++ b/tests/components/ethernet/common-rtl8201.yaml @@ -1,14 +1,15 @@ ethernet: type: RTL8201 mdc_pin: 23 - mdio_pin: 25 + mdio_pin: 32 clk: pin: 0 mode: CLK_EXT_IN phy_addr: 0 - power_pin: 26 + power_pin: 33 manual_ip: static_ip: 192.168.178.56 gateway: 192.168.178.1 subnet: 255.255.255.0 domain: .local + mac_address: "02:AA:BB:CC:DD:01" diff --git a/tests/components/ethernet/common-w5500.yaml b/tests/components/ethernet/common-w5500.yaml index 76661a75c3..b3e96f000d 100644 --- a/tests/components/ethernet/common-w5500.yaml +++ b/tests/components/ethernet/common-w5500.yaml @@ -12,3 +12,4 @@ ethernet: gateway: 192.168.178.1 subnet: 255.255.255.0 domain: .local + mac_address: "02:AA:BB:CC:DD:01" diff --git a/tests/components/ethernet/test-dm9051.esp32-ard.yaml b/tests/components/ethernet/test-dm9051.esp32-ard.yaml deleted file mode 100644 index 23e3b97740..0000000000 --- a/tests/components/ethernet/test-dm9051.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common-dm9051.yaml diff --git a/tests/components/ethernet/test-dp83848.esp32-ard.yaml b/tests/components/ethernet/test-dp83848.esp32-ard.yaml deleted file mode 100644 index 906bfba17c..0000000000 --- a/tests/components/ethernet/test-dp83848.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common-dp83848.yaml diff --git a/tests/components/ethernet/test-ip101.esp32-ard.yaml b/tests/components/ethernet/test-ip101.esp32-ard.yaml deleted file mode 100644 index e52329d7ea..0000000000 --- a/tests/components/ethernet/test-ip101.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common-ip101.yaml diff --git a/tests/components/ethernet/test-ksz8081.esp32-ard.yaml b/tests/components/ethernet/test-ksz8081.esp32-ard.yaml deleted file mode 100644 index 8f3c750c77..0000000000 --- a/tests/components/ethernet/test-ksz8081.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common-ksz8081.yaml diff --git a/tests/components/ethernet/test-ksz8081rna.esp32-ard.yaml b/tests/components/ethernet/test-ksz8081rna.esp32-ard.yaml deleted file mode 100644 index a48e591996..0000000000 --- a/tests/components/ethernet/test-ksz8081rna.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common-ksz8081rna.yaml diff --git a/tests/components/ethernet/test-lan8670.esp32-idf.yaml b/tests/components/ethernet/test-lan8670.esp32-idf.yaml new file mode 100644 index 0000000000..914a06ae88 --- /dev/null +++ b/tests/components/ethernet/test-lan8670.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common-lan8670.yaml diff --git a/tests/components/ethernet/test-lan8720.esp32-ard.yaml b/tests/components/ethernet/test-lan8720.esp32-ard.yaml deleted file mode 100644 index 3df9ac874a..0000000000 --- a/tests/components/ethernet/test-lan8720.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common-lan8720.yaml diff --git a/tests/components/ethernet/test-rtl8201.esp32-ard.yaml b/tests/components/ethernet/test-rtl8201.esp32-ard.yaml deleted file mode 100644 index e69f88dc94..0000000000 --- a/tests/components/ethernet/test-rtl8201.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common-rtl8201.yaml diff --git a/tests/components/ethernet/test-w5500.esp32-ard.yaml b/tests/components/ethernet/test-w5500.esp32-ard.yaml deleted file mode 100644 index 36f1b5365f..0000000000 --- a/tests/components/ethernet/test-w5500.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common-w5500.yaml diff --git a/tests/components/ethernet_info/common.yaml b/tests/components/ethernet_info/common.yaml index f45f345316..b720521d10 100644 --- a/tests/components/ethernet_info/common.yaml +++ b/tests/components/ethernet_info/common.yaml @@ -1,12 +1,12 @@ ethernet: type: LAN8720 mdc_pin: 23 - mdio_pin: 25 + mdio_pin: 32 clk: pin: 0 mode: CLK_EXT_IN phy_addr: 0 - power_pin: 26 + power_pin: 33 manual_ip: static_ip: 192.168.178.56 gateway: 192.168.178.1 diff --git a/tests/components/ethernet_info/test.esp32-ard.yaml b/tests/components/ethernet_info/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/ethernet_info/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/ethernet_info/test.esp32-idf.yaml b/tests/components/ethernet_info/test.esp32-idf.yaml index dade44d145..b47e39c389 100644 --- a/tests/components/ethernet_info/test.esp32-idf.yaml +++ b/tests/components/ethernet_info/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/event/test.esp32-ard.yaml b/tests/components/event/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/event/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/event/test.esp32-c3-ard.yaml b/tests/components/event/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/event/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/event/test.esp32-c3-idf.yaml b/tests/components/event/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/event/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/anova/test.esp32-c3-idf.yaml b/tests/components/event/test.nrf52-adafruit.yaml similarity index 100% rename from tests/components/anova/test.esp32-c3-idf.yaml rename to tests/components/event/test.nrf52-adafruit.yaml diff --git a/tests/components/atc_mithermometer/test.esp32-ard.yaml b/tests/components/event/test.nrf52-mcumgr.yaml similarity index 100% rename from tests/components/atc_mithermometer/test.esp32-ard.yaml rename to tests/components/event/test.nrf52-mcumgr.yaml diff --git a/tests/components/exposure_notifications/test.esp32-ard.yaml b/tests/components/exposure_notifications/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/exposure_notifications/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/exposure_notifications/test.esp32-c3-ard.yaml b/tests/components/exposure_notifications/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/exposure_notifications/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/exposure_notifications/test.esp32-c3-idf.yaml b/tests/components/exposure_notifications/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/exposure_notifications/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/exposure_notifications/test.esp32-idf.yaml b/tests/components/exposure_notifications/test.esp32-idf.yaml index dade44d145..7a6541ae76 100644 --- a/tests/components/exposure_notifications/test.esp32-idf.yaml +++ b/tests/components/exposure_notifications/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/external_components/test.esp32-ard.yaml b/tests/components/external_components/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/external_components/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/external_components/test.esp32-c3-ard.yaml b/tests/components/external_components/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/external_components/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/external_components/test.esp32-c3-idf.yaml b/tests/components/external_components/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/external_components/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/ezo/common.yaml b/tests/components/ezo/common.yaml index edeebd1a12..79afbb8aa6 100644 --- a/tests/components/ezo/common.yaml +++ b/tests/components/ezo/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_ezo - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: ezo + i2c_id: i2c_bus id: ph_ezo address: 99 unit_of_measurement: pH diff --git a/tests/components/ezo/test.esp32-ard.yaml b/tests/components/ezo/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/ezo/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/ezo/test.esp32-c3-ard.yaml b/tests/components/ezo/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/ezo/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ezo/test.esp32-c3-idf.yaml b/tests/components/ezo/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/ezo/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ezo/test.esp32-idf.yaml b/tests/components/ezo/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/ezo/test.esp32-idf.yaml +++ b/tests/components/ezo/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/ezo/test.esp8266-ard.yaml b/tests/components/ezo/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/ezo/test.esp8266-ard.yaml +++ b/tests/components/ezo/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/ezo/test.rp2040-ard.yaml b/tests/components/ezo/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/ezo/test.rp2040-ard.yaml +++ b/tests/components/ezo/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/ezo_pmp/common.yaml b/tests/components/ezo_pmp/common.yaml index 00919797be..bd2fd9193c 100644 --- a/tests/components/ezo_pmp/common.yaml +++ b/tests/components/ezo_pmp/common.yaml @@ -1,9 +1,5 @@ -i2c: - - id: i2c_ezo_pmp - scl: ${scl_pin} - sda: ${sda_pin} - ezo_pmp: + i2c_id: i2c_bus id: hcl_pump update_interval: 1s diff --git a/tests/components/ezo_pmp/test.esp32-ard.yaml b/tests/components/ezo_pmp/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/ezo_pmp/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/ezo_pmp/test.esp32-c3-ard.yaml b/tests/components/ezo_pmp/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/ezo_pmp/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ezo_pmp/test.esp32-c3-idf.yaml b/tests/components/ezo_pmp/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/ezo_pmp/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ezo_pmp/test.esp32-idf.yaml b/tests/components/ezo_pmp/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/ezo_pmp/test.esp32-idf.yaml +++ b/tests/components/ezo_pmp/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/ezo_pmp/test.esp8266-ard.yaml b/tests/components/ezo_pmp/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/ezo_pmp/test.esp8266-ard.yaml +++ b/tests/components/ezo_pmp/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/ezo_pmp/test.rp2040-ard.yaml b/tests/components/ezo_pmp/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/ezo_pmp/test.rp2040-ard.yaml +++ b/tests/components/ezo_pmp/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/factory_reset/test.esp32-ard.yaml b/tests/components/factory_reset/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/factory_reset/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/factory_reset/test.esp32-c3-ard.yaml b/tests/components/factory_reset/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/factory_reset/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/factory_reset/test.esp32-c3-idf.yaml b/tests/components/factory_reset/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/factory_reset/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/fan/common.yaml b/tests/components/fan/common.yaml new file mode 100644 index 0000000000..55c2a656fd --- /dev/null +++ b/tests/components/fan/common.yaml @@ -0,0 +1,11 @@ +fan: + - platform: template + id: test_fan + name: "Test Fan" + preset_modes: + - Eco + - Sleep + - Turbo + has_oscillating: true + has_direction: true + speed_count: 3 diff --git a/tests/components/atc_mithermometer/test.esp32-c3-ard.yaml b/tests/components/fan/test.esp8266-ard.yaml similarity index 100% rename from tests/components/atc_mithermometer/test.esp32-c3-ard.yaml rename to tests/components/fan/test.esp8266-ard.yaml diff --git a/tests/components/feedback/test.esp32-ard.yaml b/tests/components/feedback/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/feedback/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/feedback/test.esp32-c3-ard.yaml b/tests/components/feedback/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/feedback/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/feedback/test.esp32-c3-idf.yaml b/tests/components/feedback/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/feedback/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/fingerprint_grow/common.yaml b/tests/components/fingerprint_grow/common.yaml index e759ea5a02..2d2fc61437 100644 --- a/tests/components/fingerprint_grow/common.yaml +++ b/tests/components/fingerprint_grow/common.yaml @@ -9,12 +9,6 @@ esphome: finger_id: 2 - fingerprint_grow.delete_all: -uart: - - id: uart_fingerprint_grow - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 57600 - fingerprint_grow: sensing_pin: ${sensing_pin} password: 0x12FE37DC diff --git a/tests/components/fingerprint_grow/test.esp32-ard.yaml b/tests/components/fingerprint_grow/test.esp32-ard.yaml deleted file mode 100644 index 4aef3d8be6..0000000000 --- a/tests/components/fingerprint_grow/test.esp32-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - tx_pin: GPIO12 - rx_pin: GPIO14 - sensing_pin: GPIO15 - -<<: !include common.yaml diff --git a/tests/components/fingerprint_grow/test.esp32-c3-ard.yaml b/tests/components/fingerprint_grow/test.esp32-c3-ard.yaml deleted file mode 100644 index faab50e152..0000000000 --- a/tests/components/fingerprint_grow/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - sensing_pin: GPIO6 - -<<: !include common.yaml diff --git a/tests/components/fingerprint_grow/test.esp32-c3-idf.yaml b/tests/components/fingerprint_grow/test.esp32-c3-idf.yaml deleted file mode 100644 index faab50e152..0000000000 --- a/tests/components/fingerprint_grow/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - sensing_pin: GPIO6 - -<<: !include common.yaml diff --git a/tests/components/fingerprint_grow/test.esp32-idf.yaml b/tests/components/fingerprint_grow/test.esp32-idf.yaml index 4aef3d8be6..7149f07d16 100644 --- a/tests/components/fingerprint_grow/test.esp32-idf.yaml +++ b/tests/components/fingerprint_grow/test.esp32-idf.yaml @@ -3,4 +3,7 @@ substitutions: rx_pin: GPIO14 sensing_pin: GPIO15 +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/fingerprint_grow/test.esp8266-ard.yaml b/tests/components/fingerprint_grow/test.esp8266-ard.yaml index f2a864596a..204fdf0302 100644 --- a/tests/components/fingerprint_grow/test.esp8266-ard.yaml +++ b/tests/components/fingerprint_grow/test.esp8266-ard.yaml @@ -1,6 +1,9 @@ substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: GPIO0 + rx_pin: GPIO2 sensing_pin: GPIO15 +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/fingerprint_grow/test.rp2040-ard.yaml b/tests/components/fingerprint_grow/test.rp2040-ard.yaml index faab50e152..80ea1d22ca 100644 --- a/tests/components/fingerprint_grow/test.rp2040-ard.yaml +++ b/tests/components/fingerprint_grow/test.rp2040-ard.yaml @@ -3,4 +3,7 @@ substitutions: rx_pin: GPIO5 sensing_pin: GPIO6 +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/font/common.yaml b/tests/components/font/common.yaml index fb50fc3336..c156b4aea1 100644 --- a/tests/components/font/common.yaml +++ b/tests/components/font/common.yaml @@ -21,12 +21,12 @@ font: id: roboto_greek size: 20 glyphs: ["\u0300", "\u00C5", "\U000000C7"] - - file: "https://github.com/IdreesInc/Monocraft/releases/download/v3.0/Monocraft.ttf" + - file: "https://media.esphome.io/tests/fonts/Monocraft.ttf" id: monocraft size: 20 - file: type: web - url: "https://github.com/IdreesInc/Monocraft/releases/download/v3.0/Monocraft.ttf" + url: "https://media.esphome.io/tests/fonts/Monocraft.ttf" id: monocraft2 size: 24 - file: $component_dir/Monocraft.ttf @@ -47,12 +47,9 @@ font: id: bdf_font size: 7 -i2c: - scl: ${i2c_scl} - sda: ${i2c_sda} - display: - platform: ssd1306_i2c + i2c_id: i2c_bus id: ssd1306_display model: SSD1306_128X64 reset_pin: ${display_reset_pin} diff --git a/tests/components/font/test.esp32-ard.yaml b/tests/components/font/test.esp32-ard.yaml deleted file mode 100644 index d98600a51b..0000000000 --- a/tests/components/font/test.esp32-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - i2c_scl: GPIO16 - i2c_sda: GPIO17 - display_reset_pin: GPIO13 - -packages: - common: !include common.yaml diff --git a/tests/components/font/test.esp32-c3-ard.yaml b/tests/components/font/test.esp32-c3-ard.yaml deleted file mode 100644 index ad14a2e9a6..0000000000 --- a/tests/components/font/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - i2c_scl: GPIO5 - i2c_sda: GPIO4 - display_reset_pin: GPIO3 - -packages: - common: !include common.yaml diff --git a/tests/components/font/test.esp32-c3-idf.yaml b/tests/components/font/test.esp32-c3-idf.yaml deleted file mode 100644 index ad14a2e9a6..0000000000 --- a/tests/components/font/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - i2c_scl: GPIO5 - i2c_sda: GPIO4 - display_reset_pin: GPIO3 - -packages: - common: !include common.yaml diff --git a/tests/components/font/test.esp32-idf.yaml b/tests/components/font/test.esp32-idf.yaml index d98600a51b..a6a94a39da 100644 --- a/tests/components/font/test.esp32-idf.yaml +++ b/tests/components/font/test.esp32-idf.yaml @@ -1,7 +1,7 @@ substitutions: - i2c_scl: GPIO16 - i2c_sda: GPIO17 display_reset_pin: GPIO13 packages: - common: !include common.yaml + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/font/test.esp8266-ard.yaml b/tests/components/font/test.esp8266-ard.yaml index ad14a2e9a6..173f7dfaa5 100644 --- a/tests/components/font/test.esp8266-ard.yaml +++ b/tests/components/font/test.esp8266-ard.yaml @@ -1,7 +1,7 @@ substitutions: - i2c_scl: GPIO5 - i2c_sda: GPIO4 display_reset_pin: GPIO3 packages: - common: !include common.yaml + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/font/test.host.yaml b/tests/components/font/test.host.yaml index c5399f2826..387ea47335 100644 --- a/tests/components/font/test.host.yaml +++ b/tests/components/font/test.host.yaml @@ -21,12 +21,12 @@ font: id: roboto_greek size: 20 glyphs: ["\u0300", "\u00C5", "\U000000C7"] - - file: "https://github.com/IdreesInc/Monocraft/releases/download/v3.0/Monocraft.ttf" + - file: "https://media.esphome.io/tests/fonts/Monocraft.ttf" id: monocraft size: 20 - file: type: web - url: "https://github.com/IdreesInc/Monocraft/releases/download/v3.0/Monocraft.ttf" + url: "https://media.esphome.io/tests/fonts/Monocraft.ttf" id: monocraft2 size: 24 - file: $component_dir/Monocraft.ttf diff --git a/tests/components/font/test.rp2040-ard.yaml b/tests/components/font/test.rp2040-ard.yaml index ad14a2e9a6..aeecf352b4 100644 --- a/tests/components/font/test.rp2040-ard.yaml +++ b/tests/components/font/test.rp2040-ard.yaml @@ -1,7 +1,7 @@ substitutions: - i2c_scl: GPIO5 - i2c_sda: GPIO4 display_reset_pin: GPIO3 packages: - common: !include common.yaml + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/fs3000/common.yaml b/tests/components/fs3000/common.yaml index e87ac28aa9..e8ec4d0773 100644 --- a/tests/components/fs3000/common.yaml +++ b/tests/components/fs3000/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_fs3000 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: fs3000 + i2c_id: i2c_bus name: Air Velocity model: 1005 update_interval: 60s diff --git a/tests/components/fs3000/test.esp32-ard.yaml b/tests/components/fs3000/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/fs3000/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/fs3000/test.esp32-c3-ard.yaml b/tests/components/fs3000/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/fs3000/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/fs3000/test.esp32-c3-idf.yaml b/tests/components/fs3000/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/fs3000/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/fs3000/test.esp32-idf.yaml b/tests/components/fs3000/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/fs3000/test.esp32-idf.yaml +++ b/tests/components/fs3000/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/fs3000/test.esp8266-ard.yaml b/tests/components/fs3000/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/fs3000/test.esp8266-ard.yaml +++ b/tests/components/fs3000/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/fs3000/test.rp2040-ard.yaml b/tests/components/fs3000/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/fs3000/test.rp2040-ard.yaml +++ b/tests/components/fs3000/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/ft5x06/common.yaml b/tests/components/ft5x06/common.yaml index 322ee88b6d..b1f7ca4cc9 100644 --- a/tests/components/ft5x06/common.yaml +++ b/tests/components/ft5x06/common.yaml @@ -1,20 +1,19 @@ -i2c: - - id: i2c_ft5x06 - scl: ${scl_pin} - sda: ${sda_pin} - display: - platform: ssd1306_i2c - id: ssd1306_display + i2c_id: i2c_bus + id: ft5x06_display model: SSD1306_128X64 reset_pin: ${reset_pin} pages: - - id: page1 + - id: ft5x06_page1 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); touchscreen: - platform: ft5x06 + i2c_id: i2c_bus + display: ft5x06_display + id: ft5x06_touchscreen on_touch: - logger.log: format: Touch at (%d, %d) diff --git a/tests/components/ft5x06/test.esp32-ard.yaml b/tests/components/ft5x06/test.esp32-ard.yaml deleted file mode 100644 index 1ca773e06c..0000000000 --- a/tests/components/ft5x06/test.esp32-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - reset_pin: GPIO15 - -<<: !include common.yaml diff --git a/tests/components/ft5x06/test.esp32-c3-ard.yaml b/tests/components/ft5x06/test.esp32-c3-ard.yaml deleted file mode 100644 index 1e6670c196..0000000000 --- a/tests/components/ft5x06/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - reset_pin: GPIO6 - -<<: !include common.yaml diff --git a/tests/components/ft5x06/test.esp32-c3-idf.yaml b/tests/components/ft5x06/test.esp32-c3-idf.yaml deleted file mode 100644 index 1e6670c196..0000000000 --- a/tests/components/ft5x06/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - reset_pin: GPIO6 - -<<: !include common.yaml diff --git a/tests/components/ft5x06/test.esp32-idf.yaml b/tests/components/ft5x06/test.esp32-idf.yaml index 1ca773e06c..4ff2241ec9 100644 --- a/tests/components/ft5x06/test.esp32-idf.yaml +++ b/tests/components/ft5x06/test.esp32-idf.yaml @@ -1,6 +1,7 @@ substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 reset_pin: GPIO15 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/ft5x06/test.esp8266-ard.yaml b/tests/components/ft5x06/test.esp8266-ard.yaml index dfdc12a3d1..b8bb94edde 100644 --- a/tests/components/ft5x06/test.esp8266-ard.yaml +++ b/tests/components/ft5x06/test.esp8266-ard.yaml @@ -1,6 +1,7 @@ substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 reset_pin: GPIO15 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/ft5x06/test.rp2040-ard.yaml b/tests/components/ft5x06/test.rp2040-ard.yaml index 1e6670c196..1bf10642c5 100644 --- a/tests/components/ft5x06/test.rp2040-ard.yaml +++ b/tests/components/ft5x06/test.rp2040-ard.yaml @@ -1,6 +1,7 @@ substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 reset_pin: GPIO6 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/ft63x6/common.yaml b/tests/components/ft63x6/common.yaml index 1fae6da5f4..ea5a60a2f6 100644 --- a/tests/components/ft63x6/common.yaml +++ b/tests/components/ft63x6/common.yaml @@ -1,25 +1,19 @@ -spi: - - id: spi_ft63x6 - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - -i2c: - - id: i2c_ft63x6 - scl: ${scl_pin} - sda: ${sda_pin} - display: - platform: ssd1306_i2c - id: ssd1306_display + i2c_id: i2c_bus + id: ft63x6_display model: SSD1306_128X64 reset_pin: ${reset_pin} pages: - - id: page1 + - id: ft63x6_page1 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); touchscreen: - platform: ft63x6 + i2c_id: i2c_bus + display: ft63x6_display + id: ft63x6_touchscreen interrupt_pin: ${interrupt_pin} transform: swap_xy: true @@ -42,6 +36,7 @@ touchscreen: binary_sensor: - platform: touchscreen + touchscreen_id: ft63x6_touchscreen name: Bottom Left Touch use_raw: true x_min: 0 diff --git a/tests/components/ft63x6/test.esp32-ard.yaml b/tests/components/ft63x6/test.esp32-ard.yaml deleted file mode 100644 index 47b5796e8b..0000000000 --- a/tests/components/ft63x6/test.esp32-ard.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - clk_pin: GPIO0 - mosi_pin: GPIO2 - scl_pin: GPIO13 - sda_pin: GPIO14 - interrupt_pin: GPIO15 - reset_pin: GPIO16 - -<<: !include common.yaml diff --git a/tests/components/ft63x6/test.esp32-c3-ard.yaml b/tests/components/ft63x6/test.esp32-c3-ard.yaml deleted file mode 100644 index 397ac1e464..0000000000 --- a/tests/components/ft63x6/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - scl_pin: GPIO0 - sda_pin: GPIO1 - interrupt_pin: GPIO2 - reset_pin: GPIO3 - -<<: !include common.yaml diff --git a/tests/components/ft63x6/test.esp32-c3-idf.yaml b/tests/components/ft63x6/test.esp32-c3-idf.yaml deleted file mode 100644 index 397ac1e464..0000000000 --- a/tests/components/ft63x6/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - scl_pin: GPIO0 - sda_pin: GPIO1 - interrupt_pin: GPIO2 - reset_pin: GPIO3 - -<<: !include common.yaml diff --git a/tests/components/ft63x6/test.esp32-idf.yaml b/tests/components/ft63x6/test.esp32-idf.yaml index 47b5796e8b..590f6a919c 100644 --- a/tests/components/ft63x6/test.esp32-idf.yaml +++ b/tests/components/ft63x6/test.esp32-idf.yaml @@ -1,9 +1,8 @@ substitutions: - clk_pin: GPIO0 - mosi_pin: GPIO2 - scl_pin: GPIO13 - sda_pin: GPIO14 interrupt_pin: GPIO15 - reset_pin: GPIO16 + reset_pin: GPIO4 + +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/ft63x6/test.esp8266-ard.yaml b/tests/components/ft63x6/test.esp8266-ard.yaml index a4223733af..3ac5c645e3 100644 --- a/tests/components/ft63x6/test.esp8266-ard.yaml +++ b/tests/components/ft63x6/test.esp8266-ard.yaml @@ -1,9 +1,8 @@ substitutions: - clk_pin: GPIO14 - mosi_pin: GPIO13 - scl_pin: GPIO5 - sda_pin: GPIO4 - interrupt_pin: GPIO12 + interrupt_pin: GPIO0 reset_pin: GPIO16 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/ft63x6/test.rp2040-ard.yaml b/tests/components/ft63x6/test.rp2040-ard.yaml index 397ac1e464..2dd70ff33a 100644 --- a/tests/components/ft63x6/test.rp2040-ard.yaml +++ b/tests/components/ft63x6/test.rp2040-ard.yaml @@ -1,9 +1,8 @@ substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - scl_pin: GPIO0 - sda_pin: GPIO1 interrupt_pin: GPIO2 reset_pin: GPIO3 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/fujitsu_general/common.yaml b/tests/components/fujitsu_general/common.yaml index 3359b89f2a..51bd1441c0 100644 --- a/tests/components/fujitsu_general/common.yaml +++ b/tests/components/fujitsu_general/common.yaml @@ -1,7 +1,4 @@ -remote_transmitter: - pin: ${pin} - carrier_duty_percent: 50% - climate: - platform: fujitsu_general name: Fujitsu General Climate + transmitter_id: xmitr diff --git a/tests/components/fujitsu_general/test.esp32-ard.yaml b/tests/components/fujitsu_general/test.esp32-ard.yaml deleted file mode 100644 index 7b012aa64c..0000000000 --- a/tests/components/fujitsu_general/test.esp32-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/fujitsu_general/test.esp32-c3-ard.yaml b/tests/components/fujitsu_general/test.esp32-c3-ard.yaml deleted file mode 100644 index 7b012aa64c..0000000000 --- a/tests/components/fujitsu_general/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/fujitsu_general/test.esp32-c3-idf.yaml b/tests/components/fujitsu_general/test.esp32-c3-idf.yaml deleted file mode 100644 index 7b012aa64c..0000000000 --- a/tests/components/fujitsu_general/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/fujitsu_general/test.esp32-idf.yaml b/tests/components/fujitsu_general/test.esp32-idf.yaml index 7b012aa64c..e891f9dc85 100644 --- a/tests/components/fujitsu_general/test.esp32-idf.yaml +++ b/tests/components/fujitsu_general/test.esp32-idf.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO2 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/fujitsu_general/test.esp8266-ard.yaml b/tests/components/fujitsu_general/test.esp8266-ard.yaml index f5097fcf5f..4bed2f03e5 100644 --- a/tests/components/fujitsu_general/test.esp8266-ard.yaml +++ b/tests/components/fujitsu_general/test.esp8266-ard.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO5 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/gcja5/common.yaml b/tests/components/gcja5/common.yaml index 8f6250045d..64374c0e50 100644 --- a/tests/components/gcja5/common.yaml +++ b/tests/components/gcja5/common.yaml @@ -1,12 +1,6 @@ -uart: - - id: uart_gcja5 - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 9600 - parity: EVEN - sensor: - platform: gcja5 + uart_id: uart_bus pm_1_0: name: "Particulate Matter <1.0µm Concentration" pm_2_5: diff --git a/tests/components/gcja5/test.esp32-ard.yaml b/tests/components/gcja5/test.esp32-ard.yaml deleted file mode 100644 index 811f6b72a6..0000000000 --- a/tests/components/gcja5/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO12 - rx_pin: GPIO14 - -<<: !include common.yaml diff --git a/tests/components/gcja5/test.esp32-c3-ard.yaml b/tests/components/gcja5/test.esp32-c3-ard.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/gcja5/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/gcja5/test.esp32-c3-idf.yaml b/tests/components/gcja5/test.esp32-c3-idf.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/gcja5/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/gcja5/test.esp32-idf.yaml b/tests/components/gcja5/test.esp32-idf.yaml index 811f6b72a6..5ce1861902 100644 --- a/tests/components/gcja5/test.esp32-idf.yaml +++ b/tests/components/gcja5/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - tx_pin: GPIO12 - rx_pin: GPIO14 +packages: + uart_9600_even: !include ../../test_build_components/common/uart_9600_even/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/gcja5/test.esp8266-ard.yaml b/tests/components/gcja5/test.esp8266-ard.yaml index b516342f3b..a3f8cf43d4 100644 --- a/tests/components/gcja5/test.esp8266-ard.yaml +++ b/tests/components/gcja5/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 +packages: + uart_9600_even: !include ../../test_build_components/common/uart_9600_even/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/gcja5/test.rp2040-ard.yaml b/tests/components/gcja5/test.rp2040-ard.yaml index b516342f3b..7c1f4f41e2 100644 --- a/tests/components/gcja5/test.rp2040-ard.yaml +++ b/tests/components/gcja5/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 +packages: + uart_9600_even: !include ../../test_build_components/common/uart_9600_even/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/gdk101/common.yaml b/tests/components/gdk101/common.yaml index 7805ad43f0..b9b93d760f 100644 --- a/tests/components/gdk101/common.yaml +++ b/tests/components/gdk101/common.yaml @@ -1,9 +1,5 @@ -i2c: - - id: i2c_gdk101 - scl: ${scl_pin} - sda: ${sda_pin} - gdk101: + i2c_id: i2c_bus id: my_gdk101 sensor: @@ -15,8 +11,6 @@ sensor: name: Radiation Dose @ 10 min status: name: Status - version: - name: FW Version measurement_duration: name: Measuring Time @@ -25,3 +19,8 @@ binary_sensor: gdk101_id: my_gdk101 vibrations: name: Vibrations + +text_sensor: + - platform: gdk101 + version: + name: FW Version diff --git a/tests/components/gdk101/test.esp32-ard.yaml b/tests/components/gdk101/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/gdk101/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/gdk101/test.esp32-idf.yaml b/tests/components/gdk101/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/gdk101/test.esp32-idf.yaml +++ b/tests/components/gdk101/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/gdk101/test.esp8266-ard.yaml b/tests/components/gdk101/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/gdk101/test.esp8266-ard.yaml +++ b/tests/components/gdk101/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/gdk101/test.rp2040-ard.yaml b/tests/components/gdk101/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/gdk101/test.rp2040-ard.yaml +++ b/tests/components/gdk101/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/gl_r01_i2c/common.yaml b/tests/components/gl_r01_i2c/common.yaml index fe0705bdc6..272a94d349 100644 --- a/tests/components/gl_r01_i2c/common.yaml +++ b/tests/components/gl_r01_i2c/common.yaml @@ -1,12 +1,7 @@ -i2c: - - id: i2c_gl_r01_i2c - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: gl_r01_i2c + i2c_id: i2c_bus id: tof name: "ToF sensor" - i2c_id: i2c_gl_r01_i2c address: 0x74 update_interval: 15s diff --git a/tests/components/gl_r01_i2c/test.esp32-ard.yaml b/tests/components/gl_r01_i2c/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/gl_r01_i2c/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/gl_r01_i2c/test.esp32-c3-ard.yaml b/tests/components/gl_r01_i2c/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/gl_r01_i2c/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/gl_r01_i2c/test.esp32-c3-idf.yaml b/tests/components/gl_r01_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/gl_r01_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/gl_r01_i2c/test.esp32-idf.yaml b/tests/components/gl_r01_i2c/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/gl_r01_i2c/test.esp32-idf.yaml +++ b/tests/components/gl_r01_i2c/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/gl_r01_i2c/test.esp8266-ard.yaml b/tests/components/gl_r01_i2c/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/gl_r01_i2c/test.esp8266-ard.yaml +++ b/tests/components/gl_r01_i2c/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/gl_r01_i2c/test.rp2040-ard.yaml b/tests/components/gl_r01_i2c/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/gl_r01_i2c/test.rp2040-ard.yaml +++ b/tests/components/gl_r01_i2c/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/globals/test.esp32-ard.yaml b/tests/components/globals/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/globals/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/globals/test.esp32-c3-ard.yaml b/tests/components/globals/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/globals/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/globals/test.esp32-c3-idf.yaml b/tests/components/globals/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/globals/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/gp2y1010au0f/test.esp32-ard.yaml b/tests/components/gp2y1010au0f/test.esp32-ard.yaml deleted file mode 100644 index d9494a95b7..0000000000 --- a/tests/components/gp2y1010au0f/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - adc_pin: A0 - output_pin: GPIO14 - -<<: !include common.yaml diff --git a/tests/components/gp2y1010au0f/test.esp32-c3-ard.yaml b/tests/components/gp2y1010au0f/test.esp32-c3-ard.yaml deleted file mode 100644 index 0e331c893c..0000000000 --- a/tests/components/gp2y1010au0f/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - adc_pin: GPIO0 - output_pin: GPIO1 - -<<: !include common.yaml diff --git a/tests/components/gp2y1010au0f/test.esp32-c3-idf.yaml b/tests/components/gp2y1010au0f/test.esp32-c3-idf.yaml deleted file mode 100644 index 0e331c893c..0000000000 --- a/tests/components/gp2y1010au0f/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - adc_pin: GPIO0 - output_pin: GPIO1 - -<<: !include common.yaml diff --git a/tests/components/gp2y1010au0f/test.esp8266-ard.yaml b/tests/components/gp2y1010au0f/test.esp8266-ard.yaml index a61053426a..c6a7c7bf13 100644 --- a/tests/components/gp2y1010au0f/test.esp8266-ard.yaml +++ b/tests/components/gp2y1010au0f/test.esp8266-ard.yaml @@ -1,5 +1,5 @@ substitutions: adc_pin: A0 - output_pin: GPIO5 + output_pin: GPIO0 <<: !include common.yaml diff --git a/tests/components/gp8403/common.yaml b/tests/components/gp8403/common.yaml index 7074185273..1147316b02 100644 --- a/tests/components/gp8403/common.yaml +++ b/tests/components/gp8403/common.yaml @@ -1,12 +1,14 @@ -i2c: - - id: i2c_gp8403 - scl: ${scl_pin} - sda: ${sda_pin} - gp8403: - id: gp8403_5v + i2c_id: i2c_bus voltage: 5V - id: gp8403_10v + i2c_id: i2c_bus + model: GP8403 + voltage: 10V + - id: gp8413 + i2c_id: i2c_bus + model: GP8413 voltage: 10V output: @@ -18,3 +20,7 @@ output: gp8403_id: gp8403_10v id: gp8403_output_1 channel: 1 + - platform: gp8403 + gp8403_id: gp8413 + id: gp8413_output_2 + channel: 1 diff --git a/tests/components/gp8403/test.esp32-ard.yaml b/tests/components/gp8403/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/gp8403/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/gp8403/test.esp32-c3-ard.yaml b/tests/components/gp8403/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/gp8403/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/gp8403/test.esp32-c3-idf.yaml b/tests/components/gp8403/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/gp8403/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/gp8403/test.esp32-idf.yaml b/tests/components/gp8403/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/gp8403/test.esp32-idf.yaml +++ b/tests/components/gp8403/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/gp8403/test.esp8266-ard.yaml b/tests/components/gp8403/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/gp8403/test.esp8266-ard.yaml +++ b/tests/components/gp8403/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/gp8403/test.rp2040-ard.yaml b/tests/components/gp8403/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/gp8403/test.rp2040-ard.yaml +++ b/tests/components/gp8403/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/gpio/common.yaml b/tests/components/gpio/common.yaml index 4e237349d9..b8e8fa81e4 100644 --- a/tests/components/gpio/common.yaml +++ b/tests/components/gpio/common.yaml @@ -12,3 +12,20 @@ switch: - platform: gpio pin: ${switch_pin} id: gpio_switch + + - platform: gpio + pin: ${switch_pin_2} + id: gpio_switch_interlock_1 + interlock: [gpio_switch_interlock_2, gpio_switch_interlock_3] + interlock_wait_time: 100ms + + - platform: gpio + pin: ${switch_pin_3} + id: gpio_switch_interlock_2 + interlock: [gpio_switch_interlock_1, gpio_switch_interlock_3] + + - platform: gpio + pin: ${switch_pin_4} + id: gpio_switch_interlock_3 + interlock: [gpio_switch_interlock_1, gpio_switch_interlock_2] + interlock_wait_time: 50ms diff --git a/tests/components/gpio/test.esp32-ard.yaml b/tests/components/gpio/test.esp32-ard.yaml deleted file mode 100644 index 09f41abb79..0000000000 --- a/tests/components/gpio/test.esp32-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - binary_sensor_pin: GPIO12 - output_pin: GPIO13 - switch_pin: GPIO14 - -<<: !include common.yaml diff --git a/tests/components/gpio/test.esp32-c3-ard.yaml b/tests/components/gpio/test.esp32-c3-ard.yaml deleted file mode 100644 index fc7c9942d0..0000000000 --- a/tests/components/gpio/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - binary_sensor_pin: GPIO2 - output_pin: GPIO3 - switch_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/gpio/test.esp32-c3-idf.yaml b/tests/components/gpio/test.esp32-c3-idf.yaml index fc7c9942d0..e9071b4356 100644 --- a/tests/components/gpio/test.esp32-c3-idf.yaml +++ b/tests/components/gpio/test.esp32-c3-idf.yaml @@ -2,5 +2,8 @@ substitutions: binary_sensor_pin: GPIO2 output_pin: GPIO3 switch_pin: GPIO4 + switch_pin_2: GPIO5 + switch_pin_3: GPIO6 + switch_pin_4: GPIO7 <<: !include common.yaml diff --git a/tests/components/gpio/test.esp32-idf.yaml b/tests/components/gpio/test.esp32-idf.yaml index 09f41abb79..862aa533ea 100644 --- a/tests/components/gpio/test.esp32-idf.yaml +++ b/tests/components/gpio/test.esp32-idf.yaml @@ -2,5 +2,8 @@ substitutions: binary_sensor_pin: GPIO12 output_pin: GPIO13 switch_pin: GPIO14 + switch_pin_2: GPIO15 + switch_pin_3: GPIO16 + switch_pin_4: GPIO17 <<: !include common.yaml diff --git a/tests/components/gpio/test.esp8266-ard.yaml b/tests/components/gpio/test.esp8266-ard.yaml index 09f41abb79..e13b4520d1 100644 --- a/tests/components/gpio/test.esp8266-ard.yaml +++ b/tests/components/gpio/test.esp8266-ard.yaml @@ -1,6 +1,9 @@ substitutions: - binary_sensor_pin: GPIO12 - output_pin: GPIO13 - switch_pin: GPIO14 + binary_sensor_pin: GPIO0 + output_pin: GPIO2 + switch_pin: GPIO15 + switch_pin_2: GPIO12 + switch_pin_3: GPIO13 + switch_pin_4: GPIO14 <<: !include common.yaml diff --git a/tests/components/gpio/test.nrf52-adafruit.yaml b/tests/components/gpio/test.nrf52-adafruit.yaml index 912b9537c4..fb3f368e03 100644 --- a/tests/components/gpio/test.nrf52-adafruit.yaml +++ b/tests/components/gpio/test.nrf52-adafruit.yaml @@ -12,3 +12,20 @@ switch: - platform: gpio pin: P1.2 id: gpio_switch + + - platform: gpio + pin: P1.3 + id: gpio_switch_interlock_1 + interlock: [gpio_switch_interlock_2, gpio_switch_interlock_3] + interlock_wait_time: 100ms + + - platform: gpio + pin: P1.4 + id: gpio_switch_interlock_2 + interlock: [gpio_switch_interlock_1, gpio_switch_interlock_3] + + - platform: gpio + pin: P1.5 + id: gpio_switch_interlock_3 + interlock: [gpio_switch_interlock_1, gpio_switch_interlock_2] + interlock_wait_time: 50ms diff --git a/tests/components/gpio/test.nrf52-mcumgr.yaml b/tests/components/gpio/test.nrf52-mcumgr.yaml index 912b9537c4..fb3f368e03 100644 --- a/tests/components/gpio/test.nrf52-mcumgr.yaml +++ b/tests/components/gpio/test.nrf52-mcumgr.yaml @@ -12,3 +12,20 @@ switch: - platform: gpio pin: P1.2 id: gpio_switch + + - platform: gpio + pin: P1.3 + id: gpio_switch_interlock_1 + interlock: [gpio_switch_interlock_2, gpio_switch_interlock_3] + interlock_wait_time: 100ms + + - platform: gpio + pin: P1.4 + id: gpio_switch_interlock_2 + interlock: [gpio_switch_interlock_1, gpio_switch_interlock_3] + + - platform: gpio + pin: P1.5 + id: gpio_switch_interlock_3 + interlock: [gpio_switch_interlock_1, gpio_switch_interlock_2] + interlock_wait_time: 50ms diff --git a/tests/components/gpio/test.rp2040-ard.yaml b/tests/components/gpio/test.rp2040-ard.yaml index fc7c9942d0..e9071b4356 100644 --- a/tests/components/gpio/test.rp2040-ard.yaml +++ b/tests/components/gpio/test.rp2040-ard.yaml @@ -2,5 +2,8 @@ substitutions: binary_sensor_pin: GPIO2 output_pin: GPIO3 switch_pin: GPIO4 + switch_pin_2: GPIO5 + switch_pin_3: GPIO6 + switch_pin_4: GPIO7 <<: !include common.yaml diff --git a/tests/components/gps/common.yaml b/tests/components/gps/common.yaml index 53dc67e457..a99e3ef7e0 100644 --- a/tests/components/gps/common.yaml +++ b/tests/components/gps/common.yaml @@ -1,10 +1,3 @@ -uart: - - id: uart_gps - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 9600 - parity: EVEN - gps: latitude: name: "Latitude" diff --git a/tests/components/gps/test.esp32-ard.yaml b/tests/components/gps/test.esp32-ard.yaml deleted file mode 100644 index 811f6b72a6..0000000000 --- a/tests/components/gps/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO12 - rx_pin: GPIO14 - -<<: !include common.yaml diff --git a/tests/components/gps/test.esp32-c3-ard.yaml b/tests/components/gps/test.esp32-c3-ard.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/gps/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/gps/test.esp32-c3-idf.yaml b/tests/components/gps/test.esp32-c3-idf.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/gps/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/gps/test.esp32-idf.yaml b/tests/components/gps/test.esp32-idf.yaml index 811f6b72a6..64baa4ec9d 100644 --- a/tests/components/gps/test.esp32-idf.yaml +++ b/tests/components/gps/test.esp32-idf.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO12 rx_pin: GPIO14 +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/gps/test.esp8266-ard.yaml b/tests/components/gps/test.esp8266-ard.yaml index b516342f3b..96ab4ef6ac 100644 --- a/tests/components/gps/test.esp8266-ard.yaml +++ b/tests/components/gps/test.esp8266-ard.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: GPIO0 + rx_pin: GPIO2 + +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/gps/test.rp2040-ard.yaml b/tests/components/gps/test.rp2040-ard.yaml index b516342f3b..b28f2b5e05 100644 --- a/tests/components/gps/test.rp2040-ard.yaml +++ b/tests/components/gps/test.rp2040-ard.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO4 rx_pin: GPIO5 +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/graph/common.yaml b/tests/components/graph/common.yaml index d2a6a9422d..11e2a16ca1 100644 --- a/tests/components/graph/common.yaml +++ b/tests/components/graph/common.yaml @@ -1,8 +1,3 @@ -i2c: - - id: i2c_graph - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: template id: some_sensor @@ -16,10 +11,11 @@ graph: display: - platform: ssd1306_i2c + i2c_id: i2c_bus id: ssd1306_display model: SSD1306_128X64 reset_pin: ${reset_pin} pages: - - id: page1 + - id: graph_page1 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); diff --git a/tests/components/graph/test.esp32-ard.yaml b/tests/components/graph/test.esp32-ard.yaml deleted file mode 100644 index 1ca773e06c..0000000000 --- a/tests/components/graph/test.esp32-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - reset_pin: GPIO15 - -<<: !include common.yaml diff --git a/tests/components/graph/test.esp32-c3-ard.yaml b/tests/components/graph/test.esp32-c3-ard.yaml deleted file mode 100644 index 1e6670c196..0000000000 --- a/tests/components/graph/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - reset_pin: GPIO6 - -<<: !include common.yaml diff --git a/tests/components/graph/test.esp32-c3-idf.yaml b/tests/components/graph/test.esp32-c3-idf.yaml deleted file mode 100644 index 1e6670c196..0000000000 --- a/tests/components/graph/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - reset_pin: GPIO6 - -<<: !include common.yaml diff --git a/tests/components/graph/test.esp32-idf.yaml b/tests/components/graph/test.esp32-idf.yaml index 1ca773e06c..4ff2241ec9 100644 --- a/tests/components/graph/test.esp32-idf.yaml +++ b/tests/components/graph/test.esp32-idf.yaml @@ -1,6 +1,7 @@ substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 reset_pin: GPIO15 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/graph/test.esp8266-ard.yaml b/tests/components/graph/test.esp8266-ard.yaml index dfdc12a3d1..b8bb94edde 100644 --- a/tests/components/graph/test.esp8266-ard.yaml +++ b/tests/components/graph/test.esp8266-ard.yaml @@ -1,6 +1,7 @@ substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 reset_pin: GPIO15 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/graph/test.rp2040-ard.yaml b/tests/components/graph/test.rp2040-ard.yaml index 1e6670c196..1bf10642c5 100644 --- a/tests/components/graph/test.rp2040-ard.yaml +++ b/tests/components/graph/test.rp2040-ard.yaml @@ -1,6 +1,7 @@ substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 reset_pin: GPIO6 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/graphical_display_menu/common.yaml b/tests/components/graphical_display_menu/common.yaml index d979a6a30e..6cee2af232 100644 --- a/tests/components/graphical_display_menu/common.yaml +++ b/tests/components/graphical_display_menu/common.yaml @@ -1,15 +1,10 @@ -i2c: - - id: i2c_graphical_display_menu - scl: ${scl_pin} - sda: ${sda_pin} - display: - platform: ssd1306_i2c - id: ssd1306_display + id: ssd1306_i2c_display model: SSD1306_128X64 reset_pin: ${reset_pin} pages: - - id: page1 + - id: graphical_display_menu_page1 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); @@ -41,7 +36,7 @@ switch: graphical_display_menu: id: test_graphical_display_menu - display: ssd1306_display + display: ssd1306_i2c_display font: roboto active: false mode: rotary diff --git a/tests/components/graphical_display_menu/test.esp32-ard.yaml b/tests/components/graphical_display_menu/test.esp32-ard.yaml deleted file mode 100644 index 1ca773e06c..0000000000 --- a/tests/components/graphical_display_menu/test.esp32-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - reset_pin: GPIO15 - -<<: !include common.yaml diff --git a/tests/components/graphical_display_menu/test.esp32-c3-ard.yaml b/tests/components/graphical_display_menu/test.esp32-c3-ard.yaml deleted file mode 100644 index 1e6670c196..0000000000 --- a/tests/components/graphical_display_menu/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - reset_pin: GPIO6 - -<<: !include common.yaml diff --git a/tests/components/graphical_display_menu/test.esp32-c3-idf.yaml b/tests/components/graphical_display_menu/test.esp32-c3-idf.yaml deleted file mode 100644 index 1e6670c196..0000000000 --- a/tests/components/graphical_display_menu/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - reset_pin: GPIO6 - -<<: !include common.yaml diff --git a/tests/components/graphical_display_menu/test.esp32-idf.yaml b/tests/components/graphical_display_menu/test.esp32-idf.yaml index 1ca773e06c..4ff2241ec9 100644 --- a/tests/components/graphical_display_menu/test.esp32-idf.yaml +++ b/tests/components/graphical_display_menu/test.esp32-idf.yaml @@ -1,6 +1,7 @@ substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 reset_pin: GPIO15 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/graphical_display_menu/test.esp8266-ard.yaml b/tests/components/graphical_display_menu/test.esp8266-ard.yaml index dfdc12a3d1..b8bb94edde 100644 --- a/tests/components/graphical_display_menu/test.esp8266-ard.yaml +++ b/tests/components/graphical_display_menu/test.esp8266-ard.yaml @@ -1,6 +1,7 @@ substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 reset_pin: GPIO15 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/graphical_display_menu/test.rp2040-ard.yaml b/tests/components/graphical_display_menu/test.rp2040-ard.yaml index 1e6670c196..1bf10642c5 100644 --- a/tests/components/graphical_display_menu/test.rp2040-ard.yaml +++ b/tests/components/graphical_display_menu/test.rp2040-ard.yaml @@ -1,6 +1,7 @@ substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 reset_pin: GPIO6 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/gree/common.yaml b/tests/components/gree/common.yaml index c221184bbf..e706076034 100644 --- a/tests/components/gree/common.yaml +++ b/tests/components/gree/common.yaml @@ -1,8 +1,5 @@ -remote_transmitter: - pin: ${pin} - carrier_duty_percent: 50% - climate: - platform: gree name: GREE model: generic + transmitter_id: xmitr diff --git a/tests/components/gree/test.esp32-ard.yaml b/tests/components/gree/test.esp32-ard.yaml deleted file mode 100644 index 7b012aa64c..0000000000 --- a/tests/components/gree/test.esp32-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/gree/test.esp32-c3-ard.yaml b/tests/components/gree/test.esp32-c3-ard.yaml deleted file mode 100644 index 7b012aa64c..0000000000 --- a/tests/components/gree/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/gree/test.esp32-c3-idf.yaml b/tests/components/gree/test.esp32-c3-idf.yaml deleted file mode 100644 index 7b012aa64c..0000000000 --- a/tests/components/gree/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/gree/test.esp32-idf.yaml b/tests/components/gree/test.esp32-idf.yaml index 7b012aa64c..e891f9dc85 100644 --- a/tests/components/gree/test.esp32-idf.yaml +++ b/tests/components/gree/test.esp32-idf.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO2 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/gree/test.esp8266-ard.yaml b/tests/components/gree/test.esp8266-ard.yaml index f5097fcf5f..4bed2f03e5 100644 --- a/tests/components/gree/test.esp8266-ard.yaml +++ b/tests/components/gree/test.esp8266-ard.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO5 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/grove_gas_mc_v2/common.yaml b/tests/components/grove_gas_mc_v2/common.yaml index dbdb950033..0729e6b9c7 100644 --- a/tests/components/grove_gas_mc_v2/common.yaml +++ b/tests/components/grove_gas_mc_v2/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_grove_gas_mc_v2 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: grove_gas_mc_v2 + i2c_id: i2c_bus nitrogen_dioxide: name: "Nitrogen Dioxide" ethanol: diff --git a/tests/components/grove_gas_mc_v2/test.esp32-ard.yaml b/tests/components/grove_gas_mc_v2/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/grove_gas_mc_v2/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/grove_gas_mc_v2/test.esp32-c3-ard.yaml b/tests/components/grove_gas_mc_v2/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/grove_gas_mc_v2/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/grove_gas_mc_v2/test.esp32-c3-idf.yaml b/tests/components/grove_gas_mc_v2/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/grove_gas_mc_v2/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/grove_gas_mc_v2/test.esp32-idf.yaml b/tests/components/grove_gas_mc_v2/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/grove_gas_mc_v2/test.esp32-idf.yaml +++ b/tests/components/grove_gas_mc_v2/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/grove_gas_mc_v2/test.esp8266-ard.yaml b/tests/components/grove_gas_mc_v2/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/grove_gas_mc_v2/test.esp8266-ard.yaml +++ b/tests/components/grove_gas_mc_v2/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/grove_gas_mc_v2/test.rp2040-ard.yaml b/tests/components/grove_gas_mc_v2/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/grove_gas_mc_v2/test.rp2040-ard.yaml +++ b/tests/components/grove_gas_mc_v2/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/grove_tb6612fng/common.yaml b/tests/components/grove_tb6612fng/common.yaml index 0db6c00bf7..52d5ead96e 100644 --- a/tests/components/grove_tb6612fng/common.yaml +++ b/tests/components/grove_tb6612fng/common.yaml @@ -13,11 +13,7 @@ esphome: channel: 1 id: test_motor -i2c: - - id: i2c_grove_tb6612fng - scl: ${scl_pin} - sda: ${sda_pin} - grove_tb6612fng: id: test_motor + i2c_id: i2c_bus address: 0x14 diff --git a/tests/components/grove_tb6612fng/test.esp32-ard.yaml b/tests/components/grove_tb6612fng/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/grove_tb6612fng/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/grove_tb6612fng/test.esp32-c3-ard.yaml b/tests/components/grove_tb6612fng/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/grove_tb6612fng/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/grove_tb6612fng/test.esp32-c3-idf.yaml b/tests/components/grove_tb6612fng/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/grove_tb6612fng/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/grove_tb6612fng/test.esp32-idf.yaml b/tests/components/grove_tb6612fng/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/grove_tb6612fng/test.esp32-idf.yaml +++ b/tests/components/grove_tb6612fng/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/grove_tb6612fng/test.esp8266-ard.yaml b/tests/components/grove_tb6612fng/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/grove_tb6612fng/test.esp8266-ard.yaml +++ b/tests/components/grove_tb6612fng/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/grove_tb6612fng/test.rp2040-ard.yaml b/tests/components/grove_tb6612fng/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/grove_tb6612fng/test.rp2040-ard.yaml +++ b/tests/components/grove_tb6612fng/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/growatt_solar/common.yaml b/tests/components/growatt_solar/common.yaml index 7cc6b2a139..f304e84fcf 100644 --- a/tests/components/growatt_solar/common.yaml +++ b/tests/components/growatt_solar/common.yaml @@ -1,14 +1,6 @@ -uart: - - id: uart_growatt_solar - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 9600 - -modbus: - flow_control_pin: ${flow_control_pin} - sensor: - platform: growatt_solar + modbus_id: modbus_bus update_interval: 10s protocol_version: RTU inverter_status: diff --git a/tests/components/growatt_solar/test.esp32-ard.yaml b/tests/components/growatt_solar/test.esp32-ard.yaml deleted file mode 100644 index bd767a8ece..0000000000 --- a/tests/components/growatt_solar/test.esp32-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - tx_pin: GPIO12 - rx_pin: GPIO14 - flow_control_pin: GPIO13 - -<<: !include common.yaml diff --git a/tests/components/growatt_solar/test.esp32-c3-ard.yaml b/tests/components/growatt_solar/test.esp32-c3-ard.yaml deleted file mode 100644 index 452031a5aa..0000000000 --- a/tests/components/growatt_solar/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - flow_control_pin: GPIO3 - -<<: !include common.yaml diff --git a/tests/components/growatt_solar/test.esp32-c3-idf.yaml b/tests/components/growatt_solar/test.esp32-c3-idf.yaml deleted file mode 100644 index 452031a5aa..0000000000 --- a/tests/components/growatt_solar/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - flow_control_pin: GPIO3 - -<<: !include common.yaml diff --git a/tests/components/growatt_solar/test.esp32-idf.yaml b/tests/components/growatt_solar/test.esp32-idf.yaml index bd767a8ece..a755bfa66a 100644 --- a/tests/components/growatt_solar/test.esp32-idf.yaml +++ b/tests/components/growatt_solar/test.esp32-idf.yaml @@ -3,4 +3,7 @@ substitutions: rx_pin: GPIO14 flow_control_pin: GPIO13 +packages: + modbus: !include ../../test_build_components/common/modbus/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/growatt_solar/test.esp8266-ard.yaml b/tests/components/growatt_solar/test.esp8266-ard.yaml index 29c98d0957..6daa08c22b 100644 --- a/tests/components/growatt_solar/test.esp8266-ard.yaml +++ b/tests/components/growatt_solar/test.esp8266-ard.yaml @@ -1,6 +1,9 @@ substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - flow_control_pin: GPIO13 + tx_pin: GPIO0 + rx_pin: GPIO2 + flow_control_pin: GPIO15 + +packages: + modbus: !include ../../test_build_components/common/modbus/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/growatt_solar/test.rp2040-ard.yaml b/tests/components/growatt_solar/test.rp2040-ard.yaml index 452031a5aa..a00283a9e0 100644 --- a/tests/components/growatt_solar/test.rp2040-ard.yaml +++ b/tests/components/growatt_solar/test.rp2040-ard.yaml @@ -3,4 +3,7 @@ substitutions: rx_pin: GPIO5 flow_control_pin: GPIO3 +packages: + modbus: !include ../../test_build_components/common/modbus/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/gt911/common.yaml b/tests/components/gt911/common.yaml index 7bb88108da..ff464cda24 100644 --- a/tests/components/gt911/common.yaml +++ b/tests/components/gt911/common.yaml @@ -1,23 +1,21 @@ -i2c: - - id: i2c_gt911 - scl: 5 - sda: 4 - display: - platform: ssd1306_i2c - id: ssd1306_display + i2c_id: i2c_bus + id: ssd1306_i2c_display model: SSD1306_128X64 - reset_pin: 10 + reset_pin: ${display_reset_pin} pages: - - id: page1 + - id: gt911_page1 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); touchscreen: - platform: gt911 - display: ssd1306_display - interrupt_pin: 20 - reset_pin: 21 + i2c_id: i2c_bus + id: gt911_touchscreen + display: ssd1306_i2c_display + interrupt_pin: ${interrupt_pin} + reset_pin: ${reset_pin} binary_sensor: - platform: gt911 diff --git a/tests/components/gt911/test.esp32-ard.yaml b/tests/components/gt911/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/gt911/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/gt911/test.esp32-c3-ard.yaml b/tests/components/gt911/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/gt911/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/gt911/test.esp32-c3-idf.yaml b/tests/components/gt911/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/gt911/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/gt911/test.esp32-idf.yaml b/tests/components/gt911/test.esp32-idf.yaml index dade44d145..3bce86d9a3 100644 --- a/tests/components/gt911/test.esp32-idf.yaml +++ b/tests/components/gt911/test.esp32-idf.yaml @@ -1 +1,9 @@ +substitutions: + display_reset_pin: "10" + interrupt_pin: "20" + reset_pin: "21" + +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/gt911/test.esp8266-ard.yaml b/tests/components/gt911/test.esp8266-ard.yaml index 8b76eff29e..c3bc159b5b 100644 --- a/tests/components/gt911/test.esp8266-ard.yaml +++ b/tests/components/gt911/test.esp8266-ard.yaml @@ -1,24 +1,9 @@ -i2c: - - id: i2c_gt911 - scl: 5 - sda: 4 +substitutions: + display_reset_pin: "10" + interrupt_pin: "12" + reset_pin: "13" -display: - - platform: ssd1306_i2c - id: ssd1306_display - model: SSD1306_128X64 - reset_pin: 13 - pages: - - id: page1 - lambda: |- - it.rectangle(0, 0, it.get_width(), it.get_height()); +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml -touchscreen: - - platform: gt911 - display: ssd1306_display - interrupt_pin: 12 - -binary_sensor: - - platform: gt911 - id: touch_key_911 - index: 0 +<<: !include common.yaml diff --git a/tests/components/gt911/test.rp2040-ard.yaml b/tests/components/gt911/test.rp2040-ard.yaml index dade44d145..0c7f0bc504 100644 --- a/tests/components/gt911/test.rp2040-ard.yaml +++ b/tests/components/gt911/test.rp2040-ard.yaml @@ -1 +1,9 @@ +substitutions: + display_reset_pin: "10" + interrupt_pin: "20" + reset_pin: "21" + +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/haier/common.yaml b/tests/components/haier/common.yaml index 368b88b69c..926a1d1b7a 100644 --- a/tests/components/haier/common.yaml +++ b/tests/components/haier/common.yaml @@ -2,16 +2,9 @@ wifi: ssid: MySSID password: password1 -uart: - - id: uart_haier - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 9600 - climate: - platform: haier id: haier_ac - uart_id: uart_haier protocol: hOn name: Haier AC wifi_signal: true diff --git a/tests/components/haier/test.esp32-ard.yaml b/tests/components/haier/test.esp32-ard.yaml deleted file mode 100644 index f486544afa..0000000000 --- a/tests/components/haier/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 - -<<: !include common.yaml diff --git a/tests/components/haier/test.esp32-c3-ard.yaml b/tests/components/haier/test.esp32-c3-ard.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/haier/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/haier/test.esp32-c3-idf.yaml b/tests/components/haier/test.esp32-c3-idf.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/haier/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/haier/test.esp32-idf.yaml b/tests/components/haier/test.esp32-idf.yaml index f486544afa..b415125e84 100644 --- a/tests/components/haier/test.esp32-idf.yaml +++ b/tests/components/haier/test.esp32-idf.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 + tx_pin: GPIO4 + rx_pin: GPIO5 + +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/haier/test.esp8266-ard.yaml b/tests/components/haier/test.esp8266-ard.yaml index b516342f3b..96ab4ef6ac 100644 --- a/tests/components/haier/test.esp8266-ard.yaml +++ b/tests/components/haier/test.esp8266-ard.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: GPIO0 + rx_pin: GPIO2 + +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/haier/test.rp2040-ard.yaml b/tests/components/haier/test.rp2040-ard.yaml index b516342f3b..b28f2b5e05 100644 --- a/tests/components/haier/test.rp2040-ard.yaml +++ b/tests/components/haier/test.rp2040-ard.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO4 rx_pin: GPIO5 +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/havells_solar/common.yaml b/tests/components/havells_solar/common.yaml index 370b0d357d..2f00f61f14 100644 --- a/tests/components/havells_solar/common.yaml +++ b/tests/components/havells_solar/common.yaml @@ -1,14 +1,6 @@ -uart: - - id: uart_havells_solar - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 9600 - -modbus: - flow_control_pin: ${flow_control_pin} - sensor: - platform: havells_solar + modbus_id: modbus_bus update_interval: 60s phase_a: voltage: diff --git a/tests/components/havells_solar/test.esp32-ard.yaml b/tests/components/havells_solar/test.esp32-ard.yaml deleted file mode 100644 index bd767a8ece..0000000000 --- a/tests/components/havells_solar/test.esp32-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - tx_pin: GPIO12 - rx_pin: GPIO14 - flow_control_pin: GPIO13 - -<<: !include common.yaml diff --git a/tests/components/havells_solar/test.esp32-c3-ard.yaml b/tests/components/havells_solar/test.esp32-c3-ard.yaml deleted file mode 100644 index 452031a5aa..0000000000 --- a/tests/components/havells_solar/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - flow_control_pin: GPIO3 - -<<: !include common.yaml diff --git a/tests/components/havells_solar/test.esp32-c3-idf.yaml b/tests/components/havells_solar/test.esp32-c3-idf.yaml deleted file mode 100644 index 452031a5aa..0000000000 --- a/tests/components/havells_solar/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - flow_control_pin: GPIO3 - -<<: !include common.yaml diff --git a/tests/components/havells_solar/test.esp32-idf.yaml b/tests/components/havells_solar/test.esp32-idf.yaml index bd767a8ece..a755bfa66a 100644 --- a/tests/components/havells_solar/test.esp32-idf.yaml +++ b/tests/components/havells_solar/test.esp32-idf.yaml @@ -3,4 +3,7 @@ substitutions: rx_pin: GPIO14 flow_control_pin: GPIO13 +packages: + modbus: !include ../../test_build_components/common/modbus/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/havells_solar/test.esp8266-ard.yaml b/tests/components/havells_solar/test.esp8266-ard.yaml index 29c98d0957..6daa08c22b 100644 --- a/tests/components/havells_solar/test.esp8266-ard.yaml +++ b/tests/components/havells_solar/test.esp8266-ard.yaml @@ -1,6 +1,9 @@ substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - flow_control_pin: GPIO13 + tx_pin: GPIO0 + rx_pin: GPIO2 + flow_control_pin: GPIO15 + +packages: + modbus: !include ../../test_build_components/common/modbus/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/havells_solar/test.rp2040-ard.yaml b/tests/components/havells_solar/test.rp2040-ard.yaml index 452031a5aa..a00283a9e0 100644 --- a/tests/components/havells_solar/test.rp2040-ard.yaml +++ b/tests/components/havells_solar/test.rp2040-ard.yaml @@ -3,4 +3,7 @@ substitutions: rx_pin: GPIO5 flow_control_pin: GPIO3 +packages: + modbus: !include ../../test_build_components/common/modbus/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/hbridge/common.yaml b/tests/components/hbridge/common.yaml index 0504cdea03..ca619d6851 100644 --- a/tests/components/hbridge/common.yaml +++ b/tests/components/hbridge/common.yaml @@ -31,9 +31,3 @@ fan: on_preset_set: then: - logger.log: Preset mode was changed! - -switch: - - platform: hbridge - id: switch_hbridge - on_pin: ${hbridge_on_pin} - off_pin: ${hbridge_off_pin} diff --git a/tests/components/hbridge/test.esp32-ard.yaml b/tests/components/hbridge/test.esp32-ard.yaml deleted file mode 100644 index e50d537749..0000000000 --- a/tests/components/hbridge/test.esp32-ard.yaml +++ /dev/null @@ -1,17 +0,0 @@ -substitutions: - pwm_platform: ledc - output1_pin: "14" - output2_pin: "15" - output3_pin: "12" - output4_pin: "13" - hbridge_on_pin: "4" - hbridge_off_pin: "5" - -packages: - common: !include common.yaml - -switch: - - id: !extend switch_hbridge - pulse_length: 60ms - wait_time: 10ms - optimistic: false diff --git a/tests/components/hbridge/test.esp32-c3-ard.yaml b/tests/components/hbridge/test.esp32-c3-ard.yaml deleted file mode 100644 index b9e8738442..0000000000 --- a/tests/components/hbridge/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,16 +0,0 @@ -substitutions: - pwm_platform: "ledc" - output1_pin: "4" - output2_pin: "5" - output3_pin: "6" - output4_pin: "7" - hbridge_on_pin: "2" - hbridge_off_pin: "3" - -packages: - common: !include common.yaml - -switch: - - id: !extend switch_hbridge - wait_time: 10ms - optimistic: true diff --git a/tests/components/hbridge/test.esp32-c3-idf.yaml b/tests/components/hbridge/test.esp32-c3-idf.yaml deleted file mode 100644 index c73f08b6de..0000000000 --- a/tests/components/hbridge/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,15 +0,0 @@ -substitutions: - pwm_platform: "ledc" - output1_pin: "4" - output2_pin: "5" - output3_pin: "6" - output4_pin: "7" - hbridge_on_pin: "2" - hbridge_off_pin: "3" - -packages: - common: !include common.yaml - -switch: - - id: !extend switch_hbridge - pulse_length: 60ms diff --git a/tests/components/hbridge/test.esp32-idf.yaml b/tests/components/hbridge/test.esp32-idf.yaml index dbbfa738c7..7f3d9b928a 100644 --- a/tests/components/hbridge/test.esp32-idf.yaml +++ b/tests/components/hbridge/test.esp32-idf.yaml @@ -7,10 +7,12 @@ substitutions: hbridge_on_pin: "4" hbridge_off_pin: "5" -packages: - common: !include common.yaml +<<: !include common.yaml switch: - - id: !extend switch_hbridge + - platform: hbridge + id: switch_hbridge + on_pin: ${hbridge_on_pin} + off_pin: ${hbridge_off_pin} pulse_length: 60ms wait_time: 10ms diff --git a/tests/components/hbridge/test.esp8266-ard.yaml b/tests/components/hbridge/test.esp8266-ard.yaml index f560da5d38..0346c8801d 100644 --- a/tests/components/hbridge/test.esp8266-ard.yaml +++ b/tests/components/hbridge/test.esp8266-ard.yaml @@ -7,10 +7,12 @@ substitutions: hbridge_on_pin: "14" hbridge_off_pin: "15" -packages: - common: !include common.yaml +<<: !include common.yaml switch: - - id: !extend switch_hbridge + - platform: hbridge + id: switch_hbridge + on_pin: ${hbridge_on_pin} + off_pin: ${hbridge_off_pin} pulse_length: 60ms wait_time: 10ms diff --git a/tests/components/hbridge/test.rp2040-ard.yaml b/tests/components/hbridge/test.rp2040-ard.yaml index aa6e290cab..f9cb2b5618 100644 --- a/tests/components/hbridge/test.rp2040-ard.yaml +++ b/tests/components/hbridge/test.rp2040-ard.yaml @@ -7,10 +7,12 @@ substitutions: hbridge_on_pin: "2" hbridge_off_pin: "3" -packages: - common: !include common.yaml +<<: !include common.yaml switch: - - id: !extend switch_hbridge + - platform: hbridge + id: switch_hbridge + on_pin: ${hbridge_on_pin} + off_pin: ${hbridge_off_pin} wait_time: 10ms optimistic: true diff --git a/tests/components/hc8/common.yaml b/tests/components/hc8/common.yaml new file mode 100644 index 0000000000..ac3b454315 --- /dev/null +++ b/tests/components/hc8/common.yaml @@ -0,0 +1,13 @@ +esphome: + on_boot: + then: + - hc8.calibrate: + id: hc8_sensor + baseline: 420 + +sensor: + - platform: hc8 + id: hc8_sensor + co2: + name: HC8 CO2 Value + update_interval: 15s diff --git a/tests/components/hc8/test.esp32-idf.yaml b/tests/components/hc8/test.esp32-idf.yaml new file mode 100644 index 0000000000..2d29656c94 --- /dev/null +++ b/tests/components/hc8/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/hc8/test.esp8266-ard.yaml b/tests/components/hc8/test.esp8266-ard.yaml new file mode 100644 index 0000000000..5a05efa259 --- /dev/null +++ b/tests/components/hc8/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/hc8/test.rp2040-ard.yaml b/tests/components/hc8/test.rp2040-ard.yaml new file mode 100644 index 0000000000..f1df2daf83 --- /dev/null +++ b/tests/components/hc8/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/hdc1080/common.yaml b/tests/components/hdc1080/common.yaml index d260d4737c..a559392cb1 100644 --- a/tests/components/hdc1080/common.yaml +++ b/tests/components/hdc1080/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_hdc1080 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: hdc1080 + i2c_id: i2c_bus temperature: name: Temperature humidity: diff --git a/tests/components/hdc1080/test.esp32-ard.yaml b/tests/components/hdc1080/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/hdc1080/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/hdc1080/test.esp32-c3-ard.yaml b/tests/components/hdc1080/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/hdc1080/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/hdc1080/test.esp32-c3-idf.yaml b/tests/components/hdc1080/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/hdc1080/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/hdc1080/test.esp32-idf.yaml b/tests/components/hdc1080/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/hdc1080/test.esp32-idf.yaml +++ b/tests/components/hdc1080/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/hdc1080/test.esp8266-ard.yaml b/tests/components/hdc1080/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/hdc1080/test.esp8266-ard.yaml +++ b/tests/components/hdc1080/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/hdc1080/test.rp2040-ard.yaml b/tests/components/hdc1080/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/hdc1080/test.rp2040-ard.yaml +++ b/tests/components/hdc1080/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/hdc2010/common.yaml b/tests/components/hdc2010/common.yaml new file mode 100644 index 0000000000..a22b3f15ce --- /dev/null +++ b/tests/components/hdc2010/common.yaml @@ -0,0 +1,7 @@ +sensor: + - platform: hdc2010 + i2c_id: i2c_bus + temperature: + name: Temperature + humidity: + name: Humidity diff --git a/tests/components/hdc2010/test.esp32-idf.yaml b/tests/components/hdc2010/test.esp32-idf.yaml new file mode 100644 index 0000000000..b47e39c389 --- /dev/null +++ b/tests/components/hdc2010/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/hdc2010/test.esp8266-ard.yaml b/tests/components/hdc2010/test.esp8266-ard.yaml new file mode 100644 index 0000000000..4a98b9388a --- /dev/null +++ b/tests/components/hdc2010/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/hdc2010/test.rp2040-ard.yaml b/tests/components/hdc2010/test.rp2040-ard.yaml new file mode 100644 index 0000000000..319a7c71a6 --- /dev/null +++ b/tests/components/hdc2010/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/he60r/common.yaml b/tests/components/he60r/common.yaml index e10e745f57..ab5ec67f0e 100644 --- a/tests/components/he60r/common.yaml +++ b/tests/components/he60r/common.yaml @@ -1,10 +1,3 @@ -uart: - - id: uart_he60r - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 1200 - parity: EVEN - cover: - platform: he60r id: garage_door diff --git a/tests/components/he60r/test.esp32-ard.yaml b/tests/components/he60r/test.esp32-ard.yaml deleted file mode 100644 index f486544afa..0000000000 --- a/tests/components/he60r/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 - -<<: !include common.yaml diff --git a/tests/components/he60r/test.esp32-c3-ard.yaml b/tests/components/he60r/test.esp32-c3-ard.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/he60r/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/he60r/test.esp32-c3-idf.yaml b/tests/components/he60r/test.esp32-c3-idf.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/he60r/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/he60r/test.esp32-idf.yaml b/tests/components/he60r/test.esp32-idf.yaml index f486544afa..f52c2a75f8 100644 --- a/tests/components/he60r/test.esp32-idf.yaml +++ b/tests/components/he60r/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 +packages: + uart_1200_even: !include ../../test_build_components/common/uart_1200_even/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/he60r/test.esp8266-ard.yaml b/tests/components/he60r/test.esp8266-ard.yaml index b516342f3b..e28024fa4d 100644 --- a/tests/components/he60r/test.esp8266-ard.yaml +++ b/tests/components/he60r/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 +packages: + uart_1200_even: !include ../../test_build_components/common/uart_1200_even/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/he60r/test.rp2040-ard.yaml b/tests/components/he60r/test.rp2040-ard.yaml index b516342f3b..a576b03d63 100644 --- a/tests/components/he60r/test.rp2040-ard.yaml +++ b/tests/components/he60r/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 +packages: + uart_1200_even: !include ../../test_build_components/common/uart_1200_even/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/heatpumpir/common.yaml b/tests/components/heatpumpir/common.yaml index d740f31518..a2779f9803 100644 --- a/tests/components/heatpumpir/common.yaml +++ b/tests/components/heatpumpir/common.yaml @@ -1,7 +1,3 @@ -remote_transmitter: - pin: ${pin} - carrier_duty_percent: 50% - climate: - platform: heatpumpir protocol: mitsubishi_heavy_zm @@ -10,6 +6,7 @@ climate: name: HeatpumpIR Climate Mitsubishi min_temperature: 18 max_temperature: 30 + transmitter_id: xmitr - platform: heatpumpir protocol: daikin horizontal_default: mleft @@ -17,6 +14,7 @@ climate: name: HeatpumpIR Climate Daikin min_temperature: 18 max_temperature: 30 + transmitter_id: xmitr - platform: heatpumpir protocol: panasonic_altdke horizontal_default: mright @@ -24,3 +22,4 @@ climate: name: HeatpumpIR Climate Panasonic min_temperature: 18 max_temperature: 30 + transmitter_id: xmitr diff --git a/tests/components/heatpumpir/test.bk72xx-ard.yaml b/tests/components/heatpumpir/test.bk72xx-ard.yaml index 06e1aea364..6cce191825 100644 --- a/tests/components/heatpumpir/test.bk72xx-ard.yaml +++ b/tests/components/heatpumpir/test.bk72xx-ard.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO6 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/bk72xx-ard.yaml <<: !include common.yaml diff --git a/tests/components/heatpumpir/test.esp32-ard.yaml b/tests/components/heatpumpir/test.esp32-ard.yaml index 7b012aa64c..01009de071 100644 --- a/tests/components/heatpumpir/test.esp32-ard.yaml +++ b/tests/components/heatpumpir/test.esp32-ard.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO2 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-ard.yaml <<: !include common.yaml diff --git a/tests/components/heatpumpir/test.esp32-c3-ard.yaml b/tests/components/heatpumpir/test.esp32-c3-ard.yaml deleted file mode 100644 index 7b012aa64c..0000000000 --- a/tests/components/heatpumpir/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/heatpumpir/test.esp8266-ard.yaml b/tests/components/heatpumpir/test.esp8266-ard.yaml index f5097fcf5f..4bed2f03e5 100644 --- a/tests/components/heatpumpir/test.esp8266-ard.yaml +++ b/tests/components/heatpumpir/test.esp8266-ard.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO5 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/hitachi_ac344/common.yaml b/tests/components/hitachi_ac344/common.yaml index 960f032035..797e7875e5 100644 --- a/tests/components/hitachi_ac344/common.yaml +++ b/tests/components/hitachi_ac344/common.yaml @@ -1,7 +1,4 @@ -remote_transmitter: - pin: ${pin} - carrier_duty_percent: 50% - climate: - platform: hitachi_ac344 name: Hitachi Climate + transmitter_id: xmitr diff --git a/tests/components/hitachi_ac344/test.bk72xx-ard.yaml b/tests/components/hitachi_ac344/test.bk72xx-ard.yaml index 06e1aea364..6cce191825 100644 --- a/tests/components/hitachi_ac344/test.bk72xx-ard.yaml +++ b/tests/components/hitachi_ac344/test.bk72xx-ard.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO6 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/bk72xx-ard.yaml <<: !include common.yaml diff --git a/tests/components/hitachi_ac344/test.esp32-ard.yaml b/tests/components/hitachi_ac344/test.esp32-ard.yaml deleted file mode 100644 index 7b012aa64c..0000000000 --- a/tests/components/hitachi_ac344/test.esp32-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/hitachi_ac344/test.esp32-c3-ard.yaml b/tests/components/hitachi_ac344/test.esp32-c3-ard.yaml deleted file mode 100644 index 7b012aa64c..0000000000 --- a/tests/components/hitachi_ac344/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/hitachi_ac344/test.esp32-c3-idf.yaml b/tests/components/hitachi_ac344/test.esp32-c3-idf.yaml deleted file mode 100644 index 7b012aa64c..0000000000 --- a/tests/components/hitachi_ac344/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/hitachi_ac344/test.esp32-idf.yaml b/tests/components/hitachi_ac344/test.esp32-idf.yaml index 7b012aa64c..e891f9dc85 100644 --- a/tests/components/hitachi_ac344/test.esp32-idf.yaml +++ b/tests/components/hitachi_ac344/test.esp32-idf.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO2 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/hitachi_ac344/test.esp8266-ard.yaml b/tests/components/hitachi_ac344/test.esp8266-ard.yaml index f5097fcf5f..4bed2f03e5 100644 --- a/tests/components/hitachi_ac344/test.esp8266-ard.yaml +++ b/tests/components/hitachi_ac344/test.esp8266-ard.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO5 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/hitachi_ac424/common.yaml b/tests/components/hitachi_ac424/common.yaml index ad904c73a3..615bda4544 100644 --- a/tests/components/hitachi_ac424/common.yaml +++ b/tests/components/hitachi_ac424/common.yaml @@ -1,7 +1,4 @@ -remote_transmitter: - pin: ${pin} - carrier_duty_percent: 50% - climate: - platform: hitachi_ac424 name: Hitachi Climate + transmitter_id: xmitr diff --git a/tests/components/hitachi_ac424/test.bk72xx-ard.yaml b/tests/components/hitachi_ac424/test.bk72xx-ard.yaml index 06e1aea364..6cce191825 100644 --- a/tests/components/hitachi_ac424/test.bk72xx-ard.yaml +++ b/tests/components/hitachi_ac424/test.bk72xx-ard.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO6 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/bk72xx-ard.yaml <<: !include common.yaml diff --git a/tests/components/hitachi_ac424/test.esp32-ard.yaml b/tests/components/hitachi_ac424/test.esp32-ard.yaml deleted file mode 100644 index 7b012aa64c..0000000000 --- a/tests/components/hitachi_ac424/test.esp32-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/hitachi_ac424/test.esp32-c3-ard.yaml b/tests/components/hitachi_ac424/test.esp32-c3-ard.yaml deleted file mode 100644 index 7b012aa64c..0000000000 --- a/tests/components/hitachi_ac424/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/hitachi_ac424/test.esp32-c3-idf.yaml b/tests/components/hitachi_ac424/test.esp32-c3-idf.yaml deleted file mode 100644 index 7b012aa64c..0000000000 --- a/tests/components/hitachi_ac424/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/hitachi_ac424/test.esp32-idf.yaml b/tests/components/hitachi_ac424/test.esp32-idf.yaml index 7b012aa64c..e891f9dc85 100644 --- a/tests/components/hitachi_ac424/test.esp32-idf.yaml +++ b/tests/components/hitachi_ac424/test.esp32-idf.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO2 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/hitachi_ac424/test.esp8266-ard.yaml b/tests/components/hitachi_ac424/test.esp8266-ard.yaml index f5097fcf5f..4bed2f03e5 100644 --- a/tests/components/hitachi_ac424/test.esp8266-ard.yaml +++ b/tests/components/hitachi_ac424/test.esp8266-ard.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO5 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/hlk_fm22x/common.yaml b/tests/components/hlk_fm22x/common.yaml new file mode 100644 index 0000000000..6fcd9af594 --- /dev/null +++ b/tests/components/hlk_fm22x/common.yaml @@ -0,0 +1,41 @@ +esphome: + on_boot: + then: + - hlk_fm22x.enroll: + name: "Test" + direction: 1 + - hlk_fm22x.delete_all: + +hlk_fm22x: + on_face_scan_matched: + - logger.log: test_hlk_22x_face_scan_matched + on_face_scan_unmatched: + - logger.log: test_hlk_22x_face_scan_unmatched + on_face_scan_invalid: + - logger.log: test_hlk_22x_face_scan_invalid + on_face_info: + - logger.log: test_hlk_22x_face_info + on_enrollment_done: + - logger.log: test_hlk_22x_enrollment_done + on_enrollment_failed: + - logger.log: test_hlk_22x_enrollment_failed + +sensor: + - platform: hlk_fm22x + face_count: + name: "Face Count" + last_face_id: + name: "Last Face ID" + status: + name: "Face Status" + +binary_sensor: + - platform: hlk_fm22x + name: "Face Enrolling" + +text_sensor: + - platform: hlk_fm22x + version: + name: "HLK Version" + last_face_name: + name: "Last Face Name" diff --git a/tests/components/hlk_fm22x/test.esp32-idf.yaml b/tests/components/hlk_fm22x/test.esp32-idf.yaml new file mode 100644 index 0000000000..2d29656c94 --- /dev/null +++ b/tests/components/hlk_fm22x/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/hlk_fm22x/test.esp8266-ard.yaml b/tests/components/hlk_fm22x/test.esp8266-ard.yaml new file mode 100644 index 0000000000..5a05efa259 --- /dev/null +++ b/tests/components/hlk_fm22x/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/hlk_fm22x/test.rp2040-ard.yaml b/tests/components/hlk_fm22x/test.rp2040-ard.yaml new file mode 100644 index 0000000000..f1df2daf83 --- /dev/null +++ b/tests/components/hlk_fm22x/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/hlw8012/test.esp32-ard.yaml b/tests/components/hlw8012/test.esp32-ard.yaml deleted file mode 100644 index 8b42b21b54..0000000000 --- a/tests/components/hlw8012/test.esp32-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - sel_pin: GPIO12 - cf_pin: GPIO13 - cf1_pin: GPIO14 - -<<: !include common.yaml diff --git a/tests/components/hlw8012/test.esp32-c3-ard.yaml b/tests/components/hlw8012/test.esp32-c3-ard.yaml deleted file mode 100644 index 8b0d069ce2..0000000000 --- a/tests/components/hlw8012/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - sel_pin: GPIO2 - cf_pin: GPIO3 - cf1_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/hlw8012/test.esp32-c3-idf.yaml b/tests/components/hlw8012/test.esp32-c3-idf.yaml deleted file mode 100644 index 8b0d069ce2..0000000000 --- a/tests/components/hlw8012/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - sel_pin: GPIO2 - cf_pin: GPIO3 - cf1_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/hlw8012/test.esp8266-ard.yaml b/tests/components/hlw8012/test.esp8266-ard.yaml index 8b42b21b54..ec9c0e43dc 100644 --- a/tests/components/hlw8012/test.esp8266-ard.yaml +++ b/tests/components/hlw8012/test.esp8266-ard.yaml @@ -1,6 +1,6 @@ substitutions: - sel_pin: GPIO12 - cf_pin: GPIO13 - cf1_pin: GPIO14 + sel_pin: GPIO0 + cf_pin: GPIO2 + cf1_pin: GPIO15 <<: !include common.yaml diff --git a/tests/components/hm3301/common.yaml b/tests/components/hm3301/common.yaml index b533130569..a56ac7bc65 100644 --- a/tests/components/hm3301/common.yaml +++ b/tests/components/hm3301/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_hm3301 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: hm3301 + i2c_id: i2c_bus pm_1_0: name: PM1.0 pm_2_5: diff --git a/tests/components/hm3301/test.esp32-ard.yaml b/tests/components/hm3301/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/hm3301/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/hm3301/test.esp32-c3-ard.yaml b/tests/components/hm3301/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/hm3301/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/hm3301/test.esp32-c3-idf.yaml b/tests/components/hm3301/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/hm3301/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/hm3301/test.esp32-idf.yaml b/tests/components/hm3301/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/hm3301/test.esp32-idf.yaml +++ b/tests/components/hm3301/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/hm3301/test.esp8266-ard.yaml b/tests/components/hm3301/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/hm3301/test.esp8266-ard.yaml +++ b/tests/components/hm3301/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/hm3301/test.rp2040-ard.yaml b/tests/components/hm3301/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/hm3301/test.rp2040-ard.yaml +++ b/tests/components/hm3301/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/hmc5883l/common.yaml b/tests/components/hmc5883l/common.yaml index 1c90f5f1c6..19e4ba4c45 100644 --- a/tests/components/hmc5883l/common.yaml +++ b/tests/components/hmc5883l/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_hmc5883l - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: hmc5883l + i2c_id: i2c_bus address: 0x68 field_strength_x: name: HMC5883L Field Strength X diff --git a/tests/components/hmc5883l/test.esp32-ard.yaml b/tests/components/hmc5883l/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/hmc5883l/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/hmc5883l/test.esp32-c3-ard.yaml b/tests/components/hmc5883l/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/hmc5883l/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/hmc5883l/test.esp32-c3-idf.yaml b/tests/components/hmc5883l/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/hmc5883l/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/hmc5883l/test.esp32-idf.yaml b/tests/components/hmc5883l/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/hmc5883l/test.esp32-idf.yaml +++ b/tests/components/hmc5883l/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/hmc5883l/test.esp8266-ard.yaml b/tests/components/hmc5883l/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/hmc5883l/test.esp8266-ard.yaml +++ b/tests/components/hmc5883l/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/hmc5883l/test.rp2040-ard.yaml b/tests/components/hmc5883l/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/hmc5883l/test.rp2040-ard.yaml +++ b/tests/components/hmc5883l/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/homeassistant/test-tag-scanned.esp32-idf.yaml b/tests/components/homeassistant/test-tag-scanned.esp32-idf.yaml new file mode 100644 index 0000000000..ef148174d7 --- /dev/null +++ b/tests/components/homeassistant/test-tag-scanned.esp32-idf.yaml @@ -0,0 +1,14 @@ +wifi: + ssid: MySSID + password: password1 + +api: + +esphome: + on_boot: + then: + - homeassistant.tag_scanned: 'test_tag_123' + - homeassistant.tag_scanned: + tag: 'another_tag' + - homeassistant.tag_scanned: + tag: !lambda 'return "dynamic_tag";' diff --git a/tests/components/homeassistant/test.esp32-ard.yaml b/tests/components/homeassistant/test.esp32-ard.yaml deleted file mode 100644 index 25cb37a0b4..0000000000 --- a/tests/components/homeassistant/test.esp32-ard.yaml +++ /dev/null @@ -1,2 +0,0 @@ -packages: - common: !include common.yaml diff --git a/tests/components/homeassistant/test.esp32-c3-ard.yaml b/tests/components/homeassistant/test.esp32-c3-ard.yaml deleted file mode 100644 index 25cb37a0b4..0000000000 --- a/tests/components/homeassistant/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,2 +0,0 @@ -packages: - common: !include common.yaml diff --git a/tests/components/homeassistant/test.esp32-c3-idf.yaml b/tests/components/homeassistant/test.esp32-c3-idf.yaml deleted file mode 100644 index 25cb37a0b4..0000000000 --- a/tests/components/homeassistant/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,2 +0,0 @@ -packages: - common: !include common.yaml diff --git a/tests/components/honeywell_hih_i2c/common.yaml b/tests/components/honeywell_hih_i2c/common.yaml index a5f3eef187..de588a30bd 100644 --- a/tests/components/honeywell_hih_i2c/common.yaml +++ b/tests/components/honeywell_hih_i2c/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_honeywell_hih - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: honeywell_hih_i2c + i2c_id: i2c_bus temperature: name: Temperature humidity: diff --git a/tests/components/honeywell_hih_i2c/test.esp32-ard.yaml b/tests/components/honeywell_hih_i2c/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/honeywell_hih_i2c/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/honeywell_hih_i2c/test.esp32-c3-ard.yaml b/tests/components/honeywell_hih_i2c/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/honeywell_hih_i2c/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/honeywell_hih_i2c/test.esp32-c3-idf.yaml b/tests/components/honeywell_hih_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/honeywell_hih_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/honeywell_hih_i2c/test.esp32-idf.yaml b/tests/components/honeywell_hih_i2c/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/honeywell_hih_i2c/test.esp32-idf.yaml +++ b/tests/components/honeywell_hih_i2c/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/honeywell_hih_i2c/test.esp8266-ard.yaml b/tests/components/honeywell_hih_i2c/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/honeywell_hih_i2c/test.esp8266-ard.yaml +++ b/tests/components/honeywell_hih_i2c/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/honeywell_hih_i2c/test.rp2040-ard.yaml b/tests/components/honeywell_hih_i2c/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/honeywell_hih_i2c/test.rp2040-ard.yaml +++ b/tests/components/honeywell_hih_i2c/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/honeywellabp/common.yaml b/tests/components/honeywellabp/common.yaml index 21a3ef6ee3..f82d289ace 100644 --- a/tests/components/honeywellabp/common.yaml +++ b/tests/components/honeywellabp/common.yaml @@ -1,9 +1,3 @@ -spi: - - id: spi_bme280 - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - miso_pin: ${miso_pin} - sensor: - platform: honeywellabp cs_pin: ${cs_pin} diff --git a/tests/components/honeywellabp/test.esp32-ard.yaml b/tests/components/honeywellabp/test.esp32-ard.yaml deleted file mode 100644 index 54e027a614..0000000000 --- a/tests/components/honeywellabp/test.esp32-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 - cs_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/honeywellabp/test.esp32-c3-ard.yaml b/tests/components/honeywellabp/test.esp32-c3-ard.yaml deleted file mode 100644 index 2415ba5dc6..0000000000 --- a/tests/components/honeywellabp/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/honeywellabp/test.esp32-c3-idf.yaml b/tests/components/honeywellabp/test.esp32-c3-idf.yaml deleted file mode 100644 index 2415ba5dc6..0000000000 --- a/tests/components/honeywellabp/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/honeywellabp/test.esp32-idf.yaml b/tests/components/honeywellabp/test.esp32-idf.yaml index 54e027a614..a3352cf880 100644 --- a/tests/components/honeywellabp/test.esp32-idf.yaml +++ b/tests/components/honeywellabp/test.esp32-idf.yaml @@ -1,7 +1,7 @@ substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 cs_pin: GPIO5 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/honeywellabp/test.esp8266-ard.yaml b/tests/components/honeywellabp/test.esp8266-ard.yaml index dbd158d030..b4673ba8b7 100644 --- a/tests/components/honeywellabp/test.esp8266-ard.yaml +++ b/tests/components/honeywellabp/test.esp8266-ard.yaml @@ -1,7 +1,10 @@ substitutions: - clk_pin: GPIO14 - mosi_pin: GPIO13 - miso_pin: GPIO12 + clk_pin: GPIO0 + mosi_pin: GPIO2 + miso_pin: GPIO16 cs_pin: GPIO15 +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/honeywellabp/test.rp2040-ard.yaml b/tests/components/honeywellabp/test.rp2040-ard.yaml index f6c3f1eeca..1ded24de1c 100644 --- a/tests/components/honeywellabp/test.rp2040-ard.yaml +++ b/tests/components/honeywellabp/test.rp2040-ard.yaml @@ -4,4 +4,7 @@ substitutions: miso_pin: GPIO4 cs_pin: GPIO5 +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/honeywellabp2_i2c/common.yaml b/tests/components/honeywellabp2_i2c/common.yaml index 6752a69866..e1b060edcf 100644 --- a/tests/components/honeywellabp2_i2c/common.yaml +++ b/tests/components/honeywellabp2_i2c/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_honeywellabp2 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: honeywellabp2_i2c + i2c_id: i2c_bus address: 0x28 pressure: name: Honeywell2 pressure diff --git a/tests/components/honeywellabp2_i2c/test.esp32-ard.yaml b/tests/components/honeywellabp2_i2c/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/honeywellabp2_i2c/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/honeywellabp2_i2c/test.esp32-c3-ard.yaml b/tests/components/honeywellabp2_i2c/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/honeywellabp2_i2c/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/honeywellabp2_i2c/test.esp32-c3-idf.yaml b/tests/components/honeywellabp2_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/honeywellabp2_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/honeywellabp2_i2c/test.esp32-idf.yaml b/tests/components/honeywellabp2_i2c/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/honeywellabp2_i2c/test.esp32-idf.yaml +++ b/tests/components/honeywellabp2_i2c/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/honeywellabp2_i2c/test.esp8266-ard.yaml b/tests/components/honeywellabp2_i2c/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/honeywellabp2_i2c/test.esp8266-ard.yaml +++ b/tests/components/honeywellabp2_i2c/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/honeywellabp2_i2c/test.rp2040-ard.yaml b/tests/components/honeywellabp2_i2c/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/honeywellabp2_i2c/test.rp2040-ard.yaml +++ b/tests/components/honeywellabp2_i2c/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/host/common.yaml b/tests/components/host/common.yaml index 5c329c8245..d5c8446ae8 100644 --- a/tests/components/host/common.yaml +++ b/tests/components/host/common.yaml @@ -15,3 +15,10 @@ esphome: static const uint8_t my_addr[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; if (!mac_address_is_valid(my_addr)) ESP_LOGD("test", "Invalid mac address %X", my_addr[0]); // etc. + int x = 100; + x = clamp(x, 50, 90); + assert(x == 90); + x = clamp_at_least(x, 95); + assert(x == 95); + x = clamp_at_most(x, 40); + assert(x == 40); diff --git a/tests/components/hrxl_maxsonar_wr/common.yaml b/tests/components/hrxl_maxsonar_wr/common.yaml index d74ada716d..84376aa5ba 100644 --- a/tests/components/hrxl_maxsonar_wr/common.yaml +++ b/tests/components/hrxl_maxsonar_wr/common.yaml @@ -1,9 +1,3 @@ -uart: - - id: uart_hrxl_maxsonar_wr - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 115200 - sensor: - platform: hrxl_maxsonar_wr id: hrxl_maxsonar_wr_sensor diff --git a/tests/components/hrxl_maxsonar_wr/test.esp32-ard.yaml b/tests/components/hrxl_maxsonar_wr/test.esp32-ard.yaml deleted file mode 100644 index 811f6b72a6..0000000000 --- a/tests/components/hrxl_maxsonar_wr/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO12 - rx_pin: GPIO14 - -<<: !include common.yaml diff --git a/tests/components/hrxl_maxsonar_wr/test.esp32-c3-ard.yaml b/tests/components/hrxl_maxsonar_wr/test.esp32-c3-ard.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/hrxl_maxsonar_wr/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/hrxl_maxsonar_wr/test.esp32-c3-idf.yaml b/tests/components/hrxl_maxsonar_wr/test.esp32-c3-idf.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/hrxl_maxsonar_wr/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/hrxl_maxsonar_wr/test.esp32-idf.yaml b/tests/components/hrxl_maxsonar_wr/test.esp32-idf.yaml index 811f6b72a6..64baa4ec9d 100644 --- a/tests/components/hrxl_maxsonar_wr/test.esp32-idf.yaml +++ b/tests/components/hrxl_maxsonar_wr/test.esp32-idf.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO12 rx_pin: GPIO14 +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/hrxl_maxsonar_wr/test.esp8266-ard.yaml b/tests/components/hrxl_maxsonar_wr/test.esp8266-ard.yaml index b516342f3b..96ab4ef6ac 100644 --- a/tests/components/hrxl_maxsonar_wr/test.esp8266-ard.yaml +++ b/tests/components/hrxl_maxsonar_wr/test.esp8266-ard.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: GPIO0 + rx_pin: GPIO2 + +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/hrxl_maxsonar_wr/test.rp2040-ard.yaml b/tests/components/hrxl_maxsonar_wr/test.rp2040-ard.yaml index b516342f3b..b28f2b5e05 100644 --- a/tests/components/hrxl_maxsonar_wr/test.rp2040-ard.yaml +++ b/tests/components/hrxl_maxsonar_wr/test.rp2040-ard.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO4 rx_pin: GPIO5 +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/hte501/common.yaml b/tests/components/hte501/common.yaml index 7c57b5bc6a..e0641de193 100644 --- a/tests/components/hte501/common.yaml +++ b/tests/components/hte501/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_hte501 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: hte501 + i2c_id: i2c_bus address: 0x40 temperature: name: Temperature diff --git a/tests/components/hte501/test.esp32-ard.yaml b/tests/components/hte501/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/hte501/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/hte501/test.esp32-c3-ard.yaml b/tests/components/hte501/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/hte501/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/hte501/test.esp32-c3-idf.yaml b/tests/components/hte501/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/hte501/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/hte501/test.esp32-idf.yaml b/tests/components/hte501/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/hte501/test.esp32-idf.yaml +++ b/tests/components/hte501/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/hte501/test.esp8266-ard.yaml b/tests/components/hte501/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/hte501/test.esp8266-ard.yaml +++ b/tests/components/hte501/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/hte501/test.rp2040-ard.yaml b/tests/components/hte501/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/hte501/test.rp2040-ard.yaml +++ b/tests/components/hte501/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/http_request/common.yaml b/tests/components/http_request/common.yaml index 97961007e2..62d0a7941a 100644 --- a/tests/components/http_request/common.yaml +++ b/tests/components/http_request/common.yaml @@ -4,53 +4,9 @@ wifi: ssid: MySSID password: password1 -esphome: - on_boot: - then: - - http_request.get: - url: https://esphome.io - request_headers: - Content-Type: application/json - collect_headers: - - age - on_error: - logger.log: "Request failed" - on_response: - then: - - logger.log: - format: "Response status: %d, Duration: %lu ms, age: %s" - args: - - response->status_code - - (long) response->duration_ms - - response->get_response_header("age").c_str() - - http_request.post: - url: https://esphome.io - request_headers: - Content-Type: application/json - json: - key: value - - http_request.send: - method: PUT - url: https://esphome.io - request_headers: - Content-Type: application/json - body: "Some data" - -http_request: - useragent: esphome/tagreader - timeout: 10s - verify_ssl: ${verify_ssl} - -script: - - id: does_not_compile - parameters: - api_url: string - then: - - http_request.get: - url: "http://google.com" - ota: - platform: http_request + id: http_request_ota on_begin: then: - logger.log: "OTA start" @@ -77,10 +33,12 @@ button: on_press: then: - ota.http_request.flash: + id: http_request_ota md5_url: http://my.ha.net:8123/local/esphome/firmware.md5 url: http://my.ha.net:8123/local/esphome/firmware.bin - ota.http_request.flash: + id: http_request_ota md5: 0123456789abcdef0123456789abcdef url: http://my.ha.net:8123/local/esphome/firmware.bin @@ -90,6 +48,7 @@ update: - platform: http_request name: OTA Update id: ota_update + ota_id: http_request_ota source: http://my.ha.net:8123/local/esphome/manifest.json on_update_available: - logger.log: "A new update is available" diff --git a/tests/components/http_request/http_request.yaml b/tests/components/http_request/http_request.yaml index ea7f6bf5a7..13ca5ceba0 100644 --- a/tests/components/http_request/http_request.yaml +++ b/tests/components/http_request/http_request.yaml @@ -31,6 +31,20 @@ esphome: request_headers: Content-Type: application/json body: "Some data" + - http_request.post: + url: https://esphome.io + request_headers: + Content-Type: application/json + json: + key: value + capture_response: true + on_response: + then: + - logger.log: + format: "Captured response status: %d, Body: %s" + args: + - response->status_code + - body.c_str() http_request: useragent: esphome/tagreader diff --git a/tests/components/http_request/test.esp32-c3-ard.yaml b/tests/components/http_request/test.esp32-c3-ard.yaml deleted file mode 100644 index c1937b5a10..0000000000 --- a/tests/components/http_request/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - verify_ssl: "false" - -<<: !include common.yaml diff --git a/tests/components/http_request/test.esp32-c3-idf.yaml b/tests/components/http_request/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2f5aa59b..0000000000 --- a/tests/components/http_request/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - verify_ssl: "true" - -<<: !include common.yaml diff --git a/tests/components/htu21d/common.yaml b/tests/components/htu21d/common.yaml index f12c1ca46e..ad4b23d460 100644 --- a/tests/components/htu21d/common.yaml +++ b/tests/components/htu21d/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_htu21d - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: htu21d + i2c_id: i2c_bus model: htu21d temperature: name: Temperature diff --git a/tests/components/htu21d/test.esp32-ard.yaml b/tests/components/htu21d/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/htu21d/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/htu21d/test.esp32-c3-ard.yaml b/tests/components/htu21d/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/htu21d/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/htu21d/test.esp32-c3-idf.yaml b/tests/components/htu21d/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/htu21d/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/htu21d/test.esp32-idf.yaml b/tests/components/htu21d/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/htu21d/test.esp32-idf.yaml +++ b/tests/components/htu21d/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/htu21d/test.esp8266-ard.yaml b/tests/components/htu21d/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/htu21d/test.esp8266-ard.yaml +++ b/tests/components/htu21d/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/htu21d/test.rp2040-ard.yaml b/tests/components/htu21d/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/htu21d/test.rp2040-ard.yaml +++ b/tests/components/htu21d/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/htu31d/common.yaml b/tests/components/htu31d/common.yaml index 735cdc7cbf..41d718ea69 100644 --- a/tests/components/htu31d/common.yaml +++ b/tests/components/htu31d/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_htu31d - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: htu31d + i2c_id: i2c_bus temperature: name: Living Room Temperature humidity: diff --git a/tests/components/htu31d/test.esp32-ard.yaml b/tests/components/htu31d/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/htu31d/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/htu31d/test.esp32-c3-ard.yaml b/tests/components/htu31d/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/htu31d/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/htu31d/test.esp32-c3-idf.yaml b/tests/components/htu31d/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/htu31d/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/htu31d/test.esp32-idf.yaml b/tests/components/htu31d/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/htu31d/test.esp32-idf.yaml +++ b/tests/components/htu31d/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/htu31d/test.esp8266-ard.yaml b/tests/components/htu31d/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/htu31d/test.esp8266-ard.yaml +++ b/tests/components/htu31d/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/htu31d/test.rp2040-ard.yaml b/tests/components/htu31d/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/htu31d/test.rp2040-ard.yaml +++ b/tests/components/htu31d/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/hx711/test.esp32-ard.yaml b/tests/components/hx711/test.esp32-ard.yaml deleted file mode 100644 index 6423867395..0000000000 --- a/tests/components/hx711/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - clk_pin: GPIO16 - dout_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/hx711/test.esp32-c3-ard.yaml b/tests/components/hx711/test.esp32-c3-ard.yaml deleted file mode 100644 index 08a6e705c0..0000000000 --- a/tests/components/hx711/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - clk_pin: GPIO5 - dout_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/hx711/test.esp32-c3-idf.yaml b/tests/components/hx711/test.esp32-c3-idf.yaml deleted file mode 100644 index 08a6e705c0..0000000000 --- a/tests/components/hx711/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - clk_pin: GPIO5 - dout_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/hx711/test.esp32-idf.yaml b/tests/components/hx711/test.esp32-idf.yaml index 6423867395..defef165e3 100644 --- a/tests/components/hx711/test.esp32-idf.yaml +++ b/tests/components/hx711/test.esp32-idf.yaml @@ -1,5 +1,5 @@ substitutions: - clk_pin: GPIO16 - dout_pin: GPIO17 + clk_pin: GPIO4 + dout_pin: GPIO5 <<: !include common.yaml diff --git a/tests/components/hx711/test.esp8266-ard.yaml b/tests/components/hx711/test.esp8266-ard.yaml index 08a6e705c0..e7c017ed99 100644 --- a/tests/components/hx711/test.esp8266-ard.yaml +++ b/tests/components/hx711/test.esp8266-ard.yaml @@ -1,5 +1,5 @@ substitutions: - clk_pin: GPIO5 - dout_pin: GPIO4 + clk_pin: GPIO0 + dout_pin: GPIO2 <<: !include common.yaml diff --git a/tests/components/hx711/test.rp2040-ard.yaml b/tests/components/hx711/test.rp2040-ard.yaml index 08a6e705c0..defef165e3 100644 --- a/tests/components/hx711/test.rp2040-ard.yaml +++ b/tests/components/hx711/test.rp2040-ard.yaml @@ -1,5 +1,5 @@ substitutions: - clk_pin: GPIO5 - dout_pin: GPIO4 + clk_pin: GPIO4 + dout_pin: GPIO5 <<: !include common.yaml diff --git a/tests/components/hydreon_rgxx/common.yaml b/tests/components/hydreon_rgxx/common.yaml index e11c6c91c9..e96879fdec 100644 --- a/tests/components/hydreon_rgxx/common.yaml +++ b/tests/components/hydreon_rgxx/common.yaml @@ -1,9 +1,3 @@ -uart: - - id: uart_hydreon_rgxx - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 115200 - binary_sensor: - platform: hydreon_rgxx hydreon_rgxx_id: hydreon_rg9 diff --git a/tests/components/hydreon_rgxx/test.esp32-ard.yaml b/tests/components/hydreon_rgxx/test.esp32-ard.yaml deleted file mode 100644 index f486544afa..0000000000 --- a/tests/components/hydreon_rgxx/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 - -<<: !include common.yaml diff --git a/tests/components/hydreon_rgxx/test.esp32-c3-ard.yaml b/tests/components/hydreon_rgxx/test.esp32-c3-ard.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/hydreon_rgxx/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/hydreon_rgxx/test.esp32-c3-idf.yaml b/tests/components/hydreon_rgxx/test.esp32-c3-idf.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/hydreon_rgxx/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/hydreon_rgxx/test.esp32-idf.yaml b/tests/components/hydreon_rgxx/test.esp32-idf.yaml index f486544afa..b415125e84 100644 --- a/tests/components/hydreon_rgxx/test.esp32-idf.yaml +++ b/tests/components/hydreon_rgxx/test.esp32-idf.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 + tx_pin: GPIO4 + rx_pin: GPIO5 + +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/hydreon_rgxx/test.esp8266-ard.yaml b/tests/components/hydreon_rgxx/test.esp8266-ard.yaml index b516342f3b..96ab4ef6ac 100644 --- a/tests/components/hydreon_rgxx/test.esp8266-ard.yaml +++ b/tests/components/hydreon_rgxx/test.esp8266-ard.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: GPIO0 + rx_pin: GPIO2 + +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/hydreon_rgxx/test.rp2040-ard.yaml b/tests/components/hydreon_rgxx/test.rp2040-ard.yaml index b516342f3b..b28f2b5e05 100644 --- a/tests/components/hydreon_rgxx/test.rp2040-ard.yaml +++ b/tests/components/hydreon_rgxx/test.rp2040-ard.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO4 rx_pin: GPIO5 +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/hyt271/common.yaml b/tests/components/hyt271/common.yaml index 7a4371173f..5771d882da 100644 --- a/tests/components/hyt271/common.yaml +++ b/tests/components/hyt271/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_hyt271 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: hyt271 + i2c_id: i2c_bus temperature: name: Temperature humidity: diff --git a/tests/components/hyt271/test.esp32-ard.yaml b/tests/components/hyt271/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/hyt271/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/hyt271/test.esp32-c3-ard.yaml b/tests/components/hyt271/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/hyt271/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/hyt271/test.esp32-c3-idf.yaml b/tests/components/hyt271/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/hyt271/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/hyt271/test.esp32-idf.yaml b/tests/components/hyt271/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/hyt271/test.esp32-idf.yaml +++ b/tests/components/hyt271/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/hyt271/test.esp8266-ard.yaml b/tests/components/hyt271/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/hyt271/test.esp8266-ard.yaml +++ b/tests/components/hyt271/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/hyt271/test.rp2040-ard.yaml b/tests/components/hyt271/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/hyt271/test.rp2040-ard.yaml +++ b/tests/components/hyt271/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/i2c/common.yaml b/tests/components/i2c/common.yaml index d70cd595ad..e69de29bb2 100644 --- a/tests/components/i2c/common.yaml +++ b/tests/components/i2c/common.yaml @@ -1,4 +0,0 @@ -i2c: - - id: i2c_i2c - scl: ${scl_pin} - sda: ${sda_pin} diff --git a/tests/components/i2c/test.esp32-ard.yaml b/tests/components/i2c/test.esp32-ard.yaml index 63c3bd6afd..7c503b0ccb 100644 --- a/tests/components/i2c/test.esp32-ard.yaml +++ b/tests/components/i2c/test.esp32-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-ard.yaml <<: !include common.yaml diff --git a/tests/components/i2c/test.esp32-c3-ard.yaml b/tests/components/i2c/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/i2c/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/i2c/test.esp32-c3-idf.yaml b/tests/components/i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/i2c/test.esp32-idf.yaml b/tests/components/i2c/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/i2c/test.esp32-idf.yaml +++ b/tests/components/i2c/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/i2c/test.esp8266-ard.yaml b/tests/components/i2c/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/i2c/test.esp8266-ard.yaml +++ b/tests/components/i2c/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/i2c/test.nrf52-adafruit.yaml b/tests/components/i2c/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..2a0de6241c --- /dev/null +++ b/tests/components/i2c/test.nrf52-adafruit.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/nrf52.yaml + +<<: !include common.yaml diff --git a/tests/components/i2c/test.nrf52-mcumgr.yaml b/tests/components/i2c/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..2a0de6241c --- /dev/null +++ b/tests/components/i2c/test.nrf52-mcumgr.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/nrf52.yaml + +<<: !include common.yaml diff --git a/tests/components/i2c/test.nrf52-xiao-ble.yaml b/tests/components/i2c/test.nrf52-xiao-ble.yaml new file mode 100644 index 0000000000..2a0de6241c --- /dev/null +++ b/tests/components/i2c/test.nrf52-xiao-ble.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/nrf52.yaml + +<<: !include common.yaml diff --git a/tests/components/i2c/test.rp2040-ard.yaml b/tests/components/i2c/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/i2c/test.rp2040-ard.yaml +++ b/tests/components/i2c/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/i2c_device/common.yaml b/tests/components/i2c_device/common.yaml index 35a6f8f7f0..fed399eb8c 100644 --- a/tests/components/i2c_device/common.yaml +++ b/tests/components/i2c_device/common.yaml @@ -1,8 +1,4 @@ -i2c: - - id: i2c_i2c_device - scl: ${scl_pin} - sda: ${sda_pin} - i2c_device: id: i2cdev + i2c_id: i2c_bus address: 0x2C diff --git a/tests/components/i2c_device/test.esp32-ard.yaml b/tests/components/i2c_device/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/i2c_device/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/i2c_device/test.esp32-c3-ard.yaml b/tests/components/i2c_device/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/i2c_device/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/i2c_device/test.esp32-c3-idf.yaml b/tests/components/i2c_device/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/i2c_device/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/i2c_device/test.esp32-idf.yaml b/tests/components/i2c_device/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/i2c_device/test.esp32-idf.yaml +++ b/tests/components/i2c_device/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/i2c_device/test.esp8266-ard.yaml b/tests/components/i2c_device/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/i2c_device/test.esp8266-ard.yaml +++ b/tests/components/i2c_device/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/i2c_device/test.rp2040-ard.yaml b/tests/components/i2c_device/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/i2c_device/test.rp2040-ard.yaml +++ b/tests/components/i2c_device/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/i2s_audio/test.esp32-ard.yaml b/tests/components/i2s_audio/test.esp32-ard.yaml deleted file mode 100644 index ce751d7d4a..0000000000 --- a/tests/components/i2s_audio/test.esp32-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - i2s_bclk_pin: GPIO15 - i2s_lrclk_pin: GPIO16 - i2s_mclk_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/i2s_audio/test.esp32-c3-ard.yaml b/tests/components/i2s_audio/test.esp32-c3-ard.yaml deleted file mode 100644 index 5490846ae9..0000000000 --- a/tests/components/i2s_audio/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - i2s_bclk_pin: GPIO5 - i2s_lrclk_pin: GPIO6 - i2s_mclk_pin: GPIO7 - -<<: !include common.yaml diff --git a/tests/components/i2s_audio/test.esp32-idf.yaml b/tests/components/i2s_audio/test.esp32-idf.yaml index ce751d7d4a..ead1eaa1e9 100644 --- a/tests/components/i2s_audio/test.esp32-idf.yaml +++ b/tests/components/i2s_audio/test.esp32-idf.yaml @@ -1,6 +1,9 @@ substitutions: i2s_bclk_pin: GPIO15 - i2s_lrclk_pin: GPIO16 - i2s_mclk_pin: GPIO17 + i2s_lrclk_pin: GPIO4 + i2s_mclk_pin: GPIO5 + +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/iaqcore/common.yaml b/tests/components/iaqcore/common.yaml index 927b836c52..e0f9d09a00 100644 --- a/tests/components/iaqcore/common.yaml +++ b/tests/components/iaqcore/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_iaqcore - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: iaqcore + i2c_id: i2c_bus co2: name: iAQ Core CO2 Sensor tvoc: diff --git a/tests/components/iaqcore/test.esp32-ard.yaml b/tests/components/iaqcore/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/iaqcore/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/iaqcore/test.esp32-c3-ard.yaml b/tests/components/iaqcore/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/iaqcore/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/iaqcore/test.esp32-c3-idf.yaml b/tests/components/iaqcore/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/iaqcore/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/iaqcore/test.esp32-idf.yaml b/tests/components/iaqcore/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/iaqcore/test.esp32-idf.yaml +++ b/tests/components/iaqcore/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/iaqcore/test.esp8266-ard.yaml b/tests/components/iaqcore/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/iaqcore/test.esp8266-ard.yaml +++ b/tests/components/iaqcore/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/iaqcore/test.rp2040-ard.yaml b/tests/components/iaqcore/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/iaqcore/test.rp2040-ard.yaml +++ b/tests/components/iaqcore/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/ili9xxx/common.yaml b/tests/components/ili9xxx/common.yaml index b182d110cd..47384b1398 100644 --- a/tests/components/ili9xxx/common.yaml +++ b/tests/components/ili9xxx/common.yaml @@ -1,8 +1,3 @@ -spi: - - id: spi_main_lcd - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - display: - platform: ili9xxx invert_colors: true diff --git a/tests/components/ili9xxx/test.esp32-ard.yaml b/tests/components/ili9xxx/test.esp32-ard.yaml deleted file mode 100644 index 2e006d2521..0000000000 --- a/tests/components/ili9xxx/test.esp32-ard.yaml +++ /dev/null @@ -1,11 +0,0 @@ -substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - cs_pin1: GPIO12 - dc_pin1: GPIO13 - reset_pin1: GPIO14 - cs_pin2: GPIO25 - dc_pin2: GPIO26 - reset_pin2: GPIO27 - -<<: !include common.yaml diff --git a/tests/components/ili9xxx/test.esp32-c3-ard.yaml b/tests/components/ili9xxx/test.esp32-c3-ard.yaml deleted file mode 100644 index 3037785e81..0000000000 --- a/tests/components/ili9xxx/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,12 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin1: GPIO8 - dc_pin1: GPIO9 - reset_pin1: GPIO10 - cs_pin2: GPIO2 - dc_pin2: GPIO3 - reset_pin2: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ili9xxx/test.esp32-c3-idf.yaml b/tests/components/ili9xxx/test.esp32-c3-idf.yaml deleted file mode 100644 index 3037785e81..0000000000 --- a/tests/components/ili9xxx/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,12 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin1: GPIO8 - dc_pin1: GPIO9 - reset_pin1: GPIO10 - cs_pin2: GPIO2 - dc_pin2: GPIO3 - reset_pin2: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ili9xxx/test.esp32-idf.yaml b/tests/components/ili9xxx/test.esp32-idf.yaml index 2e006d2521..866e57573b 100644 --- a/tests/components/ili9xxx/test.esp32-idf.yaml +++ b/tests/components/ili9xxx/test.esp32-idf.yaml @@ -1,6 +1,4 @@ substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 cs_pin1: GPIO12 dc_pin1: GPIO13 reset_pin1: GPIO14 @@ -8,4 +6,7 @@ substitutions: dc_pin2: GPIO26 reset_pin2: GPIO27 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/ili9xxx/test.esp8266-ard.yaml b/tests/components/ili9xxx/test.esp8266-ard.yaml index 5dee055fd4..feb7773794 100644 --- a/tests/components/ili9xxx/test.esp8266-ard.yaml +++ b/tests/components/ili9xxx/test.esp8266-ard.yaml @@ -9,4 +9,7 @@ substitutions: dc_pin2: GPIO4 reset_pin2: GPIO0 +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/ili9xxx/test.rp2040-ard.yaml b/tests/components/ili9xxx/test.rp2040-ard.yaml index 74c9b906e9..2acde3e629 100644 --- a/tests/components/ili9xxx/test.rp2040-ard.yaml +++ b/tests/components/ili9xxx/test.rp2040-ard.yaml @@ -9,4 +9,7 @@ substitutions: dc_pin2: GPIO21 reset_pin2: GPIO22 +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/image/common.yaml b/tests/components/image/common.yaml index 864ca41c44..9819068970 100644 --- a/tests/components/image/common.yaml +++ b/tests/components/image/common.yaml @@ -50,16 +50,16 @@ image: transparency: opaque - id: web_svg_image - file: https://raw.githubusercontent.com/esphome/esphome-docs/a62d7ab193c1a464ed791670170c7d518189109b/images/logo.svg + file: https://media.esphome.io/logo/logo.svg resize: 256x48 type: BINARY transparency: chroma_key - id: web_tiff_image - file: https://upload.wikimedia.org/wikipedia/commons/b/b6/SIPI_Jelly_Beans_4.1.07.tiff + file: https://media.esphome.io/tests/images/SIPI_Jelly_Beans_4.1.07.tiff type: RGB resize: 48x48 - id: web_redirect_image - file: https://avatars.githubusercontent.com/u/3060199?s=48&v=4 + file: https://media.esphome.io/logo/logo.png type: RGB resize: 48x48 - id: mdi_alert diff --git a/tests/components/image/test.esp32-idf.yaml b/tests/components/image/test.esp32-idf.yaml index 814f16c36c..aea2b4bbb0 100644 --- a/tests/components/image/test.esp32-idf.yaml +++ b/tests/components/image/test.esp32-idf.yaml @@ -1,14 +1,12 @@ -spi: - - id: spi_main_lcd - clk_pin: 16 - mosi_pin: 17 - miso_pin: 18 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml display: - platform: ili9xxx id: main_lcd + spi_id: spi_bus model: ili9342 - cs_pin: 19 + cs_pin: 15 dc_pin: 13 reset_pin: 21 invert_colors: true diff --git a/tests/components/image/test.esp8266-ard.yaml b/tests/components/image/test.esp8266-ard.yaml index 626076d44e..2e7bfc5ae5 100644 --- a/tests/components/image/test.esp8266-ard.yaml +++ b/tests/components/image/test.esp8266-ard.yaml @@ -1,12 +1,10 @@ -spi: - - id: spi_main_lcd - clk_pin: 14 - mosi_pin: 13 - miso_pin: 12 +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml display: - platform: ili9xxx id: main_lcd + spi_id: spi_bus model: ili9342 cs_pin: 5 dc_pin: 15 diff --git a/tests/components/image/test.host.yaml b/tests/components/image/test.host.yaml index 0411195e2a..aa45497088 100644 --- a/tests/components/image/test.host.yaml +++ b/tests/components/image/test.host.yaml @@ -1,5 +1,6 @@ display: - platform: sdl + id: image_display auto_clear_enabled: false dimensions: width: 480 diff --git a/tests/components/image/test.rp2040-ard.yaml b/tests/components/image/test.rp2040-ard.yaml index 5167c99a7d..40f17d57fe 100644 --- a/tests/components/image/test.rp2040-ard.yaml +++ b/tests/components/image/test.rp2040-ard.yaml @@ -1,12 +1,10 @@ -spi: - - id: spi_main_lcd - clk_pin: 2 - mosi_pin: 3 - miso_pin: 4 +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml display: - platform: ili9xxx id: main_lcd + spi_id: spi_bus model: ili9342 cs_pin: 20 dc_pin: 21 diff --git a/tests/components/improv_base/common-uart0.yaml b/tests/components/improv_base/common-uart0.yaml new file mode 100644 index 0000000000..7b7730fd46 --- /dev/null +++ b/tests/components/improv_base/common-uart0.yaml @@ -0,0 +1,8 @@ +wifi: + ssid: MySSID + password: password1 + +logger: + hardware_uart: UART0 + +improv_serial: diff --git a/tests/components/improv_serial/test-uart0.esp32-ard.yaml b/tests/components/improv_base/test-uart0.esp8266-ard.yaml similarity index 100% rename from tests/components/improv_serial/test-uart0.esp32-ard.yaml rename to tests/components/improv_base/test-uart0.esp8266-ard.yaml diff --git a/tests/components/improv_serial/test-uart0.esp32-c3-ard.yaml b/tests/components/improv_serial/test-uart0.esp32-c3-ard.yaml deleted file mode 100644 index ef8c799241..0000000000 --- a/tests/components/improv_serial/test-uart0.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common-uart0.yaml diff --git a/tests/components/improv_serial/test-uart0.esp32-s2-ard.yaml b/tests/components/improv_serial/test-uart0.esp32-s2-ard.yaml deleted file mode 100644 index ef8c799241..0000000000 --- a/tests/components/improv_serial/test-uart0.esp32-s2-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common-uart0.yaml diff --git a/tests/components/improv_serial/test-uart0.esp32-s3-ard.yaml b/tests/components/improv_serial/test-uart0.esp32-s3-ard.yaml deleted file mode 100644 index ef8c799241..0000000000 --- a/tests/components/improv_serial/test-uart0.esp32-s3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common-uart0.yaml diff --git a/tests/components/improv_serial/test-usb_cdc.esp32-s3-ard.yaml b/tests/components/improv_serial/test-usb_cdc.esp32-s3-ard.yaml deleted file mode 100644 index cfdaec9771..0000000000 --- a/tests/components/improv_serial/test-usb_cdc.esp32-s3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common-usb_cdc.yaml diff --git a/tests/components/ina219/common.yaml b/tests/components/ina219/common.yaml index 4ca4c9ed8f..71291a082d 100644 --- a/tests/components/ina219/common.yaml +++ b/tests/components/ina219/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_ina219 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: ina219 + i2c_id: i2c_bus address: 0x40 shunt_resistance: 0.1 ohm current: diff --git a/tests/components/ina219/test.esp32-ard.yaml b/tests/components/ina219/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/ina219/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/ina219/test.esp32-c3-ard.yaml b/tests/components/ina219/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/ina219/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ina219/test.esp32-c3-idf.yaml b/tests/components/ina219/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/ina219/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ina219/test.esp32-idf.yaml b/tests/components/ina219/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/ina219/test.esp32-idf.yaml +++ b/tests/components/ina219/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/ina219/test.esp8266-ard.yaml b/tests/components/ina219/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/ina219/test.esp8266-ard.yaml +++ b/tests/components/ina219/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/ina219/test.rp2040-ard.yaml b/tests/components/ina219/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/ina219/test.rp2040-ard.yaml +++ b/tests/components/ina219/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/ina226/common.yaml b/tests/components/ina226/common.yaml index 8bcd038e50..7cbaf8cb2f 100644 --- a/tests/components/ina226/common.yaml +++ b/tests/components/ina226/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_ina226 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: ina226 + i2c_id: i2c_bus address: 0x40 shunt_resistance: 0.1 ohm current: diff --git a/tests/components/ina226/test.esp32-ard.yaml b/tests/components/ina226/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/ina226/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/ina226/test.esp32-c3-ard.yaml b/tests/components/ina226/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/ina226/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ina226/test.esp32-c3-idf.yaml b/tests/components/ina226/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/ina226/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ina226/test.esp32-idf.yaml b/tests/components/ina226/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/ina226/test.esp32-idf.yaml +++ b/tests/components/ina226/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/ina226/test.esp8266-ard.yaml b/tests/components/ina226/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/ina226/test.esp8266-ard.yaml +++ b/tests/components/ina226/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/ina226/test.rp2040-ard.yaml b/tests/components/ina226/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/ina226/test.rp2040-ard.yaml +++ b/tests/components/ina226/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/ina260/common.yaml b/tests/components/ina260/common.yaml index ab94b2e509..f630d0bb47 100644 --- a/tests/components/ina260/common.yaml +++ b/tests/components/ina260/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_ina260 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: ina260 + i2c_id: i2c_bus address: 0x40 current: name: INA260 Current diff --git a/tests/components/ina260/test.esp32-ard.yaml b/tests/components/ina260/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/ina260/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/ina260/test.esp32-c3-ard.yaml b/tests/components/ina260/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/ina260/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ina260/test.esp32-c3-idf.yaml b/tests/components/ina260/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/ina260/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ina260/test.esp32-idf.yaml b/tests/components/ina260/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/ina260/test.esp32-idf.yaml +++ b/tests/components/ina260/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/ina260/test.esp8266-ard.yaml b/tests/components/ina260/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/ina260/test.esp8266-ard.yaml +++ b/tests/components/ina260/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/ina260/test.rp2040-ard.yaml b/tests/components/ina260/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/ina260/test.rp2040-ard.yaml +++ b/tests/components/ina260/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/ina2xx_i2c/common.yaml b/tests/components/ina2xx_i2c/common.yaml index 320b680b6b..748ab94c98 100644 --- a/tests/components/ina2xx_i2c/common.yaml +++ b/tests/components/ina2xx_i2c/common.yaml @@ -1,20 +1,28 @@ -i2c: - - id: i2c_ina2xx - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: ina2xx_i2c - i2c_id: i2c_ina2xx + i2c_id: i2c_bus address: 0x40 model: INA228 shunt_resistance: 0.001130 ohm max_current: 40 A adc_range: 1 temperature_coefficient: 50 - shunt_voltage: "INA2xx Shunt Voltage" - bus_voltage: "INA2xx Bus Voltage" - current: "INA2xx Current" - power: "INA2xx Power" - energy: "INA2xx Energy" - charge: "INA2xx Charge" + reset_on_boot: true + shunt_voltage: + id: ina2xx_i2c_shunt_voltage + name: "INA2xx Shunt Voltage" + bus_voltage: + id: ina2xx_i2c_bus_voltage + name: "INA2xx Bus Voltage" + current: + id: ina2xx_i2c_current + name: "INA2xx Current" + power: + id: ina2xx_i2c_power + name: "INA2xx Power" + energy: + id: ina2xx_i2c_energy + name: "INA2xx Energy" + charge: + id: ina2xx_i2c_charge + name: "INA2xx Charge" diff --git a/tests/components/ina2xx_i2c/test.esp32-ard.yaml b/tests/components/ina2xx_i2c/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/ina2xx_i2c/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/ina2xx_i2c/test.esp32-c3-ard.yaml b/tests/components/ina2xx_i2c/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/ina2xx_i2c/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ina2xx_i2c/test.esp32-c3-idf.yaml b/tests/components/ina2xx_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/ina2xx_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ina2xx_i2c/test.esp32-idf.yaml b/tests/components/ina2xx_i2c/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/ina2xx_i2c/test.esp32-idf.yaml +++ b/tests/components/ina2xx_i2c/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/ina2xx_i2c/test.esp8266-ard.yaml b/tests/components/ina2xx_i2c/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/ina2xx_i2c/test.esp8266-ard.yaml +++ b/tests/components/ina2xx_i2c/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/ina2xx_i2c/test.rp2040-ard.yaml b/tests/components/ina2xx_i2c/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/ina2xx_i2c/test.rp2040-ard.yaml +++ b/tests/components/ina2xx_i2c/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/ina2xx_spi/common.yaml b/tests/components/ina2xx_spi/common.yaml index 3eab7e6f0a..8de77eba26 100644 --- a/tests/components/ina2xx_spi/common.yaml +++ b/tests/components/ina2xx_spi/common.yaml @@ -1,21 +1,26 @@ -spi: - - id: spi_ina2xx - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - miso_pin: ${miso_pin} - sensor: - platform: ina2xx_spi - spi_id: spi_ina2xx cs_pin: ${cs_pin} model: INA229 shunt_resistance: 0.001130 ohm max_current: 40 A adc_range: 1 temperature_coefficient: 50 - shunt_voltage: "INA2xx Shunt Voltage" - bus_voltage: "INA2xx Bus Voltage" - current: "INA2xx Current" - power: "INA2xx Power" - energy: "INA2xx Energy" - charge: "INA2xx Charge" + shunt_voltage: + id: ina2xx_spi_shunt_voltage + name: "INA2xx Shunt Voltage" + bus_voltage: + id: ina2xx_spi_bus_voltage + name: "INA2xx Bus Voltage" + current: + id: ina2xx_spi_current + name: "INA2xx Current" + power: + id: ina2xx_spi_power + name: "INA2xx Power" + energy: + id: ina2xx_spi_energy + name: "INA2xx Energy" + charge: + id: ina2xx_spi_charge + name: "INA2xx Charge" diff --git a/tests/components/ina2xx_spi/test.esp32-ard.yaml b/tests/components/ina2xx_spi/test.esp32-ard.yaml deleted file mode 100644 index 54e027a614..0000000000 --- a/tests/components/ina2xx_spi/test.esp32-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 - cs_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/ina2xx_spi/test.esp32-c3-ard.yaml b/tests/components/ina2xx_spi/test.esp32-c3-ard.yaml deleted file mode 100644 index 2415ba5dc6..0000000000 --- a/tests/components/ina2xx_spi/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/ina2xx_spi/test.esp32-c3-idf.yaml b/tests/components/ina2xx_spi/test.esp32-c3-idf.yaml deleted file mode 100644 index 2415ba5dc6..0000000000 --- a/tests/components/ina2xx_spi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/ina2xx_spi/test.esp32-idf.yaml b/tests/components/ina2xx_spi/test.esp32-idf.yaml index 54e027a614..a3352cf880 100644 --- a/tests/components/ina2xx_spi/test.esp32-idf.yaml +++ b/tests/components/ina2xx_spi/test.esp32-idf.yaml @@ -1,7 +1,7 @@ substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 cs_pin: GPIO5 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/ina2xx_spi/test.esp8266-ard.yaml b/tests/components/ina2xx_spi/test.esp8266-ard.yaml index dbd158d030..b4673ba8b7 100644 --- a/tests/components/ina2xx_spi/test.esp8266-ard.yaml +++ b/tests/components/ina2xx_spi/test.esp8266-ard.yaml @@ -1,7 +1,10 @@ substitutions: - clk_pin: GPIO14 - mosi_pin: GPIO13 - miso_pin: GPIO12 + clk_pin: GPIO0 + mosi_pin: GPIO2 + miso_pin: GPIO16 cs_pin: GPIO15 +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/ina2xx_spi/test.rp2040-ard.yaml b/tests/components/ina2xx_spi/test.rp2040-ard.yaml index f6c3f1eeca..1ded24de1c 100644 --- a/tests/components/ina2xx_spi/test.rp2040-ard.yaml +++ b/tests/components/ina2xx_spi/test.rp2040-ard.yaml @@ -4,4 +4,7 @@ substitutions: miso_pin: GPIO4 cs_pin: GPIO5 +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/ina3221/common.yaml b/tests/components/ina3221/common.yaml index ba1abdfe3a..570d1b0a12 100644 --- a/tests/components/ina3221/common.yaml +++ b/tests/components/ina3221/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_ina3221 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: ina3221 + i2c_id: i2c_bus address: 0x40 channel_1: shunt_resistance: 0.1 ohm diff --git a/tests/components/ina3221/test.esp32-ard.yaml b/tests/components/ina3221/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/ina3221/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/ina3221/test.esp32-c3-ard.yaml b/tests/components/ina3221/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/ina3221/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ina3221/test.esp32-c3-idf.yaml b/tests/components/ina3221/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/ina3221/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ina3221/test.esp32-idf.yaml b/tests/components/ina3221/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/ina3221/test.esp32-idf.yaml +++ b/tests/components/ina3221/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/ina3221/test.esp8266-ard.yaml b/tests/components/ina3221/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/ina3221/test.esp8266-ard.yaml +++ b/tests/components/ina3221/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/ina3221/test.rp2040-ard.yaml b/tests/components/ina3221/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/ina3221/test.rp2040-ard.yaml +++ b/tests/components/ina3221/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/inkbird_ibsth1_mini/test.esp32-ard.yaml b/tests/components/inkbird_ibsth1_mini/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/inkbird_ibsth1_mini/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/inkbird_ibsth1_mini/test.esp32-c3-ard.yaml b/tests/components/inkbird_ibsth1_mini/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/inkbird_ibsth1_mini/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/inkbird_ibsth1_mini/test.esp32-c3-idf.yaml b/tests/components/inkbird_ibsth1_mini/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/inkbird_ibsth1_mini/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/inkbird_ibsth1_mini/test.esp32-idf.yaml b/tests/components/inkbird_ibsth1_mini/test.esp32-idf.yaml index dade44d145..7a6541ae76 100644 --- a/tests/components/inkbird_ibsth1_mini/test.esp32-idf.yaml +++ b/tests/components/inkbird_ibsth1_mini/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/inkplate6/common.yaml b/tests/components/inkplate/common.yaml similarity index 94% rename from tests/components/inkplate6/common.yaml rename to tests/components/inkplate/common.yaml index 6cb5d055b6..bb18ff4f7e 100644 --- a/tests/components/inkplate6/common.yaml +++ b/tests/components/inkplate/common.yaml @@ -1,13 +1,9 @@ -i2c: - - id: i2c_inkplate6 - scl: 16 - sda: 17 - esp32: cpu_frequency: 240MHz display: - - platform: inkplate6 + - platform: inkplate + i2c_id: i2c_bus id: inkplate_display greyscale: false partial_updating: false diff --git a/tests/components/inkplate/test.esp32-idf.yaml b/tests/components/inkplate/test.esp32-idf.yaml new file mode 100644 index 0000000000..17e58ce390 --- /dev/null +++ b/tests/components/inkplate/test.esp32-idf.yaml @@ -0,0 +1,7 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + +psram: + mode: quad + +<<: !include common.yaml diff --git a/tests/components/inkplate6/test.esp32-ard.yaml b/tests/components/inkplate6/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/inkplate6/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/integration/test.esp32-ard.yaml b/tests/components/integration/test.esp32-ard.yaml deleted file mode 100644 index f84495e521..0000000000 --- a/tests/components/integration/test.esp32-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: A0 - -<<: !include common-esp32.yaml diff --git a/tests/components/integration/test.esp32-c3-ard.yaml b/tests/components/integration/test.esp32-c3-ard.yaml deleted file mode 100644 index 5105e645f3..0000000000 --- a/tests/components/integration/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO1 - -<<: !include common-esp32.yaml diff --git a/tests/components/integration/test.esp32-c3-idf.yaml b/tests/components/integration/test.esp32-c3-idf.yaml deleted file mode 100644 index 5105e645f3..0000000000 --- a/tests/components/integration/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO1 - -<<: !include common-esp32.yaml diff --git a/tests/components/internal_temperature/test.esp32-ard.yaml b/tests/components/internal_temperature/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/internal_temperature/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/internal_temperature/test.esp32-c3-ard.yaml b/tests/components/internal_temperature/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/internal_temperature/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/internal_temperature/test.esp32-s2-ard.yaml b/tests/components/internal_temperature/test.esp32-s2-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/internal_temperature/test.esp32-s2-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/internal_temperature/test.esp32-s3-ard.yaml b/tests/components/internal_temperature/test.esp32-s3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/internal_temperature/test.esp32-s3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/interval/test.esp32-ard.yaml b/tests/components/interval/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/interval/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/interval/test.esp32-c3-ard.yaml b/tests/components/interval/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/interval/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/interval/test.esp32-c3-idf.yaml b/tests/components/interval/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/interval/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/atc_mithermometer/test.esp32-c3-idf.yaml b/tests/components/interval/test.nrf52-adafruit.yaml similarity index 100% rename from tests/components/atc_mithermometer/test.esp32-c3-idf.yaml rename to tests/components/interval/test.nrf52-adafruit.yaml diff --git a/tests/components/axs15231/test.esp32-ard.yaml b/tests/components/interval/test.nrf52-mcumgr.yaml similarity index 100% rename from tests/components/axs15231/test.esp32-ard.yaml rename to tests/components/interval/test.nrf52-mcumgr.yaml diff --git a/tests/components/jsn_sr04t/common.yaml b/tests/components/jsn_sr04t/common.yaml index d6871d5e91..bc2c301c2e 100644 --- a/tests/components/jsn_sr04t/common.yaml +++ b/tests/components/jsn_sr04t/common.yaml @@ -1,9 +1,3 @@ -uart: - - id: uart_jsn_sr04t - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 9600 - sensor: - platform: jsn_sr04t id: jsn_sr04t_sensor diff --git a/tests/components/jsn_sr04t/test.esp32-ard.yaml b/tests/components/jsn_sr04t/test.esp32-ard.yaml deleted file mode 100644 index f486544afa..0000000000 --- a/tests/components/jsn_sr04t/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 - -<<: !include common.yaml diff --git a/tests/components/jsn_sr04t/test.esp32-c3-ard.yaml b/tests/components/jsn_sr04t/test.esp32-c3-ard.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/jsn_sr04t/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/jsn_sr04t/test.esp32-c3-idf.yaml b/tests/components/jsn_sr04t/test.esp32-c3-idf.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/jsn_sr04t/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/jsn_sr04t/test.esp32-idf.yaml b/tests/components/jsn_sr04t/test.esp32-idf.yaml index f486544afa..b415125e84 100644 --- a/tests/components/jsn_sr04t/test.esp32-idf.yaml +++ b/tests/components/jsn_sr04t/test.esp32-idf.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 + tx_pin: GPIO4 + rx_pin: GPIO5 + +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/jsn_sr04t/test.esp8266-ard.yaml b/tests/components/jsn_sr04t/test.esp8266-ard.yaml index b516342f3b..96ab4ef6ac 100644 --- a/tests/components/jsn_sr04t/test.esp8266-ard.yaml +++ b/tests/components/jsn_sr04t/test.esp8266-ard.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: GPIO0 + rx_pin: GPIO2 + +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/jsn_sr04t/test.rp2040-ard.yaml b/tests/components/jsn_sr04t/test.rp2040-ard.yaml index b516342f3b..b28f2b5e05 100644 --- a/tests/components/jsn_sr04t/test.rp2040-ard.yaml +++ b/tests/components/jsn_sr04t/test.rp2040-ard.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO4 rx_pin: GPIO5 +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/json/common.yaml b/tests/components/json/common.yaml new file mode 100644 index 0000000000..c36c7f2a5a --- /dev/null +++ b/tests/components/json/common.yaml @@ -0,0 +1,35 @@ +json: + +interval: + - interval: 60s + then: + - lambda: |- + // Test build_json + std::string json_str = esphome::json::build_json([](JsonObject root) { + root["sensor"] = "temperature"; + root["value"] = 23.5; + root["unit"] = "°C"; + }); + ESP_LOGD("test", "Built JSON: %s", json_str.c_str()); + + // Test parse_json + bool parse_ok = esphome::json::parse_json(json_str, [](JsonObject root) { + if (root["sensor"].is() && root["value"].is()) { + const char* sensor = root["sensor"]; + float value = root["value"]; + ESP_LOGD("test", "Parsed: sensor=%s, value=%.1f", sensor, value); + return true; + } else { + ESP_LOGD("test", "Parsed JSON missing required keys"); + return false; + } + }); + ESP_LOGD("test", "Parse result (JSON syntax only): %s", parse_ok ? "success" : "failed"); + + // Test JsonBuilder class + esphome::json::JsonBuilder builder; + JsonObject obj = builder.root(); + obj["test"] = "direct_builder"; + obj["count"] = 42; + std::string result = builder.serialize(); + ESP_LOGD("test", "JsonBuilder result: %s", result.c_str()); diff --git a/tests/components/axs15231/test.esp32-c3-ard.yaml b/tests/components/json/test.esp32-idf.yaml similarity index 100% rename from tests/components/axs15231/test.esp32-c3-ard.yaml rename to tests/components/json/test.esp32-idf.yaml diff --git a/tests/components/axs15231/test.esp32-c3-idf.yaml b/tests/components/json/test.esp8266-ard.yaml similarity index 100% rename from tests/components/axs15231/test.esp32-c3-idf.yaml rename to tests/components/json/test.esp8266-ard.yaml diff --git a/tests/components/kamstrup_kmp/common.yaml b/tests/components/kamstrup_kmp/common.yaml index b348d03c72..b3ebea523c 100644 --- a/tests/components/kamstrup_kmp/common.yaml +++ b/tests/components/kamstrup_kmp/common.yaml @@ -1,9 +1,3 @@ -uart: - tx_pin: ${uart_tx_pin} - rx_pin: ${uart_rx_pin} - baud_rate: 1200 - stop_bits: 2 - sensor: - platform: kamstrup_kmp heat_energy: diff --git a/tests/components/kamstrup_kmp/test.esp32-ard.yaml b/tests/components/kamstrup_kmp/test.esp32-ard.yaml deleted file mode 100644 index adc2c4d24a..0000000000 --- a/tests/components/kamstrup_kmp/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - uart_tx_pin: GPIO1 - uart_rx_pin: GPIO3 - -<<: !include common.yaml diff --git a/tests/components/kamstrup_kmp/test.esp32-idf.yaml b/tests/components/kamstrup_kmp/test.esp32-idf.yaml index adc2c4d24a..1016905720 100644 --- a/tests/components/kamstrup_kmp/test.esp32-idf.yaml +++ b/tests/components/kamstrup_kmp/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - uart_tx_pin: GPIO1 - uart_rx_pin: GPIO3 +packages: + uart_1200: !include ../../test_build_components/common/uart_1200/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/kamstrup_kmp/test.esp8266-ard.yaml b/tests/components/kamstrup_kmp/test.esp8266-ard.yaml index adc2c4d24a..f55c18eb76 100644 --- a/tests/components/kamstrup_kmp/test.esp8266-ard.yaml +++ b/tests/components/kamstrup_kmp/test.esp8266-ard.yaml @@ -2,4 +2,7 @@ substitutions: uart_tx_pin: GPIO1 uart_rx_pin: GPIO3 +packages: + uart_1200: !include ../../test_build_components/common/uart_1200/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/key_collector/test.esp32-ard.yaml b/tests/components/key_collector/test.esp32-ard.yaml deleted file mode 100644 index de144aa46b..0000000000 --- a/tests/components/key_collector/test.esp32-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - pin_r0: GPIO12 - pin_r1: GPIO13 - pin_c0: GPIO14 - pin_c1: GPIO15 - -<<: !include common.yaml diff --git a/tests/components/key_collector/test.esp32-c3-ard.yaml b/tests/components/key_collector/test.esp32-c3-ard.yaml deleted file mode 100644 index b580ab7843..0000000000 --- a/tests/components/key_collector/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - pin_r0: GPIO2 - pin_r1: GPIO3 - pin_c0: GPIO4 - pin_c1: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/key_collector/test.esp32-c3-idf.yaml b/tests/components/key_collector/test.esp32-c3-idf.yaml deleted file mode 100644 index b580ab7843..0000000000 --- a/tests/components/key_collector/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - pin_r0: GPIO2 - pin_r1: GPIO3 - pin_c0: GPIO4 - pin_c1: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/kmeteriso/common.yaml b/tests/components/kmeteriso/common.yaml index 6b68175904..8542bb6a06 100644 --- a/tests/components/kmeteriso/common.yaml +++ b/tests/components/kmeteriso/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_kmeteriso - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: kmeteriso + i2c_id: i2c_bus temperature: name: Outside Temperature internal_temperature: diff --git a/tests/components/kmeteriso/test.esp32-ard.yaml b/tests/components/kmeteriso/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/kmeteriso/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/kmeteriso/test.esp32-c3-ard.yaml b/tests/components/kmeteriso/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/kmeteriso/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/kmeteriso/test.esp32-c3-idf.yaml b/tests/components/kmeteriso/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/kmeteriso/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/kmeteriso/test.esp32-idf.yaml b/tests/components/kmeteriso/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/kmeteriso/test.esp32-idf.yaml +++ b/tests/components/kmeteriso/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/kmeteriso/test.esp8266-ard.yaml b/tests/components/kmeteriso/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/kmeteriso/test.esp8266-ard.yaml +++ b/tests/components/kmeteriso/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/kmeteriso/test.rp2040-ard.yaml b/tests/components/kmeteriso/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/kmeteriso/test.rp2040-ard.yaml +++ b/tests/components/kmeteriso/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/kuntze/common.yaml b/tests/components/kuntze/common.yaml index 4daecea242..32c6fa3809 100644 --- a/tests/components/kuntze/common.yaml +++ b/tests/components/kuntze/common.yaml @@ -1,14 +1,6 @@ -uart: - - id: uart_kuntze - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 9600 - -modbus: - flow_control_pin: ${flow_control_pin} - sensor: - platform: kuntze + modbus_id: modbus_bus ph: name: Kuntze pH temperature: diff --git a/tests/components/kuntze/test.esp32-ard.yaml b/tests/components/kuntze/test.esp32-ard.yaml deleted file mode 100644 index bd767a8ece..0000000000 --- a/tests/components/kuntze/test.esp32-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - tx_pin: GPIO12 - rx_pin: GPIO14 - flow_control_pin: GPIO13 - -<<: !include common.yaml diff --git a/tests/components/kuntze/test.esp32-c3-ard.yaml b/tests/components/kuntze/test.esp32-c3-ard.yaml deleted file mode 100644 index 452031a5aa..0000000000 --- a/tests/components/kuntze/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - flow_control_pin: GPIO3 - -<<: !include common.yaml diff --git a/tests/components/kuntze/test.esp32-c3-idf.yaml b/tests/components/kuntze/test.esp32-c3-idf.yaml deleted file mode 100644 index 452031a5aa..0000000000 --- a/tests/components/kuntze/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - flow_control_pin: GPIO3 - -<<: !include common.yaml diff --git a/tests/components/kuntze/test.esp32-idf.yaml b/tests/components/kuntze/test.esp32-idf.yaml index bd767a8ece..a755bfa66a 100644 --- a/tests/components/kuntze/test.esp32-idf.yaml +++ b/tests/components/kuntze/test.esp32-idf.yaml @@ -3,4 +3,7 @@ substitutions: rx_pin: GPIO14 flow_control_pin: GPIO13 +packages: + modbus: !include ../../test_build_components/common/modbus/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/kuntze/test.esp8266-ard.yaml b/tests/components/kuntze/test.esp8266-ard.yaml index 29c98d0957..6daa08c22b 100644 --- a/tests/components/kuntze/test.esp8266-ard.yaml +++ b/tests/components/kuntze/test.esp8266-ard.yaml @@ -1,6 +1,9 @@ substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - flow_control_pin: GPIO13 + tx_pin: GPIO0 + rx_pin: GPIO2 + flow_control_pin: GPIO15 + +packages: + modbus: !include ../../test_build_components/common/modbus/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/kuntze/test.rp2040-ard.yaml b/tests/components/kuntze/test.rp2040-ard.yaml index 452031a5aa..a00283a9e0 100644 --- a/tests/components/kuntze/test.rp2040-ard.yaml +++ b/tests/components/kuntze/test.rp2040-ard.yaml @@ -3,4 +3,7 @@ substitutions: rx_pin: GPIO5 flow_control_pin: GPIO3 +packages: + modbus: !include ../../test_build_components/common/modbus/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/lc709203f/common.yaml b/tests/components/lc709203f/common.yaml index 53177c0d4a..3711e5372c 100644 --- a/tests/components/lc709203f/common.yaml +++ b/tests/components/lc709203f/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_lc709203f - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: lc709203f + i2c_id: i2c_bus size: 2000 voltage: 3.7 battery_voltage: diff --git a/tests/components/lc709203f/test.esp32-ard.yaml b/tests/components/lc709203f/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/lc709203f/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/lc709203f/test.esp32-c3-ard.yaml b/tests/components/lc709203f/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/lc709203f/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/lc709203f/test.esp32-c3-idf.yaml b/tests/components/lc709203f/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/lc709203f/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/lc709203f/test.esp32-idf.yaml b/tests/components/lc709203f/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/lc709203f/test.esp32-idf.yaml +++ b/tests/components/lc709203f/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/lc709203f/test.esp8266-ard.yaml b/tests/components/lc709203f/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/lc709203f/test.esp8266-ard.yaml +++ b/tests/components/lc709203f/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/lc709203f/test.rp2040-ard.yaml b/tests/components/lc709203f/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/lc709203f/test.rp2040-ard.yaml +++ b/tests/components/lc709203f/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/lcd_gpio/test.esp32-ard.yaml b/tests/components/lcd_gpio/test.esp32-ard.yaml deleted file mode 100644 index 9c2af456b5..0000000000 --- a/tests/components/lcd_gpio/test.esp32-ard.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - d0_pin: GPIO12 - d1_pin: GPIO13 - d2_pin: GPIO14 - d3_pin: GPIO15 - enable_pin: GPIO16 - rs_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/lcd_gpio/test.esp32-c3-ard.yaml b/tests/components/lcd_gpio/test.esp32-c3-ard.yaml deleted file mode 100644 index b6b05f3ab4..0000000000 --- a/tests/components/lcd_gpio/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - d0_pin: GPIO1 - d1_pin: GPIO2 - d2_pin: GPIO3 - d3_pin: GPIO4 - enable_pin: GPIO5 - rs_pin: GPIO6 - -<<: !include common.yaml diff --git a/tests/components/lcd_gpio/test.esp32-c3-idf.yaml b/tests/components/lcd_gpio/test.esp32-c3-idf.yaml deleted file mode 100644 index b6b05f3ab4..0000000000 --- a/tests/components/lcd_gpio/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - d0_pin: GPIO1 - d1_pin: GPIO2 - d2_pin: GPIO3 - d3_pin: GPIO4 - enable_pin: GPIO5 - rs_pin: GPIO6 - -<<: !include common.yaml diff --git a/tests/components/lcd_gpio/test.esp32-idf.yaml b/tests/components/lcd_gpio/test.esp32-idf.yaml index 9c2af456b5..93abad5e38 100644 --- a/tests/components/lcd_gpio/test.esp32-idf.yaml +++ b/tests/components/lcd_gpio/test.esp32-idf.yaml @@ -3,7 +3,7 @@ substitutions: d1_pin: GPIO13 d2_pin: GPIO14 d3_pin: GPIO15 - enable_pin: GPIO16 + enable_pin: GPIO4 rs_pin: GPIO5 <<: !include common.yaml diff --git a/tests/components/lcd_gpio/test.esp8266-ard.yaml b/tests/components/lcd_gpio/test.esp8266-ard.yaml index 9c2af456b5..50471a16f4 100644 --- a/tests/components/lcd_gpio/test.esp8266-ard.yaml +++ b/tests/components/lcd_gpio/test.esp8266-ard.yaml @@ -1,6 +1,6 @@ substitutions: - d0_pin: GPIO12 - d1_pin: GPIO13 + d0_pin: GPIO0 + d1_pin: GPIO2 d2_pin: GPIO14 d3_pin: GPIO15 enable_pin: GPIO16 diff --git a/tests/components/lcd_menu/test.esp32-ard.yaml b/tests/components/lcd_menu/test.esp32-ard.yaml deleted file mode 100644 index 9c2af456b5..0000000000 --- a/tests/components/lcd_menu/test.esp32-ard.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - d0_pin: GPIO12 - d1_pin: GPIO13 - d2_pin: GPIO14 - d3_pin: GPIO15 - enable_pin: GPIO16 - rs_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/lcd_menu/test.esp32-c3-ard.yaml b/tests/components/lcd_menu/test.esp32-c3-ard.yaml deleted file mode 100644 index b6b05f3ab4..0000000000 --- a/tests/components/lcd_menu/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - d0_pin: GPIO1 - d1_pin: GPIO2 - d2_pin: GPIO3 - d3_pin: GPIO4 - enable_pin: GPIO5 - rs_pin: GPIO6 - -<<: !include common.yaml diff --git a/tests/components/lcd_menu/test.esp32-c3-idf.yaml b/tests/components/lcd_menu/test.esp32-c3-idf.yaml deleted file mode 100644 index b6b05f3ab4..0000000000 --- a/tests/components/lcd_menu/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - d0_pin: GPIO1 - d1_pin: GPIO2 - d2_pin: GPIO3 - d3_pin: GPIO4 - enable_pin: GPIO5 - rs_pin: GPIO6 - -<<: !include common.yaml diff --git a/tests/components/lcd_menu/test.esp32-idf.yaml b/tests/components/lcd_menu/test.esp32-idf.yaml index 9c2af456b5..93abad5e38 100644 --- a/tests/components/lcd_menu/test.esp32-idf.yaml +++ b/tests/components/lcd_menu/test.esp32-idf.yaml @@ -3,7 +3,7 @@ substitutions: d1_pin: GPIO13 d2_pin: GPIO14 d3_pin: GPIO15 - enable_pin: GPIO16 + enable_pin: GPIO4 rs_pin: GPIO5 <<: !include common.yaml diff --git a/tests/components/lcd_menu/test.esp8266-ard.yaml b/tests/components/lcd_menu/test.esp8266-ard.yaml index 9c2af456b5..50471a16f4 100644 --- a/tests/components/lcd_menu/test.esp8266-ard.yaml +++ b/tests/components/lcd_menu/test.esp8266-ard.yaml @@ -1,6 +1,6 @@ substitutions: - d0_pin: GPIO12 - d1_pin: GPIO13 + d0_pin: GPIO0 + d1_pin: GPIO2 d2_pin: GPIO14 d3_pin: GPIO15 enable_pin: GPIO16 diff --git a/tests/components/lcd_pcf8574/common.yaml b/tests/components/lcd_pcf8574/common.yaml index 8b03cf5497..1ec4400332 100644 --- a/tests/components/lcd_pcf8574/common.yaml +++ b/tests/components/lcd_pcf8574/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_lcd_pcf8574 - scl: ${scl_pin} - sda: ${sda_pin} - display: - platform: lcd_pcf8574 + i2c_id: i2c_bus dimensions: 18x4 address: 0x3F user_characters: diff --git a/tests/components/lcd_pcf8574/test.esp32-ard.yaml b/tests/components/lcd_pcf8574/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/lcd_pcf8574/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/lcd_pcf8574/test.esp32-c3-ard.yaml b/tests/components/lcd_pcf8574/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/lcd_pcf8574/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/lcd_pcf8574/test.esp32-c3-idf.yaml b/tests/components/lcd_pcf8574/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/lcd_pcf8574/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/lcd_pcf8574/test.esp32-idf.yaml b/tests/components/lcd_pcf8574/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/lcd_pcf8574/test.esp32-idf.yaml +++ b/tests/components/lcd_pcf8574/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/lcd_pcf8574/test.esp8266-ard.yaml b/tests/components/lcd_pcf8574/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/lcd_pcf8574/test.esp8266-ard.yaml +++ b/tests/components/lcd_pcf8574/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/lcd_pcf8574/test.rp2040-ard.yaml b/tests/components/lcd_pcf8574/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/lcd_pcf8574/test.rp2040-ard.yaml +++ b/tests/components/lcd_pcf8574/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/ld2410/common.yaml b/tests/components/ld2410/common.yaml index e975fcf757..7d168bf7ec 100644 --- a/tests/components/ld2410/common.yaml +++ b/tests/components/ld2410/common.yaml @@ -1,9 +1,3 @@ -uart: - - id: uart_ld2410 - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 9600 - ld2410: id: my_ld2410 diff --git a/tests/components/ld2410/test.esp32-ard.yaml b/tests/components/ld2410/test.esp32-ard.yaml deleted file mode 100644 index f486544afa..0000000000 --- a/tests/components/ld2410/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 - -<<: !include common.yaml diff --git a/tests/components/ld2410/test.esp32-c3-ard.yaml b/tests/components/ld2410/test.esp32-c3-ard.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/ld2410/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/ld2410/test.esp32-c3-idf.yaml b/tests/components/ld2410/test.esp32-c3-idf.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/ld2410/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/ld2410/test.esp32-idf.yaml b/tests/components/ld2410/test.esp32-idf.yaml index f486544afa..b415125e84 100644 --- a/tests/components/ld2410/test.esp32-idf.yaml +++ b/tests/components/ld2410/test.esp32-idf.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 + tx_pin: GPIO4 + rx_pin: GPIO5 + +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/ld2410/test.esp8266-ard.yaml b/tests/components/ld2410/test.esp8266-ard.yaml index b516342f3b..96ab4ef6ac 100644 --- a/tests/components/ld2410/test.esp8266-ard.yaml +++ b/tests/components/ld2410/test.esp8266-ard.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: GPIO0 + rx_pin: GPIO2 + +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/ld2410/test.rp2040-ard.yaml b/tests/components/ld2410/test.rp2040-ard.yaml index b516342f3b..b28f2b5e05 100644 --- a/tests/components/ld2410/test.rp2040-ard.yaml +++ b/tests/components/ld2410/test.rp2040-ard.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO4 rx_pin: GPIO5 +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/ld2412/common.yaml b/tests/components/ld2412/common.yaml new file mode 100644 index 0000000000..18c4612ffe --- /dev/null +++ b/tests/components/ld2412/common.yaml @@ -0,0 +1,227 @@ +ld2412: + id: my_ld2412 + +binary_sensor: + - platform: ld2412 + dynamic_background_correction_status: + name: Dynamic Background Correction Status + has_target: + name: Presence + has_moving_target: + name: Moving Target + has_still_target: + name: Still Target + +button: + - platform: ld2412 + factory_reset: + name: Factory reset + restart: + name: Restart + query_params: + name: Query params + start_dynamic_background_correction: + name: Start Dynamic Background Correction + +number: + - platform: ld2412 + light_threshold: + name: Light Threshold + timeout: + name: Presence timeout + min_distance_gate: + name: Minimum distance gate + max_distance_gate: + name: Maximum distance gate + gate_0: + move_threshold: + name: Gate 0 Move Threshold + still_threshold: + name: Gate 0 Still Threshold + gate_1: + move_threshold: + name: Gate 1 Move Threshold + still_threshold: + name: Gate 1 Still Threshold + gate_2: + move_threshold: + name: Gate 2 Move Threshold + still_threshold: + name: Gate 2 Still Threshold + gate_3: + move_threshold: + name: Gate 3 Move Threshold + still_threshold: + name: Gate 3 Still Threshold + gate_4: + move_threshold: + name: Gate 4 Move Threshold + still_threshold: + name: Gate 4 Still Threshold + gate_5: + move_threshold: + name: Gate 5 Move Threshold + still_threshold: + name: Gate 5 Still Threshold + gate_6: + move_threshold: + name: Gate 6 Move Threshold + still_threshold: + name: Gate 6 Still Threshold + gate_7: + move_threshold: + name: Gate 7 Move Threshold + still_threshold: + name: Gate 7 Still Threshold + gate_8: + move_threshold: + name: Gate 8 Move Threshold + still_threshold: + name: Gate 8 Still Threshold + gate_9: + move_threshold: + name: Gate 9 Move Threshold + still_threshold: + name: Gate 9 Still Threshold + gate_10: + move_threshold: + name: Gate 10 Move Threshold + still_threshold: + name: Gate 10 Still Threshold + gate_11: + move_threshold: + name: Gate 11 Move Threshold + still_threshold: + name: Gate 11 Still Threshold + gate_12: + move_threshold: + name: Gate 12 Move Threshold + still_threshold: + name: Gate 12 Still Threshold + gate_13: + move_threshold: + name: Gate 13 Move Threshold + still_threshold: + name: Gate 13 Still Threshold + +select: + - platform: ld2412 + light_function: + name: Light Function + out_pin_level: + name: Hardware output pin level + distance_resolution: + name: Distance resolution + baud_rate: + name: Baud rate + on_value: + - delay: 3s + - lambda: |- + id(uart_bus).flush(); + uint32_t new_baud_rate = stoi(x); + ESP_LOGD("change_baud_rate", "Changing baud rate from %i to %i",id(uart_bus).get_baud_rate(), new_baud_rate); + if (id(uart_bus).get_baud_rate() != new_baud_rate) { + id(uart_bus).set_baud_rate(new_baud_rate); + #if defined(USE_ESP8266) || defined(USE_ESP32) + id(uart_bus).load_settings(); + #endif + } + +sensor: + - platform: ld2412 + light: + name: Light + moving_distance: + name: Moving Distance + still_distance: + name: Still Distance + moving_energy: + name: Move Energy + still_energy: + name: Still Energy + detection_distance: + name: Detection Distance + gate_0: + move_energy: + name: Gate 0 Move Energy + still_energy: + name: Gate 0 Still Energy + gate_1: + move_energy: + name: Gate 1 Move Energy + still_energy: + name: Gate 1 Still Energy + gate_2: + move_energy: + name: Gate 2 Move Energy + still_energy: + name: Gate 2 Still Energy + gate_3: + move_energy: + name: Gate 3 Move Energy + still_energy: + name: Gate 3 Still Energy + gate_4: + move_energy: + name: Gate 4 Move Energy + still_energy: + name: Gate 4 Still Energy + gate_5: + move_energy: + name: Gate 5 Move Energy + still_energy: + name: Gate 5 Still Energy + gate_6: + move_energy: + name: Gate 6 Move Energy + still_energy: + name: Gate 6 Still Energy + gate_7: + move_energy: + name: Gate 7 Move Energy + still_energy: + name: Gate 7 Still Energy + gate_8: + move_energy: + name: Gate 8 Move Energy + still_energy: + name: Gate 8 Still Energy + gate_9: + move_energy: + name: Gate 9 Move Energy + still_energy: + name: Gate 9 Still Energy + gate_10: + move_energy: + name: Gate 10 Move Energy + still_energy: + name: Gate 10 Still Energy + gate_11: + move_energy: + name: Gate 11 Move Energy + still_energy: + name: Gate 11 Still Energy + gate_12: + move_energy: + name: Gate 12 Move Energy + still_energy: + name: Gate 12 Still Energy + gate_13: + move_energy: + name: Gate 13 Move Energy + still_energy: + name: Gate 13 Still Energy + +switch: + - platform: ld2412 + bluetooth: + name: Bluetooth + engineering_mode: + name: Engineering Mode + +text_sensor: + - platform: ld2412 + version: + name: Firmware version + mac_address: + name: MAC address diff --git a/tests/components/ld2412/test.esp32-idf.yaml b/tests/components/ld2412/test.esp32-idf.yaml new file mode 100644 index 0000000000..2d29656c94 --- /dev/null +++ b/tests/components/ld2412/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/ld2412/test.esp8266-ard.yaml b/tests/components/ld2412/test.esp8266-ard.yaml new file mode 100644 index 0000000000..96ab4ef6ac --- /dev/null +++ b/tests/components/ld2412/test.esp8266-ard.yaml @@ -0,0 +1,8 @@ +substitutions: + tx_pin: GPIO0 + rx_pin: GPIO2 + +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/ld2412/test.rp2040-ard.yaml b/tests/components/ld2412/test.rp2040-ard.yaml new file mode 100644 index 0000000000..b28f2b5e05 --- /dev/null +++ b/tests/components/ld2412/test.rp2040-ard.yaml @@ -0,0 +1,8 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/ld2420/common.yaml b/tests/components/ld2420/common.yaml index 76748aa8f0..1539df4d1b 100644 --- a/tests/components/ld2420/common.yaml +++ b/tests/components/ld2420/common.yaml @@ -1,9 +1,3 @@ -uart: - - id: uart_ld2420 - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 9600 - ld2420: id: my_ld2420 diff --git a/tests/components/ld2420/test.esp32-ard.yaml b/tests/components/ld2420/test.esp32-ard.yaml deleted file mode 100644 index f486544afa..0000000000 --- a/tests/components/ld2420/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 - -<<: !include common.yaml diff --git a/tests/components/ld2420/test.esp32-c3-ard.yaml b/tests/components/ld2420/test.esp32-c3-ard.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/ld2420/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/ld2420/test.esp32-c3-idf.yaml b/tests/components/ld2420/test.esp32-c3-idf.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/ld2420/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/ld2420/test.esp32-idf.yaml b/tests/components/ld2420/test.esp32-idf.yaml index f486544afa..b415125e84 100644 --- a/tests/components/ld2420/test.esp32-idf.yaml +++ b/tests/components/ld2420/test.esp32-idf.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 + tx_pin: GPIO4 + rx_pin: GPIO5 + +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/ld2420/test.esp8266-ard.yaml b/tests/components/ld2420/test.esp8266-ard.yaml index b516342f3b..96ab4ef6ac 100644 --- a/tests/components/ld2420/test.esp8266-ard.yaml +++ b/tests/components/ld2420/test.esp8266-ard.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: GPIO0 + rx_pin: GPIO2 + +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/ld2420/test.rp2040-ard.yaml b/tests/components/ld2420/test.rp2040-ard.yaml index b516342f3b..b28f2b5e05 100644 --- a/tests/components/ld2420/test.rp2040-ard.yaml +++ b/tests/components/ld2420/test.rp2040-ard.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO4 rx_pin: GPIO5 +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/ld2450/common.yaml b/tests/components/ld2450/common.yaml index 2e62efb0f5..9dcefffd09 100644 --- a/tests/components/ld2450/common.yaml +++ b/tests/components/ld2450/common.yaml @@ -1,15 +1,5 @@ -uart: - - id: ld2450_uart - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 256000 - parity: NONE - stop_bits: 1 - ld2450: - id: ld2450_radar - uart_id: ld2450_uart - throttle: 1000ms button: - platform: ld2450 diff --git a/tests/components/ld2450/test.esp32-ard.yaml b/tests/components/ld2450/test.esp32-ard.yaml deleted file mode 100644 index f486544afa..0000000000 --- a/tests/components/ld2450/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 - -<<: !include common.yaml diff --git a/tests/components/ld2450/test.esp32-c3-ard.yaml b/tests/components/ld2450/test.esp32-c3-ard.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/ld2450/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/ld2450/test.esp32-c3-idf.yaml b/tests/components/ld2450/test.esp32-c3-idf.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/ld2450/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/ld2450/test.esp32-idf.yaml b/tests/components/ld2450/test.esp32-idf.yaml index f486544afa..b415125e84 100644 --- a/tests/components/ld2450/test.esp32-idf.yaml +++ b/tests/components/ld2450/test.esp32-idf.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 + tx_pin: GPIO4 + rx_pin: GPIO5 + +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/ld2450/test.esp8266-ard.yaml b/tests/components/ld2450/test.esp8266-ard.yaml index b516342f3b..96ab4ef6ac 100644 --- a/tests/components/ld2450/test.esp8266-ard.yaml +++ b/tests/components/ld2450/test.esp8266-ard.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: GPIO0 + rx_pin: GPIO2 + +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/ld2450/test.rp2040-ard.yaml b/tests/components/ld2450/test.rp2040-ard.yaml index b516342f3b..b28f2b5e05 100644 --- a/tests/components/ld2450/test.rp2040-ard.yaml +++ b/tests/components/ld2450/test.rp2040-ard.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO4 rx_pin: GPIO5 +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/ledc/test.esp32-ard.yaml b/tests/components/ledc/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/ledc/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/ledc/test.esp32-c3-ard.yaml b/tests/components/ledc/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/ledc/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/libretiny/test.bk72xx-ard.yaml b/tests/components/libretiny/test.bk72xx-ard.yaml new file mode 100644 index 0000000000..039a261016 --- /dev/null +++ b/tests/components/libretiny/test.bk72xx-ard.yaml @@ -0,0 +1,13 @@ +logger: + level: VERBOSE + +esphome: + on_boot: + - lambda: |- + int x = 100; + x = clamp(x, 50, 90); + assert(x == 90); + x = clamp_at_least(x, 95); + assert(x == 95); + x = clamp_at_most(x, 40); + assert(x == 40); diff --git a/tests/components/light/common.yaml b/tests/components/light/common.yaml index d4f64dcdea..247fc19aba 100644 --- a/tests/components/light/common.yaml +++ b/tests/components/light/common.yaml @@ -17,6 +17,20 @@ esphome: relative_brightness: 5% brightness_limits: max_brightness: 90% + - light.turn_on: + id: test_addressable_transition + brightness: 50% + red: 100% + green: 0% + blue: 0% + transition_length: 500ms + - light.turn_on: + id: test_addressable_transition + brightness: 100% + red: 0% + green: 100% + blue: 0% + transition_length: 1s light: - platform: binary @@ -123,3 +137,49 @@ light: red: 100% green: 50% blue: 50% + # Test StrobeLightEffect with multiple colors + - platform: monochromatic + id: test_strobe_multiple + name: Strobe Multiple Colors + output: test_ledc_1 + effects: + - strobe: + name: Strobe Multi + colors: + - state: true + brightness: 100% + duration: 500ms + - state: false + duration: 250ms + - state: true + brightness: 50% + duration: 500ms + # Test StrobeLightEffect with transition + - platform: rgb + id: test_strobe_transition + name: Strobe With Transition + red: test_ledc_1 + green: test_ledc_2 + blue: test_ledc_3 + effects: + - strobe: + name: Strobe Transition + colors: + - state: true + red: 100% + green: 0% + blue: 0% + duration: 1s + transition_length: 500ms + - state: true + red: 0% + green: 100% + blue: 0% + duration: 1s + transition_length: 500ms + - platform: partition + id: test_addressable_transition + name: Addressable Transition Test + default_transition_length: 1s + segments: + - single_light_id: test_rgb_light diff --git a/tests/components/light/test.esp32-ard.yaml b/tests/components/light/test.esp32-ard.yaml deleted file mode 100644 index 925197182c..0000000000 --- a/tests/components/light/test.esp32-ard.yaml +++ /dev/null @@ -1,21 +0,0 @@ -output: - - platform: gpio - id: test_binary - pin: 12 - - platform: ledc - id: test_ledc_1 - pin: 13 - - platform: ledc - id: test_ledc_2 - pin: 14 - - platform: ledc - id: test_ledc_3 - pin: 15 - - platform: ledc - id: test_ledc_4 - pin: 16 - - platform: ledc - id: test_ledc_5 - pin: 17 - -<<: !include common.yaml diff --git a/tests/components/light/test.esp32-c3-ard.yaml b/tests/components/light/test.esp32-c3-ard.yaml deleted file mode 100644 index 317d5748a3..0000000000 --- a/tests/components/light/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,21 +0,0 @@ -output: - - platform: gpio - id: test_binary - pin: 0 - - platform: ledc - id: test_ledc_1 - pin: 1 - - platform: ledc - id: test_ledc_2 - pin: 2 - - platform: ledc - id: test_ledc_3 - pin: 3 - - platform: ledc - id: test_ledc_4 - pin: 4 - - platform: ledc - id: test_ledc_5 - pin: 5 - -<<: !include common.yaml diff --git a/tests/components/light/test.esp32-c3-idf.yaml b/tests/components/light/test.esp32-c3-idf.yaml deleted file mode 100644 index 317d5748a3..0000000000 --- a/tests/components/light/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,21 +0,0 @@ -output: - - platform: gpio - id: test_binary - pin: 0 - - platform: ledc - id: test_ledc_1 - pin: 1 - - platform: ledc - id: test_ledc_2 - pin: 2 - - platform: ledc - id: test_ledc_3 - pin: 3 - - platform: ledc - id: test_ledc_4 - pin: 4 - - platform: ledc - id: test_ledc_5 - pin: 5 - -<<: !include common.yaml diff --git a/tests/components/light/test.nrf52-adafruit.yaml b/tests/components/light/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..cb421ed4bb --- /dev/null +++ b/tests/components/light/test.nrf52-adafruit.yaml @@ -0,0 +1,19 @@ +esphome: + on_boot: + then: + - light.toggle: test_binary_light + +output: + - platform: gpio + id: test_binary + pin: 0 + +light: + - platform: binary + id: test_binary_light + name: Binary Light + output: test_binary + effects: + - strobe: + on_state: + - logger.log: Binary light state changed diff --git a/tests/components/light/test.nrf52-mcumgr.yaml b/tests/components/light/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..cb421ed4bb --- /dev/null +++ b/tests/components/light/test.nrf52-mcumgr.yaml @@ -0,0 +1,19 @@ +esphome: + on_boot: + then: + - light.toggle: test_binary_light + +output: + - platform: gpio + id: test_binary + pin: 0 + +light: + - platform: binary + id: test_binary_light + name: Binary Light + output: test_binary + effects: + - strobe: + on_state: + - logger.log: Binary light state changed diff --git a/tests/components/lilygo_t5_47/common.yaml b/tests/components/lilygo_t5_47/common.yaml index f539c58d74..18f1ba10ae 100644 --- a/tests/components/lilygo_t5_47/common.yaml +++ b/tests/components/lilygo_t5_47/common.yaml @@ -1,23 +1,20 @@ -i2c: - - id: i2c_lilygo_t5_47 - scl: ${scl_pin} - sda: ${sda_pin} - display: - platform: ssd1306_i2c - id: ssd1306_display + i2c_id: i2c_bus + id: ssd1306_i2c_display model: SSD1306_128X64 reset_pin: ${reset_pin} pages: - - id: page1 + - id: lilygo_t5_47_page1 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); touchscreen: - platform: lilygo_t5_47 + i2c_id: i2c_bus id: lilygo_touchscreen interrupt_pin: ${interrupt_pin} - display: ssd1306_display + display: ssd1306_i2c_display on_touch: - logger.log: format: Touch at (%d, %d) diff --git a/tests/components/lilygo_t5_47/test.esp32-ard.yaml b/tests/components/lilygo_t5_47/test.esp32-ard.yaml deleted file mode 100644 index 342f0b6d8b..0000000000 --- a/tests/components/lilygo_t5_47/test.esp32-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - scl_pin: GPIO13 - sda_pin: GPIO14 - interrupt_pin: GPIO15 - reset_pin: GPIO16 - -<<: !include common.yaml diff --git a/tests/components/lilygo_t5_47/test.esp32-c3-ard.yaml b/tests/components/lilygo_t5_47/test.esp32-c3-ard.yaml deleted file mode 100644 index 061a98ce24..0000000000 --- a/tests/components/lilygo_t5_47/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - scl_pin: GPIO0 - sda_pin: GPIO1 - interrupt_pin: GPIO2 - reset_pin: GPIO3 - -<<: !include common.yaml diff --git a/tests/components/lilygo_t5_47/test.esp32-c3-idf.yaml b/tests/components/lilygo_t5_47/test.esp32-c3-idf.yaml deleted file mode 100644 index 061a98ce24..0000000000 --- a/tests/components/lilygo_t5_47/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - scl_pin: GPIO0 - sda_pin: GPIO1 - interrupt_pin: GPIO2 - reset_pin: GPIO3 - -<<: !include common.yaml diff --git a/tests/components/lilygo_t5_47/test.esp32-idf.yaml b/tests/components/lilygo_t5_47/test.esp32-idf.yaml index 342f0b6d8b..590f6a919c 100644 --- a/tests/components/lilygo_t5_47/test.esp32-idf.yaml +++ b/tests/components/lilygo_t5_47/test.esp32-idf.yaml @@ -1,7 +1,8 @@ substitutions: - scl_pin: GPIO13 - sda_pin: GPIO14 interrupt_pin: GPIO15 - reset_pin: GPIO16 + reset_pin: GPIO4 + +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/lilygo_t5_47/test.esp8266-ard.yaml b/tests/components/lilygo_t5_47/test.esp8266-ard.yaml index b446a75f13..3684d5e77b 100644 --- a/tests/components/lilygo_t5_47/test.esp8266-ard.yaml +++ b/tests/components/lilygo_t5_47/test.esp8266-ard.yaml @@ -1,7 +1,8 @@ substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - interrupt_pin: GPIO12 + interrupt_pin: GPIO15 reset_pin: GPIO16 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/lilygo_t5_47/test.rp2040-ard.yaml b/tests/components/lilygo_t5_47/test.rp2040-ard.yaml index 061a98ce24..2dd70ff33a 100644 --- a/tests/components/lilygo_t5_47/test.rp2040-ard.yaml +++ b/tests/components/lilygo_t5_47/test.rp2040-ard.yaml @@ -1,7 +1,8 @@ substitutions: - scl_pin: GPIO0 - sda_pin: GPIO1 interrupt_pin: GPIO2 reset_pin: GPIO3 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/lm75b/common.yaml b/tests/components/lm75b/common.yaml new file mode 100644 index 0000000000..39c39ed8dc --- /dev/null +++ b/tests/components/lm75b/common.yaml @@ -0,0 +1,5 @@ +sensor: + - platform: lm75b + i2c_id: i2c_bus + name: LM75B Temperature + update_interval: 30s diff --git a/tests/components/lm75b/test.esp32-idf.yaml b/tests/components/lm75b/test.esp32-idf.yaml new file mode 100644 index 0000000000..b47e39c389 --- /dev/null +++ b/tests/components/lm75b/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/lm75b/test.esp8266-ard.yaml b/tests/components/lm75b/test.esp8266-ard.yaml new file mode 100644 index 0000000000..4a98b9388a --- /dev/null +++ b/tests/components/lm75b/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/lm75b/test.rp2040-ard.yaml b/tests/components/lm75b/test.rp2040-ard.yaml new file mode 100644 index 0000000000..319a7c71a6 --- /dev/null +++ b/tests/components/lm75b/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/lock/common.yaml b/tests/components/lock/common.yaml index 67da46653c..9ba7f34857 100644 --- a/tests/components/lock/common.yaml +++ b/tests/components/lock/common.yaml @@ -17,9 +17,8 @@ lock: lambda: |- if (millis() > 10000) { return LOCK_STATE_LOCKED; - } else { - return LOCK_STATE_UNLOCKED; } + return LOCK_STATE_UNLOCKED; optimistic: true assumed_state: false on_unlock: @@ -27,7 +26,9 @@ lock: id: test_lock1 state: !lambda "return LOCK_STATE_UNLOCKED;" on_lock: - - lock.template.publish: LOCKED + - lock.template.publish: + id: test_lock1 + state: LOCKED - platform: output name: Generic Output Lock id: test_lock2 diff --git a/tests/components/lock/test.esp32-ard.yaml b/tests/components/lock/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/lock/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/lock/test.esp32-c3-ard.yaml b/tests/components/lock/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/lock/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/lock/test.esp32-c3-idf.yaml b/tests/components/lock/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/lock/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/b_parasite/test.esp32-ard.yaml b/tests/components/lock/test.nrf52-adafruit.yaml similarity index 100% rename from tests/components/b_parasite/test.esp32-ard.yaml rename to tests/components/lock/test.nrf52-adafruit.yaml diff --git a/tests/components/b_parasite/test.esp32-c3-ard.yaml b/tests/components/lock/test.nrf52-mcumgr.yaml similarity index 100% rename from tests/components/b_parasite/test.esp32-c3-ard.yaml rename to tests/components/lock/test.nrf52-mcumgr.yaml diff --git a/tests/components/logger/common-default_uart.yaml b/tests/components/logger/common-default_uart.yaml index e8b56043eb..7939a5f9c5 100644 --- a/tests/components/logger/common-default_uart.yaml +++ b/tests/components/logger/common-default_uart.yaml @@ -6,11 +6,16 @@ esphome: format: "Warning: Logger level is %d" args: [id(logger_id).get_log_level()] - logger.set_level: WARN + - logger.set_level: + level: ERROR + tag: mqtt.client logger: id: logger_id level: DEBUG initial_level: INFO + logs: + mqtt.component: WARN select: - platform: logger diff --git a/tests/components/logger/test-usb_cdc.esp32-c3-ard.yaml b/tests/components/logger/test-usb_cdc.esp32-c3-ard.yaml deleted file mode 100644 index cfdaec9771..0000000000 --- a/tests/components/logger/test-usb_cdc.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common-usb_cdc.yaml diff --git a/tests/components/improv_serial/test-usb_cdc.esp32-c3-ard.yaml b/tests/components/logger/test-usb_cdc.esp32-c3-idf.yaml similarity index 100% rename from tests/components/improv_serial/test-usb_cdc.esp32-c3-ard.yaml rename to tests/components/logger/test-usb_cdc.esp32-c3-idf.yaml diff --git a/tests/components/logger/test-usb_cdc.esp32-s2-ard.yaml b/tests/components/logger/test-usb_cdc.esp32-s2-ard.yaml deleted file mode 100644 index cfdaec9771..0000000000 --- a/tests/components/logger/test-usb_cdc.esp32-s2-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common-usb_cdc.yaml diff --git a/tests/components/logger/test-usb_cdc.esp32-s3-ard.yaml b/tests/components/logger/test-usb_cdc.esp32-s3-ard.yaml deleted file mode 100644 index cfdaec9771..0000000000 --- a/tests/components/logger/test-usb_cdc.esp32-s3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common-usb_cdc.yaml diff --git a/tests/components/improv_serial/test-usb_cdc.esp32-s2-ard.yaml b/tests/components/logger/test-usb_cdc.esp32-s3-idf.yaml similarity index 100% rename from tests/components/improv_serial/test-usb_cdc.esp32-s2-ard.yaml rename to tests/components/logger/test-usb_cdc.esp32-s3-idf.yaml diff --git a/tests/components/logger/test.esp32-ard.yaml b/tests/components/logger/test.esp32-ard.yaml deleted file mode 100644 index 3fe04e18a3..0000000000 --- a/tests/components/logger/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common-default_uart.yaml diff --git a/tests/components/logger/test.esp32-c3-ard.yaml b/tests/components/logger/test.esp32-c3-ard.yaml deleted file mode 100644 index 3fe04e18a3..0000000000 --- a/tests/components/logger/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common-default_uart.yaml diff --git a/tests/components/lps22/common.yaml b/tests/components/lps22/common.yaml index e6de4752ba..026b3620cd 100644 --- a/tests/components/lps22/common.yaml +++ b/tests/components/lps22/common.yaml @@ -1,5 +1,6 @@ sensor: - platform: lps22 + i2c_id: i2c_bus address: 0x5d update_interval: 10s temperature: diff --git a/tests/components/lps22/test.esp32-ard.yaml b/tests/components/lps22/test.esp32-ard.yaml deleted file mode 100644 index 0da6a9577e..0000000000 --- a/tests/components/lps22/test.esp32-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -i2c: - - id: i2c_lps22 - scl: 16 - sda: 17 - -<<: !include common.yaml diff --git a/tests/components/lps22/test.esp32-c3-ard.yaml b/tests/components/lps22/test.esp32-c3-ard.yaml deleted file mode 100644 index 6091393d31..0000000000 --- a/tests/components/lps22/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -i2c: - - id: i2c_lps22 - scl: 5 - sda: 4 - -<<: !include common.yaml diff --git a/tests/components/lps22/test.esp32-c3-idf.yaml b/tests/components/lps22/test.esp32-c3-idf.yaml deleted file mode 100644 index 6091393d31..0000000000 --- a/tests/components/lps22/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -i2c: - - id: i2c_lps22 - scl: 5 - sda: 4 - -<<: !include common.yaml diff --git a/tests/components/lps22/test.esp32-idf.yaml b/tests/components/lps22/test.esp32-idf.yaml index 0da6a9577e..b47e39c389 100644 --- a/tests/components/lps22/test.esp32-idf.yaml +++ b/tests/components/lps22/test.esp32-idf.yaml @@ -1,6 +1,4 @@ -i2c: - - id: i2c_lps22 - scl: 16 - sda: 17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/lps22/test.esp8266-ard.yaml b/tests/components/lps22/test.esp8266-ard.yaml index 6091393d31..4a98b9388a 100644 --- a/tests/components/lps22/test.esp8266-ard.yaml +++ b/tests/components/lps22/test.esp8266-ard.yaml @@ -1,6 +1,4 @@ -i2c: - - id: i2c_lps22 - scl: 5 - sda: 4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/lps22/test.rp2040-ard.yaml b/tests/components/lps22/test.rp2040-ard.yaml index 6091393d31..319a7c71a6 100644 --- a/tests/components/lps22/test.rp2040-ard.yaml +++ b/tests/components/lps22/test.rp2040-ard.yaml @@ -1,6 +1,4 @@ -i2c: - - id: i2c_lps22 - scl: 5 - sda: 4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/ltr390/common.yaml b/tests/components/ltr390/common.yaml index e5e331e7ba..c168da557b 100644 --- a/tests/components/ltr390/common.yaml +++ b/tests/components/ltr390/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_ltr390 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: ltr390 + i2c_id: i2c_bus uv: name: LTR390 UV 1 uv_index: @@ -19,6 +15,7 @@ sensor: address: 0x53 update_interval: 60s - platform: ltr390 + i2c_id: i2c_bus uv: name: LTR390 UV 2 uv_index: diff --git a/tests/components/ltr390/test.esp32-ard.yaml b/tests/components/ltr390/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/ltr390/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/ltr390/test.esp32-c3-ard.yaml b/tests/components/ltr390/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/ltr390/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ltr390/test.esp32-c3-idf.yaml b/tests/components/ltr390/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/ltr390/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ltr390/test.esp32-idf.yaml b/tests/components/ltr390/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/ltr390/test.esp32-idf.yaml +++ b/tests/components/ltr390/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/ltr390/test.esp8266-ard.yaml b/tests/components/ltr390/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/ltr390/test.esp8266-ard.yaml +++ b/tests/components/ltr390/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/ltr390/test.rp2040-ard.yaml b/tests/components/ltr390/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/ltr390/test.rp2040-ard.yaml +++ b/tests/components/ltr390/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/ltr501/common.yaml b/tests/components/ltr501/common.yaml index b7074f52f2..77c6f13739 100644 --- a/tests/components/ltr501/common.yaml +++ b/tests/components/ltr501/common.yaml @@ -1,7 +1,6 @@ sensor: - platform: ltr501 address: 0x23 - i2c_id: i2c_ltr501 type: ALS_PS gain: 1X integration_time: 100ms diff --git a/tests/components/ltr501/test.esp32-ard.yaml b/tests/components/ltr501/test.esp32-ard.yaml deleted file mode 100644 index 4c710c74fe..0000000000 --- a/tests/components/ltr501/test.esp32-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -i2c: - - id: i2c_ltr501 - scl: 16 - sda: 17 - -<<: !include common.yaml diff --git a/tests/components/ltr501/test.esp32-c3-ard.yaml b/tests/components/ltr501/test.esp32-c3-ard.yaml deleted file mode 100644 index 9e7de2768d..0000000000 --- a/tests/components/ltr501/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -i2c: - - id: i2c_ltr501 - scl: 5 - sda: 4 - -<<: !include common.yaml diff --git a/tests/components/ltr501/test.esp32-c3-idf.yaml b/tests/components/ltr501/test.esp32-c3-idf.yaml deleted file mode 100644 index 9e7de2768d..0000000000 --- a/tests/components/ltr501/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -i2c: - - id: i2c_ltr501 - scl: 5 - sda: 4 - -<<: !include common.yaml diff --git a/tests/components/ltr501/test.esp32-idf.yaml b/tests/components/ltr501/test.esp32-idf.yaml index 4c710c74fe..7a5d01898a 100644 --- a/tests/components/ltr501/test.esp32-idf.yaml +++ b/tests/components/ltr501/test.esp32-idf.yaml @@ -1,6 +1,4 @@ -i2c: - - id: i2c_ltr501 - scl: 16 - sda: 17 +packages: + i2c_low_freq: !include ../../test_build_components/common/i2c_low_freq/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/ltr501/test.esp8266-ard.yaml b/tests/components/ltr501/test.esp8266-ard.yaml index 9e7de2768d..9e23bb3778 100644 --- a/tests/components/ltr501/test.esp8266-ard.yaml +++ b/tests/components/ltr501/test.esp8266-ard.yaml @@ -1,6 +1,4 @@ -i2c: - - id: i2c_ltr501 - scl: 5 - sda: 4 +packages: + i2c_low_freq: !include ../../test_build_components/common/i2c_low_freq/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/ltr501/test.rp2040-ard.yaml b/tests/components/ltr501/test.rp2040-ard.yaml index 9e7de2768d..a7eb30036f 100644 --- a/tests/components/ltr501/test.rp2040-ard.yaml +++ b/tests/components/ltr501/test.rp2040-ard.yaml @@ -1,6 +1,4 @@ -i2c: - - id: i2c_ltr501 - scl: 5 - sda: 4 +packages: + i2c_low_freq: !include ../../test_build_components/common/i2c_low_freq/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/ltr_als_ps/common.yaml b/tests/components/ltr_als_ps/common.yaml index aa5c8abed7..edad4b2e4f 100644 --- a/tests/components/ltr_als_ps/common.yaml +++ b/tests/components/ltr_als_ps/common.yaml @@ -1,7 +1,6 @@ sensor: - platform: ltr_als_ps address: 0x23 - i2c_id: i2c_als_ps gain: 1x integration_time: 100ms ps_cooldown: 5 s diff --git a/tests/components/ltr_als_ps/test.esp32-ard.yaml b/tests/components/ltr_als_ps/test.esp32-ard.yaml deleted file mode 100644 index 2349292a64..0000000000 --- a/tests/components/ltr_als_ps/test.esp32-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -i2c: - - id: i2c_als_ps - scl: 16 - sda: 17 - -<<: !include common.yaml diff --git a/tests/components/ltr_als_ps/test.esp32-c3-ard.yaml b/tests/components/ltr_als_ps/test.esp32-c3-ard.yaml deleted file mode 100644 index d64d70f018..0000000000 --- a/tests/components/ltr_als_ps/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -i2c: - - id: i2c_als_ps - scl: 5 - sda: 4 - -<<: !include common.yaml diff --git a/tests/components/ltr_als_ps/test.esp32-c3-idf.yaml b/tests/components/ltr_als_ps/test.esp32-c3-idf.yaml deleted file mode 100644 index d64d70f018..0000000000 --- a/tests/components/ltr_als_ps/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -i2c: - - id: i2c_als_ps - scl: 5 - sda: 4 - -<<: !include common.yaml diff --git a/tests/components/ltr_als_ps/test.esp32-idf.yaml b/tests/components/ltr_als_ps/test.esp32-idf.yaml index 2349292a64..7a5d01898a 100644 --- a/tests/components/ltr_als_ps/test.esp32-idf.yaml +++ b/tests/components/ltr_als_ps/test.esp32-idf.yaml @@ -1,6 +1,4 @@ -i2c: - - id: i2c_als_ps - scl: 16 - sda: 17 +packages: + i2c_low_freq: !include ../../test_build_components/common/i2c_low_freq/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/ltr_als_ps/test.esp8266-ard.yaml b/tests/components/ltr_als_ps/test.esp8266-ard.yaml index d64d70f018..9e23bb3778 100644 --- a/tests/components/ltr_als_ps/test.esp8266-ard.yaml +++ b/tests/components/ltr_als_ps/test.esp8266-ard.yaml @@ -1,6 +1,4 @@ -i2c: - - id: i2c_als_ps - scl: 5 - sda: 4 +packages: + i2c_low_freq: !include ../../test_build_components/common/i2c_low_freq/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/ltr_als_ps/test.rp2040-ard.yaml b/tests/components/ltr_als_ps/test.rp2040-ard.yaml index d64d70f018..a7eb30036f 100644 --- a/tests/components/ltr_als_ps/test.rp2040-ard.yaml +++ b/tests/components/ltr_als_ps/test.rp2040-ard.yaml @@ -1,6 +1,4 @@ -i2c: - - id: i2c_als_ps - scl: 5 - sda: 4 +packages: + i2c_low_freq: !include ../../test_build_components/common/i2c_low_freq/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/lvgl/common.yaml b/tests/components/lvgl/common.yaml index a035900386..652ae7e7a1 100644 --- a/tests/components/lvgl/common.yaml +++ b/tests/components/lvgl/common.yaml @@ -1,5 +1,6 @@ touchscreen: - platform: ft63x6 + i2c_id: i2c_bus id: tft_touch display: tft_display update_interval: 50ms @@ -51,6 +52,19 @@ number: widget: spinbox_id id: lvgl_spinbox_number name: LVGL Spinbox Number + - platform: template + id: test_brightness + name: "Test Brightness" + min_value: 0 + max_value: 255 + step: 1 + optimistic: true + # Test lambda in automation accessing x parameter directly + # This is a real-world pattern from user configs + on_value: + - lambda: !lambda |- + // Direct use of x parameter in automation + ESP_LOGD("test", "Brightness: %.0f", x); light: - platform: lvgl @@ -101,11 +115,29 @@ wifi: password: PASSWORD123 time: - platform: sntp - id: time_id + - platform: sntp + id: sntp_time text: - id: lvgl_text platform: lvgl widget: hello_label mode: text + +text_sensor: + - platform: template + id: test_text_sensor + name: "Test Text Sensor" + # Test nested lambdas in LVGL actions can access automation parameters + on_value: + - lvgl.label.update: + id: hello_label + text: !lambda return x.c_str(); + - lvgl.label.update: + id: hello_label + text: !lambda |- + // Test complex lambda with conditionals accessing x parameter + if (x == "*") { + return "WILDCARD"; + } + return x.c_str(); diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 7cd2e2b93e..65d629bcdf 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -16,6 +16,18 @@ binary_sensor: platform: template - id: left_sensor platform: template + - platform: lvgl + id: button_checker + name: LVGL button + widget: button_button + on_state: + then: + - lvgl.checkbox.update: + id: checkbox_id + state: + checked: !lambda |- + auto y = x; // block inlining of one line return + return y; lvgl: log_level: debug @@ -76,7 +88,7 @@ lvgl: line_width: 8 line_rounded: true - id: date_style - text_font: roboto10 + text_font: !lambda return id(roboto10); align: center text_color: !lambda return color_id2; bg_opa: cover @@ -113,9 +125,10 @@ lvgl: title: Messagebox bg_color: 0xffff widgets: - - label: - text: Hello Msgbox - id: msgbox_label + # Test single widget without list + label: + text: Hello Msgbox + id: msgbox_label body: text: This is a sample messagebox bg_color: 0x808080 @@ -190,7 +203,7 @@ lvgl: args: ['lv_event_code_name_for(event->code).c_str()'] skip: true layout: - type: flex + type: Flex pad_row: 4 pad_column: 4px flex_align_main: center @@ -257,8 +270,31 @@ lvgl: text: "Hello shiny day" text_color: 0xFFFFFF align: bottom_mid - text_font: space16 - - obj: + - label: + id: setup_lambda_label + # Test lambda in widget property during setup (LvContext) + # Should NOT receive lv_component parameter + text: !lambda |- + char buf[32]; + snprintf(buf, sizeof(buf), "Setup: %d", 42); + return std::string(buf); + align: top_mid + text_font: !lambda return id(space16); + - label: + id: chip_info_label + # Test complex setup lambda (real-world pattern) + # Should NOT receive lv_component parameter + text: !lambda |- + // Test conditional compilation and string formatting + char buf[64]; + #ifdef USE_ESP_IDF + snprintf(buf, sizeof(buf), "IDF: v%d.%d", ESP_IDF_VERSION_MAJOR, ESP_IDF_VERSION_MINOR); + #else + snprintf(buf, sizeof(buf), "Arduino"); + #endif + return std::string(buf); + align: top_left + - container: align: center arc_opa: COVER arc_color: 0xFF0000 @@ -391,6 +427,15 @@ lvgl: - buttons: - id: button_e - button: + id: button_with_text + text: Button + on_click: + lvgl.button.update: + id: button_with_text + text: Clicked + + - button: + layout: 2x1 id: button_button width: 20% height: 10% @@ -407,8 +452,13 @@ lvgl: checked: bg_color: 0x000000 widgets: - - label: - text: Button + # Test parse a dict instead of list + label: + text: Button + align: bottom_right + image: + src: cat_image + align: top_left on_click: - lvgl.widget.focus: spin_up - lvgl.widget.focus: next @@ -448,19 +498,19 @@ lvgl: id: hello_label text: time_format: "%c" - time: time_id + time: sntp_time - lvgl.label.update: id: hello_label text: time_format: "%c" - time: !lambda return id(time_id).now(); + time: !lambda return id(sntp_time).now(); - lvgl.label.update: id: hello_label text: time_format: "%c" time: !lambda |- ESP_LOGD("label", "multi-line lambda"); - return id(time_id).now(); + return id(sntp_time).now(); on_value: logger.log: format: "state now %d" @@ -507,6 +557,9 @@ lvgl: - tileview: id: tileview_id scrollbar_mode: active + scroll_dir: all + scroll_elastic: true + scroll_momentum: true on_value: then: - if: @@ -516,6 +569,10 @@ lvgl: - logger.log: "tile 1 is now showing" tiles: - id: tile_1 + scroll_snap_y: center + scroll_snap_x: start + layout: vertical + pad_all: 6px row: 0 column: 0 dir: ALL @@ -531,6 +588,7 @@ lvgl: bg_color: 0x000000 - id: page2 + layout: vertical widgets: - canvas: id: canvas_id @@ -668,6 +726,12 @@ lvgl: width: 100% height: 10% align: top_mid + on_value: + - lvgl.spinbox.update: + id: spinbox_id + value: !lambda |- + static float yyy = 83.0; + return yyy + .8; - button: styles: spin_button id: spin_up @@ -684,7 +748,7 @@ lvgl: width: 120 range_from: -10 range_to: 1000 - step: 5.0 + selected_digit: 2 rollover: false digits: 6 decimal_places: 2 @@ -694,6 +758,12 @@ lvgl: - logger.log: format: "Spinbox value is %f" args: [x] + - lvgl.label.update: + id: hello_label + text: + format: "value is %.1f now" + args: [x] + if_nan: "Value unknown" - button: styles: spin_button id: spin_down @@ -723,6 +793,32 @@ lvgl: arc_color: 0xFFFF00 focused: arc_color: 0x808080 + - arc: + align: center + id: lv_arc_1 + value: !lambda return 75; + min_value: !lambda return 50; + max_value: !lambda return 60; + arc_color: 0xFF0000 + indicator: + arc_width: !lambda return 20; + arc_color: 0xF000FF + pressed: + arc_color: 0xFFFF00 + focused: + arc_color: 0x808080 + on_click: + then: + - lvgl.arc.update: + id: lv_arc_1 + value: !lambda return (int)((float)rand() / RAND_MAX * 100); + min_value: !lambda return (int)((float)rand() / RAND_MAX * 100); + max_value: !lambda return (int)((float)rand() / RAND_MAX * 100); + start_angle: !lambda return (int)((float)rand() / RAND_MAX * 100); + end_angle: !lambda return (int)((float)rand() / RAND_MAX * 100); + rotation: !lambda return (int)((float)rand() / RAND_MAX * 100); + change_rate: !lambda return (uint)((float)rand() / RAND_MAX * 100); + mode: NORMAL - bar: id: bar_id align: top_mid @@ -817,12 +913,13 @@ lvgl: width: 100% pad_all: 8 layout: - type: grid + type: GRid grid_row_align: end grid_rows: [25px, fr(1), content] grid_columns: [40, fr(1), fr(1)] pad_row: 6px pad_column: 0 + multiple_widgets_per_cell: true widgets: - image: grid_cell_row_pos: 0 @@ -847,6 +944,10 @@ lvgl: grid_cell_row_pos: 1 grid_cell_column_pos: 0 text: "Grid cell 1/0" + - label: + grid_cell_row_pos: 1 + grid_cell_column_pos: 0 + text: "Duplicate for 1/0" - label: styles: bdr_style grid_cell_row_pos: 1 @@ -968,6 +1069,8 @@ lvgl: r_mod: -20 opa: 0% - id: page3 + layout: Horizontal + pad_all: 6px widgets: - keyboard: id: lv_keyboard diff --git a/tests/components/lvgl/test.esp32-ard.yaml b/tests/components/lvgl/test.esp32-ard.yaml deleted file mode 100644 index f85bedbde6..0000000000 --- a/tests/components/lvgl/test.esp32-ard.yaml +++ /dev/null @@ -1,58 +0,0 @@ -spi: - clk_pin: 14 - mosi_pin: 13 - -i2c: - sda: GPIO18 - scl: GPIO19 - -display: - - platform: ili9xxx - model: st7789v - id: tft_display - dimensions: - width: 240 - height: 320 - transform: - swap_xy: false - mirror_x: true - mirror_y: true - data_rate: 80MHz - cs_pin: GPIO22 - dc_pin: GPIO21 - auto_clear_enabled: false - invert_colors: false - update_interval: never - -binary_sensor: - - platform: gpio - internal: true - id: up_button - pin: - number: GPIO38 - inverted: true - - platform: gpio - internal: true - id: down_button - pin: - number: GPIO37 - inverted: true - - platform: gpio - internal: true - id: select_button - pin: - number: GPIO39 - inverted: true -lvgl: - draw_rounding: 8 - encoders: - group: switches - initial_focus: button_button - enter_button: select_button - sensor: - left_button: up_button - right_button: down_button - -packages: - lvgl: !include lvgl-package.yaml - xvgl: !include common.yaml diff --git a/tests/components/lvgl/test.esp32-idf.yaml b/tests/components/lvgl/test.esp32-idf.yaml index eacace1d4b..e6025e17fc 100644 --- a/tests/components/lvgl/test.esp32-idf.yaml +++ b/tests/components/lvgl/test.esp32-idf.yaml @@ -1,10 +1,7 @@ -spi: - clk_pin: 14 - mosi_pin: 13 - -i2c: - sda: GPIO18 - scl: GPIO19 +packages: + lvgl: !include lvgl-package.yaml + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml sensor: - platform: rotary_encoder @@ -25,6 +22,7 @@ binary_sensor: display: - platform: ili9xxx + spi_id: spi_bus model: st7789v id: second_display dimensions: @@ -44,6 +42,7 @@ display: update_interval: never - platform: ili9xxx + spi_id: spi_bus model: st7789v id: tft_display dimensions: @@ -60,10 +59,8 @@ display: invert_colors: false update_interval: never -packages: - lvgl: !include lvgl-package.yaml - lvgl: + update_when_display_idle: true displays: - tft_display - second_display @@ -72,5 +69,13 @@ lvgl: enter_button: pushbutton group: general initial_focus: lv_roller + on_draw_start: + - logger.log: draw started + on_draw_end: + - logger.log: draw ended + - lvgl.pause: + - component.update: tft_display + - delay: 60s + - lvgl.resume: <<: !include common.yaml diff --git a/tests/components/lvgl/test.host.yaml b/tests/components/lvgl/test.host.yaml index 39d9a0ebf3..00a8cd8c01 100644 --- a/tests/components/lvgl/test.host.yaml +++ b/tests/components/lvgl/test.host.yaml @@ -18,6 +18,7 @@ touchscreen: lvgl: - id: lvgl_0 + default_font: space16 displays: sdl0 - id: lvgl_1 displays: sdl1 @@ -39,3 +40,8 @@ lvgl: text: Click ME on_click: logger.log: Clicked + +font: + - file: "gfonts://Roboto" + id: space16 + bpp: 4 diff --git a/tests/components/m5stack_8angle/common.yaml b/tests/components/m5stack_8angle/common.yaml index d7f988ed3a..d0af24116c 100644 --- a/tests/components/m5stack_8angle/common.yaml +++ b/tests/components/m5stack_8angle/common.yaml @@ -1,10 +1,5 @@ -i2c: - sda: 0 - scl: 1 - id: bus_external - m5stack_8angle: - i2c_id: bus_external + i2c_id: i2c_bus id: m5stack_8angle_base light: diff --git a/tests/components/m5stack_8angle/test.esp32-ard.yaml b/tests/components/m5stack_8angle/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/m5stack_8angle/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/m5stack_8angle/test.esp32-c3-ard.yaml b/tests/components/m5stack_8angle/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/m5stack_8angle/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/m5stack_8angle/test.esp32-c3-idf.yaml b/tests/components/m5stack_8angle/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/m5stack_8angle/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/m5stack_8angle/test.esp32-idf.yaml b/tests/components/m5stack_8angle/test.esp32-idf.yaml index dade44d145..b47e39c389 100644 --- a/tests/components/m5stack_8angle/test.esp32-idf.yaml +++ b/tests/components/m5stack_8angle/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/m5stack_8angle/test.esp8266-ard.yaml b/tests/components/m5stack_8angle/test.esp8266-ard.yaml index dade44d145..4a98b9388a 100644 --- a/tests/components/m5stack_8angle/test.esp8266-ard.yaml +++ b/tests/components/m5stack_8angle/test.esp8266-ard.yaml @@ -1 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/m5stack_8angle/test.rp2040-ard.yaml b/tests/components/m5stack_8angle/test.rp2040-ard.yaml index dade44d145..319a7c71a6 100644 --- a/tests/components/m5stack_8angle/test.rp2040-ard.yaml +++ b/tests/components/m5stack_8angle/test.rp2040-ard.yaml @@ -1 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/main.cpp b/tests/components/main.cpp new file mode 100644 index 0000000000..928f0e6059 --- /dev/null +++ b/tests/components/main.cpp @@ -0,0 +1,26 @@ +#include + +/* +This special main.cpp replaces the default one. +It will run all the Google Tests found in all compiled cpp files and then exit with the result +See README.md for more information +*/ + +// Auto generated code by esphome +// ========== AUTO GENERATED INCLUDE BLOCK BEGIN =========== +// ========== AUTO GENERATED INCLUDE BLOCK END ===========" + +void original_setup() { + // This function won't be run. + + // ========== AUTO GENERATED CODE BEGIN =========== + // =========== AUTO GENERATED CODE END ============ +} + +void setup() { + ::testing::InitGoogleTest(); + int exit_code = RUN_ALL_TESTS(); + exit(exit_code); +} + +void loop() {} diff --git a/tests/components/mapping/.gitattributes b/tests/components/mapping/.gitattributes new file mode 100644 index 0000000000..9d74867fcf --- /dev/null +++ b/tests/components/mapping/.gitattributes @@ -0,0 +1 @@ +*.ttf -text diff --git a/tests/components/mapping/common.yaml b/tests/components/mapping/common.yaml index 07ca458146..7ffcfa4f67 100644 --- a/tests/components/mapping/common.yaml +++ b/tests/components/mapping/common.yaml @@ -50,6 +50,14 @@ mapping: red: red_id blue: blue_id green: green_id + - id: string_map_2 + from: string + to: string + entries: + one: "one" + two: "two" + three: "three" + seventy-seven: "seventy-seven" color: - id: red_id @@ -65,7 +73,14 @@ color: green: 0.0 blue: 1.0 +font: + - file: "$component_dir/helvetica.ttf" + id: font_id + size: 20 + display: lambda: |- - it.image(0, 0, id(weather_map)[0]); - it.image(0, 100, id(weather_map)[1]); + std::string value = id(int_map)[2]; + it.print(0, 0, id(font_id), TextAlign::TOP_LEFT, value.c_str()); + it.image(0, 0, id(weather_map)["clear-night"]); + it.image(0, 100, id(weather_map)["sunny"]); diff --git a/tests/components/mapping/helvetica.ttf b/tests/components/mapping/helvetica.ttf new file mode 100644 index 0000000000..7aec6f3f3c Binary files /dev/null and b/tests/components/mapping/helvetica.ttf differ diff --git a/tests/components/mapping/test.esp32-ard.yaml b/tests/components/mapping/test.esp32-ard.yaml deleted file mode 100644 index 951a6061f6..0000000000 --- a/tests/components/mapping/test.esp32-ard.yaml +++ /dev/null @@ -1,17 +0,0 @@ -spi: - - id: spi_main_lcd - clk_pin: 16 - mosi_pin: 17 - miso_pin: 15 - -display: - - platform: ili9xxx - id: main_lcd - model: ili9342 - cs_pin: 12 - dc_pin: 13 - reset_pin: 21 - invert_colors: false - -packages: - map: !include common.yaml diff --git a/tests/components/mapping/test.esp32-c3-ard.yaml b/tests/components/mapping/test.esp32-c3-ard.yaml deleted file mode 100644 index 55e5719e50..0000000000 --- a/tests/components/mapping/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,17 +0,0 @@ -spi: - - id: spi_main_lcd - clk_pin: 6 - mosi_pin: 7 - miso_pin: 5 - -display: - - platform: ili9xxx - id: main_lcd - model: ili9342 - cs_pin: 8 - dc_pin: 9 - reset_pin: 10 - invert_colors: false - -packages: - map: !include common.yaml diff --git a/tests/components/mapping/test.esp32-c3-idf.yaml b/tests/components/mapping/test.esp32-c3-idf.yaml deleted file mode 100644 index 55e5719e50..0000000000 --- a/tests/components/mapping/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,17 +0,0 @@ -spi: - - id: spi_main_lcd - clk_pin: 6 - mosi_pin: 7 - miso_pin: 5 - -display: - - platform: ili9xxx - id: main_lcd - model: ili9342 - cs_pin: 8 - dc_pin: 9 - reset_pin: 10 - invert_colors: false - -packages: - map: !include common.yaml diff --git a/tests/components/mapping/test.esp32-idf.yaml b/tests/components/mapping/test.esp32-idf.yaml index 951a6061f6..a35b6940c7 100644 --- a/tests/components/mapping/test.esp32-idf.yaml +++ b/tests/components/mapping/test.esp32-idf.yaml @@ -1,17 +1,13 @@ -spi: - - id: spi_main_lcd - clk_pin: 16 - mosi_pin: 17 - miso_pin: 15 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + map: !include common.yaml display: - - platform: ili9xxx - id: main_lcd - model: ili9342 - cs_pin: 12 - dc_pin: 13 - reset_pin: 21 - invert_colors: false - -packages: - map: !include common.yaml + spi_id: spi_bus + platform: ili9xxx + id: main_lcd + model: ili9342 + cs_pin: 12 + dc_pin: 13 + reset_pin: 21 + invert_colors: false diff --git a/tests/components/mapping/test.esp8266-ard.yaml b/tests/components/mapping/test.esp8266-ard.yaml index dd4642b8fe..c59821a211 100644 --- a/tests/components/mapping/test.esp8266-ard.yaml +++ b/tests/components/mapping/test.esp8266-ard.yaml @@ -1,17 +1,13 @@ -spi: - - id: spi_main_lcd - clk_pin: 14 - mosi_pin: 13 - miso_pin: 12 +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + map: !include common.yaml display: - - platform: ili9xxx - id: main_lcd - model: ili9342 - cs_pin: 5 - dc_pin: 15 - reset_pin: 16 - invert_colors: false - -packages: - map: !include common.yaml + spi_id: spi_bus + platform: ili9xxx + id: main_lcd + model: ili9342 + cs_pin: 5 + dc_pin: 15 + reset_pin: 16 + invert_colors: false diff --git a/tests/components/mapping/test.host.yaml b/tests/components/mapping/test.host.yaml index 98406767a4..19937e1f21 100644 --- a/tests/components/mapping/test.host.yaml +++ b/tests/components/mapping/test.host.yaml @@ -1,12 +1,12 @@ display: - - platform: sdl - id: sdl_display - update_interval: 1s - auto_clear_enabled: false - show_test_card: true - dimensions: - width: 450 - height: 600 + platform: sdl + id: sdl_display + update_interval: 1s + auto_clear_enabled: false + show_test_card: true + dimensions: + width: 450 + height: 600 packages: map: !include common.yaml diff --git a/tests/components/mapping/test.rp2040-ard.yaml b/tests/components/mapping/test.rp2040-ard.yaml index 1b7e796246..acc305a701 100644 --- a/tests/components/mapping/test.rp2040-ard.yaml +++ b/tests/components/mapping/test.rp2040-ard.yaml @@ -1,17 +1,13 @@ -spi: - - id: spi_main_lcd - clk_pin: 2 - mosi_pin: 3 - miso_pin: 4 +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml + map: !include common.yaml display: - - platform: ili9xxx - id: main_lcd - model: ili9342 - cs_pin: 20 - dc_pin: 21 - reset_pin: 22 - invert_colors: false - -packages: - map: !include common.yaml + spi_id: spi_bus + platform: ili9xxx + id: main_lcd + model: ili9342 + cs_pin: 20 + dc_pin: 21 + reset_pin: 22 + invert_colors: false diff --git a/tests/components/matrix_keypad/test.esp32-ard.yaml b/tests/components/matrix_keypad/test.esp32-ard.yaml deleted file mode 100644 index 70bb70638d..0000000000 --- a/tests/components/matrix_keypad/test.esp32-ard.yaml +++ /dev/null @@ -1,15 +0,0 @@ -packages: - common: !include common.yaml - -matrix_keypad: - id: keypad - rows: - - pin: 12 - - pin: 13 - columns: - - pin: 14 - - pin: 15 - keys: "1234" - has_pulldowns: true - on_key: - - lambda: ESP_LOGI("KEY", "key %d pressed", x); diff --git a/tests/components/matrix_keypad/test.esp32-c3-ard.yaml b/tests/components/matrix_keypad/test.esp32-c3-ard.yaml deleted file mode 100644 index 75d9c0b263..0000000000 --- a/tests/components/matrix_keypad/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,15 +0,0 @@ -packages: - common: !include common.yaml - -matrix_keypad: - id: keypad - rows: - - pin: 1 - - pin: 2 - columns: - - pin: 3 - - pin: 4 - keys: "1234" - has_pulldowns: true - on_key: - - lambda: ESP_LOGI("KEY", "key %d pressed", x); diff --git a/tests/components/matrix_keypad/test.esp32-c3-idf.yaml b/tests/components/matrix_keypad/test.esp32-c3-idf.yaml deleted file mode 100644 index 75d9c0b263..0000000000 --- a/tests/components/matrix_keypad/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,15 +0,0 @@ -packages: - common: !include common.yaml - -matrix_keypad: - id: keypad - rows: - - pin: 1 - - pin: 2 - columns: - - pin: 3 - - pin: 4 - keys: "1234" - has_pulldowns: true - on_key: - - lambda: ESP_LOGI("KEY", "key %d pressed", x); diff --git a/tests/components/matrix_keypad/test.esp32-s3-idf.yaml b/tests/components/matrix_keypad/test.esp32-s3-idf.yaml deleted file mode 100644 index a491f2ed59..0000000000 --- a/tests/components/matrix_keypad/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,15 +0,0 @@ -packages: - common: !include common.yaml - -matrix_keypad: - id: keypad - rows: - - pin: 10 - - pin: 11 - columns: - - pin: 12 - - pin: 13 - keys: "1234" - has_pulldowns: true - on_key: - - lambda: ESP_LOGI("KEY", "key %d pressed", x); diff --git a/tests/components/max17043/common.yaml b/tests/components/max17043/common.yaml index c2f324212e..f58006c460 100644 --- a/tests/components/max17043/common.yaml +++ b/tests/components/max17043/common.yaml @@ -3,15 +3,10 @@ esphome: then: - max17043.sleep_mode: max17043_id -i2c: - - id: i2c_id - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: max17043 + i2c_id: i2c_bus id: max17043_id - i2c_id: i2c_id battery_voltage: name: "Battery Voltage" battery_level: diff --git a/tests/components/max17043/test.esp32-ard.yaml b/tests/components/max17043/test.esp32-ard.yaml deleted file mode 100644 index c6615f51cd..0000000000 --- a/tests/components/max17043/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - sda_pin: GPIO21 - scl_pin: GPIO22 - -<<: !include common.yaml diff --git a/tests/components/max17043/test.esp32-c3-ard.yaml b/tests/components/max17043/test.esp32-c3-ard.yaml deleted file mode 100644 index 9a1477d4b9..0000000000 --- a/tests/components/max17043/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - sda_pin: GPIO8 - scl_pin: GPIO10 - -<<: !include common.yaml diff --git a/tests/components/max17043/test.esp32-c3-idf.yaml b/tests/components/max17043/test.esp32-c3-idf.yaml deleted file mode 100644 index 9a1477d4b9..0000000000 --- a/tests/components/max17043/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - sda_pin: GPIO8 - scl_pin: GPIO10 - -<<: !include common.yaml diff --git a/tests/components/max17043/test.esp32-idf.yaml b/tests/components/max17043/test.esp32-idf.yaml index c6615f51cd..b47e39c389 100644 --- a/tests/components/max17043/test.esp32-idf.yaml +++ b/tests/components/max17043/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - sda_pin: GPIO21 - scl_pin: GPIO22 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/max17043/test.esp8266-ard.yaml b/tests/components/max17043/test.esp8266-ard.yaml index a87353b78b..4a98b9388a 100644 --- a/tests/components/max17043/test.esp8266-ard.yaml +++ b/tests/components/max17043/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - sda_pin: GPIO4 - scl_pin: GPIO5 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/max17043/test.rp2040-ard.yaml b/tests/components/max17043/test.rp2040-ard.yaml index c6615f51cd..319a7c71a6 100644 --- a/tests/components/max17043/test.rp2040-ard.yaml +++ b/tests/components/max17043/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - sda_pin: GPIO21 - scl_pin: GPIO22 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/max31855/common.yaml b/tests/components/max31855/common.yaml index 7136c597d5..905b111d71 100644 --- a/tests/components/max31855/common.yaml +++ b/tests/components/max31855/common.yaml @@ -1,9 +1,3 @@ -spi: - - id: spi_max31855 - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - miso_pin: ${miso_pin} - sensor: - platform: max31855 name: MAX31855 Temperature diff --git a/tests/components/max31855/test.esp32-ard.yaml b/tests/components/max31855/test.esp32-ard.yaml deleted file mode 100644 index 54e027a614..0000000000 --- a/tests/components/max31855/test.esp32-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 - cs_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/max31855/test.esp32-c3-ard.yaml b/tests/components/max31855/test.esp32-c3-ard.yaml deleted file mode 100644 index 2415ba5dc6..0000000000 --- a/tests/components/max31855/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/max31855/test.esp32-c3-idf.yaml b/tests/components/max31855/test.esp32-c3-idf.yaml deleted file mode 100644 index 2415ba5dc6..0000000000 --- a/tests/components/max31855/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/max31855/test.esp32-idf.yaml b/tests/components/max31855/test.esp32-idf.yaml index 54e027a614..a3352cf880 100644 --- a/tests/components/max31855/test.esp32-idf.yaml +++ b/tests/components/max31855/test.esp32-idf.yaml @@ -1,7 +1,7 @@ substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 cs_pin: GPIO5 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/max31855/test.esp8266-ard.yaml b/tests/components/max31855/test.esp8266-ard.yaml index dbd158d030..b4673ba8b7 100644 --- a/tests/components/max31855/test.esp8266-ard.yaml +++ b/tests/components/max31855/test.esp8266-ard.yaml @@ -1,7 +1,10 @@ substitutions: - clk_pin: GPIO14 - mosi_pin: GPIO13 - miso_pin: GPIO12 + clk_pin: GPIO0 + mosi_pin: GPIO2 + miso_pin: GPIO16 cs_pin: GPIO15 +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/max31855/test.rp2040-ard.yaml b/tests/components/max31855/test.rp2040-ard.yaml index f6c3f1eeca..1ded24de1c 100644 --- a/tests/components/max31855/test.rp2040-ard.yaml +++ b/tests/components/max31855/test.rp2040-ard.yaml @@ -4,4 +4,7 @@ substitutions: miso_pin: GPIO4 cs_pin: GPIO5 +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/max31856/common.yaml b/tests/components/max31856/common.yaml index 4f7c3ad408..9d420662d7 100644 --- a/tests/components/max31856/common.yaml +++ b/tests/components/max31856/common.yaml @@ -1,9 +1,3 @@ -spi: - - id: spi_max31856 - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - miso_pin: ${miso_pin} - sensor: - platform: max31856 name: MAX31856 Temperature diff --git a/tests/components/max31856/test.esp32-ard.yaml b/tests/components/max31856/test.esp32-ard.yaml deleted file mode 100644 index 54e027a614..0000000000 --- a/tests/components/max31856/test.esp32-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 - cs_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/max31856/test.esp32-c3-ard.yaml b/tests/components/max31856/test.esp32-c3-ard.yaml deleted file mode 100644 index 2415ba5dc6..0000000000 --- a/tests/components/max31856/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/max31856/test.esp32-c3-idf.yaml b/tests/components/max31856/test.esp32-c3-idf.yaml deleted file mode 100644 index 2415ba5dc6..0000000000 --- a/tests/components/max31856/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/max31856/test.esp32-idf.yaml b/tests/components/max31856/test.esp32-idf.yaml index 54e027a614..a3352cf880 100644 --- a/tests/components/max31856/test.esp32-idf.yaml +++ b/tests/components/max31856/test.esp32-idf.yaml @@ -1,7 +1,7 @@ substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 cs_pin: GPIO5 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/max31856/test.esp8266-ard.yaml b/tests/components/max31856/test.esp8266-ard.yaml index dbd158d030..b4673ba8b7 100644 --- a/tests/components/max31856/test.esp8266-ard.yaml +++ b/tests/components/max31856/test.esp8266-ard.yaml @@ -1,7 +1,10 @@ substitutions: - clk_pin: GPIO14 - mosi_pin: GPIO13 - miso_pin: GPIO12 + clk_pin: GPIO0 + mosi_pin: GPIO2 + miso_pin: GPIO16 cs_pin: GPIO15 +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/max31856/test.rp2040-ard.yaml b/tests/components/max31856/test.rp2040-ard.yaml index f6c3f1eeca..1ded24de1c 100644 --- a/tests/components/max31856/test.rp2040-ard.yaml +++ b/tests/components/max31856/test.rp2040-ard.yaml @@ -4,4 +4,7 @@ substitutions: miso_pin: GPIO4 cs_pin: GPIO5 +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/max31865/common.yaml b/tests/components/max31865/common.yaml index 5bb7bda5aa..6e71f17efc 100644 --- a/tests/components/max31865/common.yaml +++ b/tests/components/max31865/common.yaml @@ -1,9 +1,3 @@ -spi: - - id: spi_max31865 - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - miso_pin: ${miso_pin} - sensor: - platform: max31865 name: MAX31865 Temperature diff --git a/tests/components/max31865/test.esp32-ard.yaml b/tests/components/max31865/test.esp32-ard.yaml deleted file mode 100644 index 54e027a614..0000000000 --- a/tests/components/max31865/test.esp32-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 - cs_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/max31865/test.esp32-c3-ard.yaml b/tests/components/max31865/test.esp32-c3-ard.yaml deleted file mode 100644 index 2415ba5dc6..0000000000 --- a/tests/components/max31865/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/max31865/test.esp32-c3-idf.yaml b/tests/components/max31865/test.esp32-c3-idf.yaml deleted file mode 100644 index 2415ba5dc6..0000000000 --- a/tests/components/max31865/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/max31865/test.esp32-idf.yaml b/tests/components/max31865/test.esp32-idf.yaml index 54e027a614..a3352cf880 100644 --- a/tests/components/max31865/test.esp32-idf.yaml +++ b/tests/components/max31865/test.esp32-idf.yaml @@ -1,7 +1,7 @@ substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 cs_pin: GPIO5 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/max31865/test.esp8266-ard.yaml b/tests/components/max31865/test.esp8266-ard.yaml index dbd158d030..b4673ba8b7 100644 --- a/tests/components/max31865/test.esp8266-ard.yaml +++ b/tests/components/max31865/test.esp8266-ard.yaml @@ -1,7 +1,10 @@ substitutions: - clk_pin: GPIO14 - mosi_pin: GPIO13 - miso_pin: GPIO12 + clk_pin: GPIO0 + mosi_pin: GPIO2 + miso_pin: GPIO16 cs_pin: GPIO15 +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/max31865/test.rp2040-ard.yaml b/tests/components/max31865/test.rp2040-ard.yaml index f6c3f1eeca..1ded24de1c 100644 --- a/tests/components/max31865/test.rp2040-ard.yaml +++ b/tests/components/max31865/test.rp2040-ard.yaml @@ -4,4 +4,7 @@ substitutions: miso_pin: GPIO4 cs_pin: GPIO5 +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/max44009/common.yaml b/tests/components/max44009/common.yaml index ef51740895..523387e1cc 100644 --- a/tests/components/max44009/common.yaml +++ b/tests/components/max44009/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_max44009 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: max44009 + i2c_id: i2c_bus name: MAX44009 Brightness internal: true mode: low_power diff --git a/tests/components/max44009/test.esp32-ard.yaml b/tests/components/max44009/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/max44009/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/max44009/test.esp32-c3-ard.yaml b/tests/components/max44009/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/max44009/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/max44009/test.esp32-c3-idf.yaml b/tests/components/max44009/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/max44009/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/max44009/test.esp32-idf.yaml b/tests/components/max44009/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/max44009/test.esp32-idf.yaml +++ b/tests/components/max44009/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/max44009/test.esp8266-ard.yaml b/tests/components/max44009/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/max44009/test.esp8266-ard.yaml +++ b/tests/components/max44009/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/max44009/test.rp2040-ard.yaml b/tests/components/max44009/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/max44009/test.rp2040-ard.yaml +++ b/tests/components/max44009/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/max6675/common.yaml b/tests/components/max6675/common.yaml index 5b4e04b317..31a4e7035a 100644 --- a/tests/components/max6675/common.yaml +++ b/tests/components/max6675/common.yaml @@ -1,9 +1,3 @@ -spi: - - id: spi_max6675 - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - miso_pin: ${miso_pin} - sensor: - platform: max6675 name: Temperature diff --git a/tests/components/max6675/test.esp32-ard.yaml b/tests/components/max6675/test.esp32-ard.yaml deleted file mode 100644 index 54e027a614..0000000000 --- a/tests/components/max6675/test.esp32-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 - cs_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/max6675/test.esp32-c3-ard.yaml b/tests/components/max6675/test.esp32-c3-ard.yaml deleted file mode 100644 index 2415ba5dc6..0000000000 --- a/tests/components/max6675/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/max6675/test.esp32-c3-idf.yaml b/tests/components/max6675/test.esp32-c3-idf.yaml deleted file mode 100644 index 2415ba5dc6..0000000000 --- a/tests/components/max6675/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/max6675/test.esp32-idf.yaml b/tests/components/max6675/test.esp32-idf.yaml index 54e027a614..a3352cf880 100644 --- a/tests/components/max6675/test.esp32-idf.yaml +++ b/tests/components/max6675/test.esp32-idf.yaml @@ -1,7 +1,7 @@ substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 cs_pin: GPIO5 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/max6675/test.esp8266-ard.yaml b/tests/components/max6675/test.esp8266-ard.yaml index dbd158d030..b4673ba8b7 100644 --- a/tests/components/max6675/test.esp8266-ard.yaml +++ b/tests/components/max6675/test.esp8266-ard.yaml @@ -1,7 +1,10 @@ substitutions: - clk_pin: GPIO14 - mosi_pin: GPIO13 - miso_pin: GPIO12 + clk_pin: GPIO0 + mosi_pin: GPIO2 + miso_pin: GPIO16 cs_pin: GPIO15 +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/max6675/test.rp2040-ard.yaml b/tests/components/max6675/test.rp2040-ard.yaml index f6c3f1eeca..1ded24de1c 100644 --- a/tests/components/max6675/test.rp2040-ard.yaml +++ b/tests/components/max6675/test.rp2040-ard.yaml @@ -4,4 +4,7 @@ substitutions: miso_pin: GPIO4 cs_pin: GPIO5 +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/max6956/common.yaml b/tests/components/max6956/common.yaml index e44e3464f8..665a606027 100644 --- a/tests/components/max6956/common.yaml +++ b/tests/components/max6956/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_max6956 - scl: ${scl_pin} - sda: ${sda_pin} - max6956: - id: max6956_1 + i2c_id: i2c_bus address: 0x40 binary_sensor: diff --git a/tests/components/max6956/test.esp32-ard.yaml b/tests/components/max6956/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/max6956/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/max6956/test.esp32-c3-ard.yaml b/tests/components/max6956/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/max6956/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/max6956/test.esp32-c3-idf.yaml b/tests/components/max6956/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/max6956/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/max6956/test.esp32-idf.yaml b/tests/components/max6956/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/max6956/test.esp32-idf.yaml +++ b/tests/components/max6956/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/max6956/test.esp8266-ard.yaml b/tests/components/max6956/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/max6956/test.esp8266-ard.yaml +++ b/tests/components/max6956/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/max6956/test.rp2040-ard.yaml b/tests/components/max6956/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/max6956/test.rp2040-ard.yaml +++ b/tests/components/max6956/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/max7219/common.yaml b/tests/components/max7219/common.yaml index 0060db191e..5d7f54af17 100644 --- a/tests/components/max7219/common.yaml +++ b/tests/components/max7219/common.yaml @@ -1,9 +1,3 @@ -spi: - - id: spi_max6675 - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - miso_pin: ${miso_pin} - display: - platform: max7219 cs_pin: ${cs_pin} diff --git a/tests/components/max7219/test.esp32-ard.yaml b/tests/components/max7219/test.esp32-ard.yaml deleted file mode 100644 index 54e027a614..0000000000 --- a/tests/components/max7219/test.esp32-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 - cs_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/max7219/test.esp32-c3-ard.yaml b/tests/components/max7219/test.esp32-c3-ard.yaml deleted file mode 100644 index 2415ba5dc6..0000000000 --- a/tests/components/max7219/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/max7219/test.esp32-c3-idf.yaml b/tests/components/max7219/test.esp32-c3-idf.yaml deleted file mode 100644 index 2415ba5dc6..0000000000 --- a/tests/components/max7219/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/max7219/test.esp32-idf.yaml b/tests/components/max7219/test.esp32-idf.yaml index 54e027a614..a3352cf880 100644 --- a/tests/components/max7219/test.esp32-idf.yaml +++ b/tests/components/max7219/test.esp32-idf.yaml @@ -1,7 +1,7 @@ substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 cs_pin: GPIO5 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/max7219/test.esp8266-ard.yaml b/tests/components/max7219/test.esp8266-ard.yaml index dbd158d030..b4673ba8b7 100644 --- a/tests/components/max7219/test.esp8266-ard.yaml +++ b/tests/components/max7219/test.esp8266-ard.yaml @@ -1,7 +1,10 @@ substitutions: - clk_pin: GPIO14 - mosi_pin: GPIO13 - miso_pin: GPIO12 + clk_pin: GPIO0 + mosi_pin: GPIO2 + miso_pin: GPIO16 cs_pin: GPIO15 +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/max7219/test.rp2040-ard.yaml b/tests/components/max7219/test.rp2040-ard.yaml index f6c3f1eeca..1ded24de1c 100644 --- a/tests/components/max7219/test.rp2040-ard.yaml +++ b/tests/components/max7219/test.rp2040-ard.yaml @@ -4,4 +4,7 @@ substitutions: miso_pin: GPIO4 cs_pin: GPIO5 +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/max7219digit/common.yaml b/tests/components/max7219digit/common.yaml index 84edc7eb3d..525b7b8d3e 100644 --- a/tests/components/max7219digit/common.yaml +++ b/tests/components/max7219digit/common.yaml @@ -1,9 +1,3 @@ -spi: - - id: spi_max7219digit - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - miso_pin: ${miso_pin} - display: - platform: max7219digit cs_pin: ${cs_pin} diff --git a/tests/components/max7219digit/test.esp32-ard.yaml b/tests/components/max7219digit/test.esp32-ard.yaml deleted file mode 100644 index 54e027a614..0000000000 --- a/tests/components/max7219digit/test.esp32-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 - cs_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/max7219digit/test.esp32-c3-ard.yaml b/tests/components/max7219digit/test.esp32-c3-ard.yaml deleted file mode 100644 index 2415ba5dc6..0000000000 --- a/tests/components/max7219digit/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/max7219digit/test.esp32-c3-idf.yaml b/tests/components/max7219digit/test.esp32-c3-idf.yaml deleted file mode 100644 index 2415ba5dc6..0000000000 --- a/tests/components/max7219digit/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/max7219digit/test.esp32-idf.yaml b/tests/components/max7219digit/test.esp32-idf.yaml index 54e027a614..a3352cf880 100644 --- a/tests/components/max7219digit/test.esp32-idf.yaml +++ b/tests/components/max7219digit/test.esp32-idf.yaml @@ -1,7 +1,7 @@ substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 cs_pin: GPIO5 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/max7219digit/test.esp8266-ard.yaml b/tests/components/max7219digit/test.esp8266-ard.yaml index dbd158d030..b4673ba8b7 100644 --- a/tests/components/max7219digit/test.esp8266-ard.yaml +++ b/tests/components/max7219digit/test.esp8266-ard.yaml @@ -1,7 +1,10 @@ substitutions: - clk_pin: GPIO14 - mosi_pin: GPIO13 - miso_pin: GPIO12 + clk_pin: GPIO0 + mosi_pin: GPIO2 + miso_pin: GPIO16 cs_pin: GPIO15 +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/max7219digit/test.rp2040-ard.yaml b/tests/components/max7219digit/test.rp2040-ard.yaml index f6c3f1eeca..1ded24de1c 100644 --- a/tests/components/max7219digit/test.rp2040-ard.yaml +++ b/tests/components/max7219digit/test.rp2040-ard.yaml @@ -4,4 +4,7 @@ substitutions: miso_pin: GPIO4 cs_pin: GPIO5 +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/max9611/common.yaml b/tests/components/max9611/common.yaml index c3c00fdf85..ca9ee59038 100644 --- a/tests/components/max9611/common.yaml +++ b/tests/components/max9611/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_max9611 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: max9611 + i2c_id: i2c_bus shunt_resistance: 0.2 ohm gain: 1X voltage: diff --git a/tests/components/max9611/test.esp32-ard.yaml b/tests/components/max9611/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/max9611/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/max9611/test.esp32-c3-ard.yaml b/tests/components/max9611/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/max9611/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/max9611/test.esp32-c3-idf.yaml b/tests/components/max9611/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/max9611/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/max9611/test.esp32-idf.yaml b/tests/components/max9611/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/max9611/test.esp32-idf.yaml +++ b/tests/components/max9611/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/max9611/test.esp8266-ard.yaml b/tests/components/max9611/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/max9611/test.esp8266-ard.yaml +++ b/tests/components/max9611/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/max9611/test.rp2040-ard.yaml b/tests/components/max9611/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/max9611/test.rp2040-ard.yaml +++ b/tests/components/max9611/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/mcp23008/common.yaml b/tests/components/mcp23008/common.yaml index 1954766d25..4a407adfd8 100644 --- a/tests/components/mcp23008/common.yaml +++ b/tests/components/mcp23008/common.yaml @@ -1,9 +1,5 @@ -i2c: - - id: i2c_mcp23008 - scl: ${scl_pin} - sda: ${sda_pin} - mcp23008: + i2c_id: i2c_bus id: mcp23008_hub binary_sensor: diff --git a/tests/components/mcp23008/test.esp32-ard.yaml b/tests/components/mcp23008/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/mcp23008/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/mcp23008/test.esp32-c3-ard.yaml b/tests/components/mcp23008/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/mcp23008/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/mcp23008/test.esp32-c3-idf.yaml b/tests/components/mcp23008/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/mcp23008/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/mcp23008/test.esp32-idf.yaml b/tests/components/mcp23008/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/mcp23008/test.esp32-idf.yaml +++ b/tests/components/mcp23008/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/mcp23008/test.esp8266-ard.yaml b/tests/components/mcp23008/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/mcp23008/test.esp8266-ard.yaml +++ b/tests/components/mcp23008/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/mcp23008/test.rp2040-ard.yaml b/tests/components/mcp23008/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/mcp23008/test.rp2040-ard.yaml +++ b/tests/components/mcp23008/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/mcp23016/common.yaml b/tests/components/mcp23016/common.yaml index 109cb34b21..e8e3ad9d08 100644 --- a/tests/components/mcp23016/common.yaml +++ b/tests/components/mcp23016/common.yaml @@ -1,9 +1,5 @@ -i2c: - - id: i2c_mcp23016 - scl: ${scl_pin} - sda: ${sda_pin} - mcp23016: + i2c_id: i2c_bus id: mcp23016_hub binary_sensor: diff --git a/tests/components/mcp23016/test.esp32-ard.yaml b/tests/components/mcp23016/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/mcp23016/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/mcp23016/test.esp32-c3-ard.yaml b/tests/components/mcp23016/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/mcp23016/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/mcp23016/test.esp32-c3-idf.yaml b/tests/components/mcp23016/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/mcp23016/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/mcp23016/test.esp32-idf.yaml b/tests/components/mcp23016/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/mcp23016/test.esp32-idf.yaml +++ b/tests/components/mcp23016/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/mcp23016/test.esp8266-ard.yaml b/tests/components/mcp23016/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/mcp23016/test.esp8266-ard.yaml +++ b/tests/components/mcp23016/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/mcp23016/test.rp2040-ard.yaml b/tests/components/mcp23016/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/mcp23016/test.rp2040-ard.yaml +++ b/tests/components/mcp23016/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/mcp23017/common.yaml b/tests/components/mcp23017/common.yaml index 74949bba76..54a97e911f 100644 --- a/tests/components/mcp23017/common.yaml +++ b/tests/components/mcp23017/common.yaml @@ -1,9 +1,5 @@ -i2c: - - id: i2c_mcp23017 - scl: ${scl_pin} - sda: ${sda_pin} - mcp23017: + i2c_id: i2c_bus id: mcp23017_hub binary_sensor: diff --git a/tests/components/mcp23017/test.esp32-ard.yaml b/tests/components/mcp23017/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/mcp23017/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/mcp23017/test.esp32-c3-ard.yaml b/tests/components/mcp23017/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/mcp23017/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/mcp23017/test.esp32-c3-idf.yaml b/tests/components/mcp23017/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/mcp23017/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/mcp23017/test.esp32-idf.yaml b/tests/components/mcp23017/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/mcp23017/test.esp32-idf.yaml +++ b/tests/components/mcp23017/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/mcp23017/test.esp8266-ard.yaml b/tests/components/mcp23017/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/mcp23017/test.esp8266-ard.yaml +++ b/tests/components/mcp23017/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/mcp23017/test.rp2040-ard.yaml b/tests/components/mcp23017/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/mcp23017/test.rp2040-ard.yaml +++ b/tests/components/mcp23017/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/mcp23s08/common.yaml b/tests/components/mcp23s08/common.yaml index b89088fe15..2170ae0459 100644 --- a/tests/components/mcp23s08/common.yaml +++ b/tests/components/mcp23s08/common.yaml @@ -1,9 +1,3 @@ -spi: - - id: spi_mcp23s08 - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - miso_pin: ${miso_pin} - mcp23s08: - id: mcp23s08_hub cs_pin: ${cs_pin} diff --git a/tests/components/mcp23s08/test.esp32-ard.yaml b/tests/components/mcp23s08/test.esp32-ard.yaml deleted file mode 100644 index 54e027a614..0000000000 --- a/tests/components/mcp23s08/test.esp32-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 - cs_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/mcp23s08/test.esp32-c3-ard.yaml b/tests/components/mcp23s08/test.esp32-c3-ard.yaml deleted file mode 100644 index 2415ba5dc6..0000000000 --- a/tests/components/mcp23s08/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/mcp23s08/test.esp32-c3-idf.yaml b/tests/components/mcp23s08/test.esp32-c3-idf.yaml deleted file mode 100644 index 2415ba5dc6..0000000000 --- a/tests/components/mcp23s08/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/mcp23s08/test.esp32-idf.yaml b/tests/components/mcp23s08/test.esp32-idf.yaml index 54e027a614..a3352cf880 100644 --- a/tests/components/mcp23s08/test.esp32-idf.yaml +++ b/tests/components/mcp23s08/test.esp32-idf.yaml @@ -1,7 +1,7 @@ substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 cs_pin: GPIO5 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/mcp23s08/test.esp8266-ard.yaml b/tests/components/mcp23s08/test.esp8266-ard.yaml index dbd158d030..595f31046a 100644 --- a/tests/components/mcp23s08/test.esp8266-ard.yaml +++ b/tests/components/mcp23s08/test.esp8266-ard.yaml @@ -1,7 +1,7 @@ substitutions: - clk_pin: GPIO14 - mosi_pin: GPIO13 - miso_pin: GPIO12 cs_pin: GPIO15 +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/mcp23s08/test.rp2040-ard.yaml b/tests/components/mcp23s08/test.rp2040-ard.yaml index f6c3f1eeca..79ea6ce90b 100644 --- a/tests/components/mcp23s08/test.rp2040-ard.yaml +++ b/tests/components/mcp23s08/test.rp2040-ard.yaml @@ -1,7 +1,7 @@ substitutions: - clk_pin: GPIO2 - mosi_pin: GPIO3 - miso_pin: GPIO4 cs_pin: GPIO5 +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/mcp23s17/common.yaml b/tests/components/mcp23s17/common.yaml index 3fb27ef625..a89beeb16b 100644 --- a/tests/components/mcp23s17/common.yaml +++ b/tests/components/mcp23s17/common.yaml @@ -1,9 +1,3 @@ -spi: - - id: spi_mcp23s17 - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - miso_pin: ${miso_pin} - mcp23s17: - id: mcp23s17_hub cs_pin: ${cs_pin} diff --git a/tests/components/mcp23s17/test.esp32-ard.yaml b/tests/components/mcp23s17/test.esp32-ard.yaml deleted file mode 100644 index 54e027a614..0000000000 --- a/tests/components/mcp23s17/test.esp32-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 - cs_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/mcp23s17/test.esp32-c3-ard.yaml b/tests/components/mcp23s17/test.esp32-c3-ard.yaml deleted file mode 100644 index 2415ba5dc6..0000000000 --- a/tests/components/mcp23s17/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/mcp23s17/test.esp32-c3-idf.yaml b/tests/components/mcp23s17/test.esp32-c3-idf.yaml deleted file mode 100644 index 2415ba5dc6..0000000000 --- a/tests/components/mcp23s17/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/mcp23s17/test.esp32-idf.yaml b/tests/components/mcp23s17/test.esp32-idf.yaml index 54e027a614..a3352cf880 100644 --- a/tests/components/mcp23s17/test.esp32-idf.yaml +++ b/tests/components/mcp23s17/test.esp32-idf.yaml @@ -1,7 +1,7 @@ substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 cs_pin: GPIO5 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/mcp23s17/test.esp8266-ard.yaml b/tests/components/mcp23s17/test.esp8266-ard.yaml index dbd158d030..595f31046a 100644 --- a/tests/components/mcp23s17/test.esp8266-ard.yaml +++ b/tests/components/mcp23s17/test.esp8266-ard.yaml @@ -1,7 +1,7 @@ substitutions: - clk_pin: GPIO14 - mosi_pin: GPIO13 - miso_pin: GPIO12 cs_pin: GPIO15 +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/mcp23s17/test.rp2040-ard.yaml b/tests/components/mcp23s17/test.rp2040-ard.yaml index f6c3f1eeca..79ea6ce90b 100644 --- a/tests/components/mcp23s17/test.rp2040-ard.yaml +++ b/tests/components/mcp23s17/test.rp2040-ard.yaml @@ -1,7 +1,7 @@ substitutions: - clk_pin: GPIO2 - mosi_pin: GPIO3 - miso_pin: GPIO4 cs_pin: GPIO5 +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/mcp2515/common.yaml b/tests/components/mcp2515/common.yaml index 96a72a3ec3..15639df5fe 100644 --- a/tests/components/mcp2515/common.yaml +++ b/tests/components/mcp2515/common.yaml @@ -1,9 +1,3 @@ -spi: - - id: spi_mcp2515 - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - miso_pin: ${miso_pin} - canbus: - platform: mcp2515 id: mcp2515_can diff --git a/tests/components/mcp2515/test.esp32-ard.yaml b/tests/components/mcp2515/test.esp32-ard.yaml deleted file mode 100644 index 54e027a614..0000000000 --- a/tests/components/mcp2515/test.esp32-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 - cs_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/mcp2515/test.esp32-c3-ard.yaml b/tests/components/mcp2515/test.esp32-c3-ard.yaml deleted file mode 100644 index 2415ba5dc6..0000000000 --- a/tests/components/mcp2515/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/mcp2515/test.esp32-c3-idf.yaml b/tests/components/mcp2515/test.esp32-c3-idf.yaml deleted file mode 100644 index 2415ba5dc6..0000000000 --- a/tests/components/mcp2515/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/mcp2515/test.esp32-idf.yaml b/tests/components/mcp2515/test.esp32-idf.yaml index 54e027a614..a3352cf880 100644 --- a/tests/components/mcp2515/test.esp32-idf.yaml +++ b/tests/components/mcp2515/test.esp32-idf.yaml @@ -1,7 +1,7 @@ substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 cs_pin: GPIO5 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/mcp2515/test.esp8266-ard.yaml b/tests/components/mcp2515/test.esp8266-ard.yaml index dbd158d030..b4673ba8b7 100644 --- a/tests/components/mcp2515/test.esp8266-ard.yaml +++ b/tests/components/mcp2515/test.esp8266-ard.yaml @@ -1,7 +1,10 @@ substitutions: - clk_pin: GPIO14 - mosi_pin: GPIO13 - miso_pin: GPIO12 + clk_pin: GPIO0 + mosi_pin: GPIO2 + miso_pin: GPIO16 cs_pin: GPIO15 +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/mcp2515/test.rp2040-ard.yaml b/tests/components/mcp2515/test.rp2040-ard.yaml index f6c3f1eeca..1ded24de1c 100644 --- a/tests/components/mcp2515/test.rp2040-ard.yaml +++ b/tests/components/mcp2515/test.rp2040-ard.yaml @@ -4,4 +4,7 @@ substitutions: miso_pin: GPIO4 cs_pin: GPIO5 +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/mcp3008/common.yaml b/tests/components/mcp3008/common.yaml index 646d3a20e9..57d0155f73 100644 --- a/tests/components/mcp3008/common.yaml +++ b/tests/components/mcp3008/common.yaml @@ -1,9 +1,3 @@ -spi: - - id: spi_mcp3008 - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - miso_pin: ${miso_pin} - mcp3008: - id: mcp3008_hub cs_pin: ${cs_pin} diff --git a/tests/components/mcp3008/test.esp32-ard.yaml b/tests/components/mcp3008/test.esp32-ard.yaml deleted file mode 100644 index 54e027a614..0000000000 --- a/tests/components/mcp3008/test.esp32-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 - cs_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/mcp3008/test.esp32-c3-ard.yaml b/tests/components/mcp3008/test.esp32-c3-ard.yaml deleted file mode 100644 index 2415ba5dc6..0000000000 --- a/tests/components/mcp3008/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/mcp3008/test.esp32-c3-idf.yaml b/tests/components/mcp3008/test.esp32-c3-idf.yaml deleted file mode 100644 index 2415ba5dc6..0000000000 --- a/tests/components/mcp3008/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/mcp3008/test.esp32-idf.yaml b/tests/components/mcp3008/test.esp32-idf.yaml index 54e027a614..a3352cf880 100644 --- a/tests/components/mcp3008/test.esp32-idf.yaml +++ b/tests/components/mcp3008/test.esp32-idf.yaml @@ -1,7 +1,7 @@ substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 cs_pin: GPIO5 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/mcp3008/test.esp8266-ard.yaml b/tests/components/mcp3008/test.esp8266-ard.yaml index dbd158d030..b4673ba8b7 100644 --- a/tests/components/mcp3008/test.esp8266-ard.yaml +++ b/tests/components/mcp3008/test.esp8266-ard.yaml @@ -1,7 +1,10 @@ substitutions: - clk_pin: GPIO14 - mosi_pin: GPIO13 - miso_pin: GPIO12 + clk_pin: GPIO0 + mosi_pin: GPIO2 + miso_pin: GPIO16 cs_pin: GPIO15 +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/mcp3008/test.rp2040-ard.yaml b/tests/components/mcp3008/test.rp2040-ard.yaml index f6c3f1eeca..1ded24de1c 100644 --- a/tests/components/mcp3008/test.rp2040-ard.yaml +++ b/tests/components/mcp3008/test.rp2040-ard.yaml @@ -4,4 +4,7 @@ substitutions: miso_pin: GPIO4 cs_pin: GPIO5 +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/mcp3204/common.yaml b/tests/components/mcp3204/common.yaml index f102500c81..9750f0af8e 100644 --- a/tests/components/mcp3204/common.yaml +++ b/tests/components/mcp3204/common.yaml @@ -1,16 +1,24 @@ -spi: - - id: spi_mcp3204 - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - miso_pin: ${miso_pin} - mcp3204: - id: mcp3204_hub cs_pin: ${cs_pin} sensor: - platform: mcp3204 - id: mcp3204_sensor + id: mcp3204_default_single_0 mcp3204_id: mcp3204_hub number: 0 update_interval: 5s + + - platform: mcp3204 + id: mcp3204_single_0 + mcp3204_id: mcp3204_hub + number: 0 + diff_mode: false + update_interval: 5s + + - platform: mcp3204 + id: mcp3204_diff_0_1 + mcp3204_id: mcp3204_hub + number: 0 + diff_mode: true + update_interval: 5s diff --git a/tests/components/mcp3204/test.esp32-ard.yaml b/tests/components/mcp3204/test.esp32-ard.yaml deleted file mode 100644 index 54e027a614..0000000000 --- a/tests/components/mcp3204/test.esp32-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 - cs_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/mcp3204/test.esp32-c3-ard.yaml b/tests/components/mcp3204/test.esp32-c3-ard.yaml deleted file mode 100644 index 2415ba5dc6..0000000000 --- a/tests/components/mcp3204/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/mcp3204/test.esp32-c3-idf.yaml b/tests/components/mcp3204/test.esp32-c3-idf.yaml deleted file mode 100644 index 2415ba5dc6..0000000000 --- a/tests/components/mcp3204/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/mcp3204/test.esp32-idf.yaml b/tests/components/mcp3204/test.esp32-idf.yaml index 54e027a614..a3352cf880 100644 --- a/tests/components/mcp3204/test.esp32-idf.yaml +++ b/tests/components/mcp3204/test.esp32-idf.yaml @@ -1,7 +1,7 @@ substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 cs_pin: GPIO5 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/mcp3204/test.esp8266-ard.yaml b/tests/components/mcp3204/test.esp8266-ard.yaml index dbd158d030..b4673ba8b7 100644 --- a/tests/components/mcp3204/test.esp8266-ard.yaml +++ b/tests/components/mcp3204/test.esp8266-ard.yaml @@ -1,7 +1,10 @@ substitutions: - clk_pin: GPIO14 - mosi_pin: GPIO13 - miso_pin: GPIO12 + clk_pin: GPIO0 + mosi_pin: GPIO2 + miso_pin: GPIO16 cs_pin: GPIO15 +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/mcp3204/test.rp2040-ard.yaml b/tests/components/mcp3204/test.rp2040-ard.yaml index f6c3f1eeca..1ded24de1c 100644 --- a/tests/components/mcp3204/test.rp2040-ard.yaml +++ b/tests/components/mcp3204/test.rp2040-ard.yaml @@ -4,4 +4,7 @@ substitutions: miso_pin: GPIO4 cs_pin: GPIO5 +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/mcp3221/common.yaml b/tests/components/mcp3221/common.yaml new file mode 100644 index 0000000000..cc3eadbf4f --- /dev/null +++ b/tests/components/mcp3221/common.yaml @@ -0,0 +1,6 @@ +sensor: + - platform: mcp3221 + id: test_id + name: voltage + i2c_id: i2c_bus + reference_voltage: 3.3V diff --git a/tests/components/mcp3221/test.esp32-c3-idf.yaml b/tests/components/mcp3221/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..9990d96d29 --- /dev/null +++ b/tests/components/mcp3221/test.esp32-c3-idf.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/mcp3221/test.esp32-idf.yaml b/tests/components/mcp3221/test.esp32-idf.yaml new file mode 100644 index 0000000000..b47e39c389 --- /dev/null +++ b/tests/components/mcp3221/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/mcp3221/test.esp8266-ard.yaml b/tests/components/mcp3221/test.esp8266-ard.yaml new file mode 100644 index 0000000000..4a98b9388a --- /dev/null +++ b/tests/components/mcp3221/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/mcp3221/test.rp2040-ard.yaml b/tests/components/mcp3221/test.rp2040-ard.yaml new file mode 100644 index 0000000000..319a7c71a6 --- /dev/null +++ b/tests/components/mcp3221/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/mcp4461/common.yaml b/tests/components/mcp4461/common.yaml index ce1866fdb8..92fd789dcb 100644 --- a/tests/components/mcp4461/common.yaml +++ b/tests/components/mcp4461/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_mcp4461 - sda: ${sda_pin} - scl: ${scl_pin} - mcp4461: - id: mcp4461_digipot_01 + i2c_id: i2c_bus output: - platform: mcp4461 diff --git a/tests/components/mcp4461/test.esp32-ard.yaml b/tests/components/mcp4461/test.esp32-ard.yaml deleted file mode 100644 index c5deb7ca0a..0000000000 --- a/tests/components/mcp4461/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - sda_pin: GPIO16 - scl_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/mcp4461/test.esp32-c3-ard.yaml b/tests/components/mcp4461/test.esp32-c3-ard.yaml deleted file mode 100644 index a87353b78b..0000000000 --- a/tests/components/mcp4461/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - sda_pin: GPIO4 - scl_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/mcp4461/test.esp32-c3-idf.yaml b/tests/components/mcp4461/test.esp32-c3-idf.yaml deleted file mode 100644 index a87353b78b..0000000000 --- a/tests/components/mcp4461/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - sda_pin: GPIO4 - scl_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/mcp4461/test.esp32-idf.yaml b/tests/components/mcp4461/test.esp32-idf.yaml index c5deb7ca0a..b47e39c389 100644 --- a/tests/components/mcp4461/test.esp32-idf.yaml +++ b/tests/components/mcp4461/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - sda_pin: GPIO16 - scl_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/mcp4461/test.esp8266-ard.yaml b/tests/components/mcp4461/test.esp8266-ard.yaml index a87353b78b..4a98b9388a 100644 --- a/tests/components/mcp4461/test.esp8266-ard.yaml +++ b/tests/components/mcp4461/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - sda_pin: GPIO4 - scl_pin: GPIO5 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/mcp4725/common.yaml b/tests/components/mcp4725/common.yaml index 0ccc649f2e..9352ebfd19 100644 --- a/tests/components/mcp4725/common.yaml +++ b/tests/components/mcp4725/common.yaml @@ -1,8 +1,4 @@ -i2c: - - id: i2c_mcp4725 - scl: ${scl_pin} - sda: ${sda_pin} - output: - platform: mcp4725 id: mcp4725_dac_output + i2c_id: i2c_bus diff --git a/tests/components/mcp4725/test.esp32-ard.yaml b/tests/components/mcp4725/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/mcp4725/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/mcp4725/test.esp32-c3-ard.yaml b/tests/components/mcp4725/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/mcp4725/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/mcp4725/test.esp32-c3-idf.yaml b/tests/components/mcp4725/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/mcp4725/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/mcp4725/test.esp32-idf.yaml b/tests/components/mcp4725/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/mcp4725/test.esp32-idf.yaml +++ b/tests/components/mcp4725/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/mcp4725/test.esp8266-ard.yaml b/tests/components/mcp4725/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/mcp4725/test.esp8266-ard.yaml +++ b/tests/components/mcp4725/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/mcp4725/test.rp2040-ard.yaml b/tests/components/mcp4725/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/mcp4725/test.rp2040-ard.yaml +++ b/tests/components/mcp4725/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/mcp4728/common.yaml b/tests/components/mcp4728/common.yaml index b42818e4e6..e60f4795e1 100644 --- a/tests/components/mcp4728/common.yaml +++ b/tests/components/mcp4728/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_mcp4728 - scl: ${scl_pin} - sda: ${sda_pin} - mcp4728: - id: mcp4728_dac + i2c_id: i2c_bus output: - platform: mcp4728 diff --git a/tests/components/mcp4728/test.esp32-ard.yaml b/tests/components/mcp4728/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/mcp4728/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/mcp4728/test.esp32-c3-ard.yaml b/tests/components/mcp4728/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/mcp4728/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/mcp4728/test.esp32-c3-idf.yaml b/tests/components/mcp4728/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/mcp4728/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/mcp4728/test.esp32-idf.yaml b/tests/components/mcp4728/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/mcp4728/test.esp32-idf.yaml +++ b/tests/components/mcp4728/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/mcp4728/test.esp8266-ard.yaml b/tests/components/mcp4728/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/mcp4728/test.esp8266-ard.yaml +++ b/tests/components/mcp4728/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/mcp4728/test.rp2040-ard.yaml b/tests/components/mcp4728/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/mcp4728/test.rp2040-ard.yaml +++ b/tests/components/mcp4728/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/mcp47a1/common.yaml b/tests/components/mcp47a1/common.yaml index 59e28d37b3..5a5786ff0f 100644 --- a/tests/components/mcp47a1/common.yaml +++ b/tests/components/mcp47a1/common.yaml @@ -1,8 +1,4 @@ -i2c: - - id: i2c_mcp47a1 - scl: ${scl_pin} - sda: ${sda_pin} - output: - platform: mcp47a1 id: output_mcp47a1 + i2c_id: i2c_bus diff --git a/tests/components/mcp47a1/test.esp32-ard.yaml b/tests/components/mcp47a1/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/mcp47a1/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/mcp47a1/test.esp32-c3-ard.yaml b/tests/components/mcp47a1/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/mcp47a1/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/mcp47a1/test.esp32-c3-idf.yaml b/tests/components/mcp47a1/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/mcp47a1/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/mcp47a1/test.esp32-idf.yaml b/tests/components/mcp47a1/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/mcp47a1/test.esp32-idf.yaml +++ b/tests/components/mcp47a1/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/mcp47a1/test.esp8266-ard.yaml b/tests/components/mcp47a1/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/mcp47a1/test.esp8266-ard.yaml +++ b/tests/components/mcp47a1/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/mcp47a1/test.rp2040-ard.yaml b/tests/components/mcp47a1/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/mcp47a1/test.rp2040-ard.yaml +++ b/tests/components/mcp47a1/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/mcp9600/common.yaml b/tests/components/mcp9600/common.yaml index e3c9df79e4..33e33d183e 100644 --- a/tests/components/mcp9600/common.yaml +++ b/tests/components/mcp9600/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_mcp9600 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: mcp9600 + i2c_id: i2c_bus thermocouple_type: K hot_junction: name: Thermocouple Temperature diff --git a/tests/components/mcp9600/test.esp32-ard.yaml b/tests/components/mcp9600/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/mcp9600/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/mcp9600/test.esp32-c3-ard.yaml b/tests/components/mcp9600/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/mcp9600/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/mcp9600/test.esp32-c3-idf.yaml b/tests/components/mcp9600/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/mcp9600/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/mcp9600/test.esp32-idf.yaml b/tests/components/mcp9600/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/mcp9600/test.esp32-idf.yaml +++ b/tests/components/mcp9600/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/mcp9600/test.esp8266-ard.yaml b/tests/components/mcp9600/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/mcp9600/test.esp8266-ard.yaml +++ b/tests/components/mcp9600/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/mcp9600/test.rp2040-ard.yaml b/tests/components/mcp9600/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/mcp9600/test.rp2040-ard.yaml +++ b/tests/components/mcp9600/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/mcp9808/common.yaml b/tests/components/mcp9808/common.yaml index ccfd5d13ce..86d0661552 100644 --- a/tests/components/mcp9808/common.yaml +++ b/tests/components/mcp9808/common.yaml @@ -1,8 +1,4 @@ -i2c: - - id: i2c_mcp9808 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: mcp9808 + i2c_id: i2c_bus name: MCP9808 Temperature diff --git a/tests/components/mcp9808/test.esp32-ard.yaml b/tests/components/mcp9808/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/mcp9808/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/mcp9808/test.esp32-c3-ard.yaml b/tests/components/mcp9808/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/mcp9808/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/mcp9808/test.esp32-c3-idf.yaml b/tests/components/mcp9808/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/mcp9808/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/mcp9808/test.esp32-idf.yaml b/tests/components/mcp9808/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/mcp9808/test.esp32-idf.yaml +++ b/tests/components/mcp9808/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/mcp9808/test.esp8266-ard.yaml b/tests/components/mcp9808/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/mcp9808/test.esp8266-ard.yaml +++ b/tests/components/mcp9808/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/mcp9808/test.rp2040-ard.yaml b/tests/components/mcp9808/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/mcp9808/test.rp2040-ard.yaml +++ b/tests/components/mcp9808/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/mdns/test-comprehensive.esp8266-ard.yaml b/tests/components/mdns/test-comprehensive.esp8266-ard.yaml new file mode 100644 index 0000000000..3129ca3143 --- /dev/null +++ b/tests/components/mdns/test-comprehensive.esp8266-ard.yaml @@ -0,0 +1,45 @@ +# Comprehensive ESP8266 test for mdns with multiple network components +# Tests the complete priority chain: +# wifi (60) -> mdns (55) -> ota (54) -> web_server_ota (52) + +esphome: + name: mdns-comprehensive-test + +esp8266: + board: esp01_1m + +logger: + level: DEBUG + +wifi: + ssid: MySSID + password: password1 + +# web_server_base should run at priority 65 (before wifi) +web_server: + port: 80 + +# mdns should run at priority 55 (after wifi at 60) +mdns: + services: + - service: _http + protocol: _tcp + port: 80 + txt: + version: "1.0" + path: "/" + +# OTA should run at priority 54 (after mdns) +ota: + - platform: esphome + password: "otapassword" + +# Test status LED at priority 80 +status_led: + pin: + number: GPIO2 + inverted: true + +# Include API at priority 40 +api: + password: "apipassword" diff --git a/tests/components/mdns/test-enabled.esp32-ard.yaml b/tests/components/mdns/test-enabled.esp32-ard.yaml deleted file mode 100644 index 97fd63d70e..0000000000 --- a/tests/components/mdns/test-enabled.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common-enabled.yaml diff --git a/tests/components/mdns/test-enabled.esp32-c3-ard.yaml b/tests/components/mdns/test-enabled.esp32-c3-ard.yaml deleted file mode 100644 index 97fd63d70e..0000000000 --- a/tests/components/mdns/test-enabled.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common-enabled.yaml diff --git a/tests/components/mdns/test-enabled.esp32-c3-idf.yaml b/tests/components/mdns/test-enabled.esp32-c3-idf.yaml deleted file mode 100644 index 97fd63d70e..0000000000 --- a/tests/components/mdns/test-enabled.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common-enabled.yaml diff --git a/tests/components/media_player/test.esp32-ard.yaml b/tests/components/media_player/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/media_player/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/mhz19/common.yaml b/tests/components/mhz19/common.yaml index 8b7e732068..94989fecbe 100644 --- a/tests/components/mhz19/common.yaml +++ b/tests/components/mhz19/common.yaml @@ -1,9 +1,3 @@ -uart: - - id: uart_mhz19 - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 9600 - sensor: - platform: mhz19 co2: diff --git a/tests/components/mhz19/test.esp32-ard.yaml b/tests/components/mhz19/test.esp32-ard.yaml deleted file mode 100644 index f486544afa..0000000000 --- a/tests/components/mhz19/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 - -<<: !include common.yaml diff --git a/tests/components/mhz19/test.esp32-c3-ard.yaml b/tests/components/mhz19/test.esp32-c3-ard.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/mhz19/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/mhz19/test.esp32-c3-idf.yaml b/tests/components/mhz19/test.esp32-c3-idf.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/mhz19/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/mhz19/test.esp32-idf.yaml b/tests/components/mhz19/test.esp32-idf.yaml index f486544afa..b415125e84 100644 --- a/tests/components/mhz19/test.esp32-idf.yaml +++ b/tests/components/mhz19/test.esp32-idf.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 + tx_pin: GPIO4 + rx_pin: GPIO5 + +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/mhz19/test.esp8266-ard.yaml b/tests/components/mhz19/test.esp8266-ard.yaml index b516342f3b..96ab4ef6ac 100644 --- a/tests/components/mhz19/test.esp8266-ard.yaml +++ b/tests/components/mhz19/test.esp8266-ard.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: GPIO0 + rx_pin: GPIO2 + +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/mhz19/test.rp2040-ard.yaml b/tests/components/mhz19/test.rp2040-ard.yaml index b516342f3b..b28f2b5e05 100644 --- a/tests/components/mhz19/test.rp2040-ard.yaml +++ b/tests/components/mhz19/test.rp2040-ard.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO4 rx_pin: GPIO5 +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/micronova/common.yaml b/tests/components/micronova/common.yaml index 661c9330c6..3cf8e36fb6 100644 --- a/tests/components/micronova/common.yaml +++ b/tests/components/micronova/common.yaml @@ -1,9 +1,3 @@ -uart: - - id: uart_micronova - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 9600 - micronova: enable_rx_pin: ${enable_rx_pin} diff --git a/tests/components/micronova/test.esp32-ard.yaml b/tests/components/micronova/test.esp32-ard.yaml deleted file mode 100644 index 35d041e047..0000000000 --- a/tests/components/micronova/test.esp32-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - tx_pin: GPIO12 - rx_pin: GPIO14 - enable_rx_pin: GPIO13 - -<<: !include common.yaml diff --git a/tests/components/micronova/test.esp32-c3-ard.yaml b/tests/components/micronova/test.esp32-c3-ard.yaml deleted file mode 100644 index 993071999f..0000000000 --- a/tests/components/micronova/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - enable_rx_pin: GPIO3 - -<<: !include common.yaml diff --git a/tests/components/micronova/test.esp32-c3-idf.yaml b/tests/components/micronova/test.esp32-c3-idf.yaml deleted file mode 100644 index 993071999f..0000000000 --- a/tests/components/micronova/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - enable_rx_pin: GPIO3 - -<<: !include common.yaml diff --git a/tests/components/micronova/test.esp32-idf.yaml b/tests/components/micronova/test.esp32-idf.yaml index 35d041e047..5cc3a234ca 100644 --- a/tests/components/micronova/test.esp32-idf.yaml +++ b/tests/components/micronova/test.esp32-idf.yaml @@ -1,6 +1,7 @@ substitutions: - tx_pin: GPIO12 - rx_pin: GPIO14 enable_rx_pin: GPIO13 +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/micronova/test.esp8266-ard.yaml b/tests/components/micronova/test.esp8266-ard.yaml index 048fb82d72..ffe1e0a063 100644 --- a/tests/components/micronova/test.esp8266-ard.yaml +++ b/tests/components/micronova/test.esp8266-ard.yaml @@ -1,6 +1,7 @@ substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - enable_rx_pin: GPIO13 + enable_rx_pin: GPIO15 + +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/micronova/test.rp2040-ard.yaml b/tests/components/micronova/test.rp2040-ard.yaml index 993071999f..6dc030e6b6 100644 --- a/tests/components/micronova/test.rp2040-ard.yaml +++ b/tests/components/micronova/test.rp2040-ard.yaml @@ -1,6 +1,7 @@ substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 enable_rx_pin: GPIO3 +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/microphone/test.esp32-ard.yaml b/tests/components/microphone/test.esp32-ard.yaml deleted file mode 100644 index 392df582cc..0000000000 --- a/tests/components/microphone/test.esp32-ard.yaml +++ /dev/null @@ -1,13 +0,0 @@ -substitutions: - i2s_bclk_pin: GPIO15 - i2s_lrclk_pin: GPIO16 - i2s_mclk_pin: GPIO17 - i2s_din_pin: GPIO33 - -<<: !include common.yaml - -microphone: - - platform: i2s_audio - id: mic_id_adc - adc_pin: 32 - adc_type: internal diff --git a/tests/components/microphone/test.esp32-c3-ard.yaml b/tests/components/microphone/test.esp32-c3-ard.yaml deleted file mode 100644 index c28dc553f5..0000000000 --- a/tests/components/microphone/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - i2s_bclk_pin: GPIO6 - i2s_lrclk_pin: GPIO7 - i2s_mclk_pin: GPIO8 - i2s_din_pin: GPIO3 - -<<: !include common.yaml diff --git a/tests/components/microphone/test.esp32-c3-idf.yaml b/tests/components/microphone/test.esp32-c3-idf.yaml deleted file mode 100644 index c28dc553f5..0000000000 --- a/tests/components/microphone/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - i2s_bclk_pin: GPIO6 - i2s_lrclk_pin: GPIO7 - i2s_mclk_pin: GPIO8 - i2s_din_pin: GPIO3 - -<<: !include common.yaml diff --git a/tests/components/microphone/test.esp32-idf.yaml b/tests/components/microphone/test.esp32-idf.yaml index fe9feb9888..830f0156d7 100644 --- a/tests/components/microphone/test.esp32-idf.yaml +++ b/tests/components/microphone/test.esp32-idf.yaml @@ -1,7 +1,7 @@ substitutions: i2s_bclk_pin: GPIO15 - i2s_lrclk_pin: GPIO16 - i2s_mclk_pin: GPIO17 + i2s_lrclk_pin: GPIO4 + i2s_mclk_pin: GPIO5 i2s_din_pin: GPIO33 i2s_audio: diff --git a/tests/components/mics_4514/common.yaml b/tests/components/mics_4514/common.yaml index 0bc3f3e654..3c1d264680 100644 --- a/tests/components/mics_4514/common.yaml +++ b/tests/components/mics_4514/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_mics_4514 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: mics_4514 + i2c_id: i2c_bus update_interval: 60s nitrogen_dioxide: name: MICS-4514 NO2 diff --git a/tests/components/mics_4514/test.esp32-ard.yaml b/tests/components/mics_4514/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/mics_4514/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/mics_4514/test.esp32-c3-ard.yaml b/tests/components/mics_4514/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/mics_4514/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/mics_4514/test.esp32-c3-idf.yaml b/tests/components/mics_4514/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/mics_4514/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/mics_4514/test.esp32-idf.yaml b/tests/components/mics_4514/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/mics_4514/test.esp32-idf.yaml +++ b/tests/components/mics_4514/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/mics_4514/test.esp8266-ard.yaml b/tests/components/mics_4514/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/mics_4514/test.esp8266-ard.yaml +++ b/tests/components/mics_4514/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/mics_4514/test.rp2040-ard.yaml b/tests/components/mics_4514/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/mics_4514/test.rp2040-ard.yaml +++ b/tests/components/mics_4514/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/midea/common.yaml b/tests/components/midea/common.yaml index 07385e29a1..fec85aee96 100644 --- a/tests/components/midea/common.yaml +++ b/tests/components/midea/common.yaml @@ -2,16 +2,6 @@ wifi: ssid: MySSID password: password1 -remote_transmitter: - pin: ${pin} - carrier_duty_percent: 50% - -uart: - - id: uart_midea - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 9600 - climate: - platform: midea id: midea_unit @@ -22,7 +12,7 @@ climate: x.set_mode(CLIMATE_MODE_FAN_ONLY); on_state: - logger.log: State changed! - transmitter_id: + transmitter_id: xmitr period: 1s num_attempts: 5 timeout: 2s diff --git a/tests/components/midea/test.esp32-ard.yaml b/tests/components/midea/test.esp32-ard.yaml index 7f55d6a52d..1e3fe0ff51 100644 --- a/tests/components/midea/test.esp32-ard.yaml +++ b/tests/components/midea/test.esp32-ard.yaml @@ -1,6 +1,5 @@ -substitutions: - tx_pin: GPIO12 - rx_pin: GPIO14 - pin: GPIO2 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-ard.yaml + uart: !include ../../test_build_components/common/uart/esp32-ard.yaml <<: !include common.yaml diff --git a/tests/components/midea/test.esp32-c3-ard.yaml b/tests/components/midea/test.esp32-c3-ard.yaml deleted file mode 100644 index a879df3ca4..0000000000 --- a/tests/components/midea/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/midea/test.esp8266-ard.yaml b/tests/components/midea/test.esp8266-ard.yaml index 4f50bd7e5e..9825ff85a1 100644 --- a/tests/components/midea/test.esp8266-ard.yaml +++ b/tests/components/midea/test.esp8266-ard.yaml @@ -1,6 +1,5 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - pin: GPIO15 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/midea_ir/common.yaml b/tests/components/midea_ir/common.yaml index e8d89cecc2..e4cc4bb19c 100644 --- a/tests/components/midea_ir/common.yaml +++ b/tests/components/midea_ir/common.yaml @@ -1,8 +1,5 @@ -remote_transmitter: - pin: 4 - carrier_duty_percent: 50% - climate: - platform: midea_ir name: Midea IR use_fahrenheit: true + transmitter_id: xmitr diff --git a/tests/components/midea_ir/test.esp32-ard.yaml b/tests/components/midea_ir/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/midea_ir/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/midea_ir/test.esp32-c3-ard.yaml b/tests/components/midea_ir/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/midea_ir/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/midea_ir/test.esp32-c3-idf.yaml b/tests/components/midea_ir/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/midea_ir/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/midea_ir/test.esp32-idf.yaml b/tests/components/midea_ir/test.esp32-idf.yaml index dade44d145..e891f9dc85 100644 --- a/tests/components/midea_ir/test.esp32-idf.yaml +++ b/tests/components/midea_ir/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/midea_ir/test.esp8266-ard.yaml b/tests/components/midea_ir/test.esp8266-ard.yaml index dade44d145..4bed2f03e5 100644 --- a/tests/components/midea_ir/test.esp8266-ard.yaml +++ b/tests/components/midea_ir/test.esp8266-ard.yaml @@ -1 +1,4 @@ +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/mipi_dsi/test.esp32-p4-idf.yaml b/tests/components/mipi_dsi/test.esp32-p4-idf.yaml index 9c4eb07d9b..770b11d089 100644 --- a/tests/components/mipi_dsi/test.esp32-p4-idf.yaml +++ b/tests/components/mipi_dsi/test.esp32-p4-idf.yaml @@ -1,3 +1,6 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-p4-idf.yaml + esp_ldo: - id: ldo_id channel: 3 @@ -13,9 +16,3 @@ display: #id: backlight_id psram: - -i2c: - sda: GPIO7 - scl: GPIO8 - scan: true - frequency: 400kHz diff --git a/tests/components/mipi_rgb/test.esp32-s3-idf.yaml b/tests/components/mipi_rgb/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..642292f7c4 --- /dev/null +++ b/tests/components/mipi_rgb/test.esp32-s3-idf.yaml @@ -0,0 +1,61 @@ +packages: + spi: !include ../../test_build_components/common/spi/esp32-s3-idf.yaml + +psram: + mode: octal + +display: + - platform: mipi_rgb + spi_id: spi_bus + model: ZX2D10GE01R-V4848 + update_interval: 1s + color_order: BGR + draw_rounding: 2 + pixel_mode: 18bit + invert_colors: false + use_axis_flips: true + pclk_frequency: 15000000.0 + pclk_inverted: true + byte_order: big_endian + hsync_pulse_width: 10 + hsync_back_porch: 10 + hsync_front_porch: 10 + vsync_pulse_width: 2 + vsync_back_porch: 12 + vsync_front_porch: 14 + data_pins: + red: + - number: 10 + - number: 16 + - number: 9 + - number: 15 + - number: 46 + ignore_strapping_warning: true + green: + - number: 8 + - number: 13 + - number: 18 + - number: 12 + - number: 11 + - number: 17 + blue: + - number: 47 + - number: 1 + - number: 0 + ignore_strapping_warning: true + - number: 42 + - number: 14 + de_pin: + number: 39 + pclk_pin: + number: 45 + ignore_strapping_warning: true + hsync_pin: + number: 38 + vsync_pin: + number: 48 + data_rate: 1000000.0 + spi_mode: MODE0 + cs_pin: + number: 21 + show_test_card: true diff --git a/tests/components/mipi_spi/common.yaml b/tests/components/mipi_spi/common.yaml index 2c84489ec7..692a9f436e 100644 --- a/tests/components/mipi_spi/common.yaml +++ b/tests/components/mipi_spi/common.yaml @@ -1,11 +1,3 @@ -spi: - - id: spi_single - clk_pin: - number: ${clk_pin} - allow_other_uses: true - mosi_pin: - number: ${mosi_pin} - display: - platform: mipi_spi spi_16: true @@ -18,7 +10,7 @@ display: invert_colors: true show_test_card: true spi_mode: mode0 - draw_rounding: 8 + draw_rounding: 4 use_axis_flips: true init_sequence: - [0xd0, 1, 2, 3] @@ -30,8 +22,5 @@ display: dimensions: width: 100 height: 200 - enable_pin: - - number: ${clk_pin} - allow_other_uses: true - - number: ${enable_pin} + enable_pin: ${enable_pin} bus_mode: single diff --git a/tests/components/mipi_spi/test-lvgl.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-lvgl.esp32-s3-idf.yaml index e0f65a3a6a..14f864d326 100644 --- a/tests/components/mipi_spi/test-lvgl.esp32-s3-idf.yaml +++ b/tests/components/mipi_spi/test-lvgl.esp32-s3-idf.yaml @@ -1,18 +1,12 @@ -substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - -spi: - - id: spi_single - clk_pin: - number: ${clk_pin} - mosi_pin: - number: ${mosi_pin} +packages: + spi: !include ../../test_build_components/common/spi/esp32-s3-idf.yaml display: - platform: mipi_spi + spi_id: spi_bus model: t-display-s3-pro lvgl: psram: + mode: quad diff --git a/tests/components/mipi_spi/test.esp32-ard.yaml b/tests/components/mipi_spi/test.esp32-ard.yaml deleted file mode 100644 index a5ef77dabc..0000000000 --- a/tests/components/mipi_spi/test.esp32-ard.yaml +++ /dev/null @@ -1,15 +0,0 @@ -substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 - dc_pin: GPIO14 - cs_pin: GPIO13 - enable_pin: GPIO19 - reset_pin: GPIO20 - -display: - - platform: mipi_spi - model: LANBON-L8 - -packages: - display: !include common.yaml diff --git a/tests/components/mipi_spi/test.esp32-c3-ard.yaml b/tests/components/mipi_spi/test.esp32-c3-ard.yaml deleted file mode 100644 index c17748c569..0000000000 --- a/tests/components/mipi_spi/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,10 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - dc_pin: GPIO21 - cs_pin: GPIO18 - enable_pin: GPIO19 - reset_pin: GPIO20 - -<<: !include common.yaml diff --git a/tests/components/mipi_spi/test.esp32-c3-idf.yaml b/tests/components/mipi_spi/test.esp32-c3-idf.yaml index c17748c569..55e25d8318 100644 --- a/tests/components/mipi_spi/test.esp32-c3-idf.yaml +++ b/tests/components/mipi_spi/test.esp32-c3-idf.yaml @@ -1,10 +1,10 @@ substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - dc_pin: GPIO21 - cs_pin: GPIO18 - enable_pin: GPIO19 + dc_pin: GPIO7 + cs_pin: GPIO8 + enable_pin: GPIO9 reset_pin: GPIO20 +packages: + spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml + <<: !include common.yaml diff --git a/tests/components/mipi_spi/test.esp32-idf.yaml b/tests/components/mipi_spi/test.esp32-idf.yaml index 653ccb4910..b173b8de87 100644 --- a/tests/components/mipi_spi/test.esp32-idf.yaml +++ b/tests/components/mipi_spi/test.esp32-idf.yaml @@ -1,15 +1,10 @@ substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 - dc_pin: GPIO21 - cs_pin: GPIO18 - enable_pin: GPIO19 + dc_pin: GPIO14 + cs_pin: GPIO13 + enable_pin: GPIO4 reset_pin: GPIO20 packages: - display: !include common.yaml + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml -display: - - platform: mipi_spi - model: m5core +<<: !include common.yaml diff --git a/tests/components/mipi_spi/test.rp2040-ard.yaml b/tests/components/mipi_spi/test.rp2040-ard.yaml index 5d7333853b..6336652999 100644 --- a/tests/components/mipi_spi/test.rp2040-ard.yaml +++ b/tests/components/mipi_spi/test.rp2040-ard.yaml @@ -1,10 +1,10 @@ substitutions: - clk_pin: GPIO2 - mosi_pin: GPIO3 - miso_pin: GPIO4 dc_pin: GPIO14 cs_pin: GPIO13 - enable_pin: GPIO19 + enable_pin: GPIO17 reset_pin: GPIO20 +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/mitsubishi/common.yaml b/tests/components/mitsubishi/common.yaml index c0fc959c5b..4a2deda163 100644 --- a/tests/components/mitsubishi/common.yaml +++ b/tests/components/mitsubishi/common.yaml @@ -1,7 +1,4 @@ -remote_transmitter: - pin: 4 - carrier_duty_percent: 50% - climate: - platform: mitsubishi name: Mitsubishi + transmitter_id: xmitr diff --git a/tests/components/mitsubishi/test.esp32-ard.yaml b/tests/components/mitsubishi/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/mitsubishi/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/mitsubishi/test.esp32-c3-ard.yaml b/tests/components/mitsubishi/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/mitsubishi/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/mitsubishi/test.esp32-c3-idf.yaml b/tests/components/mitsubishi/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/mitsubishi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/mitsubishi/test.esp32-idf.yaml b/tests/components/mitsubishi/test.esp32-idf.yaml index dade44d145..e891f9dc85 100644 --- a/tests/components/mitsubishi/test.esp32-idf.yaml +++ b/tests/components/mitsubishi/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/mitsubishi/test.esp8266-ard.yaml b/tests/components/mitsubishi/test.esp8266-ard.yaml index dade44d145..4bed2f03e5 100644 --- a/tests/components/mitsubishi/test.esp8266-ard.yaml +++ b/tests/components/mitsubishi/test.esp8266-ard.yaml @@ -1 +1,4 @@ +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/mixer/test.esp32-ard.yaml b/tests/components/mixer/test.esp32-ard.yaml deleted file mode 100644 index 96d2d37458..0000000000 --- a/tests/components/mixer/test.esp32-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - lrclk_pin: GPIO16 - bclk_pin: GPIO17 - mclk_pin: GPIO15 - dout_pin: GPIO14 - -<<: !include common.yaml diff --git a/tests/components/mixer/test.esp32-c3-ard.yaml b/tests/components/mixer/test.esp32-c3-ard.yaml deleted file mode 100644 index f1721f0862..0000000000 --- a/tests/components/mixer/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - lrclk_pin: GPIO4 - bclk_pin: GPIO5 - mclk_pin: GPIO6 - dout_pin: GPIO7 - -<<: !include common.yaml diff --git a/tests/components/mixer/test.esp32-c3-idf.yaml b/tests/components/mixer/test.esp32-c3-idf.yaml deleted file mode 100644 index f1721f0862..0000000000 --- a/tests/components/mixer/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - lrclk_pin: GPIO4 - bclk_pin: GPIO5 - mclk_pin: GPIO6 - dout_pin: GPIO7 - -<<: !include common.yaml diff --git a/tests/components/mixer/test.esp32-idf.yaml b/tests/components/mixer/test.esp32-idf.yaml index 96d2d37458..6712f1e468 100644 --- a/tests/components/mixer/test.esp32-idf.yaml +++ b/tests/components/mixer/test.esp32-idf.yaml @@ -1,7 +1,10 @@ substitutions: - lrclk_pin: GPIO16 - bclk_pin: GPIO17 + lrclk_pin: GPIO4 + bclk_pin: GPIO5 mclk_pin: GPIO15 dout_pin: GPIO14 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/mixer/test.esp32-s3-ard.yaml b/tests/components/mixer/test.esp32-s3-ard.yaml deleted file mode 100644 index f1721f0862..0000000000 --- a/tests/components/mixer/test.esp32-s3-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - lrclk_pin: GPIO4 - bclk_pin: GPIO5 - mclk_pin: GPIO6 - dout_pin: GPIO7 - -<<: !include common.yaml diff --git a/tests/components/mlx90393/common.yaml b/tests/components/mlx90393/common.yaml index 58f3b6ecf5..9e85e06c89 100644 --- a/tests/components/mlx90393/common.yaml +++ b/tests/components/mlx90393/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_mlx90393 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: mlx90393 + i2c_id: i2c_bus oversampling: 3 gain: 1X temperature_compensation: true diff --git a/tests/components/mlx90393/test.esp32-ard.yaml b/tests/components/mlx90393/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/mlx90393/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/mlx90393/test.esp32-c3-ard.yaml b/tests/components/mlx90393/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/mlx90393/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/mlx90393/test.esp32-c3-idf.yaml b/tests/components/mlx90393/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/mlx90393/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/mlx90393/test.esp32-idf.yaml b/tests/components/mlx90393/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/mlx90393/test.esp32-idf.yaml +++ b/tests/components/mlx90393/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/mlx90393/test.esp32-s3-ard.yaml b/tests/components/mlx90393/test.esp32-s3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/mlx90393/test.esp32-s3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/mlx90393/test.esp32-s3-idf.yaml b/tests/components/mlx90393/test.esp32-s3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/mlx90393/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/mlx90393/test.esp8266-ard.yaml b/tests/components/mlx90393/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/mlx90393/test.esp8266-ard.yaml +++ b/tests/components/mlx90393/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/mlx90393/test.rp2040-ard.yaml b/tests/components/mlx90393/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/mlx90393/test.rp2040-ard.yaml +++ b/tests/components/mlx90393/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/mlx90614/common.yaml b/tests/components/mlx90614/common.yaml index 03279e6e14..8d408fb016 100644 --- a/tests/components/mlx90614/common.yaml +++ b/tests/components/mlx90614/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_mlx90614 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: mlx90614 + i2c_id: i2c_bus ambient: name: Ambient object: diff --git a/tests/components/mlx90614/test.esp32-ard.yaml b/tests/components/mlx90614/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/mlx90614/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/mlx90614/test.esp32-c3-ard.yaml b/tests/components/mlx90614/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/mlx90614/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/mlx90614/test.esp32-c3-idf.yaml b/tests/components/mlx90614/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/mlx90614/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/mlx90614/test.esp32-idf.yaml b/tests/components/mlx90614/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/mlx90614/test.esp32-idf.yaml +++ b/tests/components/mlx90614/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/mlx90614/test.esp8266-ard.yaml b/tests/components/mlx90614/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/mlx90614/test.esp8266-ard.yaml +++ b/tests/components/mlx90614/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/mlx90614/test.rp2040-ard.yaml b/tests/components/mlx90614/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/mlx90614/test.rp2040-ard.yaml +++ b/tests/components/mlx90614/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/mmc5603/common.yaml b/tests/components/mmc5603/common.yaml index 83235f596e..6f6e35e9af 100644 --- a/tests/components/mmc5603/common.yaml +++ b/tests/components/mmc5603/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_mmc5603 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: mmc5603 + i2c_id: i2c_bus address: 0x30 field_strength_x: name: HMC5883L Field Strength X diff --git a/tests/components/mmc5603/test.esp32-ard.yaml b/tests/components/mmc5603/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/mmc5603/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/mmc5603/test.esp32-c3-ard.yaml b/tests/components/mmc5603/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/mmc5603/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/mmc5603/test.esp32-c3-idf.yaml b/tests/components/mmc5603/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/mmc5603/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/mmc5603/test.esp32-idf.yaml b/tests/components/mmc5603/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/mmc5603/test.esp32-idf.yaml +++ b/tests/components/mmc5603/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/mmc5603/test.esp8266-ard.yaml b/tests/components/mmc5603/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/mmc5603/test.esp8266-ard.yaml +++ b/tests/components/mmc5603/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/mmc5603/test.rp2040-ard.yaml b/tests/components/mmc5603/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/mmc5603/test.rp2040-ard.yaml +++ b/tests/components/mmc5603/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/mmc5983/common.yaml b/tests/components/mmc5983/common.yaml index 949ff527e7..963a2527c1 100644 --- a/tests/components/mmc5983/common.yaml +++ b/tests/components/mmc5983/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_mmc5983 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: mmc5983 + i2c_id: i2c_bus field_strength_x: name: "Magnet X" id: magnet_x diff --git a/tests/components/mmc5983/test.esp32-ard.yaml b/tests/components/mmc5983/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/mmc5983/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/mmc5983/test.esp32-c3-ard.yaml b/tests/components/mmc5983/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/mmc5983/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/mmc5983/test.esp32-c3-idf.yaml b/tests/components/mmc5983/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/mmc5983/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/mmc5983/test.esp32-idf.yaml b/tests/components/mmc5983/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/mmc5983/test.esp32-idf.yaml +++ b/tests/components/mmc5983/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/mmc5983/test.esp8266-ard.yaml b/tests/components/mmc5983/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/mmc5983/test.esp8266-ard.yaml +++ b/tests/components/mmc5983/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/mmc5983/test.rp2040-ard.yaml b/tests/components/mmc5983/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/mmc5983/test.rp2040-ard.yaml +++ b/tests/components/mmc5983/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/modbus/common.yaml b/tests/components/modbus/common.yaml index 3ec9518585..d636143ec9 100644 --- a/tests/components/modbus/common.yaml +++ b/tests/components/modbus/common.yaml @@ -1,9 +1,3 @@ -uart: - - id: uart_modbus - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 9600 - modbus: id: mod_bus1 flow_control_pin: ${flow_control_pin} diff --git a/tests/components/modbus/test.esp32-ard.yaml b/tests/components/modbus/test.esp32-ard.yaml deleted file mode 100644 index bd767a8ece..0000000000 --- a/tests/components/modbus/test.esp32-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - tx_pin: GPIO12 - rx_pin: GPIO14 - flow_control_pin: GPIO13 - -<<: !include common.yaml diff --git a/tests/components/modbus/test.esp32-c3-ard.yaml b/tests/components/modbus/test.esp32-c3-ard.yaml deleted file mode 100644 index 452031a5aa..0000000000 --- a/tests/components/modbus/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - flow_control_pin: GPIO3 - -<<: !include common.yaml diff --git a/tests/components/modbus/test.esp32-c3-idf.yaml b/tests/components/modbus/test.esp32-c3-idf.yaml deleted file mode 100644 index 452031a5aa..0000000000 --- a/tests/components/modbus/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - flow_control_pin: GPIO3 - -<<: !include common.yaml diff --git a/tests/components/modbus/test.esp32-idf.yaml b/tests/components/modbus/test.esp32-idf.yaml index bd767a8ece..8a08f8a821 100644 --- a/tests/components/modbus/test.esp32-idf.yaml +++ b/tests/components/modbus/test.esp32-idf.yaml @@ -3,4 +3,7 @@ substitutions: rx_pin: GPIO14 flow_control_pin: GPIO13 +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/modbus/test.esp8266-ard.yaml b/tests/components/modbus/test.esp8266-ard.yaml index 29c98d0957..dfea36d957 100644 --- a/tests/components/modbus/test.esp8266-ard.yaml +++ b/tests/components/modbus/test.esp8266-ard.yaml @@ -1,6 +1,9 @@ substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - flow_control_pin: GPIO13 + tx_pin: GPIO0 + rx_pin: GPIO2 + flow_control_pin: GPIO15 + +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/modbus/test.rp2040-ard.yaml b/tests/components/modbus/test.rp2040-ard.yaml index 452031a5aa..43f12ea28d 100644 --- a/tests/components/modbus/test.rp2040-ard.yaml +++ b/tests/components/modbus/test.rp2040-ard.yaml @@ -3,4 +3,7 @@ substitutions: rx_pin: GPIO5 flow_control_pin: GPIO3 +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/modbus_controller/common.yaml b/tests/components/modbus_controller/common.yaml index 7d342ee353..ffaa1491c5 100644 --- a/tests/components/modbus_controller/common.yaml +++ b/tests/components/modbus_controller/common.yaml @@ -1,25 +1,12 @@ -uart: - - id: uart_modbus_client - tx_pin: ${client_tx_pin} - rx_pin: ${client_rx_pin} - baud_rate: 9600 - - id: uart_modbus_server - tx_pin: ${server_tx_pin} - rx_pin: ${server_rx_pin} - baud_rate: 9600 - modbus: - - id: mod_bus1 - uart_id: uart_modbus_client - flow_control_pin: ${flow_control_pin} - id: mod_bus2 - uart_id: uart_modbus_server + uart_id: uart_bus role: server modbus_controller: - id: modbus_controller1 address: 0x2 - modbus_id: mod_bus1 + modbus_id: modbus_bus allow_duplicate_commands: false on_online: then: @@ -45,6 +32,22 @@ modbus_controller: printf("address=%d, value=%d", x); return true; max_cmd_retries: 0 + - id: modbus_controller4 + modbus_id: mod_bus2 + address: 0x4 + server_courtesy_response: + enabled: true + register_last_address: 100 + register_value: 0 + server_registers: + - address: 0x0001 + value_type: U_WORD + read_lambda: |- + return 0x8; + - address: 0x0005 + value_type: U_WORD + read_lambda: |- + return (random_uint32() % 100); binary_sensor: - platform: modbus_controller modbus_controller_id: modbus_controller1 @@ -53,6 +56,14 @@ binary_sensor: register_type: read address: 0x3200 bitmask: 0x80 + - platform: modbus_controller + modbus_controller_id: modbus_controller1 + id: modbus_binary_sensor2 + name: Test Binary Sensor with Lambda + register_type: read + address: 0x3201 + lambda: |- + return x; number: - platform: modbus_controller @@ -62,6 +73,16 @@ number: address: 0x9001 value_type: U_WORD multiply: 1.0 + - platform: modbus_controller + modbus_controller_id: modbus_controller1 + id: modbus_number2 + name: Test Number with Lambda + address: 0x9002 + value_type: U_WORD + lambda: |- + return x * 2.0; + write_lambda: |- + return x / 2.0; output: - platform: modbus_controller @@ -71,6 +92,14 @@ output: register_type: holding value_type: U_WORD multiply: 1000 + - platform: modbus_controller + modbus_controller_id: modbus_controller1 + id: modbus_output2 + address: 2049 + register_type: holding + value_type: U_WORD + write_lambda: |- + return x * 100.0; select: - platform: modbus_controller @@ -84,6 +113,34 @@ select: "One": 1 "Two": 2 "Three": 3 + - platform: modbus_controller + modbus_controller_id: modbus_controller1 + id: modbus_select2 + name: Test Select with Lambda + address: 1001 + value_type: U_WORD + optionsmap: + "Off": 0 + "On": 1 + "Two": 2 + lambda: |- + ESP_LOGD("Reg1001", "Received value %lld", x); + if (x > 1) { + return std::string("Two"); + } else if (x == 1) { + return std::string("On"); + } + return std::string("Off"); + write_lambda: |- + ESP_LOGD("Reg1001", "Set option to %s (%lld)", x.c_str(), value); + if (x == "On") { + return 1; + } + if (x == "Two") { + payload.push_back(0x0002); + return 0; + } + return value; sensor: - platform: modbus_controller @@ -94,6 +151,15 @@ sensor: address: 0x9001 unit_of_measurement: "AH" value_type: U_WORD + - platform: modbus_controller + modbus_controller_id: modbus_controller1 + id: modbus_sensor2 + name: Test Sensor with Lambda + register_type: holding + address: 0x9002 + value_type: U_WORD + lambda: |- + return x / 10.0; switch: - platform: modbus_controller @@ -103,6 +169,16 @@ switch: register_type: coil address: 0x15 bitmask: 1 + - platform: modbus_controller + modbus_controller_id: modbus_controller1 + id: modbus_switch2 + name: Test Switch with Lambda + register_type: coil + address: 0x16 + lambda: |- + return !x; + write_lambda: |- + return !x; text_sensor: - platform: modbus_controller @@ -114,3 +190,13 @@ text_sensor: register_count: 3 raw_encode: HEXBYTES response_size: 6 + - platform: modbus_controller + modbus_controller_id: modbus_controller1 + id: modbus_text_sensor2 + name: Test Text Sensor with Lambda + register_type: holding + address: 0x9014 + register_count: 2 + response_size: 4 + lambda: |- + return "Modified: " + x; diff --git a/tests/components/modbus_controller/test.esp32-ard.yaml b/tests/components/modbus_controller/test.esp32-ard.yaml deleted file mode 100644 index 548b8c0666..0000000000 --- a/tests/components/modbus_controller/test.esp32-ard.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - client_tx_pin: GPIO12 - client_rx_pin: GPIO14 - server_tx_pin: GPIO16 - server_rx_pin: GPIO17 - flow_control_pin: GPIO13 - -<<: !include common.yaml diff --git a/tests/components/modbus_controller/test.esp32-c3-ard.yaml b/tests/components/modbus_controller/test.esp32-c3-ard.yaml deleted file mode 100644 index f5b770ff58..0000000000 --- a/tests/components/modbus_controller/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - client_tx_pin: GPIO4 - client_rx_pin: GPIO5 - server_tx_pin: GPIO6 - server_rx_pin: GPIO7 - flow_control_pin: GPIO3 - -<<: !include common.yaml diff --git a/tests/components/modbus_controller/test.esp32-c3-idf.yaml b/tests/components/modbus_controller/test.esp32-c3-idf.yaml deleted file mode 100644 index f5b770ff58..0000000000 --- a/tests/components/modbus_controller/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - client_tx_pin: GPIO4 - client_rx_pin: GPIO5 - server_tx_pin: GPIO6 - server_rx_pin: GPIO7 - flow_control_pin: GPIO3 - -<<: !include common.yaml diff --git a/tests/components/modbus_controller/test.esp32-idf.yaml b/tests/components/modbus_controller/test.esp32-idf.yaml index 548b8c0666..ace2d95a0b 100644 --- a/tests/components/modbus_controller/test.esp32-idf.yaml +++ b/tests/components/modbus_controller/test.esp32-idf.yaml @@ -1,8 +1,4 @@ -substitutions: - client_tx_pin: GPIO12 - client_rx_pin: GPIO14 - server_tx_pin: GPIO16 - server_rx_pin: GPIO17 - flow_control_pin: GPIO13 +packages: + modbus: !include ../../test_build_components/common/modbus/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/modbus_controller/test.esp8266-ard.yaml b/tests/components/modbus_controller/test.esp8266-ard.yaml index c68a57cbde..560629b0cd 100644 --- a/tests/components/modbus_controller/test.esp8266-ard.yaml +++ b/tests/components/modbus_controller/test.esp8266-ard.yaml @@ -1,8 +1,4 @@ -substitutions: - client_tx_pin: GPIO1 - client_rx_pin: GPIO3 - server_tx_pin: GPIO4 - server_rx_pin: GPIO5 - flow_control_pin: GPIO13 +packages: + modbus: !include ../../test_build_components/common/modbus/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/modbus_controller/test.rp2040-ard.yaml b/tests/components/modbus_controller/test.rp2040-ard.yaml index 80528bc12b..eeebbd2a8a 100644 --- a/tests/components/modbus_controller/test.rp2040-ard.yaml +++ b/tests/components/modbus_controller/test.rp2040-ard.yaml @@ -1,8 +1,4 @@ -substitutions: - client_tx_pin: GPIO2 - client_rx_pin: GPIO3 - server_tx_pin: GPIO4 - server_rx_pin: GPIO5 - flow_control_pin: GPIO6 +packages: + modbus: !include ../../test_build_components/common/modbus/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/monochromatic/test.esp32-ard.yaml b/tests/components/monochromatic/test.esp32-ard.yaml deleted file mode 100644 index feabf013fd..0000000000 --- a/tests/components/monochromatic/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - light_platform: ledc - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/monochromatic/test.esp32-c3-ard.yaml b/tests/components/monochromatic/test.esp32-c3-ard.yaml deleted file mode 100644 index feabf013fd..0000000000 --- a/tests/components/monochromatic/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - light_platform: ledc - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/monochromatic/test.esp32-c3-idf.yaml b/tests/components/monochromatic/test.esp32-c3-idf.yaml deleted file mode 100644 index feabf013fd..0000000000 --- a/tests/components/monochromatic/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - light_platform: ledc - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/mopeka_ble/test.esp32-ard.yaml b/tests/components/mopeka_ble/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/mopeka_ble/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/mopeka_ble/test.esp32-c3-ard.yaml b/tests/components/mopeka_ble/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/mopeka_ble/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/mopeka_ble/test.esp32-c3-idf.yaml b/tests/components/mopeka_ble/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/mopeka_ble/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/mopeka_ble/test.esp32-idf.yaml b/tests/components/mopeka_ble/test.esp32-idf.yaml index dade44d145..7a6541ae76 100644 --- a/tests/components/mopeka_ble/test.esp32-idf.yaml +++ b/tests/components/mopeka_ble/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/mopeka_pro_check/test.esp32-ard.yaml b/tests/components/mopeka_pro_check/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/mopeka_pro_check/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/mopeka_pro_check/test.esp32-c3-ard.yaml b/tests/components/mopeka_pro_check/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/mopeka_pro_check/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/mopeka_pro_check/test.esp32-c3-idf.yaml b/tests/components/mopeka_pro_check/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/mopeka_pro_check/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/mopeka_pro_check/test.esp32-idf.yaml b/tests/components/mopeka_pro_check/test.esp32-idf.yaml index dade44d145..7a6541ae76 100644 --- a/tests/components/mopeka_pro_check/test.esp32-idf.yaml +++ b/tests/components/mopeka_pro_check/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/mopeka_std_check/test.esp32-ard.yaml b/tests/components/mopeka_std_check/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/mopeka_std_check/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/mopeka_std_check/test.esp32-c3-ard.yaml b/tests/components/mopeka_std_check/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/mopeka_std_check/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/mopeka_std_check/test.esp32-c3-idf.yaml b/tests/components/mopeka_std_check/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/mopeka_std_check/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/mopeka_std_check/test.esp32-idf.yaml b/tests/components/mopeka_std_check/test.esp32-idf.yaml index dade44d145..7a6541ae76 100644 --- a/tests/components/mopeka_std_check/test.esp32-idf.yaml +++ b/tests/components/mopeka_std_check/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/mpl3115a2/common.yaml b/tests/components/mpl3115a2/common.yaml index f2f65885b3..aa6c2b77fc 100644 --- a/tests/components/mpl3115a2/common.yaml +++ b/tests/components/mpl3115a2/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_mpl3115a2 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: mpl3115a2 + i2c_id: i2c_bus temperature: name: MPL3115A2 Temperature pressure: diff --git a/tests/components/mpl3115a2/test.esp32-ard.yaml b/tests/components/mpl3115a2/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/mpl3115a2/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/mpl3115a2/test.esp32-c3-ard.yaml b/tests/components/mpl3115a2/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/mpl3115a2/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/mpl3115a2/test.esp32-c3-idf.yaml b/tests/components/mpl3115a2/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/mpl3115a2/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/mpl3115a2/test.esp32-idf.yaml b/tests/components/mpl3115a2/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/mpl3115a2/test.esp32-idf.yaml +++ b/tests/components/mpl3115a2/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/mpl3115a2/test.esp8266-ard.yaml b/tests/components/mpl3115a2/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/mpl3115a2/test.esp8266-ard.yaml +++ b/tests/components/mpl3115a2/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/mpl3115a2/test.rp2040-ard.yaml b/tests/components/mpl3115a2/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/mpl3115a2/test.rp2040-ard.yaml +++ b/tests/components/mpl3115a2/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/mpr121/common.yaml b/tests/components/mpr121/common.yaml index fcf61b57f3..67a06cf9c1 100644 --- a/tests/components/mpr121/common.yaml +++ b/tests/components/mpr121/common.yaml @@ -1,9 +1,5 @@ -i2c: - - id: i2c_mpr121 - scl: ${i2c_scl} - sda: ${i2c_sda} - mpr121: + i2c_id: i2c_bus id: mpr121_first address: 0x5A diff --git a/tests/components/mpr121/test.esp32-ard.yaml b/tests/components/mpr121/test.esp32-ard.yaml deleted file mode 100644 index 1037d5d35b..0000000000 --- a/tests/components/mpr121/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - i2c_scl: GPIO16 - i2c_sda: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/mpr121/test.esp32-c3-ard.yaml b/tests/components/mpr121/test.esp32-c3-ard.yaml deleted file mode 100644 index d7ae0d5161..0000000000 --- a/tests/components/mpr121/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - i2c_scl: GPIO5 - i2c_sda: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/mpr121/test.esp32-c3-idf.yaml b/tests/components/mpr121/test.esp32-c3-idf.yaml deleted file mode 100644 index d7ae0d5161..0000000000 --- a/tests/components/mpr121/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - i2c_scl: GPIO5 - i2c_sda: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/mpr121/test.esp32-idf.yaml b/tests/components/mpr121/test.esp32-idf.yaml index 1037d5d35b..4598505c3a 100644 --- a/tests/components/mpr121/test.esp32-idf.yaml +++ b/tests/components/mpr121/test.esp32-idf.yaml @@ -2,4 +2,7 @@ substitutions: i2c_scl: GPIO16 i2c_sda: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/mpr121/test.esp8266-ard.yaml b/tests/components/mpr121/test.esp8266-ard.yaml index d7ae0d5161..5565bb8c35 100644 --- a/tests/components/mpr121/test.esp8266-ard.yaml +++ b/tests/components/mpr121/test.esp8266-ard.yaml @@ -2,4 +2,7 @@ substitutions: i2c_scl: GPIO5 i2c_sda: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/mpr121/test.rp2040-ard.yaml b/tests/components/mpr121/test.rp2040-ard.yaml index d7ae0d5161..888762a742 100644 --- a/tests/components/mpr121/test.rp2040-ard.yaml +++ b/tests/components/mpr121/test.rp2040-ard.yaml @@ -2,4 +2,7 @@ substitutions: i2c_scl: GPIO5 i2c_sda: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/mpu6050/common.yaml b/tests/components/mpu6050/common.yaml index e9d4995534..7ac123f8c7 100644 --- a/tests/components/mpu6050/common.yaml +++ b/tests/components/mpu6050/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_mpu6050 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: mpu6050 + i2c_id: i2c_bus address: 0x68 accel_x: name: MPU6050 Accel X diff --git a/tests/components/mpu6050/test.esp32-ard.yaml b/tests/components/mpu6050/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/mpu6050/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/mpu6050/test.esp32-c3-ard.yaml b/tests/components/mpu6050/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/mpu6050/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/mpu6050/test.esp32-c3-idf.yaml b/tests/components/mpu6050/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/mpu6050/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/mpu6050/test.esp32-idf.yaml b/tests/components/mpu6050/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/mpu6050/test.esp32-idf.yaml +++ b/tests/components/mpu6050/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/mpu6050/test.esp8266-ard.yaml b/tests/components/mpu6050/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/mpu6050/test.esp8266-ard.yaml +++ b/tests/components/mpu6050/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/mpu6050/test.rp2040-ard.yaml b/tests/components/mpu6050/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/mpu6050/test.rp2040-ard.yaml +++ b/tests/components/mpu6050/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/mpu6886/common.yaml b/tests/components/mpu6886/common.yaml index 9c3e283cc3..9f7324f384 100644 --- a/tests/components/mpu6886/common.yaml +++ b/tests/components/mpu6886/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_mpu6886 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: mpu6886 + i2c_id: i2c_bus address: 0x68 accel_x: name: MPU6886 Accel X diff --git a/tests/components/mpu6886/test.esp32-ard.yaml b/tests/components/mpu6886/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/mpu6886/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/mpu6886/test.esp32-c3-ard.yaml b/tests/components/mpu6886/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/mpu6886/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/mpu6886/test.esp32-c3-idf.yaml b/tests/components/mpu6886/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/mpu6886/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/mpu6886/test.esp32-idf.yaml b/tests/components/mpu6886/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/mpu6886/test.esp32-idf.yaml +++ b/tests/components/mpu6886/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/mpu6886/test.esp8266-ard.yaml b/tests/components/mpu6886/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/mpu6886/test.esp8266-ard.yaml +++ b/tests/components/mpu6886/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/mpu6886/test.rp2040-ard.yaml b/tests/components/mpu6886/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/mpu6886/test.rp2040-ard.yaml +++ b/tests/components/mpu6886/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/mqtt/common-update.yaml b/tests/components/mqtt/common-update.yaml index 25f57cfef2..2e21e9687a 100644 --- a/tests/components/mqtt/common-update.yaml +++ b/tests/components/mqtt/common-update.yaml @@ -6,8 +6,10 @@ http_request: ota: - platform: http_request + id: mqtt_http_request_ota update: - platform: http_request name: "OTA Update" + ota_id: mqtt_http_request_ota source: https://example.com/ota.json diff --git a/tests/components/mqtt/common.yaml b/tests/components/mqtt/common.yaml index 1ab8872fdb..284ac30337 100644 --- a/tests/components/mqtt/common.yaml +++ b/tests/components/mqtt/common.yaml @@ -4,6 +4,7 @@ wifi: time: - platform: sntp + id: sntp_time mqtt: broker: "192.168.178.84" @@ -72,10 +73,9 @@ binary_sensor: if (id(template_sens).state > 30) { // Garage Door is open. return true; - } else { - // Garage Door is closed. - return false; } + // Garage Door is closed. + return false; on_state: - mqtt.publish: topic: some/topic/binary_sensor @@ -217,9 +217,8 @@ cover: lambda: |- if (id(some_binary_sensor).state) { return COVER_OPEN; - } else { - return COVER_CLOSED; } + return COVER_CLOSED; open_action: - logger.log: open_action close_action: @@ -321,9 +320,8 @@ lock: lambda: |- if (id(some_binary_sensor).state) { return LOCK_STATE_LOCKED; - } else { - return LOCK_STATE_UNLOCKED; } + return LOCK_STATE_UNLOCKED; lock_action: - logger.log: lock_action unlock_action: @@ -360,9 +358,8 @@ sensor: lambda: |- if (id(some_binary_sensor).state) { return 42.0; - } else { - return 0.0; } + return 0.0; update_interval: 60s on_value: - mqtt.publish: @@ -390,9 +387,8 @@ switch: lambda: |- if (id(some_binary_sensor).state) { return true; - } else { - return false; } + return false; turn_on_action: - logger.log: turn_on_action turn_off_action: @@ -436,9 +432,8 @@ valve: lambda: |- if (id(some_binary_sensor).state) { return VALVE_OPEN; - } else { - return VALVE_CLOSED; } + return VALVE_CLOSED; alarm_control_panel: - platform: template diff --git a/tests/components/mqtt/test.esp32-ard.yaml b/tests/components/mqtt/test.esp32-ard.yaml deleted file mode 100644 index 4c70fb37d9..0000000000 --- a/tests/components/mqtt/test.esp32-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - verify_ssl: "false" - -packages: - common: !include common.yaml - update: !include common-update.yaml diff --git a/tests/components/mqtt/test.esp32-c3-ard.yaml b/tests/components/mqtt/test.esp32-c3-ard.yaml deleted file mode 100644 index 4c70fb37d9..0000000000 --- a/tests/components/mqtt/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - verify_ssl: "false" - -packages: - common: !include common.yaml - update: !include common-update.yaml diff --git a/tests/components/mqtt/test.esp32-c3-idf.yaml b/tests/components/mqtt/test.esp32-c3-idf.yaml deleted file mode 100644 index d19609b55e..0000000000 --- a/tests/components/mqtt/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,3 +0,0 @@ -packages: - common: !include common.yaml - update: !include common-update.yaml diff --git a/tests/components/mqtt_subscribe/test.esp32-ard.yaml b/tests/components/mqtt_subscribe/test.esp32-ard.yaml deleted file mode 100644 index 28589ee06c..0000000000 --- a/tests/components/mqtt_subscribe/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common-ard.yaml diff --git a/tests/components/mqtt_subscribe/test.esp32-c3-ard.yaml b/tests/components/mqtt_subscribe/test.esp32-c3-ard.yaml deleted file mode 100644 index 28589ee06c..0000000000 --- a/tests/components/mqtt_subscribe/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common-ard.yaml diff --git a/tests/components/mqtt_subscribe/test.esp32-c3-idf.yaml b/tests/components/mqtt_subscribe/test.esp32-c3-idf.yaml deleted file mode 100644 index 2cb6d82536..0000000000 --- a/tests/components/mqtt_subscribe/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common-idf.yaml diff --git a/tests/components/ms5611/common.yaml b/tests/components/ms5611/common.yaml index d0417faa86..12644fa330 100644 --- a/tests/components/ms5611/common.yaml +++ b/tests/components/ms5611/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_ms5611 - scl: ${i2c_scl} - sda: ${i2c_sda} - sensor: - platform: ms5611 + i2c_id: i2c_bus temperature: name: Outside Temperature pressure: diff --git a/tests/components/ms5611/test.esp32-ard.yaml b/tests/components/ms5611/test.esp32-ard.yaml deleted file mode 100644 index 1037d5d35b..0000000000 --- a/tests/components/ms5611/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - i2c_scl: GPIO16 - i2c_sda: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/ms5611/test.esp32-c3-ard.yaml b/tests/components/ms5611/test.esp32-c3-ard.yaml deleted file mode 100644 index d7ae0d5161..0000000000 --- a/tests/components/ms5611/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - i2c_scl: GPIO5 - i2c_sda: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ms5611/test.esp32-c3-idf.yaml b/tests/components/ms5611/test.esp32-c3-idf.yaml deleted file mode 100644 index d7ae0d5161..0000000000 --- a/tests/components/ms5611/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - i2c_scl: GPIO5 - i2c_sda: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ms5611/test.esp32-idf.yaml b/tests/components/ms5611/test.esp32-idf.yaml index 1037d5d35b..4598505c3a 100644 --- a/tests/components/ms5611/test.esp32-idf.yaml +++ b/tests/components/ms5611/test.esp32-idf.yaml @@ -2,4 +2,7 @@ substitutions: i2c_scl: GPIO16 i2c_sda: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/ms5611/test.esp8266-ard.yaml b/tests/components/ms5611/test.esp8266-ard.yaml index d7ae0d5161..5565bb8c35 100644 --- a/tests/components/ms5611/test.esp8266-ard.yaml +++ b/tests/components/ms5611/test.esp8266-ard.yaml @@ -2,4 +2,7 @@ substitutions: i2c_scl: GPIO5 i2c_sda: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/ms5611/test.rp2040-ard.yaml b/tests/components/ms5611/test.rp2040-ard.yaml index d7ae0d5161..888762a742 100644 --- a/tests/components/ms5611/test.rp2040-ard.yaml +++ b/tests/components/ms5611/test.rp2040-ard.yaml @@ -2,4 +2,7 @@ substitutions: i2c_scl: GPIO5 i2c_sda: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/msa3xx/common.yaml b/tests/components/msa3xx/common.yaml index 8de6a8a89a..297ed8a741 100644 --- a/tests/components/msa3xx/common.yaml +++ b/tests/components/msa3xx/common.yaml @@ -1,5 +1,5 @@ msa3xx: - i2c_id: i2c_msa3xx + i2c_id: i2c_bus type: msa301 range: 4G resolution: 14 diff --git a/tests/components/msa3xx/test.esp32-ard.yaml b/tests/components/msa3xx/test.esp32-ard.yaml deleted file mode 100644 index 7202e7b9bf..0000000000 --- a/tests/components/msa3xx/test.esp32-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -i2c: - - id: i2c_msa3xx - scl: GPIO16 - sda: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/msa3xx/test.esp32-c3-ard.yaml b/tests/components/msa3xx/test.esp32-c3-ard.yaml deleted file mode 100644 index b972ce8cdb..0000000000 --- a/tests/components/msa3xx/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -i2c: - - id: i2c_msa3xx - scl: GPIO5 - sda: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/msa3xx/test.esp32-c3-idf.yaml b/tests/components/msa3xx/test.esp32-c3-idf.yaml deleted file mode 100644 index b972ce8cdb..0000000000 --- a/tests/components/msa3xx/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -i2c: - - id: i2c_msa3xx - scl: GPIO5 - sda: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/msa3xx/test.esp32-idf.yaml b/tests/components/msa3xx/test.esp32-idf.yaml index 7202e7b9bf..b47e39c389 100644 --- a/tests/components/msa3xx/test.esp32-idf.yaml +++ b/tests/components/msa3xx/test.esp32-idf.yaml @@ -1,6 +1,4 @@ -i2c: - - id: i2c_msa3xx - scl: GPIO16 - sda: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/msa3xx/test.esp8266-ard.yaml b/tests/components/msa3xx/test.esp8266-ard.yaml index b972ce8cdb..4a98b9388a 100644 --- a/tests/components/msa3xx/test.esp8266-ard.yaml +++ b/tests/components/msa3xx/test.esp8266-ard.yaml @@ -1,6 +1,4 @@ -i2c: - - id: i2c_msa3xx - scl: GPIO5 - sda: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/msa3xx/test.rp2040-ard.yaml b/tests/components/msa3xx/test.rp2040-ard.yaml index b972ce8cdb..319a7c71a6 100644 --- a/tests/components/msa3xx/test.rp2040-ard.yaml +++ b/tests/components/msa3xx/test.rp2040-ard.yaml @@ -1,6 +1,4 @@ -i2c: - - id: i2c_msa3xx - scl: GPIO5 - sda: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/my9231/test.esp32-ard.yaml b/tests/components/my9231/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/my9231/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/my9231/test.esp32-c3-ard.yaml b/tests/components/my9231/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/my9231/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/my9231/test.esp32-c3-idf.yaml b/tests/components/my9231/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/my9231/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/nau7802/common.yaml b/tests/components/nau7802/common.yaml index 2501911a3f..5251910df9 100644 --- a/tests/components/nau7802/common.yaml +++ b/tests/components/nau7802/common.yaml @@ -1,13 +1,13 @@ sensor: - platform: nau7802 - id: test_id + i2c_id: i2c_bus + id: nau7802_test_id name: weight - i2c_id: i2c_nau7802 gain: 32 ldo_voltage: "3.0v" samples_per_second: 10 on_value: then: - - nau7802.calibrate_external_offset: test_id - - nau7802.calibrate_internal_offset: test_id - - nau7802.calibrate_gain: test_id + - nau7802.calibrate_external_offset: nau7802_test_id + - nau7802.calibrate_internal_offset: nau7802_test_id + - nau7802.calibrate_gain: nau7802_test_id diff --git a/tests/components/nau7802/test.esp32-ard.yaml b/tests/components/nau7802/test.esp32-ard.yaml deleted file mode 100644 index 73a4aa4251..0000000000 --- a/tests/components/nau7802/test.esp32-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -i2c: - - id: i2c_nau7802 - scl: 16 - sda: 17 - -<<: !include common.yaml diff --git a/tests/components/nau7802/test.esp32-c3-ard.yaml b/tests/components/nau7802/test.esp32-c3-ard.yaml deleted file mode 100644 index 769468f9ec..0000000000 --- a/tests/components/nau7802/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -i2c: - - id: i2c_nau7802 - scl: 5 - sda: 4 - -<<: !include common.yaml diff --git a/tests/components/nau7802/test.esp32-c3-idf.yaml b/tests/components/nau7802/test.esp32-c3-idf.yaml deleted file mode 100644 index 769468f9ec..0000000000 --- a/tests/components/nau7802/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -i2c: - - id: i2c_nau7802 - scl: 5 - sda: 4 - -<<: !include common.yaml diff --git a/tests/components/nau7802/test.esp32-idf.yaml b/tests/components/nau7802/test.esp32-idf.yaml index 73a4aa4251..b47e39c389 100644 --- a/tests/components/nau7802/test.esp32-idf.yaml +++ b/tests/components/nau7802/test.esp32-idf.yaml @@ -1,6 +1,4 @@ -i2c: - - id: i2c_nau7802 - scl: 16 - sda: 17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/nau7802/test.esp8266-ard.yaml b/tests/components/nau7802/test.esp8266-ard.yaml index 769468f9ec..4a98b9388a 100644 --- a/tests/components/nau7802/test.esp8266-ard.yaml +++ b/tests/components/nau7802/test.esp8266-ard.yaml @@ -1,6 +1,4 @@ -i2c: - - id: i2c_nau7802 - scl: 5 - sda: 4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/nau7802/test.rp2040-ard.yaml b/tests/components/nau7802/test.rp2040-ard.yaml index 769468f9ec..319a7c71a6 100644 --- a/tests/components/nau7802/test.rp2040-ard.yaml +++ b/tests/components/nau7802/test.rp2040-ard.yaml @@ -1,6 +1,4 @@ -i2c: - - id: i2c_nau7802 - scl: 5 - sda: 4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/neopixelbus/test.esp32-c3-ard.yaml b/tests/components/neopixelbus/test.esp32-c3-ard.yaml deleted file mode 100644 index f2b53ab1e9..0000000000 --- a/tests/components/neopixelbus/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,19 +0,0 @@ -light: - - platform: neopixelbus - id: addr3 - name: Neopixelbus Light - gamma_correct: 2.8 - color_correct: [0.0, 0.0, 0.0, 0.0] - default_transition_length: 10s - effects: - - addressable_flicker: - name: Flicker Effect With Custom Values - update_interval: 16ms - intensity: 5% - type: GRBW - variant: SK6812 - method: - type: esp32_rmt - channel: 0 - num_leds: 5 - pin: 4 diff --git a/tests/components/network/test-ipv6.bk72xx-ard.yaml b/tests/components/network/test-ipv6.bk72xx-ard.yaml index d0c4bbfcb9..da1324b17e 100644 --- a/tests/components/network/test-ipv6.bk72xx-ard.yaml +++ b/tests/components/network/test-ipv6.bk72xx-ard.yaml @@ -1,8 +1,4 @@ substitutions: network_enable_ipv6: "true" -bk72xx: - framework: - version: 1.7.0 - <<: !include common.yaml diff --git a/tests/components/network/test-ipv6.esp32-ard.yaml b/tests/components/network/test-ipv6.esp32-ard.yaml deleted file mode 100644 index da1324b17e..0000000000 --- a/tests/components/network/test-ipv6.esp32-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - network_enable_ipv6: "true" - -<<: !include common.yaml diff --git a/tests/components/network/test-ipv6.esp32-c3-ard.yaml b/tests/components/network/test-ipv6.esp32-c3-ard.yaml deleted file mode 100644 index da1324b17e..0000000000 --- a/tests/components/network/test-ipv6.esp32-c3-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - network_enable_ipv6: "true" - -<<: !include common.yaml diff --git a/tests/components/network/test-ipv6.esp32-c3-idf.yaml b/tests/components/network/test-ipv6.esp32-c3-idf.yaml deleted file mode 100644 index da1324b17e..0000000000 --- a/tests/components/network/test-ipv6.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - network_enable_ipv6: "true" - -<<: !include common.yaml diff --git a/tests/components/wireguard/test.esp32-ard.yaml b/tests/components/network/test-ipv6.host.yaml similarity index 52% rename from tests/components/wireguard/test.esp32-ard.yaml rename to tests/components/network/test-ipv6.host.yaml index 2798f8e566..d9eeab89ea 100644 --- a/tests/components/wireguard/test.esp32-ard.yaml +++ b/tests/components/network/test-ipv6.host.yaml @@ -1,4 +1,2 @@ -<<: !include common.yaml - network: enable_ipv6: true diff --git a/tests/components/b_parasite/test.esp32-c3-idf.yaml b/tests/components/network/test.bk72xx-ard.yaml similarity index 100% rename from tests/components/b_parasite/test.esp32-c3-idf.yaml rename to tests/components/network/test.bk72xx-ard.yaml diff --git a/tests/components/network/test.esp32-ard.yaml b/tests/components/network/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/network/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/network/test.esp32-c3-ard.yaml b/tests/components/network/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/network/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/network/test.esp32-c3-idf.yaml b/tests/components/network/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/network/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/network/test.esp32-idf.yaml b/tests/components/network/test.esp32-idf.yaml index dade44d145..7c01bafa0d 100644 --- a/tests/components/network/test.esp32-idf.yaml +++ b/tests/components/network/test.esp32-idf.yaml @@ -1 +1,4 @@ <<: !include common.yaml + +network: + enable_high_performance: true diff --git a/tests/components/nextion/common.yaml b/tests/components/nextion/common.yaml index 767c868d0b..f5ee12a51c 100644 --- a/tests/components/nextion/common.yaml +++ b/tests/components/nextion/common.yaml @@ -236,12 +236,6 @@ wifi: ssid: MySSID password: password1 -uart: - - id: uart_nextion - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 115200 - binary_sensor: - platform: nextion page_id: 0 diff --git a/tests/components/nextion/test.esp32-ard.yaml b/tests/components/nextion/test.esp32-ard.yaml index d5e02b8b85..7e94a9b4a5 100644 --- a/tests/components/nextion/test.esp32-ard.yaml +++ b/tests/components/nextion/test.esp32-ard.yaml @@ -1,8 +1,5 @@ -substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 - packages: + uart: !include ../../test_build_components/common/uart/esp32-ard.yaml base: !include common.yaml display: diff --git a/tests/components/nextion/test.esp32-c3-ard.yaml b/tests/components/nextion/test.esp32-c3-ard.yaml deleted file mode 100644 index 5135c7e4f4..0000000000 --- a/tests/components/nextion/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,10 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -packages: - base: !include common.yaml - -display: - - id: !extend main_lcd - tft_url: http://esphome.io/default35.tft diff --git a/tests/components/nextion/test.esp32-c3-idf.yaml b/tests/components/nextion/test.esp32-c3-idf.yaml deleted file mode 100644 index 5135c7e4f4..0000000000 --- a/tests/components/nextion/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,10 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -packages: - base: !include common.yaml - -display: - - id: !extend main_lcd - tft_url: http://esphome.io/default35.tft diff --git a/tests/components/nextion/test.esp32-idf.yaml b/tests/components/nextion/test.esp32-idf.yaml index d5e02b8b85..99820f0f8d 100644 --- a/tests/components/nextion/test.esp32-idf.yaml +++ b/tests/components/nextion/test.esp32-idf.yaml @@ -1,8 +1,5 @@ -substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 - packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml base: !include common.yaml display: diff --git a/tests/components/nextion/test.esp8266-ard.yaml b/tests/components/nextion/test.esp8266-ard.yaml index 5135c7e4f4..49f79b2f4c 100644 --- a/tests/components/nextion/test.esp8266-ard.yaml +++ b/tests/components/nextion/test.esp8266-ard.yaml @@ -1,8 +1,5 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml base: !include common.yaml display: diff --git a/tests/components/nextion/test.rp2040-ard.yaml b/tests/components/nextion/test.rp2040-ard.yaml index 44534b97a8..23cc71ee7c 100644 --- a/tests/components/nextion/test.rp2040-ard.yaml +++ b/tests/components/nextion/test.rp2040-ard.yaml @@ -1,6 +1,3 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml base: !include common.yaml diff --git a/tests/components/noblex/common.yaml b/tests/components/noblex/common.yaml index f5e471a9a7..8053d84d4b 100644 --- a/tests/components/noblex/common.yaml +++ b/tests/components/noblex/common.yaml @@ -1,12 +1,3 @@ -remote_receiver: - id: rcvr - pin: 4 - dump: all - -remote_transmitter: - pin: 2 - carrier_duty_percent: 50% - sensor: - platform: template id: noblex_ac_sensor diff --git a/tests/components/noblex/test.esp32-ard.yaml b/tests/components/noblex/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/noblex/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/noblex/test.esp32-c3-ard.yaml b/tests/components/noblex/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/noblex/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/noblex/test.esp32-c3-idf.yaml b/tests/components/noblex/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/noblex/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/noblex/test.esp32-idf.yaml b/tests/components/noblex/test.esp32-idf.yaml index dade44d145..b241dbd159 100644 --- a/tests/components/noblex/test.esp32-idf.yaml +++ b/tests/components/noblex/test.esp32-idf.yaml @@ -1 +1,5 @@ +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml + remote_receiver: !include ../../test_build_components/common/remote_receiver/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/noblex/test.esp8266-ard.yaml b/tests/components/noblex/test.esp8266-ard.yaml index dade44d145..aa8651e556 100644 --- a/tests/components/noblex/test.esp8266-ard.yaml +++ b/tests/components/noblex/test.esp8266-ard.yaml @@ -1 +1,5 @@ +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml + remote_receiver: !include ../../test_build_components/common/remote_receiver/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/npi19/common.yaml b/tests/components/npi19/common.yaml index 012e05695e..03550c269f 100644 --- a/tests/components/npi19/common.yaml +++ b/tests/components/npi19/common.yaml @@ -1,13 +1,7 @@ -i2c: - id: i2c_bus - scl: ${scl_pin} - sda: ${sda_pin} - frequency: 200kHz - sensor: - platform: npi19 - update_interval: 1s i2c_id: i2c_bus + update_interval: 1s temperature: name: water temperature diff --git a/tests/components/npi19/test.esp32-ard.yaml b/tests/components/npi19/test.esp32-ard.yaml deleted file mode 100644 index 3b761d3fc1..0000000000 --- a/tests/components/npi19/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO22 - sda_pin: GPIO21 - -<<: !include common.yaml diff --git a/tests/components/npi19/test.esp32-idf.yaml b/tests/components/npi19/test.esp32-idf.yaml index 3b761d3fc1..b47e39c389 100644 --- a/tests/components/npi19/test.esp32-idf.yaml +++ b/tests/components/npi19/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO22 - sda_pin: GPIO21 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/npi19/test.esp32-s3-ard.yaml b/tests/components/npi19/test.esp32-s3-ard.yaml deleted file mode 100644 index 4942e3c2b3..0000000000 --- a/tests/components/npi19/test.esp32-s3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO40 - sda_pin: GPIO41 - -<<: !include common.yaml diff --git a/tests/components/npi19/test.esp32-s3-idf.yaml b/tests/components/npi19/test.esp32-s3-idf.yaml deleted file mode 100644 index 4942e3c2b3..0000000000 --- a/tests/components/npi19/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO40 - sda_pin: GPIO41 - -<<: !include common.yaml diff --git a/tests/components/npi19/test.esp8266-ard.yaml b/tests/components/npi19/test.esp8266-ard.yaml index 3be5e53dcb..4a98b9388a 100644 --- a/tests/components/npi19/test.esp8266-ard.yaml +++ b/tests/components/npi19/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO05 - sda_pin: GPIO04 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/nrf52/test-bootloader.nrf52-xiao-ble.yaml b/tests/components/nrf52/test-bootloader.nrf52-xiao-ble.yaml new file mode 100644 index 0000000000..022ab9c753 --- /dev/null +++ b/tests/components/nrf52/test-bootloader.nrf52-xiao-ble.yaml @@ -0,0 +1,3 @@ +nrf52: + # it is not correct bootloader for the board + bootloader: adafruit_nrf52_sd140_v6 diff --git a/tests/components/nrf52/test.nrf52-adafruit.yaml b/tests/components/nrf52/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..5fa0d6e88f --- /dev/null +++ b/tests/components/nrf52/test.nrf52-adafruit.yaml @@ -0,0 +1,21 @@ +esphome: + on_boot: + - lambda: |- + int x = 100; + x = clamp(x, 50, 90); + assert(x == 90); + x = clamp_at_least(x, 95); + assert(x == 95); + x = clamp_at_most(x, 40); + assert(x == 40); +nrf52: + dfu: + reset_pin: + number: 14 + inverted: true + mode: + output: true + dcdc: False + reg0: + voltage: 2.1V + uicr_erase: true diff --git a/tests/components/nrf52/test.nrf52-mcumgr.yaml b/tests/components/nrf52/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..89ec637db6 --- /dev/null +++ b/tests/components/nrf52/test.nrf52-mcumgr.yaml @@ -0,0 +1,4 @@ +nrf52: + reg0: + voltage: 3.3V + uicr_erase: true diff --git a/tests/components/nrf52/test.nrf52-xiao-ble.yaml b/tests/components/nrf52/test.nrf52-xiao-ble.yaml new file mode 100644 index 0000000000..d53c692001 --- /dev/null +++ b/tests/components/nrf52/test.nrf52-xiao-ble.yaml @@ -0,0 +1,9 @@ +nrf52: + dfu: + reset_pin: + number: 14 + inverted: true + mode: + output: true + reg0: + voltage: 1.8V diff --git a/tests/components/ntc/test.esp32-ard.yaml b/tests/components/ntc/test.esp32-ard.yaml deleted file mode 100644 index 06864605a6..0000000000 --- a/tests/components/ntc/test.esp32-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO32 - -<<: !include common.yaml diff --git a/tests/components/ntc/test.esp32-c3-ard.yaml b/tests/components/ntc/test.esp32-c3-ard.yaml deleted file mode 100644 index 37fb325f4a..0000000000 --- a/tests/components/ntc/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ntc/test.esp32-c3-idf.yaml b/tests/components/ntc/test.esp32-c3-idf.yaml deleted file mode 100644 index 37fb325f4a..0000000000 --- a/tests/components/ntc/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ntc/test.esp32-s2-ard.yaml b/tests/components/ntc/test.esp32-s2-ard.yaml deleted file mode 100644 index 37fb325f4a..0000000000 --- a/tests/components/ntc/test.esp32-s2-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ntc/test.esp32-s3-ard.yaml b/tests/components/ntc/test.esp32-s3-ard.yaml deleted file mode 100644 index 37fb325f4a..0000000000 --- a/tests/components/ntc/test.esp32-s3-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ntc/test.esp32-s3-idf.yaml b/tests/components/ntc/test.esp32-s3-idf.yaml deleted file mode 100644 index 37fb325f4a..0000000000 --- a/tests/components/ntc/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/online_image/common-esp32.yaml b/tests/components/online_image/common-esp32.yaml index 787a1ad368..32c909d351 100644 --- a/tests/components/online_image/common-esp32.yaml +++ b/tests/components/online_image/common-esp32.yaml @@ -1,13 +1,11 @@ -<<: !include common.yaml +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml -spi: - - id: spi_main_lcd - clk_pin: 16 - mosi_pin: 17 - miso_pin: 18 +<<: !include common.yaml display: - platform: ili9xxx + spi_id: spi_bus id: main_lcd model: ili9342 cs_pin: 20 diff --git a/tests/components/online_image/common-esp8266.yaml b/tests/components/online_image/common-esp8266.yaml index ba15b5025c..d7722d171a 100644 --- a/tests/components/online_image/common-esp8266.yaml +++ b/tests/components/online_image/common-esp8266.yaml @@ -1,13 +1,11 @@ -<<: !include common.yaml +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml -spi: - - id: spi_main_lcd - clk_pin: 14 - mosi_pin: 13 - miso_pin: 12 +<<: !include common.yaml display: - platform: ili9xxx + spi_id: spi_bus id: main_lcd model: ili9342 cs_pin: 15 diff --git a/tests/components/online_image/common-rp2040.yaml b/tests/components/online_image/common-rp2040.yaml index 16bb2b2c44..25891b94bc 100644 --- a/tests/components/online_image/common-rp2040.yaml +++ b/tests/components/online_image/common-rp2040.yaml @@ -1,13 +1,11 @@ -<<: !include common.yaml +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml -spi: - - id: spi_main_lcd - clk_pin: 18 - mosi_pin: 19 - miso_pin: 16 +<<: !include common.yaml display: - platform: ili9xxx + spi_id: spi_bus id: main_lcd model: ili9342 cs_pin: 20 diff --git a/tests/components/online_image/test.esp32-ard.yaml b/tests/components/online_image/test.esp32-ard.yaml deleted file mode 100644 index 4111cbd0ad..0000000000 --- a/tests/components/online_image/test.esp32-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -<<: !include common-esp32.yaml - -http_request: - verify_ssl: false diff --git a/tests/components/opentherm/test.esp32-ard.yaml b/tests/components/opentherm/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/opentherm/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/opentherm/test.esp32-c3-ard.yaml b/tests/components/opentherm/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/opentherm/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/openthread/test.esp32-c6-idf.yaml b/tests/components/openthread/test.esp32-c6-idf.yaml index da5339fb39..9df63b2f29 100644 --- a/tests/components/openthread/test.esp32-c6-idf.yaml +++ b/tests/components/openthread/test.esp32-c6-idf.yaml @@ -2,7 +2,7 @@ network: enable_ipv6: true openthread: - device_type: FTD + device_type: MTD channel: 13 network_name: OpenThread-8f28 network_key: 0xdfd34f0f05cad978ec4e32b0413038ff @@ -11,3 +11,5 @@ openthread: pskc: 0xc23a76e98f1a6483639b1ac1271e2e27 mesh_local_prefix: fd53:145f:ed22:ad81::/64 force_dataset: true + use_address: open-thread-test.local + poll_period: 20sec diff --git a/tests/components/opt3001/common.yaml b/tests/components/opt3001/common.yaml index dab4f824f8..7b2cb339af 100644 --- a/tests/components/opt3001/common.yaml +++ b/tests/components/opt3001/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_opt3001 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: opt3001 + i2c_id: i2c_bus name: Living Room Brightness address: 0x44 update_interval: 30s diff --git a/tests/components/opt3001/test.esp32-ard.yaml b/tests/components/opt3001/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/opt3001/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/opt3001/test.esp32-c3-ard.yaml b/tests/components/opt3001/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/opt3001/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/opt3001/test.esp32-c3-idf.yaml b/tests/components/opt3001/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/opt3001/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/opt3001/test.esp32-idf.yaml b/tests/components/opt3001/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/opt3001/test.esp32-idf.yaml +++ b/tests/components/opt3001/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/opt3001/test.esp8266-ard.yaml b/tests/components/opt3001/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/opt3001/test.esp8266-ard.yaml +++ b/tests/components/opt3001/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/opt3001/test.rp2040-ard.yaml b/tests/components/opt3001/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/opt3001/test.rp2040-ard.yaml +++ b/tests/components/opt3001/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/ota/test.esp32-c3-ard.yaml b/tests/components/ota/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/ota/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/ota/test.esp32-c3-idf.yaml b/tests/components/ota/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/ota/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/output/test.esp32-ard.yaml b/tests/components/output/test.esp32-ard.yaml deleted file mode 100644 index 7687f2a7c8..0000000000 --- a/tests/components/output/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - output_platform: ledc - pin: GPIO12 - -<<: !include common.yaml diff --git a/tests/components/output/test.esp32-c3-ard.yaml b/tests/components/output/test.esp32-c3-ard.yaml deleted file mode 100644 index 2227643703..0000000000 --- a/tests/components/output/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - output_platform: ledc - pin: GPIO1 - -<<: !include common.yaml diff --git a/tests/components/output/test.esp32-c3-idf.yaml b/tests/components/output/test.esp32-c3-idf.yaml deleted file mode 100644 index 2227643703..0000000000 --- a/tests/components/output/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - output_platform: ledc - pin: GPIO1 - -<<: !include common.yaml diff --git a/tests/components/packages/test.esp32-ard.yaml b/tests/components/packages/test.esp32-ard.yaml deleted file mode 100644 index 9e4ceb09d6..0000000000 --- a/tests/components/packages/test.esp32-ard.yaml +++ /dev/null @@ -1,11 +0,0 @@ -packages: - - sensor: - - platform: template - id: inline_sensor - - !include package.yaml - - github://esphome/esphome/tests/components/template/common.yaml@dev - - url: https://github.com/esphome/esphome - path: tests/components/absolute_humidity - file: common.yaml - ref: dev - refresh: 1d diff --git a/tests/components/packet_transport/test.esp32-ard.yaml b/tests/components/packet_transport/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/packet_transport/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/packet_transport/test.esp32-c3-ard.yaml b/tests/components/packet_transport/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/packet_transport/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/packet_transport/test.esp32-c3-idf.yaml b/tests/components/packet_transport/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/packet_transport/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/packet_transport/test.esp32-idf.yaml b/tests/components/packet_transport/test.esp32-idf.yaml index dade44d145..b47e39c389 100644 --- a/tests/components/packet_transport/test.esp32-idf.yaml +++ b/tests/components/packet_transport/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/packet_transport/test.esp8266-ard.yaml b/tests/components/packet_transport/test.esp8266-ard.yaml index dade44d145..4a98b9388a 100644 --- a/tests/components/packet_transport/test.esp8266-ard.yaml +++ b/tests/components/packet_transport/test.esp8266-ard.yaml @@ -1 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/packet_transport/test.host.yaml b/tests/components/packet_transport/test.host.yaml index e735c37e4d..49fdbbc9b2 100644 --- a/tests/components/packet_transport/test.host.yaml +++ b/tests/components/packet_transport/test.host.yaml @@ -1,4 +1,40 @@ -packages: - common: !include common.yaml +udp: + listen_address: 239.0.60.53 + addresses: ["239.0.60.53"] -wifi: !remove +packet_transport: + platform: udp + update_interval: 5s + encryption: "our key goes here" + rolling_code_enable: true + ping_pong_enable: true + binary_sensors: + - binary_sensor_id1 + - id: binary_sensor_id1 + broadcast_id: other_id + sensors: + - sensor_id1 + - id: sensor_id1 + broadcast_id: other_id + providers: + - name: some-device-name + encryption: "their key goes here" + +sensor: + - platform: template + id: sensor_id1 + - platform: packet_transport + provider: some-device-name + id: our_id + remote_id: some_sensor_id + +binary_sensor: + - platform: packet_transport + provider: unencrypted-device + id: other_binary_sensor_id + - platform: packet_transport + provider: some-device-name + type: status + name: Some-Device Status + - platform: template + id: binary_sensor_id1 diff --git a/tests/components/packet_transport/test.rp2040-ard.yaml b/tests/components/packet_transport/test.rp2040-ard.yaml index dade44d145..319a7c71a6 100644 --- a/tests/components/packet_transport/test.rp2040-ard.yaml +++ b/tests/components/packet_transport/test.rp2040-ard.yaml @@ -1 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/partition/test.esp32-ard.yaml b/tests/components/partition/test.esp32-ard.yaml deleted file mode 100644 index 32cf2a64ba..0000000000 --- a/tests/components/partition/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - light_platform: esp32_rmt_led_strip - pin: GPIO2 - -<<: !include common-ard.yaml diff --git a/tests/components/partition/test.esp32-c3-ard.yaml b/tests/components/partition/test.esp32-c3-ard.yaml deleted file mode 100644 index 32cf2a64ba..0000000000 --- a/tests/components/partition/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - light_platform: esp32_rmt_led_strip - pin: GPIO2 - -<<: !include common-ard.yaml diff --git a/tests/components/pca6416a/common.yaml b/tests/components/pca6416a/common.yaml index ea538387e4..9ad6e2fb15 100644 --- a/tests/components/pca6416a/common.yaml +++ b/tests/components/pca6416a/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_pca6416a - scl: ${scl_pin} - sda: ${sda_pin} - pca6416a: - id: pca6416a_hub + i2c_id: i2c_bus address: 0x21 binary_sensor: diff --git a/tests/components/pca6416a/test.esp32-ard.yaml b/tests/components/pca6416a/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/pca6416a/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/pca6416a/test.esp32-c3-ard.yaml b/tests/components/pca6416a/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/pca6416a/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/pca6416a/test.esp32-c3-idf.yaml b/tests/components/pca6416a/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/pca6416a/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/pca6416a/test.esp32-idf.yaml b/tests/components/pca6416a/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/pca6416a/test.esp32-idf.yaml +++ b/tests/components/pca6416a/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/pca6416a/test.esp8266-ard.yaml b/tests/components/pca6416a/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/pca6416a/test.esp8266-ard.yaml +++ b/tests/components/pca6416a/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/pca6416a/test.rp2040-ard.yaml b/tests/components/pca6416a/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/pca6416a/test.rp2040-ard.yaml +++ b/tests/components/pca6416a/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/pca9554/common.yaml b/tests/components/pca9554/common.yaml index db2ec2b826..9e5e7f3342 100644 --- a/tests/components/pca9554/common.yaml +++ b/tests/components/pca9554/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_pca9554 - scl: ${scl_pin} - sda: ${sda_pin} - pca9554: - id: pca9554_hub + i2c_id: i2c_bus pin_count: 8 address: 0x3F diff --git a/tests/components/pca9554/test.esp32-ard.yaml b/tests/components/pca9554/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/pca9554/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/pca9554/test.esp32-c3-ard.yaml b/tests/components/pca9554/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/pca9554/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/pca9554/test.esp32-c3-idf.yaml b/tests/components/pca9554/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/pca9554/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/pca9554/test.esp32-idf.yaml b/tests/components/pca9554/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/pca9554/test.esp32-idf.yaml +++ b/tests/components/pca9554/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/pca9554/test.esp8266-ard.yaml b/tests/components/pca9554/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/pca9554/test.esp8266-ard.yaml +++ b/tests/components/pca9554/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/pca9554/test.rp2040-ard.yaml b/tests/components/pca9554/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/pca9554/test.rp2040-ard.yaml +++ b/tests/components/pca9554/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/pca9685/common.yaml b/tests/components/pca9685/common.yaml index 57cdc5de9b..2e238b481c 100644 --- a/tests/components/pca9685/common.yaml +++ b/tests/components/pca9685/common.yaml @@ -1,9 +1,5 @@ -i2c: - - id: i2c_pca9685 - scl: ${scl_pin} - sda: ${sda_pin} - pca9685: + i2c_id: i2c_bus frequency: 500 address: 0x0 diff --git a/tests/components/pca9685/test.esp32-ard.yaml b/tests/components/pca9685/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/pca9685/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/pca9685/test.esp32-c3-ard.yaml b/tests/components/pca9685/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/pca9685/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/pca9685/test.esp32-c3-idf.yaml b/tests/components/pca9685/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/pca9685/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/pca9685/test.esp32-idf.yaml b/tests/components/pca9685/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/pca9685/test.esp32-idf.yaml +++ b/tests/components/pca9685/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/pca9685/test.esp8266-ard.yaml b/tests/components/pca9685/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/pca9685/test.esp8266-ard.yaml +++ b/tests/components/pca9685/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/pca9685/test.rp2040-ard.yaml b/tests/components/pca9685/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/pca9685/test.rp2040-ard.yaml +++ b/tests/components/pca9685/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/pcd8544/common.yaml b/tests/components/pcd8544/common.yaml index 0fb969eeb8..418cd97253 100644 --- a/tests/components/pcd8544/common.yaml +++ b/tests/components/pcd8544/common.yaml @@ -1,9 +1,3 @@ -spi: - - id: spi_pcd8544 - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - miso_pin: ${miso_pin} - display: - platform: pcd8544 cs_pin: ${cs_pin} diff --git a/tests/components/pcd8544/test.esp32-ard.yaml b/tests/components/pcd8544/test.esp32-ard.yaml deleted file mode 100644 index 09e9db5a38..0000000000 --- a/tests/components/pcd8544/test.esp32-ard.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO18 - cs_pin: GPIO12 - dc_pin: GPIO13 - reset_pin: GPIO14 - -<<: !include common.yaml diff --git a/tests/components/pcd8544/test.esp32-c3-ard.yaml b/tests/components/pcd8544/test.esp32-c3-ard.yaml deleted file mode 100644 index c5c932c92c..0000000000 --- a/tests/components/pcd8544/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - dc_pin: GPIO9 - reset_pin: GPIO10 - -<<: !include common.yaml diff --git a/tests/components/pcd8544/test.esp32-c3-idf.yaml b/tests/components/pcd8544/test.esp32-c3-idf.yaml deleted file mode 100644 index c5c932c92c..0000000000 --- a/tests/components/pcd8544/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - dc_pin: GPIO9 - reset_pin: GPIO10 - -<<: !include common.yaml diff --git a/tests/components/pcd8544/test.esp32-idf.yaml b/tests/components/pcd8544/test.esp32-idf.yaml index 09e9db5a38..ff174a4656 100644 --- a/tests/components/pcd8544/test.esp32-idf.yaml +++ b/tests/components/pcd8544/test.esp32-idf.yaml @@ -1,9 +1,9 @@ substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO18 cs_pin: GPIO12 dc_pin: GPIO13 reset_pin: GPIO14 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/pcd8544/test.esp8266-ard.yaml b/tests/components/pcd8544/test.esp8266-ard.yaml index 3f023a60eb..56cb29f29e 100644 --- a/tests/components/pcd8544/test.esp8266-ard.yaml +++ b/tests/components/pcd8544/test.esp8266-ard.yaml @@ -1,9 +1,12 @@ substitutions: - clk_pin: GPIO14 - mosi_pin: GPIO13 + clk_pin: GPIO0 + mosi_pin: GPIO2 miso_pin: GPIO12 cs_pin: GPIO5 dc_pin: GPIO15 reset_pin: GPIO16 +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/pcd8544/test.rp2040-ard.yaml b/tests/components/pcd8544/test.rp2040-ard.yaml index d7fd6ee294..66caa956f7 100644 --- a/tests/components/pcd8544/test.rp2040-ard.yaml +++ b/tests/components/pcd8544/test.rp2040-ard.yaml @@ -6,4 +6,7 @@ substitutions: dc_pin: GPIO15 reset_pin: GPIO16 +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/pcf85063/common.yaml b/tests/components/pcf85063/common.yaml index f3b68412c5..170029ad85 100644 --- a/tests/components/pcf85063/common.yaml +++ b/tests/components/pcf85063/common.yaml @@ -3,10 +3,6 @@ esphome: - pcf85063.read_time - pcf85063.write_time -i2c: - - id: i2c_pcf85063 - scl: ${scl_pin} - sda: ${sda_pin} - time: - platform: pcf85063 + i2c_id: i2c_bus diff --git a/tests/components/pcf85063/test.esp32-ard.yaml b/tests/components/pcf85063/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/pcf85063/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/pcf85063/test.esp32-c3-ard.yaml b/tests/components/pcf85063/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/pcf85063/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/pcf85063/test.esp32-c3-idf.yaml b/tests/components/pcf85063/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/pcf85063/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/pcf85063/test.esp32-idf.yaml b/tests/components/pcf85063/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/pcf85063/test.esp32-idf.yaml +++ b/tests/components/pcf85063/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/pcf85063/test.esp8266-ard.yaml b/tests/components/pcf85063/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/pcf85063/test.esp8266-ard.yaml +++ b/tests/components/pcf85063/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/pcf85063/test.nrf52-adafruit.yaml b/tests/components/pcf85063/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..2a0de6241c --- /dev/null +++ b/tests/components/pcf85063/test.nrf52-adafruit.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/nrf52.yaml + +<<: !include common.yaml diff --git a/tests/components/pcf85063/test.rp2040-ard.yaml b/tests/components/pcf85063/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/pcf85063/test.rp2040-ard.yaml +++ b/tests/components/pcf85063/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/pcf8563/common.yaml b/tests/components/pcf8563/common.yaml index 30be6b893c..ac5f3afed1 100644 --- a/tests/components/pcf8563/common.yaml +++ b/tests/components/pcf8563/common.yaml @@ -3,10 +3,6 @@ esphome: - pcf8563.read_time - pcf8563.write_time -i2c: - - id: i2c_pcf8563 - scl: ${scl_pin} - sda: ${sda_pin} - time: - platform: pcf8563 + i2c_id: i2c_bus diff --git a/tests/components/pcf8563/test.esp32-ard.yaml b/tests/components/pcf8563/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/pcf8563/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/pcf8563/test.esp32-c3-ard.yaml b/tests/components/pcf8563/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/pcf8563/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/pcf8563/test.esp32-c3-idf.yaml b/tests/components/pcf8563/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/pcf8563/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/pcf8563/test.esp32-idf.yaml b/tests/components/pcf8563/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/pcf8563/test.esp32-idf.yaml +++ b/tests/components/pcf8563/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/pcf8563/test.esp8266-ard.yaml b/tests/components/pcf8563/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/pcf8563/test.esp8266-ard.yaml +++ b/tests/components/pcf8563/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/pcf8563/test.nrf52-adafruit.yaml b/tests/components/pcf8563/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..2a0de6241c --- /dev/null +++ b/tests/components/pcf8563/test.nrf52-adafruit.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/nrf52.yaml + +<<: !include common.yaml diff --git a/tests/components/pcf8563/test.rp2040-ard.yaml b/tests/components/pcf8563/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/pcf8563/test.rp2040-ard.yaml +++ b/tests/components/pcf8563/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/pcf8574/common.yaml b/tests/components/pcf8574/common.yaml index f5fcfad64a..09fa33164e 100644 --- a/tests/components/pcf8574/common.yaml +++ b/tests/components/pcf8574/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_pcf8574 - scl: ${scl_pin} - sda: ${sda_pin} - pcf8574: - id: pcf8574_hub + i2c_id: i2c_bus address: 0x21 pcf8575: false diff --git a/tests/components/pcf8574/test.esp32-ard.yaml b/tests/components/pcf8574/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/pcf8574/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/pcf8574/test.esp32-c3-ard.yaml b/tests/components/pcf8574/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/pcf8574/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/pcf8574/test.esp32-c3-idf.yaml b/tests/components/pcf8574/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/pcf8574/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/pcf8574/test.esp32-idf.yaml b/tests/components/pcf8574/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/pcf8574/test.esp32-idf.yaml +++ b/tests/components/pcf8574/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/pcf8574/test.esp8266-ard.yaml b/tests/components/pcf8574/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/pcf8574/test.esp8266-ard.yaml +++ b/tests/components/pcf8574/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/pcf8574/test.rp2040-ard.yaml b/tests/components/pcf8574/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/pcf8574/test.rp2040-ard.yaml +++ b/tests/components/pcf8574/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/pi4ioe5v6408/common.yaml b/tests/components/pi4ioe5v6408/common.yaml index 4130dc2652..2344622081 100644 --- a/tests/components/pi4ioe5v6408/common.yaml +++ b/tests/components/pi4ioe5v6408/common.yaml @@ -1,9 +1,5 @@ -i2c: - id: i2c_pi4ioe5v6408 - sda: ${i2c_sda} - scl: ${i2c_scl} - pi4ioe5v6408: + i2c_id: i2c_bus id: pi4ioe1 address: 0x44 diff --git a/tests/components/pi4ioe5v6408/test.esp32-ard.yaml b/tests/components/pi4ioe5v6408/test.esp32-ard.yaml deleted file mode 100644 index 55e6edfbf3..0000000000 --- a/tests/components/pi4ioe5v6408/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - i2c_sda: GPIO21 - i2c_scl: GPIO22 - -<<: !include common.yaml diff --git a/tests/components/pi4ioe5v6408/test.esp32-idf.yaml b/tests/components/pi4ioe5v6408/test.esp32-idf.yaml index 55e6edfbf3..9a4779d822 100644 --- a/tests/components/pi4ioe5v6408/test.esp32-idf.yaml +++ b/tests/components/pi4ioe5v6408/test.esp32-idf.yaml @@ -2,4 +2,7 @@ substitutions: i2c_sda: GPIO21 i2c_scl: GPIO22 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/pi4ioe5v6408/test.rp2040-ard.yaml b/tests/components/pi4ioe5v6408/test.rp2040-ard.yaml index b7b6b13bfe..3429a2952a 100644 --- a/tests/components/pi4ioe5v6408/test.rp2040-ard.yaml +++ b/tests/components/pi4ioe5v6408/test.rp2040-ard.yaml @@ -2,4 +2,7 @@ substitutions: i2c_sda: GPIO4 i2c_scl: GPIO5 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/pid/common.yaml b/tests/components/pid/common.yaml index 5f7762872f..262e75591e 100644 --- a/tests/components/pid/common.yaml +++ b/tests/components/pid/common.yaml @@ -27,9 +27,8 @@ sensor: lambda: |- if (millis() > 10000) { return 42.0; - } else { - return 0.0; } + return 0.0; update_interval: 60s climate: diff --git a/tests/components/pid/test.esp32-ard.yaml b/tests/components/pid/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/pid/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/pid/test.esp32-c3-ard.yaml b/tests/components/pid/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/pid/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/pid/test.esp32-c3-idf.yaml b/tests/components/pid/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/pid/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/pipsolar/common.yaml b/tests/components/pipsolar/common.yaml index 64e2c0476d..819764a7ea 100644 --- a/tests/components/pipsolar/common.yaml +++ b/tests/components/pipsolar/common.yaml @@ -5,12 +5,6 @@ esphome: id: inverter0_battery_recharge_voltage_out value: 48.0 -uart: - - id: uart_pipsolar - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 115200 - pipsolar: id: inverter0 diff --git a/tests/components/pipsolar/test.esp32-ard.yaml b/tests/components/pipsolar/test.esp32-ard.yaml deleted file mode 100644 index f486544afa..0000000000 --- a/tests/components/pipsolar/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 - -<<: !include common.yaml diff --git a/tests/components/pipsolar/test.esp32-c3-ard.yaml b/tests/components/pipsolar/test.esp32-c3-ard.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/pipsolar/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/pipsolar/test.esp32-c3-idf.yaml b/tests/components/pipsolar/test.esp32-c3-idf.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/pipsolar/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/pipsolar/test.esp32-idf.yaml b/tests/components/pipsolar/test.esp32-idf.yaml index f486544afa..b415125e84 100644 --- a/tests/components/pipsolar/test.esp32-idf.yaml +++ b/tests/components/pipsolar/test.esp32-idf.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 + tx_pin: GPIO4 + rx_pin: GPIO5 + +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/pipsolar/test.esp32-s2-idf.yaml b/tests/components/pipsolar/test.esp32-s2-idf.yaml index b516342f3b..2d29656c94 100644 --- a/tests/components/pipsolar/test.esp32-s2-idf.yaml +++ b/tests/components/pipsolar/test.esp32-s2-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/pipsolar/test.esp8266-ard.yaml b/tests/components/pipsolar/test.esp8266-ard.yaml index b516342f3b..96ab4ef6ac 100644 --- a/tests/components/pipsolar/test.esp8266-ard.yaml +++ b/tests/components/pipsolar/test.esp8266-ard.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: GPIO0 + rx_pin: GPIO2 + +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/pipsolar/test.rp2040-ard.yaml b/tests/components/pipsolar/test.rp2040-ard.yaml index b516342f3b..b28f2b5e05 100644 --- a/tests/components/pipsolar/test.rp2040-ard.yaml +++ b/tests/components/pipsolar/test.rp2040-ard.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO4 rx_pin: GPIO5 +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/pm1006/common.yaml b/tests/components/pm1006/common.yaml index 4e3e880f4e..43955bb099 100644 --- a/tests/components/pm1006/common.yaml +++ b/tests/components/pm1006/common.yaml @@ -1,9 +1,3 @@ -uart: - - id: uart_pm1006 - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 9600 - sensor: - platform: pm1006 pm_2_5: diff --git a/tests/components/pm1006/test.esp32-ard.yaml b/tests/components/pm1006/test.esp32-ard.yaml deleted file mode 100644 index f486544afa..0000000000 --- a/tests/components/pm1006/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 - -<<: !include common.yaml diff --git a/tests/components/pm1006/test.esp32-c3-ard.yaml b/tests/components/pm1006/test.esp32-c3-ard.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/pm1006/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/pm1006/test.esp32-c3-idf.yaml b/tests/components/pm1006/test.esp32-c3-idf.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/pm1006/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/pm1006/test.esp32-idf.yaml b/tests/components/pm1006/test.esp32-idf.yaml index f486544afa..b415125e84 100644 --- a/tests/components/pm1006/test.esp32-idf.yaml +++ b/tests/components/pm1006/test.esp32-idf.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 + tx_pin: GPIO4 + rx_pin: GPIO5 + +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/pm1006/test.esp8266-ard.yaml b/tests/components/pm1006/test.esp8266-ard.yaml index b516342f3b..96ab4ef6ac 100644 --- a/tests/components/pm1006/test.esp8266-ard.yaml +++ b/tests/components/pm1006/test.esp8266-ard.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: GPIO0 + rx_pin: GPIO2 + +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/pm1006/test.rp2040-ard.yaml b/tests/components/pm1006/test.rp2040-ard.yaml index b516342f3b..b28f2b5e05 100644 --- a/tests/components/pm1006/test.rp2040-ard.yaml +++ b/tests/components/pm1006/test.rp2040-ard.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO4 rx_pin: GPIO5 +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/pm2005/common.yaml b/tests/components/pm2005/common.yaml index b8f6683b22..034752d0b9 100644 --- a/tests/components/pm2005/common.yaml +++ b/tests/components/pm2005/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_pm2005 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: pm2005 + i2c_id: i2c_bus pm_1_0: name: PM1.0 pm_2_5: diff --git a/tests/components/pm2005/test.esp32-ard.yaml b/tests/components/pm2005/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/pm2005/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/pm2005/test.esp32-c3-ard.yaml b/tests/components/pm2005/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/pm2005/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/pm2005/test.esp32-c3-idf.yaml b/tests/components/pm2005/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/pm2005/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/pm2005/test.esp32-idf.yaml b/tests/components/pm2005/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/pm2005/test.esp32-idf.yaml +++ b/tests/components/pm2005/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/pm2005/test.esp8266-ard.yaml b/tests/components/pm2005/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/pm2005/test.esp8266-ard.yaml +++ b/tests/components/pm2005/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/pm2005/test.rp2040-ard.yaml b/tests/components/pm2005/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/pm2005/test.rp2040-ard.yaml +++ b/tests/components/pm2005/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/pmsa003i/common.yaml b/tests/components/pmsa003i/common.yaml index 95e62da694..7267bd58f3 100644 --- a/tests/components/pmsa003i/common.yaml +++ b/tests/components/pmsa003i/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_pmsa003i - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: pmsa003i + i2c_id: i2c_bus pm_1_0: name: PMSA003i PM1.0 pm_2_5: diff --git a/tests/components/pmsa003i/test.esp32-ard.yaml b/tests/components/pmsa003i/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/pmsa003i/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/pmsa003i/test.esp32-c3-ard.yaml b/tests/components/pmsa003i/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/pmsa003i/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/pmsa003i/test.esp32-c3-idf.yaml b/tests/components/pmsa003i/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/pmsa003i/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/pmsa003i/test.esp32-idf.yaml b/tests/components/pmsa003i/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/pmsa003i/test.esp32-idf.yaml +++ b/tests/components/pmsa003i/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/pmsa003i/test.esp8266-ard.yaml b/tests/components/pmsa003i/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/pmsa003i/test.esp8266-ard.yaml +++ b/tests/components/pmsa003i/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/pmsa003i/test.rp2040-ard.yaml b/tests/components/pmsa003i/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/pmsa003i/test.rp2040-ard.yaml +++ b/tests/components/pmsa003i/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/pmsx003/common.yaml b/tests/components/pmsx003/common.yaml index 7b9ca5b091..3c60995804 100644 --- a/tests/components/pmsx003/common.yaml +++ b/tests/components/pmsx003/common.yaml @@ -1,9 +1,3 @@ -uart: - - id: uart_pmsx003 - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 9600 - sensor: - platform: pmsx003 type: PMSX003 diff --git a/tests/components/pmsx003/test.esp32-ard.yaml b/tests/components/pmsx003/test.esp32-ard.yaml deleted file mode 100644 index f486544afa..0000000000 --- a/tests/components/pmsx003/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 - -<<: !include common.yaml diff --git a/tests/components/pmsx003/test.esp32-c3-ard.yaml b/tests/components/pmsx003/test.esp32-c3-ard.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/pmsx003/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/pmsx003/test.esp32-c3-idf.yaml b/tests/components/pmsx003/test.esp32-c3-idf.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/pmsx003/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/pmsx003/test.esp32-idf.yaml b/tests/components/pmsx003/test.esp32-idf.yaml index f486544afa..b415125e84 100644 --- a/tests/components/pmsx003/test.esp32-idf.yaml +++ b/tests/components/pmsx003/test.esp32-idf.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 + tx_pin: GPIO4 + rx_pin: GPIO5 + +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/pmsx003/test.esp8266-ard.yaml b/tests/components/pmsx003/test.esp8266-ard.yaml index b516342f3b..96ab4ef6ac 100644 --- a/tests/components/pmsx003/test.esp8266-ard.yaml +++ b/tests/components/pmsx003/test.esp8266-ard.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: GPIO0 + rx_pin: GPIO2 + +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/pmsx003/test.rp2040-ard.yaml b/tests/components/pmsx003/test.rp2040-ard.yaml index b516342f3b..b28f2b5e05 100644 --- a/tests/components/pmsx003/test.rp2040-ard.yaml +++ b/tests/components/pmsx003/test.rp2040-ard.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO4 rx_pin: GPIO5 +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/pmwcs3/common.yaml b/tests/components/pmwcs3/common.yaml index dcf59d0b6e..e06400d4d4 100644 --- a/tests/components/pmwcs3/common.yaml +++ b/tests/components/pmwcs3/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_pmwcs3 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: pmwcs3 + i2c_id: i2c_bus e25: name: pmwcs3_e25 ec: diff --git a/tests/components/pmwcs3/test.esp32-ard.yaml b/tests/components/pmwcs3/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/pmwcs3/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/pmwcs3/test.esp32-c3-ard.yaml b/tests/components/pmwcs3/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/pmwcs3/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/pmwcs3/test.esp32-c3-idf.yaml b/tests/components/pmwcs3/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/pmwcs3/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/pmwcs3/test.esp32-idf.yaml b/tests/components/pmwcs3/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/pmwcs3/test.esp32-idf.yaml +++ b/tests/components/pmwcs3/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/pmwcs3/test.esp8266-ard.yaml b/tests/components/pmwcs3/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/pmwcs3/test.esp8266-ard.yaml +++ b/tests/components/pmwcs3/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/pmwcs3/test.rp2040-ard.yaml b/tests/components/pmwcs3/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/pmwcs3/test.rp2040-ard.yaml +++ b/tests/components/pmwcs3/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/pn532_i2c/common.yaml b/tests/components/pn532_i2c/common.yaml index db9abd4b13..947e1151aa 100644 --- a/tests/components/pn532_i2c/common.yaml +++ b/tests/components/pn532_i2c/common.yaml @@ -1,13 +1,9 @@ -i2c: - - id: i2c_pn532 - scl: ${scl_pin} - sda: ${sda_pin} - pn532_i2c: - id: pn532_nfcc + i2c_id: i2c_bus + id: pn532_nfcc_i2c binary_sensor: - platform: pn532 - pn532_id: pn532_nfcc + pn532_id: pn532_nfcc_i2c name: PN532 NFC Tag uid: 74-10-37-94 diff --git a/tests/components/pn532_i2c/test.esp32-ard.yaml b/tests/components/pn532_i2c/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/pn532_i2c/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/pn532_i2c/test.esp32-c3-ard.yaml b/tests/components/pn532_i2c/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/pn532_i2c/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/pn532_i2c/test.esp32-c3-idf.yaml b/tests/components/pn532_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/pn532_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/pn532_i2c/test.esp32-idf.yaml b/tests/components/pn532_i2c/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/pn532_i2c/test.esp32-idf.yaml +++ b/tests/components/pn532_i2c/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/pn532_i2c/test.esp8266-ard.yaml b/tests/components/pn532_i2c/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/pn532_i2c/test.esp8266-ard.yaml +++ b/tests/components/pn532_i2c/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/pn532_i2c/test.rp2040-ard.yaml b/tests/components/pn532_i2c/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/pn532_i2c/test.rp2040-ard.yaml +++ b/tests/components/pn532_i2c/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/pn532_spi/common.yaml b/tests/components/pn532_spi/common.yaml index d5b8bc405e..f9149af35f 100644 --- a/tests/components/pn532_spi/common.yaml +++ b/tests/components/pn532_spi/common.yaml @@ -1,15 +1,9 @@ -spi: - - id: spi_pn532 - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - miso_pin: ${miso_pin} - pn532_spi: - id: pn532_nfcc + id: pn532_nfcc_spi cs_pin: ${cs_pin} binary_sensor: - platform: pn532 - pn532_id: pn532_nfcc + pn532_id: pn532_nfcc_spi name: PN532 NFC Tag uid: 74-10-37-94 diff --git a/tests/components/pn532_spi/test.esp32-ard.yaml b/tests/components/pn532_spi/test.esp32-ard.yaml deleted file mode 100644 index bce56f398a..0000000000 --- a/tests/components/pn532_spi/test.esp32-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO18 - cs_pin: GPIO12 - -<<: !include common.yaml diff --git a/tests/components/pn532_spi/test.esp32-c3-ard.yaml b/tests/components/pn532_spi/test.esp32-c3-ard.yaml deleted file mode 100644 index 2415ba5dc6..0000000000 --- a/tests/components/pn532_spi/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/pn532_spi/test.esp32-c3-idf.yaml b/tests/components/pn532_spi/test.esp32-c3-idf.yaml deleted file mode 100644 index 2415ba5dc6..0000000000 --- a/tests/components/pn532_spi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/pn532_spi/test.esp32-idf.yaml b/tests/components/pn532_spi/test.esp32-idf.yaml index bce56f398a..9bb524aa65 100644 --- a/tests/components/pn532_spi/test.esp32-idf.yaml +++ b/tests/components/pn532_spi/test.esp32-idf.yaml @@ -1,7 +1,7 @@ substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO18 cs_pin: GPIO12 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/pn532_spi/test.esp8266-ard.yaml b/tests/components/pn532_spi/test.esp8266-ard.yaml index bd5c203e35..1aac800592 100644 --- a/tests/components/pn532_spi/test.esp8266-ard.yaml +++ b/tests/components/pn532_spi/test.esp8266-ard.yaml @@ -1,7 +1,10 @@ substitutions: - clk_pin: GPIO14 - mosi_pin: GPIO13 - miso_pin: GPIO12 - cs_pin: GPIO5 + clk_pin: GPIO0 + mosi_pin: GPIO2 + miso_pin: GPIO15 + cs_pin: GPIO16 + +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/pn532_spi/test.rp2040-ard.yaml b/tests/components/pn532_spi/test.rp2040-ard.yaml index f6c3f1eeca..1ded24de1c 100644 --- a/tests/components/pn532_spi/test.rp2040-ard.yaml +++ b/tests/components/pn532_spi/test.rp2040-ard.yaml @@ -4,4 +4,7 @@ substitutions: miso_pin: GPIO4 cs_pin: GPIO5 +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/pn7150_i2c/common.yaml b/tests/components/pn7150_i2c/common.yaml index ef852b7a78..a317b72b9c 100644 --- a/tests/components/pn7150_i2c/common.yaml +++ b/tests/components/pn7150_i2c/common.yaml @@ -16,13 +16,9 @@ esphome: - tag.polling_off: nfcc_pn7150 - tag.polling_on: nfcc_pn7150 -i2c: - - id: i2c_pn7150 - scl: ${scl_pin} - sda: ${sda_pin} - pn7150_i2c: id: nfcc_pn7150 + i2c_id: i2c_bus irq_pin: ${irq_pin} ven_pin: ${ven_pin} emulation_message: https://www.home-assistant.io/tag/pulse_ce diff --git a/tests/components/pn7150_i2c/test.esp32-ard.yaml b/tests/components/pn7150_i2c/test.esp32-ard.yaml deleted file mode 100644 index 1643bec317..0000000000 --- a/tests/components/pn7150_i2c/test.esp32-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - irq_pin: GPIO14 - ven_pin: GPIO15 - -<<: !include common.yaml diff --git a/tests/components/pn7150_i2c/test.esp32-c3-ard.yaml b/tests/components/pn7150_i2c/test.esp32-c3-ard.yaml deleted file mode 100644 index 2067143411..0000000000 --- a/tests/components/pn7150_i2c/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - irq_pin: GPIO6 - ven_pin: GPIO7 - -<<: !include common.yaml diff --git a/tests/components/pn7150_i2c/test.esp32-c3-idf.yaml b/tests/components/pn7150_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index 2067143411..0000000000 --- a/tests/components/pn7150_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - irq_pin: GPIO6 - ven_pin: GPIO7 - -<<: !include common.yaml diff --git a/tests/components/pn7150_i2c/test.esp32-idf.yaml b/tests/components/pn7150_i2c/test.esp32-idf.yaml index 1643bec317..9a4cc6669e 100644 --- a/tests/components/pn7150_i2c/test.esp32-idf.yaml +++ b/tests/components/pn7150_i2c/test.esp32-idf.yaml @@ -1,7 +1,8 @@ substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 irq_pin: GPIO14 ven_pin: GPIO15 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/pn7150_i2c/test.esp8266-ard.yaml b/tests/components/pn7150_i2c/test.esp8266-ard.yaml index 7111fc9e00..b907e56f80 100644 --- a/tests/components/pn7150_i2c/test.esp8266-ard.yaml +++ b/tests/components/pn7150_i2c/test.esp8266-ard.yaml @@ -1,7 +1,8 @@ substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - irq_pin: GPIO12 - ven_pin: GPIO13 + irq_pin: GPIO15 + ven_pin: GPIO16 + +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/pn7150_i2c/test.rp2040-ard.yaml b/tests/components/pn7150_i2c/test.rp2040-ard.yaml index 2067143411..b320b947e3 100644 --- a/tests/components/pn7150_i2c/test.rp2040-ard.yaml +++ b/tests/components/pn7150_i2c/test.rp2040-ard.yaml @@ -1,7 +1,8 @@ substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 irq_pin: GPIO6 ven_pin: GPIO7 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/pn7160_i2c/common.yaml b/tests/components/pn7160_i2c/common.yaml index 0a7c9bd6bb..fa9a876d1c 100644 --- a/tests/components/pn7160_i2c/common.yaml +++ b/tests/components/pn7160_i2c/common.yaml @@ -1,28 +1,24 @@ esphome: on_boot: then: - - tag.set_clean_mode: nfcc_pn7160 - - tag.set_format_mode: nfcc_pn7160 - - tag.set_read_mode: nfcc_pn7160 + - tag.set_clean_mode: nfcc_pn7160_i2c + - tag.set_format_mode: nfcc_pn7160_i2c + - tag.set_read_mode: nfcc_pn7160_i2c - tag.set_write_message: message: https://www.home-assistant.io/tag/pulse include_android_app_record: false - - tag.set_write_mode: nfcc_pn7160 + - tag.set_write_mode: nfcc_pn7160_i2c - tag.set_emulation_message: message: https://www.home-assistant.io/tag/pulse include_android_app_record: false - - tag.emulation_off: nfcc_pn7160 - - tag.emulation_on: nfcc_pn7160 - - tag.polling_off: nfcc_pn7160 - - tag.polling_on: nfcc_pn7160 - -i2c: - - id: i2c_pn7160 - scl: ${scl_pin} - sda: ${sda_pin} + - tag.emulation_off: nfcc_pn7160_i2c + - tag.emulation_on: nfcc_pn7160_i2c + - tag.polling_off: nfcc_pn7160_i2c + - tag.polling_on: nfcc_pn7160_i2c pn7150_i2c: - id: nfcc_pn7160 + id: nfcc_pn7160_i2c + i2c_id: i2c_bus irq_pin: ${irq_pin} ven_pin: ${ven_pin} emulation_message: https://www.home-assistant.io/tag/pulse_ce diff --git a/tests/components/pn7160_i2c/test.esp32-ard.yaml b/tests/components/pn7160_i2c/test.esp32-ard.yaml deleted file mode 100644 index 1643bec317..0000000000 --- a/tests/components/pn7160_i2c/test.esp32-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - irq_pin: GPIO14 - ven_pin: GPIO15 - -<<: !include common.yaml diff --git a/tests/components/pn7160_i2c/test.esp32-c3-ard.yaml b/tests/components/pn7160_i2c/test.esp32-c3-ard.yaml deleted file mode 100644 index 2067143411..0000000000 --- a/tests/components/pn7160_i2c/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - irq_pin: GPIO6 - ven_pin: GPIO7 - -<<: !include common.yaml diff --git a/tests/components/pn7160_i2c/test.esp32-c3-idf.yaml b/tests/components/pn7160_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index 2067143411..0000000000 --- a/tests/components/pn7160_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - irq_pin: GPIO6 - ven_pin: GPIO7 - -<<: !include common.yaml diff --git a/tests/components/pn7160_i2c/test.esp32-idf.yaml b/tests/components/pn7160_i2c/test.esp32-idf.yaml index 1643bec317..9a4cc6669e 100644 --- a/tests/components/pn7160_i2c/test.esp32-idf.yaml +++ b/tests/components/pn7160_i2c/test.esp32-idf.yaml @@ -1,7 +1,8 @@ substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 irq_pin: GPIO14 ven_pin: GPIO15 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/pn7160_i2c/test.esp8266-ard.yaml b/tests/components/pn7160_i2c/test.esp8266-ard.yaml index 7111fc9e00..b907e56f80 100644 --- a/tests/components/pn7160_i2c/test.esp8266-ard.yaml +++ b/tests/components/pn7160_i2c/test.esp8266-ard.yaml @@ -1,7 +1,8 @@ substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - irq_pin: GPIO12 - ven_pin: GPIO13 + irq_pin: GPIO15 + ven_pin: GPIO16 + +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/pn7160_i2c/test.rp2040-ard.yaml b/tests/components/pn7160_i2c/test.rp2040-ard.yaml index 2067143411..b320b947e3 100644 --- a/tests/components/pn7160_i2c/test.rp2040-ard.yaml +++ b/tests/components/pn7160_i2c/test.rp2040-ard.yaml @@ -1,7 +1,8 @@ substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 irq_pin: GPIO6 ven_pin: GPIO7 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/pn7160_spi/common.yaml b/tests/components/pn7160_spi/common.yaml index 9e8d22f835..53b37b38f4 100644 --- a/tests/components/pn7160_spi/common.yaml +++ b/tests/components/pn7160_spi/common.yaml @@ -1,29 +1,23 @@ esphome: on_boot: then: - - tag.set_clean_mode: nfcc_pn7160 - - tag.set_format_mode: nfcc_pn7160 - - tag.set_read_mode: nfcc_pn7160 + - tag.set_clean_mode: nfcc_pn7160_spi + - tag.set_format_mode: nfcc_pn7160_spi + - tag.set_read_mode: nfcc_pn7160_spi - tag.set_write_message: message: https://www.home-assistant.io/tag/pulse include_android_app_record: false - - tag.set_write_mode: nfcc_pn7160 + - tag.set_write_mode: nfcc_pn7160_spi - tag.set_emulation_message: message: https://www.home-assistant.io/tag/pulse include_android_app_record: false - - tag.emulation_off: nfcc_pn7160 - - tag.emulation_on: nfcc_pn7160 - - tag.polling_off: nfcc_pn7160 - - tag.polling_on: nfcc_pn7160 - -spi: - - id: spi_pn7160 - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - miso_pin: ${miso_pin} + - tag.emulation_off: nfcc_pn7160_spi + - tag.emulation_on: nfcc_pn7160_spi + - tag.polling_off: nfcc_pn7160_spi + - tag.polling_on: nfcc_pn7160_spi pn7160_spi: - id: nfcc_pn7160 + id: nfcc_pn7160_spi cs_pin: ${cs_pin} irq_pin: ${irq_pin} ven_pin: ${ven_pin} diff --git a/tests/components/pn7160_spi/test.esp32-ard.yaml b/tests/components/pn7160_spi/test.esp32-ard.yaml deleted file mode 100644 index f6073d0416..0000000000 --- a/tests/components/pn7160_spi/test.esp32-ard.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO18 - cs_pin: GPIO12 - irq_pin: GPIO13 - ven_pin: GPIO14 - -<<: !include common.yaml diff --git a/tests/components/pn7160_spi/test.esp32-c3-ard.yaml b/tests/components/pn7160_spi/test.esp32-c3-ard.yaml deleted file mode 100644 index f8a07fad2f..0000000000 --- a/tests/components/pn7160_spi/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - irq_pin: GPIO9 - ven_pin: GPIO10 - -<<: !include common.yaml diff --git a/tests/components/pn7160_spi/test.esp32-c3-idf.yaml b/tests/components/pn7160_spi/test.esp32-c3-idf.yaml deleted file mode 100644 index f8a07fad2f..0000000000 --- a/tests/components/pn7160_spi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - irq_pin: GPIO9 - ven_pin: GPIO10 - -<<: !include common.yaml diff --git a/tests/components/pn7160_spi/test.esp32-idf.yaml b/tests/components/pn7160_spi/test.esp32-idf.yaml index f6073d0416..f903e4b7be 100644 --- a/tests/components/pn7160_spi/test.esp32-idf.yaml +++ b/tests/components/pn7160_spi/test.esp32-idf.yaml @@ -1,9 +1,9 @@ substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO18 cs_pin: GPIO12 irq_pin: GPIO13 ven_pin: GPIO14 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/pn7160_spi/test.esp8266-ard.yaml b/tests/components/pn7160_spi/test.esp8266-ard.yaml index cbe27533a7..7ec89dc012 100644 --- a/tests/components/pn7160_spi/test.esp8266-ard.yaml +++ b/tests/components/pn7160_spi/test.esp8266-ard.yaml @@ -1,9 +1,12 @@ substitutions: - clk_pin: GPIO14 - mosi_pin: GPIO13 + clk_pin: GPIO0 + mosi_pin: GPIO2 miso_pin: GPIO12 cs_pin: GPIO5 irq_pin: GPIO15 ven_pin: GPIO16 +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/pn7160_spi/test.rp2040-ard.yaml b/tests/components/pn7160_spi/test.rp2040-ard.yaml index 70cd2425fa..b4a4b436cd 100644 --- a/tests/components/pn7160_spi/test.rp2040-ard.yaml +++ b/tests/components/pn7160_spi/test.rp2040-ard.yaml @@ -6,4 +6,7 @@ substitutions: irq_pin: GPIO15 ven_pin: GPIO16 +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/power_supply/test.esp32-ard.yaml b/tests/components/power_supply/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/power_supply/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/power_supply/test.esp32-c3-ard.yaml b/tests/components/power_supply/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/power_supply/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/power_supply/test.esp32-c3-idf.yaml b/tests/components/power_supply/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/power_supply/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/bang_bang/test.esp32-ard.yaml b/tests/components/power_supply/test.nrf52-adafruit.yaml similarity index 100% rename from tests/components/bang_bang/test.esp32-ard.yaml rename to tests/components/power_supply/test.nrf52-adafruit.yaml diff --git a/tests/components/bang_bang/test.esp32-c3-ard.yaml b/tests/components/power_supply/test.nrf52-mcumgr.yaml similarity index 100% rename from tests/components/bang_bang/test.esp32-c3-ard.yaml rename to tests/components/power_supply/test.nrf52-mcumgr.yaml diff --git a/tests/components/prometheus/common.yaml b/tests/components/prometheus/common.yaml index 131d135f8b..7ff416dccb 100644 --- a/tests/components/prometheus/common.yaml +++ b/tests/components/prometheus/common.yaml @@ -21,10 +21,12 @@ http_request: ota: - platform: http_request + id: prometheus_http_request_ota update: - platform: http_request name: Firmware Update + ota_id: prometheus_http_request_ota source: http://example.com/manifest.json sensor: @@ -33,11 +35,19 @@ sensor: lambda: |- if (millis() > 10000) { return 42.0; - } else { - return 0.0; } + return 0.0; update_interval: 60s +text: + - platform: template + name: "Template text" + optimistic: true + min_length: 0 + max_length: 100 + mode: text + initial_value: "Hello World" + text_sensor: - platform: version name: "ESPHome Version" @@ -47,20 +57,37 @@ text_sensor: lambda: |- if (millis() > 10000) { return {"Hello World"}; - } else { - return {"Goodbye (cruel) World"}; } + return {"Goodbye (cruel) World"}; update_interval: 60s +event: + - platform: template + name: "Template Event" + id: template_event1 + event_types: + - "custom_event_1" + - "custom_event_2" + +button: + - platform: template + name: "Template Event Button" + on_press: + - logger.log: "Template Event Button pressed" + - lambda: |- + ESP_LOGD("template_event_button", "Template Event Button pressed"); + - event.trigger: + id: template_event1 + event_type: custom_event_1 + binary_sensor: - platform: template id: template_binary_sensor1 lambda: |- if (millis() > 10000) { return true; - } else { - return false; } + return false; switch: - platform: template @@ -68,9 +95,8 @@ switch: lambda: |- if (millis() > 10000) { return true; - } else { - return false; } + return false; optimistic: true fan: @@ -83,9 +109,27 @@ cover: lambda: |- if (millis() > 10000) { return COVER_OPEN; - } else { - return COVER_CLOSED; } + return COVER_CLOSED; + +light: + - platform: binary + name: "Binary Light" + output: test_output + - platform: monochromatic + name: "Brightness Light" + output: test_output + - platform: rgb + name: "RGB Light" + red: test_output + green: test_output + blue: test_output + - platform: rgbw + name: "RGBW Light" + red: test_output + green: test_output + blue: test_output + white: test_output lock: - platform: template @@ -93,11 +137,18 @@ lock: lambda: |- if (millis() > 10000) { return LOCK_STATE_LOCKED; - } else { - return LOCK_STATE_UNLOCKED; } + return LOCK_STATE_UNLOCKED; optimistic: true +output: + - platform: template + id: test_output + type: float + write_action: + - lambda: |- + // no-op for CI/build tests + (void)state; select: - platform: template id: template_select1 @@ -126,13 +177,10 @@ valve: optimistic: true has_position: true -remote_transmitter: - pin: ${pin} - carrier_duty_percent: 50% - climate: - platform: climate_ir_lg name: LG Climate + transmitter_id: xmitr prometheus: include_internal: true diff --git a/tests/components/prometheus/test.esp32-ard.yaml b/tests/components/prometheus/test.esp32-ard.yaml deleted file mode 100644 index 9eedaabd82..0000000000 --- a/tests/components/prometheus/test.esp32-ard.yaml +++ /dev/null @@ -1,38 +0,0 @@ -substitutions: - verify_ssl: "false" - pin: GPIO5 - -<<: !include common.yaml - -i2s_audio: - i2s_lrclk_pin: 1 - i2s_bclk_pin: 2 - i2s_mclk_pin: 3 - -media_player: - - platform: i2s_audio - name: "Media Player" - dac_type: external - i2s_dout_pin: 18 - mute_pin: 19 - on_state: - - media_player.play: - - media_player.play_media: http://localhost/media.mp3 - - media_player.play_media: !lambda 'return "http://localhost/media.mp3";' - on_idle: - - media_player.pause: - on_play: - - media_player.stop: - on_pause: - - media_player.toggle: - - wait_until: - media_player.is_idle: - - wait_until: - media_player.is_playing: - - wait_until: - media_player.is_announcing: - - wait_until: - media_player.is_paused: - - media_player.volume_up: - - media_player.volume_down: - - media_player.volume_set: 50% diff --git a/tests/components/prometheus/test.esp32-c3-ard.yaml b/tests/components/prometheus/test.esp32-c3-ard.yaml deleted file mode 100644 index f00bca5947..0000000000 --- a/tests/components/prometheus/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - verify_ssl: "false" - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/prometheus/test.esp32-c3-idf.yaml b/tests/components/prometheus/test.esp32-c3-idf.yaml deleted file mode 100644 index f00bca5947..0000000000 --- a/tests/components/prometheus/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - verify_ssl: "false" - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/prometheus/test.esp32-idf.yaml b/tests/components/prometheus/test.esp32-idf.yaml index f00bca5947..e590417623 100644 --- a/tests/components/prometheus/test.esp32-idf.yaml +++ b/tests/components/prometheus/test.esp32-idf.yaml @@ -1,5 +1,8 @@ substitutions: verify_ssl: "false" - pin: GPIO2 + +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/prometheus/test.esp8266-ard.yaml b/tests/components/prometheus/test.esp8266-ard.yaml index 6ee1831769..bae76751e8 100644 --- a/tests/components/prometheus/test.esp8266-ard.yaml +++ b/tests/components/prometheus/test.esp8266-ard.yaml @@ -1,5 +1,7 @@ substitutions: verify_ssl: "false" - pin: GPIO5 + +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/psram/test.esp32-ard.yaml b/tests/components/psram/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/psram/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/psram/test.esp32-s2-ard.yaml b/tests/components/psram/test.esp32-s2-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/psram/test.esp32-s2-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/psram/test.esp32-s3-ard.yaml b/tests/components/psram/test.esp32-s3-ard.yaml deleted file mode 100644 index cfd39f77fe..0000000000 --- a/tests/components/psram/test.esp32-s3-ard.yaml +++ /dev/null @@ -1,3 +0,0 @@ -psram: - mode: octal - speed: 80MHz diff --git a/tests/components/psram/test.esp32-s3-idf.yaml b/tests/components/psram/test.esp32-s3-idf.yaml index 75d4ee539c..548b8324d0 100644 --- a/tests/components/psram/test.esp32-s3-idf.yaml +++ b/tests/components/psram/test.esp32-s3-idf.yaml @@ -9,3 +9,4 @@ psram: mode: octal speed: 120MHz enable_ecc: true + ignore_not_found: false diff --git a/tests/components/pulse_counter/test.esp32-ard.yaml b/tests/components/pulse_counter/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/pulse_counter/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/pulse_counter/test.esp32-c3-ard.yaml b/tests/components/pulse_counter/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/pulse_counter/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/bang_bang/test.esp32-c3-idf.yaml b/tests/components/pulse_counter/test.nrf52-adafruit.yaml similarity index 100% rename from tests/components/bang_bang/test.esp32-c3-idf.yaml rename to tests/components/pulse_counter/test.nrf52-adafruit.yaml diff --git a/tests/components/bedjet/test.esp32-ard.yaml b/tests/components/pulse_counter/test.nrf52-mcumgr.yaml similarity index 100% rename from tests/components/bedjet/test.esp32-ard.yaml rename to tests/components/pulse_counter/test.nrf52-mcumgr.yaml diff --git a/tests/components/pulse_meter/test.esp32-ard.yaml b/tests/components/pulse_meter/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/pulse_meter/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/pulse_meter/test.esp32-c3-ard.yaml b/tests/components/pulse_meter/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/pulse_meter/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/pulse_meter/test.esp32-c3-idf.yaml b/tests/components/pulse_meter/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/pulse_meter/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/bedjet/test.esp32-c3-ard.yaml b/tests/components/pulse_meter/test.nrf52-adafruit.yaml similarity index 100% rename from tests/components/bedjet/test.esp32-c3-ard.yaml rename to tests/components/pulse_meter/test.nrf52-adafruit.yaml diff --git a/tests/components/bedjet/test.esp32-c3-idf.yaml b/tests/components/pulse_meter/test.nrf52-mcumgr.yaml similarity index 100% rename from tests/components/bedjet/test.esp32-c3-idf.yaml rename to tests/components/pulse_meter/test.nrf52-mcumgr.yaml diff --git a/tests/components/pulse_width/test.esp32-ard.yaml b/tests/components/pulse_width/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/pulse_width/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/pulse_width/test.esp32-c3-ard.yaml b/tests/components/pulse_width/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/pulse_width/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/pulse_width/test.esp32-c3-idf.yaml b/tests/components/pulse_width/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/pulse_width/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/binary_sensor_map/test.esp32-ard.yaml b/tests/components/pulse_width/test.nrf52-adafruit.yaml similarity index 100% rename from tests/components/binary_sensor_map/test.esp32-ard.yaml rename to tests/components/pulse_width/test.nrf52-adafruit.yaml diff --git a/tests/components/binary_sensor_map/test.esp32-c3-ard.yaml b/tests/components/pulse_width/test.nrf52-mcumgr.yaml similarity index 100% rename from tests/components/binary_sensor_map/test.esp32-c3-ard.yaml rename to tests/components/pulse_width/test.nrf52-mcumgr.yaml diff --git a/tests/components/pvvx_mithermometer/test.esp32-ard.yaml b/tests/components/pvvx_mithermometer/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/pvvx_mithermometer/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/pvvx_mithermometer/test.esp32-c3-ard.yaml b/tests/components/pvvx_mithermometer/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/pvvx_mithermometer/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/pvvx_mithermometer/test.esp32-c3-idf.yaml b/tests/components/pvvx_mithermometer/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/pvvx_mithermometer/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/pvvx_mithermometer/test.esp32-idf.yaml b/tests/components/pvvx_mithermometer/test.esp32-idf.yaml index dade44d145..7a6541ae76 100644 --- a/tests/components/pvvx_mithermometer/test.esp32-idf.yaml +++ b/tests/components/pvvx_mithermometer/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/pylontech/common.yaml b/tests/components/pylontech/common.yaml index 6852685be7..537450a3b6 100644 --- a/tests/components/pylontech/common.yaml +++ b/tests/components/pylontech/common.yaml @@ -1,9 +1,3 @@ -uart: - - id: uart_pylontech0 - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 115200 - pylontech: - id: pylontech0 - id: pylontech1 diff --git a/tests/components/pylontech/test.esp32-ard.yaml b/tests/components/pylontech/test.esp32-ard.yaml deleted file mode 100644 index f486544afa..0000000000 --- a/tests/components/pylontech/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 - -<<: !include common.yaml diff --git a/tests/components/pylontech/test.esp32-c3-ard.yaml b/tests/components/pylontech/test.esp32-c3-ard.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/pylontech/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/pylontech/test.esp32-c3-idf.yaml b/tests/components/pylontech/test.esp32-c3-idf.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/pylontech/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/pylontech/test.esp32-idf.yaml b/tests/components/pylontech/test.esp32-idf.yaml index f486544afa..b415125e84 100644 --- a/tests/components/pylontech/test.esp32-idf.yaml +++ b/tests/components/pylontech/test.esp32-idf.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 + tx_pin: GPIO4 + rx_pin: GPIO5 + +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/pylontech/test.esp8266-ard.yaml b/tests/components/pylontech/test.esp8266-ard.yaml index b516342f3b..96ab4ef6ac 100644 --- a/tests/components/pylontech/test.esp8266-ard.yaml +++ b/tests/components/pylontech/test.esp8266-ard.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: GPIO0 + rx_pin: GPIO2 + +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/pylontech/test.rp2040-ard.yaml b/tests/components/pylontech/test.rp2040-ard.yaml index b516342f3b..b28f2b5e05 100644 --- a/tests/components/pylontech/test.rp2040-ard.yaml +++ b/tests/components/pylontech/test.rp2040-ard.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO4 rx_pin: GPIO5 +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/pzem004t/common.yaml b/tests/components/pzem004t/common.yaml index 75f7f30fc9..4584f9c273 100644 --- a/tests/components/pzem004t/common.yaml +++ b/tests/components/pzem004t/common.yaml @@ -1,9 +1,3 @@ -uart: - - id: uart_pzem004t - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 115200 - sensor: - platform: pzem004t voltage: diff --git a/tests/components/pzem004t/test.esp32-ard.yaml b/tests/components/pzem004t/test.esp32-ard.yaml deleted file mode 100644 index f486544afa..0000000000 --- a/tests/components/pzem004t/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 - -<<: !include common.yaml diff --git a/tests/components/pzem004t/test.esp32-c3-ard.yaml b/tests/components/pzem004t/test.esp32-c3-ard.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/pzem004t/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/pzem004t/test.esp32-c3-idf.yaml b/tests/components/pzem004t/test.esp32-c3-idf.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/pzem004t/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/pzem004t/test.esp32-idf.yaml b/tests/components/pzem004t/test.esp32-idf.yaml index f486544afa..b415125e84 100644 --- a/tests/components/pzem004t/test.esp32-idf.yaml +++ b/tests/components/pzem004t/test.esp32-idf.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 + tx_pin: GPIO4 + rx_pin: GPIO5 + +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/pzem004t/test.esp8266-ard.yaml b/tests/components/pzem004t/test.esp8266-ard.yaml index b516342f3b..96ab4ef6ac 100644 --- a/tests/components/pzem004t/test.esp8266-ard.yaml +++ b/tests/components/pzem004t/test.esp8266-ard.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: GPIO0 + rx_pin: GPIO2 + +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/pzem004t/test.rp2040-ard.yaml b/tests/components/pzem004t/test.rp2040-ard.yaml index b516342f3b..b28f2b5e05 100644 --- a/tests/components/pzem004t/test.rp2040-ard.yaml +++ b/tests/components/pzem004t/test.rp2040-ard.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO4 rx_pin: GPIO5 +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/pzemac/common.yaml b/tests/components/pzemac/common.yaml index e50f4ad2f2..2566051baa 100644 --- a/tests/components/pzemac/common.yaml +++ b/tests/components/pzemac/common.yaml @@ -3,16 +3,9 @@ esphome: then: - pzemac.reset_energy: pzemac1 -uart: - - id: uart_pzemac - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 9600 - -modbus: - sensor: - platform: pzemac + modbus_id: modbus_bus id: pzemac1 voltage: name: PZEMAC Voltage diff --git a/tests/components/pzemac/test.esp32-ard.yaml b/tests/components/pzemac/test.esp32-ard.yaml deleted file mode 100644 index f486544afa..0000000000 --- a/tests/components/pzemac/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 - -<<: !include common.yaml diff --git a/tests/components/pzemac/test.esp32-c3-ard.yaml b/tests/components/pzemac/test.esp32-c3-ard.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/pzemac/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/pzemac/test.esp32-c3-idf.yaml b/tests/components/pzemac/test.esp32-c3-idf.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/pzemac/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/pzemac/test.esp32-idf.yaml b/tests/components/pzemac/test.esp32-idf.yaml index f486544afa..b631e16677 100644 --- a/tests/components/pzemac/test.esp32-idf.yaml +++ b/tests/components/pzemac/test.esp32-idf.yaml @@ -1,5 +1,9 @@ substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 + tx_pin: GPIO4 + rx_pin: GPIO5 + flow_control_pin: GPIO13 + +packages: + modbus: !include ../../test_build_components/common/modbus/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/pzemac/test.esp8266-ard.yaml b/tests/components/pzemac/test.esp8266-ard.yaml index b516342f3b..421389ae97 100644 --- a/tests/components/pzemac/test.esp8266-ard.yaml +++ b/tests/components/pzemac/test.esp8266-ard.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: GPIO0 + rx_pin: GPIO2 + +packages: + modbus: !include ../../test_build_components/common/modbus/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/pzemac/test.rp2040-ard.yaml b/tests/components/pzemac/test.rp2040-ard.yaml index b516342f3b..d78d84c983 100644 --- a/tests/components/pzemac/test.rp2040-ard.yaml +++ b/tests/components/pzemac/test.rp2040-ard.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO4 rx_pin: GPIO5 +packages: + modbus: !include ../../test_build_components/common/modbus/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/pzemdc/common.yaml b/tests/components/pzemdc/common.yaml index db1868d682..78a0ab0d07 100644 --- a/tests/components/pzemdc/common.yaml +++ b/tests/components/pzemdc/common.yaml @@ -3,15 +3,9 @@ esphome: then: - pzemdc.reset_energy: pzemdc1 -uart: - - id: uart_pzemdc - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 9600 - stop_bits: 2 - sensor: - platform: pzemdc + modbus_id: modbus_bus id: pzemdc1 voltage: name: PZEMDC Voltage diff --git a/tests/components/pzemdc/test.esp32-ard.yaml b/tests/components/pzemdc/test.esp32-ard.yaml deleted file mode 100644 index f486544afa..0000000000 --- a/tests/components/pzemdc/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 - -<<: !include common.yaml diff --git a/tests/components/pzemdc/test.esp32-c3-ard.yaml b/tests/components/pzemdc/test.esp32-c3-ard.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/pzemdc/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/pzemdc/test.esp32-c3-idf.yaml b/tests/components/pzemdc/test.esp32-c3-idf.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/pzemdc/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/pzemdc/test.esp32-idf.yaml b/tests/components/pzemdc/test.esp32-idf.yaml index f486544afa..b631e16677 100644 --- a/tests/components/pzemdc/test.esp32-idf.yaml +++ b/tests/components/pzemdc/test.esp32-idf.yaml @@ -1,5 +1,9 @@ substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 + tx_pin: GPIO4 + rx_pin: GPIO5 + flow_control_pin: GPIO13 + +packages: + modbus: !include ../../test_build_components/common/modbus/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/pzemdc/test.esp8266-ard.yaml b/tests/components/pzemdc/test.esp8266-ard.yaml index b516342f3b..421389ae97 100644 --- a/tests/components/pzemdc/test.esp8266-ard.yaml +++ b/tests/components/pzemdc/test.esp8266-ard.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: GPIO0 + rx_pin: GPIO2 + +packages: + modbus: !include ../../test_build_components/common/modbus/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/pzemdc/test.rp2040-ard.yaml b/tests/components/pzemdc/test.rp2040-ard.yaml index b516342f3b..d78d84c983 100644 --- a/tests/components/pzemdc/test.rp2040-ard.yaml +++ b/tests/components/pzemdc/test.rp2040-ard.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO4 rx_pin: GPIO5 +packages: + modbus: !include ../../test_build_components/common/modbus/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/qmc5883l/common.yaml b/tests/components/qmc5883l/common.yaml index 5d8ac73b4f..98d0350a60 100644 --- a/tests/components/qmc5883l/common.yaml +++ b/tests/components/qmc5883l/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_qmc5883l - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: qmc5883l + i2c_id: i2c_bus address: 0x0D field_strength_x: name: QMC5883L Field Strength X @@ -17,5 +13,7 @@ sensor: temperature: name: QMC5883L Temperature range: 800uT + data_rate: 200Hz oversampling: 256x update_interval: 15s + drdy_pin: ${drdy_pin} diff --git a/tests/components/qmc5883l/test.esp32-ard.yaml b/tests/components/qmc5883l/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/qmc5883l/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/qmc5883l/test.esp32-c3-ard.yaml b/tests/components/qmc5883l/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/qmc5883l/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/qmc5883l/test.esp32-c3-idf.yaml b/tests/components/qmc5883l/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/qmc5883l/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/qmc5883l/test.esp32-idf.yaml b/tests/components/qmc5883l/test.esp32-idf.yaml index 63c3bd6afd..07bf2b14e2 100644 --- a/tests/components/qmc5883l/test.esp32-idf.yaml +++ b/tests/components/qmc5883l/test.esp32-idf.yaml @@ -1,5 +1,7 @@ substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 + drdy_pin: GPIO12 + +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/qmc5883l/test.esp8266-ard.yaml b/tests/components/qmc5883l/test.esp8266-ard.yaml index ee2c29ca4e..0c02b3b96a 100644 --- a/tests/components/qmc5883l/test.esp8266-ard.yaml +++ b/tests/components/qmc5883l/test.esp8266-ard.yaml @@ -1,5 +1,7 @@ substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 + drdy_pin: GPIO2 + +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/qmc5883l/test.rp2040-ard.yaml b/tests/components/qmc5883l/test.rp2040-ard.yaml index ee2c29ca4e..3b10540b1f 100644 --- a/tests/components/qmc5883l/test.rp2040-ard.yaml +++ b/tests/components/qmc5883l/test.rp2040-ard.yaml @@ -1,5 +1,7 @@ substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 + drdy_pin: GPIO2 + +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/qmp6988/common.yaml b/tests/components/qmp6988/common.yaml index cb4b221df0..2aea228a0d 100644 --- a/tests/components/qmp6988/common.yaml +++ b/tests/components/qmp6988/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_qmp6988 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: qmp6988 + i2c_id: i2c_bus temperature: name: QMP6988 Temperature oversampling: 32x diff --git a/tests/components/qmp6988/test.esp32-ard.yaml b/tests/components/qmp6988/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/qmp6988/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/qmp6988/test.esp32-c3-ard.yaml b/tests/components/qmp6988/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/qmp6988/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/qmp6988/test.esp32-c3-idf.yaml b/tests/components/qmp6988/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/qmp6988/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/qmp6988/test.esp32-idf.yaml b/tests/components/qmp6988/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/qmp6988/test.esp32-idf.yaml +++ b/tests/components/qmp6988/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/qmp6988/test.esp8266-ard.yaml b/tests/components/qmp6988/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/qmp6988/test.esp8266-ard.yaml +++ b/tests/components/qmp6988/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/qmp6988/test.rp2040-ard.yaml b/tests/components/qmp6988/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/qmp6988/test.rp2040-ard.yaml +++ b/tests/components/qmp6988/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/qr_code/common.yaml b/tests/components/qr_code/common.yaml index 85c2ee076b..5fec26c1cc 100644 --- a/tests/components/qr_code/common.yaml +++ b/tests/components/qr_code/common.yaml @@ -1,11 +1,6 @@ -spi: - - id: spi_main_lcd - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - display: - platform: ili9xxx - id: main_lcd + id: qr_code_main_lcd model: ili9342 cs_pin: ${cs_pin} dc_pin: ${dc_pin} @@ -14,11 +9,11 @@ display: lambda: |- // Draw a QR code in the center of the screen auto scale = 2; - auto size = id(homepage_qr).get_size() * scale; + auto size = id(qr_code_homepage_qr).get_size() * scale; auto x = (it.get_width() / 2) - (size / 2); auto y = (it.get_height() / 2) - (size / 2); - it.qr_code(x, y, id(homepage_qr), Color(255,255,255), scale); + it.qr_code(x, y, id(qr_code_homepage_qr), Color(255,255,255), scale); qr_code: - - id: homepage_qr + - id: qr_code_homepage_qr value: https://esphome.io/index.html diff --git a/tests/components/qr_code/test.esp32-ard.yaml b/tests/components/qr_code/test.esp32-ard.yaml deleted file mode 100644 index bad5241f79..0000000000 --- a/tests/components/qr_code/test.esp32-ard.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - cs_pin: GPIO12 - dc_pin: GPIO13 - reset_pin: GPIO14 - -<<: !include common.yaml diff --git a/tests/components/qr_code/test.esp32-c3-ard.yaml b/tests/components/qr_code/test.esp32-c3-ard.yaml deleted file mode 100644 index c5c932c92c..0000000000 --- a/tests/components/qr_code/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - dc_pin: GPIO9 - reset_pin: GPIO10 - -<<: !include common.yaml diff --git a/tests/components/qr_code/test.esp32-c3-idf.yaml b/tests/components/qr_code/test.esp32-c3-idf.yaml deleted file mode 100644 index c5c932c92c..0000000000 --- a/tests/components/qr_code/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - dc_pin: GPIO9 - reset_pin: GPIO10 - -<<: !include common.yaml diff --git a/tests/components/qr_code/test.esp32-idf.yaml b/tests/components/qr_code/test.esp32-idf.yaml index bad5241f79..ff174a4656 100644 --- a/tests/components/qr_code/test.esp32-idf.yaml +++ b/tests/components/qr_code/test.esp32-idf.yaml @@ -1,8 +1,9 @@ substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 cs_pin: GPIO12 dc_pin: GPIO13 reset_pin: GPIO14 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/qr_code/test.esp8266-ard.yaml b/tests/components/qr_code/test.esp8266-ard.yaml index 3f023a60eb..56cb29f29e 100644 --- a/tests/components/qr_code/test.esp8266-ard.yaml +++ b/tests/components/qr_code/test.esp8266-ard.yaml @@ -1,9 +1,12 @@ substitutions: - clk_pin: GPIO14 - mosi_pin: GPIO13 + clk_pin: GPIO0 + mosi_pin: GPIO2 miso_pin: GPIO12 cs_pin: GPIO5 dc_pin: GPIO15 reset_pin: GPIO16 +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/qr_code/test.rp2040-ard.yaml b/tests/components/qr_code/test.rp2040-ard.yaml index d7fd6ee294..66caa956f7 100644 --- a/tests/components/qr_code/test.rp2040-ard.yaml +++ b/tests/components/qr_code/test.rp2040-ard.yaml @@ -6,4 +6,7 @@ substitutions: dc_pin: GPIO15 reset_pin: GPIO16 +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/qspi_dbi/common.yaml b/tests/components/qspi_dbi/common.yaml index f4b15f2b90..109db65b63 100644 --- a/tests/components/qspi_dbi/common.yaml +++ b/tests/components/qspi_dbi/common.yaml @@ -1,9 +1,3 @@ -spi: - id: quad_spi - clk_pin: 15 - type: quad - data_pins: [14, 10, 16, 12] - display: - platform: qspi_dbi model: RM690B0 diff --git a/tests/components/qspi_dbi/test.esp32-s3-idf.yaml b/tests/components/qspi_dbi/test.esp32-s3-idf.yaml index dade44d145..c335dee1f3 100644 --- a/tests/components/qspi_dbi/test.esp32-s3-idf.yaml +++ b/tests/components/qspi_dbi/test.esp32-s3-idf.yaml @@ -1 +1,4 @@ +packages: + qspi: !include ../../test_build_components/common/qspi/esp32-s3-idf.yaml + <<: !include common.yaml diff --git a/tests/components/qwiic_pir/common.yaml b/tests/components/qwiic_pir/common.yaml index d4b733405d..30418ff6b9 100644 --- a/tests/components/qwiic_pir/common.yaml +++ b/tests/components/qwiic_pir/common.yaml @@ -1,8 +1,4 @@ -i2c: - - id: i2c_qwiic_pir - scl: ${scl_pin} - sda: ${sda_pin} - binary_sensor: - platform: qwiic_pir + i2c_id: i2c_bus name: Qwiic PIR Motion Sensor diff --git a/tests/components/qwiic_pir/test.esp32-ard.yaml b/tests/components/qwiic_pir/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/qwiic_pir/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/qwiic_pir/test.esp32-c3-ard.yaml b/tests/components/qwiic_pir/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/qwiic_pir/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/qwiic_pir/test.esp32-c3-idf.yaml b/tests/components/qwiic_pir/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/qwiic_pir/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/qwiic_pir/test.esp32-idf.yaml b/tests/components/qwiic_pir/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/qwiic_pir/test.esp32-idf.yaml +++ b/tests/components/qwiic_pir/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/qwiic_pir/test.esp8266-ard.yaml b/tests/components/qwiic_pir/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/qwiic_pir/test.esp8266-ard.yaml +++ b/tests/components/qwiic_pir/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/qwiic_pir/test.rp2040-ard.yaml b/tests/components/qwiic_pir/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/qwiic_pir/test.rp2040-ard.yaml +++ b/tests/components/qwiic_pir/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/radon_eye_ble/test.esp32-ard.yaml b/tests/components/radon_eye_ble/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/radon_eye_ble/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/radon_eye_ble/test.esp32-c3-ard.yaml b/tests/components/radon_eye_ble/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/radon_eye_ble/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/radon_eye_ble/test.esp32-c3-idf.yaml b/tests/components/radon_eye_ble/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/radon_eye_ble/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/radon_eye_ble/test.esp32-idf.yaml b/tests/components/radon_eye_ble/test.esp32-idf.yaml index dade44d145..7a6541ae76 100644 --- a/tests/components/radon_eye_ble/test.esp32-idf.yaml +++ b/tests/components/radon_eye_ble/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/radon_eye_rd200/test.esp32-ard.yaml b/tests/components/radon_eye_rd200/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/radon_eye_rd200/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/radon_eye_rd200/test.esp32-c3-ard.yaml b/tests/components/radon_eye_rd200/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/radon_eye_rd200/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/radon_eye_rd200/test.esp32-c3-idf.yaml b/tests/components/radon_eye_rd200/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/radon_eye_rd200/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/radon_eye_rd200/test.esp32-idf.yaml b/tests/components/radon_eye_rd200/test.esp32-idf.yaml index dade44d145..7a6541ae76 100644 --- a/tests/components/radon_eye_rd200/test.esp32-idf.yaml +++ b/tests/components/radon_eye_rd200/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/rc522_i2c/common.yaml b/tests/components/rc522_i2c/common.yaml index b8b7a41bc7..0e624351d4 100644 --- a/tests/components/rc522_i2c/common.yaml +++ b/tests/components/rc522_i2c/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_rc522 - scl: ${scl_pin} - sda: ${sda_pin} - rc522_i2c: - - id: rc522_nfcc + - id: rc522_nfcc_i2c + i2c_id: i2c_bus update_interval: 1s on_tag: - lambda: |- @@ -12,6 +8,6 @@ rc522_i2c: binary_sensor: - platform: rc522 - rc522_id: rc522_nfcc + rc522_id: rc522_nfcc_i2c name: RC522 NFC Tag uid: 74-10-37-94 diff --git a/tests/components/rc522_i2c/test.esp32-ard.yaml b/tests/components/rc522_i2c/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/rc522_i2c/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/rc522_i2c/test.esp32-c3-ard.yaml b/tests/components/rc522_i2c/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/rc522_i2c/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/rc522_i2c/test.esp32-c3-idf.yaml b/tests/components/rc522_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/rc522_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/rc522_i2c/test.esp32-idf.yaml b/tests/components/rc522_i2c/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/rc522_i2c/test.esp32-idf.yaml +++ b/tests/components/rc522_i2c/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/rc522_i2c/test.esp8266-ard.yaml b/tests/components/rc522_i2c/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/rc522_i2c/test.esp8266-ard.yaml +++ b/tests/components/rc522_i2c/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/rc522_i2c/test.rp2040-ard.yaml b/tests/components/rc522_i2c/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/rc522_i2c/test.rp2040-ard.yaml +++ b/tests/components/rc522_i2c/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/rc522_spi/common.yaml b/tests/components/rc522_spi/common.yaml index 5c42858993..c4830850de 100644 --- a/tests/components/rc522_spi/common.yaml +++ b/tests/components/rc522_spi/common.yaml @@ -1,15 +1,9 @@ -spi: - - id: spi_rc522 - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - miso_pin: ${miso_pin} - rc522_spi: - id: rc522_nfcc + id: rc522_nfcc_spi cs_pin: ${cs_pin} binary_sensor: - platform: rc522 - rc522_id: rc522_nfcc - name: PN532 NFC Tag + rc522_id: rc522_nfcc_spi + name: RC522 NFC Tag uid: 74-10-37-94 diff --git a/tests/components/rc522_spi/test.esp32-ard.yaml b/tests/components/rc522_spi/test.esp32-ard.yaml deleted file mode 100644 index 54e027a614..0000000000 --- a/tests/components/rc522_spi/test.esp32-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 - cs_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/rc522_spi/test.esp32-c3-ard.yaml b/tests/components/rc522_spi/test.esp32-c3-ard.yaml deleted file mode 100644 index 2415ba5dc6..0000000000 --- a/tests/components/rc522_spi/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/rc522_spi/test.esp32-c3-idf.yaml b/tests/components/rc522_spi/test.esp32-c3-idf.yaml deleted file mode 100644 index 2415ba5dc6..0000000000 --- a/tests/components/rc522_spi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/rc522_spi/test.esp32-idf.yaml b/tests/components/rc522_spi/test.esp32-idf.yaml index 54e027a614..a3352cf880 100644 --- a/tests/components/rc522_spi/test.esp32-idf.yaml +++ b/tests/components/rc522_spi/test.esp32-idf.yaml @@ -1,7 +1,7 @@ substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 cs_pin: GPIO5 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/rc522_spi/test.esp8266-ard.yaml b/tests/components/rc522_spi/test.esp8266-ard.yaml index dbd158d030..b4673ba8b7 100644 --- a/tests/components/rc522_spi/test.esp8266-ard.yaml +++ b/tests/components/rc522_spi/test.esp8266-ard.yaml @@ -1,7 +1,10 @@ substitutions: - clk_pin: GPIO14 - mosi_pin: GPIO13 - miso_pin: GPIO12 + clk_pin: GPIO0 + mosi_pin: GPIO2 + miso_pin: GPIO16 cs_pin: GPIO15 +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/rc522_spi/test.rp2040-ard.yaml b/tests/components/rc522_spi/test.rp2040-ard.yaml index f6c3f1eeca..1ded24de1c 100644 --- a/tests/components/rc522_spi/test.rp2040-ard.yaml +++ b/tests/components/rc522_spi/test.rp2040-ard.yaml @@ -4,4 +4,7 @@ substitutions: miso_pin: GPIO4 cs_pin: GPIO5 +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/rdm6300/common.yaml b/tests/components/rdm6300/common.yaml index 118a295471..f1a5305013 100644 --- a/tests/components/rdm6300/common.yaml +++ b/tests/components/rdm6300/common.yaml @@ -1,9 +1,3 @@ -uart: - - id: uart_rdm6300 - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 115200 - rdm6300: binary_sensor: diff --git a/tests/components/rdm6300/test.esp32-ard.yaml b/tests/components/rdm6300/test.esp32-ard.yaml deleted file mode 100644 index f486544afa..0000000000 --- a/tests/components/rdm6300/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 - -<<: !include common.yaml diff --git a/tests/components/rdm6300/test.esp32-c3-ard.yaml b/tests/components/rdm6300/test.esp32-c3-ard.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/rdm6300/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/rdm6300/test.esp32-c3-idf.yaml b/tests/components/rdm6300/test.esp32-c3-idf.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/rdm6300/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/rdm6300/test.esp32-idf.yaml b/tests/components/rdm6300/test.esp32-idf.yaml index f486544afa..b415125e84 100644 --- a/tests/components/rdm6300/test.esp32-idf.yaml +++ b/tests/components/rdm6300/test.esp32-idf.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 + tx_pin: GPIO4 + rx_pin: GPIO5 + +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/rdm6300/test.esp8266-ard.yaml b/tests/components/rdm6300/test.esp8266-ard.yaml index b516342f3b..96ab4ef6ac 100644 --- a/tests/components/rdm6300/test.esp8266-ard.yaml +++ b/tests/components/rdm6300/test.esp8266-ard.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: GPIO0 + rx_pin: GPIO2 + +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/rdm6300/test.rp2040-ard.yaml b/tests/components/rdm6300/test.rp2040-ard.yaml index b516342f3b..b28f2b5e05 100644 --- a/tests/components/rdm6300/test.rp2040-ard.yaml +++ b/tests/components/rdm6300/test.rp2040-ard.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO4 rx_pin: GPIO5 +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/remote_receiver/common-actions.yaml b/tests/components/remote_receiver/common-actions.yaml index ca7713f58a..de01fa3602 100644 --- a/tests/components/remote_receiver/common-actions.yaml +++ b/tests/components/remote_receiver/common-actions.yaml @@ -48,6 +48,11 @@ on_drayton: - logger.log: format: "on_drayton: %u %u %u" args: ["x.address", "x.channel", "x.command"] +on_dyson: + then: + - logger.log: + format: "on_dyson: %u %u" + args: ["x.code", "x.index"] on_gobox: then: - logger.log: @@ -143,6 +148,11 @@ on_sony: - logger.log: format: "on_sony: %lu %u" args: ["long(x.data)", "x.nbits"] +on_symphony: + then: + - logger.log: + format: "on_symphony: 0x%lX %u" + args: ["long(x.data)", "x.nbits"] on_toshiba_ac: then: - logger.log: diff --git a/tests/components/remote_receiver/test.esp32-ard.yaml b/tests/components/remote_receiver/test.esp32-ard.yaml deleted file mode 100644 index 10dd767598..0000000000 --- a/tests/components/remote_receiver/test.esp32-ard.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - pin: GPIO2 - clock_resolution: "2000000" - filter_symbols: "2" - receive_symbols: "4" - rmt_symbols: "64" - -packages: - common: !include esp32-common.yaml diff --git a/tests/components/remote_receiver/test.esp32-c3-ard.yaml b/tests/components/remote_receiver/test.esp32-c3-ard.yaml deleted file mode 100644 index 10dd767598..0000000000 --- a/tests/components/remote_receiver/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - pin: GPIO2 - clock_resolution: "2000000" - filter_symbols: "2" - receive_symbols: "4" - rmt_symbols: "64" - -packages: - common: !include esp32-common.yaml diff --git a/tests/components/remote_receiver/test.esp32-idf.yaml b/tests/components/remote_receiver/test.esp32-idf.yaml index 10dd767598..cdeeab2c4a 100644 --- a/tests/components/remote_receiver/test.esp32-idf.yaml +++ b/tests/components/remote_receiver/test.esp32-idf.yaml @@ -1,6 +1,8 @@ substitutions: pin: GPIO2 clock_resolution: "2000000" + carrier_duty_percent: "25" + carrier_frequency: "30000" filter_symbols: "2" receive_symbols: "4" rmt_symbols: "64" diff --git a/tests/components/remote_receiver/test.esp32-s3-idf.yaml b/tests/components/remote_receiver/test.esp32-s3-idf.yaml index cdae8b1e4e..0d7a20e0c0 100644 --- a/tests/components/remote_receiver/test.esp32-s3-idf.yaml +++ b/tests/components/remote_receiver/test.esp32-s3-idf.yaml @@ -5,9 +5,22 @@ substitutions: receive_symbols: "4" rmt_symbols: "64" -packages: - common: !include esp32-common.yaml - +# WARNING: Using !extend or !remove prevents automatic component grouping in CI, making builds slower. remote_receiver: - - id: !extend rcvr + - id: rcvr + pin: ${pin} + dump: all + tolerance: 25% + clock_resolution: ${clock_resolution} + filter_symbols: ${filter_symbols} + receive_symbols: ${receive_symbols} + rmt_symbols: ${rmt_symbols} use_dma: "true" + <<: !include common-actions.yaml + +binary_sensor: + - platform: remote_receiver + name: Panasonic Remote Input + panasonic: + address: 0x4004 + command: 0x100BCBD diff --git a/tests/components/remote_transmitter/common-buttons.yaml b/tests/components/remote_transmitter/common-buttons.yaml index 29f48d995d..d48d36bd54 100644 --- a/tests/components/remote_transmitter/common-buttons.yaml +++ b/tests/components/remote_transmitter/common-buttons.yaml @@ -1,3 +1,11 @@ +number: + - platform: template + id: test_number + optimistic: true + min_value: 0 + max_value: 255 + step: 1 + button: - platform: template name: Beo4 audio mute @@ -6,6 +14,13 @@ button: remote_transmitter.transmit_beo4: source: 0x01 command: 0x0C + - platform: template + name: Dyson fan up + id: dyson_fan_up + on_press: + remote_transmitter.transmit_dyson: + code: 0x1215 + index: 0x0 - platform: template name: JVC Off id: living_room_lights_on @@ -53,6 +68,12 @@ button: remote_transmitter.transmit_sony: data: 0xABCDEF nbits: 12 + - platform: template + name: Symphony + on_press: + remote_transmitter.transmit_symphony: + data: 0xE88 + nbits: 12 - platform: template name: Panasonic on_press: @@ -115,10 +136,16 @@ button: address: 0x00 command: 0x0B - platform: template - name: RC5 Raw + name: RC5 Raw static on_press: remote_transmitter.transmit_raw: code: [1000, -1000] + - platform: template + name: RC5 Raw lambda + on_press: + remote_transmitter.transmit_raw: + code: !lambda |- + return {(int32_t)id(test_number).state * 100, -1000}; - platform: template name: AEHA id: eaha_hitachi_climate_power_on @@ -204,3 +231,26 @@ button: command: 0xEC rc_code_1: 0x0D rc_code_2: 0x0D + - platform: template + name: ABBWelcome static + on_press: + remote_transmitter.transmit_abbwelcome: + source_address: 0x1234 + destination_address: 0x5678 + message_type: 0x01 + data: [0x10, 0x20, 0x30] + - platform: template + name: ABBWelcome lambda + on_press: + remote_transmitter.transmit_abbwelcome: + source_address: 0x1234 + destination_address: 0x5678 + message_type: 0x01 + data: !lambda |- + return {(uint8_t)id(test_number).state, 0x20, 0x30}; + - platform: template + name: Digital Write + on_press: + - remote_transmitter.digital_write: true + - remote_transmitter.digital_write: + value: false diff --git a/tests/components/remote_transmitter/esp32-common.yaml b/tests/components/remote_transmitter/esp32-common.yaml index 8b26c45149..79fd47ae21 100644 --- a/tests/components/remote_transmitter/esp32-common.yaml +++ b/tests/components/remote_transmitter/esp32-common.yaml @@ -2,6 +2,7 @@ remote_transmitter: - id: xmitr pin: ${pin} carrier_duty_percent: 50% + non_blocking: true clock_resolution: ${clock_resolution} rmt_symbols: ${rmt_symbols} diff --git a/tests/components/remote_transmitter/test.esp32-ard.yaml b/tests/components/remote_transmitter/test.esp32-ard.yaml deleted file mode 100644 index 0522f4d181..0000000000 --- a/tests/components/remote_transmitter/test.esp32-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - pin: GPIO2 - clock_resolution: "2000000" - rmt_symbols: "64" - -packages: - common: !include esp32-common.yaml diff --git a/tests/components/remote_transmitter/test.esp32-c3-ard.yaml b/tests/components/remote_transmitter/test.esp32-c3-ard.yaml deleted file mode 100644 index 0522f4d181..0000000000 --- a/tests/components/remote_transmitter/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - pin: GPIO2 - clock_resolution: "2000000" - rmt_symbols: "64" - -packages: - common: !include esp32-common.yaml diff --git a/tests/components/remote_transmitter/test.esp32-s3-idf.yaml b/tests/components/remote_transmitter/test.esp32-s3-idf.yaml index fe4c46d9e7..8a038beb4a 100644 --- a/tests/components/remote_transmitter/test.esp32-s3-idf.yaml +++ b/tests/components/remote_transmitter/test.esp32-s3-idf.yaml @@ -3,9 +3,14 @@ substitutions: clock_resolution: "2000000" rmt_symbols: "64" -packages: - common: !include esp32-common.yaml - +# WARNING: Using !extend or !remove prevents automatic component grouping in CI, making builds slower. remote_transmitter: - - id: !extend xmitr + - id: xmitr + pin: ${pin} + carrier_duty_percent: 50% + clock_resolution: ${clock_resolution} + rmt_symbols: ${rmt_symbols} use_dma: "true" + +packages: + buttons: !include common-buttons.yaml diff --git a/tests/components/resampler/test.esp32-ard.yaml b/tests/components/resampler/test.esp32-ard.yaml deleted file mode 100644 index 96d2d37458..0000000000 --- a/tests/components/resampler/test.esp32-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - lrclk_pin: GPIO16 - bclk_pin: GPIO17 - mclk_pin: GPIO15 - dout_pin: GPIO14 - -<<: !include common.yaml diff --git a/tests/components/resampler/test.esp32-c3-ard.yaml b/tests/components/resampler/test.esp32-c3-ard.yaml deleted file mode 100644 index f1721f0862..0000000000 --- a/tests/components/resampler/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - lrclk_pin: GPIO4 - bclk_pin: GPIO5 - mclk_pin: GPIO6 - dout_pin: GPIO7 - -<<: !include common.yaml diff --git a/tests/components/resampler/test.esp32-c3-idf.yaml b/tests/components/resampler/test.esp32-c3-idf.yaml deleted file mode 100644 index f1721f0862..0000000000 --- a/tests/components/resampler/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - lrclk_pin: GPIO4 - bclk_pin: GPIO5 - mclk_pin: GPIO6 - dout_pin: GPIO7 - -<<: !include common.yaml diff --git a/tests/components/resampler/test.esp32-idf.yaml b/tests/components/resampler/test.esp32-idf.yaml index 96d2d37458..6712f1e468 100644 --- a/tests/components/resampler/test.esp32-idf.yaml +++ b/tests/components/resampler/test.esp32-idf.yaml @@ -1,7 +1,10 @@ substitutions: - lrclk_pin: GPIO16 - bclk_pin: GPIO17 + lrclk_pin: GPIO4 + bclk_pin: GPIO5 mclk_pin: GPIO15 dout_pin: GPIO14 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/resampler/test.esp32-s3-ard.yaml b/tests/components/resampler/test.esp32-s3-ard.yaml deleted file mode 100644 index f1721f0862..0000000000 --- a/tests/components/resampler/test.esp32-s3-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - lrclk_pin: GPIO4 - bclk_pin: GPIO5 - mclk_pin: GPIO6 - dout_pin: GPIO7 - -<<: !include common.yaml diff --git a/tests/components/resistance/test.esp32-ard.yaml b/tests/components/resistance/test.esp32-ard.yaml deleted file mode 100644 index 06864605a6..0000000000 --- a/tests/components/resistance/test.esp32-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO32 - -<<: !include common.yaml diff --git a/tests/components/resistance/test.esp32-c3-ard.yaml b/tests/components/resistance/test.esp32-c3-ard.yaml deleted file mode 100644 index 37fb325f4a..0000000000 --- a/tests/components/resistance/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/resistance/test.esp32-c3-idf.yaml b/tests/components/resistance/test.esp32-c3-idf.yaml deleted file mode 100644 index 37fb325f4a..0000000000 --- a/tests/components/resistance/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/resistance/test.esp32-s2-ard.yaml b/tests/components/resistance/test.esp32-s2-ard.yaml deleted file mode 100644 index 1910f325ae..0000000000 --- a/tests/components/resistance/test.esp32-s2-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO1 - -<<: !include common.yaml diff --git a/tests/components/resistance/test.esp32-s3-ard.yaml b/tests/components/resistance/test.esp32-s3-ard.yaml deleted file mode 100644 index 1910f325ae..0000000000 --- a/tests/components/resistance/test.esp32-s3-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO1 - -<<: !include common.yaml diff --git a/tests/components/resistance/test.esp32-s3-idf.yaml b/tests/components/resistance/test.esp32-s3-idf.yaml deleted file mode 100644 index 1910f325ae..0000000000 --- a/tests/components/resistance/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO1 - -<<: !include common.yaml diff --git a/tests/components/restart/test.esp32-ard.yaml b/tests/components/restart/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/restart/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/restart/test.esp32-c3-ard.yaml b/tests/components/restart/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/restart/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/restart/test.esp32-c3-idf.yaml b/tests/components/restart/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/restart/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/binary_sensor_map/test.esp32-c3-idf.yaml b/tests/components/restart/test.nrf52-adafruit.yaml similarity index 100% rename from tests/components/binary_sensor_map/test.esp32-c3-idf.yaml rename to tests/components/restart/test.nrf52-adafruit.yaml diff --git a/tests/components/ble_client/test.esp32-ard.yaml b/tests/components/restart/test.nrf52-mcumgr.yaml similarity index 100% rename from tests/components/ble_client/test.esp32-ard.yaml rename to tests/components/restart/test.nrf52-mcumgr.yaml diff --git a/tests/components/rf_bridge/common.yaml b/tests/components/rf_bridge/common.yaml index eaadc4bb9c..427c3d783d 100644 --- a/tests/components/rf_bridge/common.yaml +++ b/tests/components/rf_bridge/common.yaml @@ -1,9 +1,3 @@ -uart: - - id: uart_rf_bridge - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 115200 - rf_bridge: on_code_received: - lambda: |- diff --git a/tests/components/rf_bridge/test.esp32-ard.yaml b/tests/components/rf_bridge/test.esp32-ard.yaml deleted file mode 100644 index f486544afa..0000000000 --- a/tests/components/rf_bridge/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 - -<<: !include common.yaml diff --git a/tests/components/rf_bridge/test.esp32-c3-ard.yaml b/tests/components/rf_bridge/test.esp32-c3-ard.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/rf_bridge/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/rf_bridge/test.esp32-c3-idf.yaml b/tests/components/rf_bridge/test.esp32-c3-idf.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/rf_bridge/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/rf_bridge/test.esp32-idf.yaml b/tests/components/rf_bridge/test.esp32-idf.yaml index f486544afa..2d29656c94 100644 --- a/tests/components/rf_bridge/test.esp32-idf.yaml +++ b/tests/components/rf_bridge/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/rf_bridge/test.esp8266-ard.yaml b/tests/components/rf_bridge/test.esp8266-ard.yaml index b516342f3b..5a05efa259 100644 --- a/tests/components/rf_bridge/test.esp8266-ard.yaml +++ b/tests/components/rf_bridge/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/rf_bridge/test.rp2040-ard.yaml b/tests/components/rf_bridge/test.rp2040-ard.yaml index b516342f3b..f1df2daf83 100644 --- a/tests/components/rf_bridge/test.rp2040-ard.yaml +++ b/tests/components/rf_bridge/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/rgb/test.esp32-ard.yaml b/tests/components/rgb/test.esp32-ard.yaml deleted file mode 100644 index d78ccec952..0000000000 --- a/tests/components/rgb/test.esp32-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - light_platform: ledc - pin1: GPIO12 - pin2: GPIO13 - pin3: GPIO14 - -<<: !include common.yaml diff --git a/tests/components/rgb/test.esp32-c3-ard.yaml b/tests/components/rgb/test.esp32-c3-ard.yaml deleted file mode 100644 index 1fe4a4bb90..0000000000 --- a/tests/components/rgb/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - light_platform: ledc - pin1: GPIO2 - pin2: GPIO3 - pin3: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/rgb/test.esp32-c3-idf.yaml b/tests/components/rgb/test.esp32-c3-idf.yaml deleted file mode 100644 index 1fe4a4bb90..0000000000 --- a/tests/components/rgb/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - light_platform: ledc - pin1: GPIO2 - pin2: GPIO3 - pin3: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/rgbct/test.esp32-ard.yaml b/tests/components/rgbct/test.esp32-ard.yaml deleted file mode 100644 index 1ecc626e9c..0000000000 --- a/tests/components/rgbct/test.esp32-ard.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - light_platform: ledc - pin1: GPIO12 - pin2: GPIO13 - pin3: GPIO14 - pin4: GPIO15 - pin5: GPIO16 - -<<: !include common.yaml diff --git a/tests/components/rgbct/test.esp32-c3-ard.yaml b/tests/components/rgbct/test.esp32-c3-ard.yaml deleted file mode 100644 index 27a1fbca4d..0000000000 --- a/tests/components/rgbct/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - light_platform: ledc - pin1: GPIO2 - pin2: GPIO3 - pin3: GPIO4 - pin4: GPIO5 - pin5: GPIO6 - -<<: !include common.yaml diff --git a/tests/components/rgbct/test.esp32-c3-idf.yaml b/tests/components/rgbct/test.esp32-c3-idf.yaml deleted file mode 100644 index 27a1fbca4d..0000000000 --- a/tests/components/rgbct/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - light_platform: ledc - pin1: GPIO2 - pin2: GPIO3 - pin3: GPIO4 - pin4: GPIO5 - pin5: GPIO6 - -<<: !include common.yaml diff --git a/tests/components/rgbw/test.esp32-ard.yaml b/tests/components/rgbw/test.esp32-ard.yaml deleted file mode 100644 index ea8efd1a34..0000000000 --- a/tests/components/rgbw/test.esp32-ard.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - light_platform: ledc - pin1: GPIO12 - pin2: GPIO13 - pin3: GPIO14 - pin4: GPIO15 - -<<: !include common.yaml diff --git a/tests/components/rgbw/test.esp32-c3-ard.yaml b/tests/components/rgbw/test.esp32-c3-ard.yaml deleted file mode 100644 index b44734344e..0000000000 --- a/tests/components/rgbw/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - light_platform: ledc - pin1: GPIO2 - pin2: GPIO3 - pin3: GPIO4 - pin4: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/rgbw/test.esp32-c3-idf.yaml b/tests/components/rgbw/test.esp32-c3-idf.yaml deleted file mode 100644 index b44734344e..0000000000 --- a/tests/components/rgbw/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - light_platform: ledc - pin1: GPIO2 - pin2: GPIO3 - pin3: GPIO4 - pin4: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/rgbww/test.esp32-ard.yaml b/tests/components/rgbww/test.esp32-ard.yaml deleted file mode 100644 index 1ecc626e9c..0000000000 --- a/tests/components/rgbww/test.esp32-ard.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - light_platform: ledc - pin1: GPIO12 - pin2: GPIO13 - pin3: GPIO14 - pin4: GPIO15 - pin5: GPIO16 - -<<: !include common.yaml diff --git a/tests/components/rgbww/test.esp32-c3-ard.yaml b/tests/components/rgbww/test.esp32-c3-ard.yaml deleted file mode 100644 index 27a1fbca4d..0000000000 --- a/tests/components/rgbww/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - light_platform: ledc - pin1: GPIO2 - pin2: GPIO3 - pin3: GPIO4 - pin4: GPIO5 - pin5: GPIO6 - -<<: !include common.yaml diff --git a/tests/components/rgbww/test.esp32-c3-idf.yaml b/tests/components/rgbww/test.esp32-c3-idf.yaml deleted file mode 100644 index 27a1fbca4d..0000000000 --- a/tests/components/rgbww/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - light_platform: ledc - pin1: GPIO2 - pin2: GPIO3 - pin3: GPIO4 - pin4: GPIO5 - pin5: GPIO6 - -<<: !include common.yaml diff --git a/tests/components/rotary_encoder/test.esp32-ard.yaml b/tests/components/rotary_encoder/test.esp32-ard.yaml deleted file mode 100644 index 48a624aa37..0000000000 --- a/tests/components/rotary_encoder/test.esp32-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - pin_a: GPIO12 - pin_b: GPIO13 - pin_reset: GPIO14 - -<<: !include common.yaml diff --git a/tests/components/rotary_encoder/test.esp32-c3-ard.yaml b/tests/components/rotary_encoder/test.esp32-c3-ard.yaml deleted file mode 100644 index b71a454bdd..0000000000 --- a/tests/components/rotary_encoder/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - pin_a: GPIO2 - pin_b: GPIO3 - pin_reset: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/rotary_encoder/test.esp32-c3-idf.yaml b/tests/components/rotary_encoder/test.esp32-c3-idf.yaml deleted file mode 100644 index b71a454bdd..0000000000 --- a/tests/components/rotary_encoder/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - pin_a: GPIO2 - pin_b: GPIO3 - pin_reset: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/rp2040/test.rp2040-ard.yaml b/tests/components/rp2040/test.rp2040-ard.yaml new file mode 100644 index 0000000000..039a261016 --- /dev/null +++ b/tests/components/rp2040/test.rp2040-ard.yaml @@ -0,0 +1,13 @@ +logger: + level: VERBOSE + +esphome: + on_boot: + - lambda: |- + int x = 100; + x = clamp(x, 50, 90); + assert(x == 90); + x = clamp_at_least(x, 95); + assert(x == 95); + x = clamp_at_most(x, 40); + assert(x == 40); diff --git a/tests/components/rtttl/test.esp32-ard.yaml b/tests/components/rtttl/test.esp32-ard.yaml deleted file mode 100644 index 26da1ce1d6..0000000000 --- a/tests/components/rtttl/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - output_platform: ledc - pin: GPIO14 - -<<: !include common.yaml diff --git a/tests/components/rtttl/test.esp32-c3-ard.yaml b/tests/components/rtttl/test.esp32-c3-ard.yaml deleted file mode 100644 index 7476963591..0000000000 --- a/tests/components/rtttl/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - output_platform: ledc - pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/rtttl/test.esp32-c3-idf.yaml b/tests/components/rtttl/test.esp32-c3-idf.yaml deleted file mode 100644 index 7476963591..0000000000 --- a/tests/components/rtttl/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - output_platform: ledc - pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/runtime_stats/test.esp32-ard.yaml b/tests/components/runtime_stats/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/runtime_stats/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/ruuvi_ble/test.esp32-ard.yaml b/tests/components/ruuvi_ble/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/ruuvi_ble/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/ruuvi_ble/test.esp32-c3-ard.yaml b/tests/components/ruuvi_ble/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/ruuvi_ble/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/ruuvi_ble/test.esp32-c3-idf.yaml b/tests/components/ruuvi_ble/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/ruuvi_ble/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/ruuvi_ble/test.esp32-idf.yaml b/tests/components/ruuvi_ble/test.esp32-idf.yaml index dade44d145..7a6541ae76 100644 --- a/tests/components/ruuvi_ble/test.esp32-idf.yaml +++ b/tests/components/ruuvi_ble/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/ruuvitag/test.esp32-ard.yaml b/tests/components/ruuvitag/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/ruuvitag/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/ruuvitag/test.esp32-c3-ard.yaml b/tests/components/ruuvitag/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/ruuvitag/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/ruuvitag/test.esp32-c3-idf.yaml b/tests/components/ruuvitag/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/ruuvitag/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/ruuvitag/test.esp32-idf.yaml b/tests/components/ruuvitag/test.esp32-idf.yaml index dade44d145..7a6541ae76 100644 --- a/tests/components/ruuvitag/test.esp32-idf.yaml +++ b/tests/components/ruuvitag/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/rx8130/common.yaml b/tests/components/rx8130/common.yaml new file mode 100644 index 0000000000..e6b849e25b --- /dev/null +++ b/tests/components/rx8130/common.yaml @@ -0,0 +1,8 @@ +esphome: + on_boot: + - rx8130.read_time + - rx8130.write_time + +time: + - platform: rx8130 + i2c_id: i2c_bus diff --git a/tests/components/rx8130/test.esp32-idf.yaml b/tests/components/rx8130/test.esp32-idf.yaml new file mode 100644 index 0000000000..b47e39c389 --- /dev/null +++ b/tests/components/rx8130/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/rx8130/test.esp8266-ard.yaml b/tests/components/rx8130/test.esp8266-ard.yaml new file mode 100644 index 0000000000..4a98b9388a --- /dev/null +++ b/tests/components/rx8130/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/rx8130/test.nrf52-adafruit.yaml b/tests/components/rx8130/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..2a0de6241c --- /dev/null +++ b/tests/components/rx8130/test.nrf52-adafruit.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/nrf52.yaml + +<<: !include common.yaml diff --git a/tests/components/rx8130/test.rp2040-ard.yaml b/tests/components/rx8130/test.rp2040-ard.yaml new file mode 100644 index 0000000000..319a7c71a6 --- /dev/null +++ b/tests/components/rx8130/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/safe_mode/test-enabled.esp32-ard.yaml b/tests/components/safe_mode/test-enabled.esp32-ard.yaml deleted file mode 100644 index 97fd63d70e..0000000000 --- a/tests/components/safe_mode/test-enabled.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common-enabled.yaml diff --git a/tests/components/safe_mode/test-enabled.esp32-c3-ard.yaml b/tests/components/safe_mode/test-enabled.esp32-c3-ard.yaml deleted file mode 100644 index 97fd63d70e..0000000000 --- a/tests/components/safe_mode/test-enabled.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common-enabled.yaml diff --git a/tests/components/safe_mode/test-enabled.esp32-c3-idf.yaml b/tests/components/safe_mode/test-enabled.esp32-c3-idf.yaml deleted file mode 100644 index 97fd63d70e..0000000000 --- a/tests/components/safe_mode/test-enabled.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common-enabled.yaml diff --git a/tests/components/scd30/common.yaml b/tests/components/scd30/common.yaml index 1c45c67af0..f21d8944dc 100644 --- a/tests/components/scd30/common.yaml +++ b/tests/components/scd30/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_scd30 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: scd30 + i2c_id: i2c_bus co2: name: SCD30 CO2 temperature: diff --git a/tests/components/scd30/test.esp32-ard.yaml b/tests/components/scd30/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/scd30/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/scd30/test.esp32-c3-ard.yaml b/tests/components/scd30/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/scd30/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/scd30/test.esp32-c3-idf.yaml b/tests/components/scd30/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/scd30/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/scd30/test.esp32-idf.yaml b/tests/components/scd30/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/scd30/test.esp32-idf.yaml +++ b/tests/components/scd30/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/scd30/test.esp8266-ard.yaml b/tests/components/scd30/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/scd30/test.esp8266-ard.yaml +++ b/tests/components/scd30/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/scd30/test.rp2040-ard.yaml b/tests/components/scd30/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/scd30/test.rp2040-ard.yaml +++ b/tests/components/scd30/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/scd4x/common.yaml b/tests/components/scd4x/common.yaml index dfd35e57de..cedb88977e 100644 --- a/tests/components/scd4x/common.yaml +++ b/tests/components/scd4x/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_scd4x - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: scd4x + i2c_id: i2c_bus id: scd40 co2: name: SCD4X CO2 diff --git a/tests/components/scd4x/test.esp32-ard.yaml b/tests/components/scd4x/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/scd4x/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/scd4x/test.esp32-c3-ard.yaml b/tests/components/scd4x/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/scd4x/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/scd4x/test.esp32-c3-idf.yaml b/tests/components/scd4x/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/scd4x/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/scd4x/test.esp32-idf.yaml b/tests/components/scd4x/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/scd4x/test.esp32-idf.yaml +++ b/tests/components/scd4x/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/scd4x/test.esp8266-ard.yaml b/tests/components/scd4x/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/scd4x/test.esp8266-ard.yaml +++ b/tests/components/scd4x/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/scd4x/test.rp2040-ard.yaml b/tests/components/scd4x/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/scd4x/test.rp2040-ard.yaml +++ b/tests/components/scd4x/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/script/test.esp32-ard.yaml b/tests/components/script/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/script/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/script/test.esp32-c3-ard.yaml b/tests/components/script/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/script/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/script/test.esp32-c3-idf.yaml b/tests/components/script/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/script/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/ble_client/test.esp32-c3-ard.yaml b/tests/components/script/test.nrf52-adafruit.yaml similarity index 100% rename from tests/components/ble_client/test.esp32-c3-ard.yaml rename to tests/components/script/test.nrf52-adafruit.yaml diff --git a/tests/components/ble_client/test.esp32-c3-idf.yaml b/tests/components/script/test.nrf52-mcumgr.yaml similarity index 100% rename from tests/components/ble_client/test.esp32-c3-idf.yaml rename to tests/components/script/test.nrf52-mcumgr.yaml diff --git a/tests/components/sdl/common.yaml b/tests/components/sdl/common.yaml index 50fa4a5990..66f93915b6 100644 --- a/tests/components/sdl/common.yaml +++ b/tests/components/sdl/common.yaml @@ -13,11 +13,14 @@ display: binary_sensor: - platform: sdl + sdl_id: sdl_display id: key_up - key: SDLK_a + key: SDLK_UP - platform: sdl + sdl_id: sdl_display id: key_down - key: SDLK_d + key: SDLK_DOWN - platform: sdl + sdl_id: sdl_display id: key_enter - key: SDLK_s + key: SDLK_RETURN diff --git a/tests/components/sdm_meter/common.yaml b/tests/components/sdm_meter/common.yaml index 60c71a796b..760f134451 100644 --- a/tests/components/sdm_meter/common.yaml +++ b/tests/components/sdm_meter/common.yaml @@ -1,11 +1,6 @@ -uart: - - id: uart_sdm_meter - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 9600 - sensor: - platform: sdm_meter + modbus_id: modbus_bus phase_a: current: name: Phase A Current diff --git a/tests/components/sdm_meter/test.esp32-ard.yaml b/tests/components/sdm_meter/test.esp32-ard.yaml deleted file mode 100644 index f486544afa..0000000000 --- a/tests/components/sdm_meter/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 - -<<: !include common.yaml diff --git a/tests/components/sdm_meter/test.esp32-c3-ard.yaml b/tests/components/sdm_meter/test.esp32-c3-ard.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/sdm_meter/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/sdm_meter/test.esp32-c3-idf.yaml b/tests/components/sdm_meter/test.esp32-c3-idf.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/sdm_meter/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/sdm_meter/test.esp32-idf.yaml b/tests/components/sdm_meter/test.esp32-idf.yaml index f486544afa..b631e16677 100644 --- a/tests/components/sdm_meter/test.esp32-idf.yaml +++ b/tests/components/sdm_meter/test.esp32-idf.yaml @@ -1,5 +1,9 @@ substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 + tx_pin: GPIO4 + rx_pin: GPIO5 + flow_control_pin: GPIO13 + +packages: + modbus: !include ../../test_build_components/common/modbus/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/sdm_meter/test.esp8266-ard.yaml b/tests/components/sdm_meter/test.esp8266-ard.yaml index b516342f3b..421389ae97 100644 --- a/tests/components/sdm_meter/test.esp8266-ard.yaml +++ b/tests/components/sdm_meter/test.esp8266-ard.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: GPIO0 + rx_pin: GPIO2 + +packages: + modbus: !include ../../test_build_components/common/modbus/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/sdm_meter/test.rp2040-ard.yaml b/tests/components/sdm_meter/test.rp2040-ard.yaml index b516342f3b..d78d84c983 100644 --- a/tests/components/sdm_meter/test.rp2040-ard.yaml +++ b/tests/components/sdm_meter/test.rp2040-ard.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO4 rx_pin: GPIO5 +packages: + modbus: !include ../../test_build_components/common/modbus/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/sdp3x/common.yaml b/tests/components/sdp3x/common.yaml index d3c5491ca5..5d06f8eddb 100644 --- a/tests/components/sdp3x/common.yaml +++ b/tests/components/sdp3x/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_sdp3x - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: sdp3x + i2c_id: i2c_bus id: filter_pressure name: HVAC Filter Pressure drop accuracy_decimals: 3 diff --git a/tests/components/sdp3x/test.esp32-ard.yaml b/tests/components/sdp3x/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/sdp3x/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/sdp3x/test.esp32-c3-ard.yaml b/tests/components/sdp3x/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/sdp3x/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/sdp3x/test.esp32-c3-idf.yaml b/tests/components/sdp3x/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/sdp3x/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/sdp3x/test.esp32-idf.yaml b/tests/components/sdp3x/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/sdp3x/test.esp32-idf.yaml +++ b/tests/components/sdp3x/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/sdp3x/test.esp8266-ard.yaml b/tests/components/sdp3x/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/sdp3x/test.esp8266-ard.yaml +++ b/tests/components/sdp3x/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/sdp3x/test.rp2040-ard.yaml b/tests/components/sdp3x/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/sdp3x/test.rp2040-ard.yaml +++ b/tests/components/sdp3x/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/sds011/common.yaml b/tests/components/sds011/common.yaml index c7574e1d7d..abae0f9bd8 100644 --- a/tests/components/sds011/common.yaml +++ b/tests/components/sds011/common.yaml @@ -1,9 +1,3 @@ -uart: - - id: uart_sdm_sds011 - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 115200 - sensor: - platform: sds011 pm_2_5: diff --git a/tests/components/sds011/test.esp32-ard.yaml b/tests/components/sds011/test.esp32-ard.yaml deleted file mode 100644 index f486544afa..0000000000 --- a/tests/components/sds011/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 - -<<: !include common.yaml diff --git a/tests/components/sds011/test.esp32-c3-ard.yaml b/tests/components/sds011/test.esp32-c3-ard.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/sds011/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/sds011/test.esp32-c3-idf.yaml b/tests/components/sds011/test.esp32-c3-idf.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/sds011/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/sds011/test.esp32-idf.yaml b/tests/components/sds011/test.esp32-idf.yaml index f486544afa..b415125e84 100644 --- a/tests/components/sds011/test.esp32-idf.yaml +++ b/tests/components/sds011/test.esp32-idf.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 + tx_pin: GPIO4 + rx_pin: GPIO5 + +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/sds011/test.esp8266-ard.yaml b/tests/components/sds011/test.esp8266-ard.yaml index b516342f3b..96ab4ef6ac 100644 --- a/tests/components/sds011/test.esp8266-ard.yaml +++ b/tests/components/sds011/test.esp8266-ard.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: GPIO0 + rx_pin: GPIO2 + +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/sds011/test.rp2040-ard.yaml b/tests/components/sds011/test.rp2040-ard.yaml index b516342f3b..b28f2b5e05 100644 --- a/tests/components/sds011/test.rp2040-ard.yaml +++ b/tests/components/sds011/test.rp2040-ard.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO4 rx_pin: GPIO5 +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/seeed_mr24hpc1/common.yaml b/tests/components/seeed_mr24hpc1/common.yaml index 1bc9d65295..3a52888c5c 100644 --- a/tests/components/seeed_mr24hpc1/common.yaml +++ b/tests/components/seeed_mr24hpc1/common.yaml @@ -1,24 +1,5 @@ -esphome: - name: test-esp8266 - friendly_name: "Test ESP8266" - -esp8266: - board: d1_mini - -logger: - level: VERY_VERBOSE - -uart: - - id: seeed_mr24hpc1_uart - tx_pin: ${uart_tx_pin} - rx_pin: ${uart_rx_pin} - baud_rate: 115200 - parity: NONE - stop_bits: 1 - seeed_mr24hpc1: id: my_seeed_mr24hpc1 - uart_id: seeed_mr24hpc1_uart sensor: - platform: seeed_mr24hpc1 diff --git a/tests/components/seeed_mr24hpc1/test.esp32-c3-ard.yaml b/tests/components/seeed_mr24hpc1/test.esp32-c3-ard.yaml deleted file mode 100644 index 4fb884abf4..0000000000 --- a/tests/components/seeed_mr24hpc1/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - uart_tx_pin: GPIO5 - uart_rx_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/seeed_mr24hpc1/test.esp32-c3-idf.yaml b/tests/components/seeed_mr24hpc1/test.esp32-c3-idf.yaml deleted file mode 100644 index 4fb884abf4..0000000000 --- a/tests/components/seeed_mr24hpc1/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - uart_tx_pin: GPIO5 - uart_rx_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/seeed_mr60bha2/common.yaml b/tests/components/seeed_mr60bha2/common.yaml index 9eb0c8d527..7d1a02b3ea 100644 --- a/tests/components/seeed_mr60bha2/common.yaml +++ b/tests/components/seeed_mr60bha2/common.yaml @@ -1,11 +1,3 @@ -uart: - - id: seeed_mr60fda2_uart - tx_pin: ${uart_tx_pin} - rx_pin: ${uart_rx_pin} - baud_rate: 115200 - parity: NONE - stop_bits: 1 - seeed_mr60bha2: id: my_seeed_mr60bha2 diff --git a/tests/components/seeed_mr60bha2/test.esp32-c3-ard.yaml b/tests/components/seeed_mr60bha2/test.esp32-c3-ard.yaml deleted file mode 100644 index 4fb884abf4..0000000000 --- a/tests/components/seeed_mr60bha2/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - uart_tx_pin: GPIO5 - uart_rx_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/seeed_mr60bha2/test.esp32-c3-idf.yaml b/tests/components/seeed_mr60bha2/test.esp32-c3-idf.yaml deleted file mode 100644 index 4fb884abf4..0000000000 --- a/tests/components/seeed_mr60bha2/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - uart_tx_pin: GPIO5 - uart_rx_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/seeed_mr60fda2/common.yaml b/tests/components/seeed_mr60fda2/common.yaml index 55a7cc1ab3..e0e4614481 100644 --- a/tests/components/seeed_mr60fda2/common.yaml +++ b/tests/components/seeed_mr60fda2/common.yaml @@ -1,14 +1,5 @@ -uart: - - id: seeed_mr60fda2_uart - tx_pin: ${uart_tx_pin} - rx_pin: ${uart_rx_pin} - baud_rate: 115200 - parity: NONE - stop_bits: 1 - seeed_mr60fda2: id: my_seeed_mr60fda2 - uart_id: seeed_mr60fda2_uart binary_sensor: - platform: seeed_mr60fda2 diff --git a/tests/components/seeed_mr60fda2/test.esp32-c3-ard.yaml b/tests/components/seeed_mr60fda2/test.esp32-c3-ard.yaml deleted file mode 100644 index 4fb884abf4..0000000000 --- a/tests/components/seeed_mr60fda2/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - uart_tx_pin: GPIO5 - uart_rx_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/seeed_mr60fda2/test.esp32-c3-idf.yaml b/tests/components/seeed_mr60fda2/test.esp32-c3-idf.yaml deleted file mode 100644 index 4fb884abf4..0000000000 --- a/tests/components/seeed_mr60fda2/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - uart_tx_pin: GPIO5 - uart_rx_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/selec_meter/common.yaml b/tests/components/selec_meter/common.yaml index f2714ce828..2febbee540 100644 --- a/tests/components/selec_meter/common.yaml +++ b/tests/components/selec_meter/common.yaml @@ -1,11 +1,6 @@ -uart: - - id: uart_selec_meter - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 9600 - sensor: - platform: selec_meter + modbus_id: modbus_bus total_active_energy: name: SelecEM2M Total Active Energy import_active_energy: diff --git a/tests/components/selec_meter/test.esp32-ard.yaml b/tests/components/selec_meter/test.esp32-ard.yaml deleted file mode 100644 index f486544afa..0000000000 --- a/tests/components/selec_meter/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 - -<<: !include common.yaml diff --git a/tests/components/selec_meter/test.esp32-c3-ard.yaml b/tests/components/selec_meter/test.esp32-c3-ard.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/selec_meter/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/selec_meter/test.esp32-c3-idf.yaml b/tests/components/selec_meter/test.esp32-c3-idf.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/selec_meter/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/selec_meter/test.esp32-idf.yaml b/tests/components/selec_meter/test.esp32-idf.yaml index f486544afa..4df9f5863e 100644 --- a/tests/components/selec_meter/test.esp32-idf.yaml +++ b/tests/components/selec_meter/test.esp32-idf.yaml @@ -1,5 +1,9 @@ substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 + tx_pin: GPIO4 + rx_pin: GPIO5 + flow_control_pin: GPIO26 + +packages: + modbus: !include ../../test_build_components/common/modbus/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/selec_meter/test.esp8266-ard.yaml b/tests/components/selec_meter/test.esp8266-ard.yaml index b516342f3b..6daa08c22b 100644 --- a/tests/components/selec_meter/test.esp8266-ard.yaml +++ b/tests/components/selec_meter/test.esp8266-ard.yaml @@ -1,5 +1,9 @@ substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: GPIO0 + rx_pin: GPIO2 + flow_control_pin: GPIO15 + +packages: + modbus: !include ../../test_build_components/common/modbus/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/selec_meter/test.rp2040-ard.yaml b/tests/components/selec_meter/test.rp2040-ard.yaml index b516342f3b..a65500ccc3 100644 --- a/tests/components/selec_meter/test.rp2040-ard.yaml +++ b/tests/components/selec_meter/test.rp2040-ard.yaml @@ -1,5 +1,9 @@ substitutions: tx_pin: GPIO4 rx_pin: GPIO5 + flow_control_pin: GPIO6 + +packages: + modbus: !include ../../test_build_components/common/modbus/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/sen0321/common.yaml b/tests/components/sen0321/common.yaml index 8b9fdff4a1..9228c7b820 100644 --- a/tests/components/sen0321/common.yaml +++ b/tests/components/sen0321/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_sen0321 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: sen0321 + i2c_id: i2c_bus name: Workshop Ozone Sensor id: sen0321_ozone update_interval: 10s diff --git a/tests/components/sen0321/test.esp32-ard.yaml b/tests/components/sen0321/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/sen0321/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/sen0321/test.esp32-c3-ard.yaml b/tests/components/sen0321/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/sen0321/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/sen0321/test.esp32-c3-idf.yaml b/tests/components/sen0321/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/sen0321/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/sen0321/test.esp32-idf.yaml b/tests/components/sen0321/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/sen0321/test.esp32-idf.yaml +++ b/tests/components/sen0321/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/sen0321/test.esp8266-ard.yaml b/tests/components/sen0321/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/sen0321/test.esp8266-ard.yaml +++ b/tests/components/sen0321/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/sen0321/test.rp2040-ard.yaml b/tests/components/sen0321/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/sen0321/test.rp2040-ard.yaml +++ b/tests/components/sen0321/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/sen21231/common.yaml b/tests/components/sen21231/common.yaml index 6fa1d04aa2..bfe0f9d7f8 100644 --- a/tests/components/sen21231/common.yaml +++ b/tests/components/sen21231/common.yaml @@ -1,9 +1,5 @@ -i2c: - - id: i2c_sen21231 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: sen21231 + i2c_id: i2c_bus id: sen21231_sensor1 name: Person Sensor diff --git a/tests/components/sen21231/test.esp32-ard.yaml b/tests/components/sen21231/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/sen21231/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/sen21231/test.esp32-c3-ard.yaml b/tests/components/sen21231/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/sen21231/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/sen21231/test.esp32-c3-idf.yaml b/tests/components/sen21231/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/sen21231/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/sen21231/test.esp32-idf.yaml b/tests/components/sen21231/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/sen21231/test.esp32-idf.yaml +++ b/tests/components/sen21231/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/sen21231/test.esp8266-ard.yaml b/tests/components/sen21231/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/sen21231/test.esp8266-ard.yaml +++ b/tests/components/sen21231/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/sen21231/test.rp2040-ard.yaml b/tests/components/sen21231/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/sen21231/test.rp2040-ard.yaml +++ b/tests/components/sen21231/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/sen5x/common.yaml b/tests/components/sen5x/common.yaml index 9adf268048..a4462a16ea 100644 --- a/tests/components/sen5x/common.yaml +++ b/tests/components/sen5x/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_sen5x - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: sen5x + i2c_id: i2c_bus id: sen54 temperature: name: Temperature diff --git a/tests/components/sen5x/test.esp32-ard.yaml b/tests/components/sen5x/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/sen5x/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/sen5x/test.esp32-c3-ard.yaml b/tests/components/sen5x/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/sen5x/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/sen5x/test.esp32-c3-idf.yaml b/tests/components/sen5x/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/sen5x/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/sen5x/test.esp32-idf.yaml b/tests/components/sen5x/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/sen5x/test.esp32-idf.yaml +++ b/tests/components/sen5x/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/sen5x/test.esp8266-ard.yaml b/tests/components/sen5x/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/sen5x/test.esp8266-ard.yaml +++ b/tests/components/sen5x/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/sen5x/test.rp2040-ard.yaml b/tests/components/sen5x/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/sen5x/test.rp2040-ard.yaml +++ b/tests/components/sen5x/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/senseair/common.yaml b/tests/components/senseair/common.yaml index 23a933affe..c8066896d0 100644 --- a/tests/components/senseair/common.yaml +++ b/tests/components/senseair/common.yaml @@ -1,9 +1,3 @@ -uart: - - id: uart_senseair - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 9600 - sensor: - platform: senseair id: senseair0 diff --git a/tests/components/senseair/test.esp32-ard.yaml b/tests/components/senseair/test.esp32-ard.yaml deleted file mode 100644 index f486544afa..0000000000 --- a/tests/components/senseair/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 - -<<: !include common.yaml diff --git a/tests/components/senseair/test.esp32-c3-ard.yaml b/tests/components/senseair/test.esp32-c3-ard.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/senseair/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/senseair/test.esp32-c3-idf.yaml b/tests/components/senseair/test.esp32-c3-idf.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/senseair/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/senseair/test.esp32-idf.yaml b/tests/components/senseair/test.esp32-idf.yaml index f486544afa..b415125e84 100644 --- a/tests/components/senseair/test.esp32-idf.yaml +++ b/tests/components/senseair/test.esp32-idf.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 + tx_pin: GPIO4 + rx_pin: GPIO5 + +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/senseair/test.esp8266-ard.yaml b/tests/components/senseair/test.esp8266-ard.yaml index b516342f3b..96ab4ef6ac 100644 --- a/tests/components/senseair/test.esp8266-ard.yaml +++ b/tests/components/senseair/test.esp8266-ard.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: GPIO0 + rx_pin: GPIO2 + +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/senseair/test.rp2040-ard.yaml b/tests/components/senseair/test.rp2040-ard.yaml index b516342f3b..b28f2b5e05 100644 --- a/tests/components/senseair/test.rp2040-ard.yaml +++ b/tests/components/senseair/test.rp2040-ard.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO4 rx_pin: GPIO5 +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/sensor/common.yaml b/tests/components/sensor/common.yaml new file mode 100644 index 0000000000..2180f66da8 --- /dev/null +++ b/tests/components/sensor/common.yaml @@ -0,0 +1,238 @@ +sensor: + # Source sensor for testing filters + - platform: template + name: "Source Sensor" + id: source_sensor + lambda: return 42.0; + update_interval: 1s + + # Streaming filters (window_size == send_every) - uses StreamingFilter base class + - platform: copy + source_id: source_sensor + name: "Streaming Min Filter" + filters: + - min: + window_size: 10 + send_every: 10 # Batch window → StreamingMinFilter + + - platform: copy + source_id: source_sensor + name: "Streaming Max Filter" + filters: + - max: + window_size: 10 + send_every: 10 # Batch window → StreamingMaxFilter + + - platform: copy + source_id: source_sensor + name: "Streaming Moving Average Filter" + filters: + - sliding_window_moving_average: + window_size: 10 + send_every: 10 # Batch window → StreamingMovingAverageFilter + + # Sliding window filters (window_size != send_every) - uses SlidingWindowFilter base class with ring buffer + - platform: copy + source_id: source_sensor + name: "Sliding Min Filter" + filters: + - min: + window_size: 10 + send_every: 5 # Sliding window → MinFilter with ring buffer + + - platform: copy + source_id: source_sensor + name: "Sliding Max Filter" + filters: + - max: + window_size: 10 + send_every: 5 # Sliding window → MaxFilter with ring buffer + + - platform: copy + source_id: source_sensor + name: "Sliding Median Filter" + filters: + - median: + window_size: 10 + send_every: 5 # Sliding window → MedianFilter with ring buffer + + - platform: copy + source_id: source_sensor + name: "Sliding Quantile Filter" + filters: + - quantile: + window_size: 10 + send_every: 5 + quantile: 0.9 # Sliding window → QuantileFilter with ring buffer + + - platform: copy + source_id: source_sensor + name: "Sliding Moving Average Filter" + filters: + - sliding_window_moving_average: + window_size: 10 + send_every: 5 # Sliding window → SlidingWindowMovingAverageFilter with ring buffer + + # Edge cases + - platform: copy + source_id: source_sensor + name: "Large Batch Window Min" + filters: + - min: + window_size: 1000 + send_every: 1000 # Large batch → StreamingMinFilter (4 bytes, not 4KB) + + - platform: copy + source_id: source_sensor + name: "Small Sliding Window" + filters: + - median: + window_size: 3 + send_every: 1 # Frequent output → MedianFilter with 3-element ring buffer + + # send_first_at parameter test + - platform: copy + source_id: source_sensor + name: "Early Send Filter" + filters: + - max: + window_size: 10 + send_every: 10 + send_first_at: 1 # Send after first value + + # ValueListFilter-based filters tests + # FilterOutValueFilter - single value + - platform: copy + source_id: source_sensor + name: "Filter Out Single Value" + filters: + - filter_out: 42.0 # Should filter out exactly 42.0 + + # FilterOutValueFilter - multiple values + - platform: copy + source_id: source_sensor + name: "Filter Out Multiple Values" + filters: + - filter_out: [0.0, 42.0, 100.0] # List of values to filter + + # FilterOutValueFilter - with NaN + - platform: copy + source_id: source_sensor + name: "Filter Out NaN" + filters: + - filter_out: nan # Filter out NaN values + + # FilterOutValueFilter - mixed values with NaN + - platform: copy + source_id: source_sensor + name: "Filter Out Mixed with NaN" + filters: + - filter_out: [nan, 0.0, 42.0] + + # ThrottleWithPriorityFilter - single priority value + - platform: copy + source_id: source_sensor + name: "Throttle with Single Priority" + filters: + - throttle_with_priority: + timeout: 1000ms + value: 42.0 # Priority value bypasses throttle + + # ThrottleWithPriorityFilter - multiple priority values + - platform: copy + source_id: source_sensor + name: "Throttle with Multiple Priorities" + filters: + - throttle_with_priority: + timeout: 500ms + value: [0.0, 42.0, 100.0] # Multiple priority values + + # ThrottleWithPriorityFilter - with NaN priority + - platform: copy + source_id: source_sensor + name: "Throttle with NaN Priority" + filters: + - throttle_with_priority: + timeout: 1000ms + value: nan # NaN as priority value + + # Combined filters - FilterOutValueFilter + other filters + - platform: copy + source_id: source_sensor + name: "Filter Out Then Throttle" + filters: + - filter_out: [0.0, 100.0] + - throttle: 500ms + + # Combined filters - ThrottleWithPriorityFilter + other filters + - platform: copy + source_id: source_sensor + name: "Throttle Priority Then Scale" + filters: + - throttle_with_priority: + timeout: 1000ms + value: [42.0] + - multiply: 2.0 + + # CalibrateLinearFilter - piecewise linear calibration + - platform: copy + source_id: source_sensor + name: "Calibrate Linear Two Points" + filters: + - calibrate_linear: + - 0.0 -> 0.0 + - 100.0 -> 100.0 + + - platform: copy + source_id: source_sensor + name: "Calibrate Linear Multiple Segments" + filters: + - calibrate_linear: + - 0.0 -> 0.0 + - 50.0 -> 55.0 + - 100.0 -> 102.5 + + - platform: copy + source_id: source_sensor + name: "Calibrate Linear Least Squares" + filters: + - calibrate_linear: + method: least_squares + datapoints: + - 0.0 -> 0.0 + - 50.0 -> 55.0 + - 100.0 -> 102.5 + + # CalibratePolynomialFilter - polynomial calibration + - platform: copy + source_id: source_sensor + name: "Calibrate Polynomial Degree 2" + filters: + - calibrate_polynomial: + degree: 2 + datapoints: + - 0.0 -> 0.0 + - 50.0 -> 55.0 + - 100.0 -> 102.5 + + - platform: copy + source_id: source_sensor + name: "Calibrate Polynomial Degree 3" + filters: + - calibrate_polynomial: + degree: 3 + datapoints: + - 0.0 -> 0.0 + - 25.0 -> 26.0 + - 50.0 -> 55.0 + - 100.0 -> 102.5 + + # OrFilter - filter branching + - platform: copy + source_id: source_sensor + name: "Or Filter with Multiple Branches" + filters: + - or: + - multiply: 2.0 + - offset: 10.0 + - lambda: return x * 3.0; diff --git a/tests/components/ble_presence/test.esp32-ard.yaml b/tests/components/sensor/test.esp8266-ard.yaml similarity index 100% rename from tests/components/ble_presence/test.esp32-ard.yaml rename to tests/components/sensor/test.esp8266-ard.yaml diff --git a/tests/components/servo/test.esp32-ard.yaml b/tests/components/servo/test.esp32-ard.yaml deleted file mode 100644 index 26da1ce1d6..0000000000 --- a/tests/components/servo/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - output_platform: ledc - pin: GPIO14 - -<<: !include common.yaml diff --git a/tests/components/servo/test.esp32-c3-ard.yaml b/tests/components/servo/test.esp32-c3-ard.yaml deleted file mode 100644 index 7476963591..0000000000 --- a/tests/components/servo/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - output_platform: ledc - pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/servo/test.esp32-c3-idf.yaml b/tests/components/servo/test.esp32-c3-idf.yaml deleted file mode 100644 index 7476963591..0000000000 --- a/tests/components/servo/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - output_platform: ledc - pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/sfa30/common.yaml b/tests/components/sfa30/common.yaml index e3b38aa7fb..3cd2483abc 100644 --- a/tests/components/sfa30/common.yaml +++ b/tests/components/sfa30/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_sfa30 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: sfa30 + i2c_id: i2c_bus formaldehyde: name: SFA30 formaldehyde temperature: diff --git a/tests/components/sfa30/test.esp32-ard.yaml b/tests/components/sfa30/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/sfa30/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/sfa30/test.esp32-c3-ard.yaml b/tests/components/sfa30/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/sfa30/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/sfa30/test.esp32-c3-idf.yaml b/tests/components/sfa30/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/sfa30/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/sfa30/test.esp32-idf.yaml b/tests/components/sfa30/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/sfa30/test.esp32-idf.yaml +++ b/tests/components/sfa30/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/sfa30/test.esp8266-ard.yaml b/tests/components/sfa30/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/sfa30/test.esp8266-ard.yaml +++ b/tests/components/sfa30/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/sfa30/test.rp2040-ard.yaml b/tests/components/sfa30/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/sfa30/test.rp2040-ard.yaml +++ b/tests/components/sfa30/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/sgp30/common.yaml b/tests/components/sgp30/common.yaml index 1db5bc67d1..9d4ed46615 100644 --- a/tests/components/sgp30/common.yaml +++ b/tests/components/sgp30/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_sgp30 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: sgp30 + i2c_id: i2c_bus eco2: name: Workshop eCO2 accuracy_decimals: 1 diff --git a/tests/components/sgp30/test.esp32-ard.yaml b/tests/components/sgp30/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/sgp30/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/sgp30/test.esp32-c3-ard.yaml b/tests/components/sgp30/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/sgp30/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/sgp30/test.esp32-c3-idf.yaml b/tests/components/sgp30/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/sgp30/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/sgp30/test.esp32-idf.yaml b/tests/components/sgp30/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/sgp30/test.esp32-idf.yaml +++ b/tests/components/sgp30/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/sgp30/test.esp8266-ard.yaml b/tests/components/sgp30/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/sgp30/test.esp8266-ard.yaml +++ b/tests/components/sgp30/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/sgp30/test.rp2040-ard.yaml b/tests/components/sgp30/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/sgp30/test.rp2040-ard.yaml +++ b/tests/components/sgp30/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/sgp4x/common.yaml b/tests/components/sgp4x/common.yaml index adb678d542..4edda8fd1b 100644 --- a/tests/components/sgp4x/common.yaml +++ b/tests/components/sgp4x/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_sgp4x - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: sgp4x + i2c_id: i2c_bus voc: name: VOC Index id: sgp40_voc_index diff --git a/tests/components/sgp4x/test.esp32-ard.yaml b/tests/components/sgp4x/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/sgp4x/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/sgp4x/test.esp32-c3-ard.yaml b/tests/components/sgp4x/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/sgp4x/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/sgp4x/test.esp32-c3-idf.yaml b/tests/components/sgp4x/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/sgp4x/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/sgp4x/test.esp32-idf.yaml b/tests/components/sgp4x/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/sgp4x/test.esp32-idf.yaml +++ b/tests/components/sgp4x/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/sgp4x/test.esp8266-ard.yaml b/tests/components/sgp4x/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/sgp4x/test.esp8266-ard.yaml +++ b/tests/components/sgp4x/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/sgp4x/test.rp2040-ard.yaml b/tests/components/sgp4x/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/sgp4x/test.rp2040-ard.yaml +++ b/tests/components/sgp4x/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/sha256/common.yaml b/tests/components/sha256/common.yaml new file mode 100644 index 0000000000..fa884c1958 --- /dev/null +++ b/tests/components/sha256/common.yaml @@ -0,0 +1,32 @@ +esphome: + on_boot: + - lambda: |- + // Test SHA256 functionality + #ifdef USE_SHA256 + using esphome::sha256::SHA256; + SHA256 hasher; + hasher.init(); + + // Test with "Hello World" - known SHA256 + const char* test_string = "Hello World"; + hasher.add(test_string, strlen(test_string)); + hasher.calculate(); + + char hex_output[65]; + hasher.get_hex(hex_output); + hex_output[64] = '\0'; + + ESP_LOGD("SHA256", "SHA256('Hello World') = %s", hex_output); + + // Expected: a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e + const char* expected = "a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e"; + if (strcmp(hex_output, expected) == 0) { + ESP_LOGI("SHA256", "Test PASSED"); + } else { + ESP_LOGE("SHA256", "Test FAILED. Expected %s", expected); + } + #else + ESP_LOGW("SHA256", "SHA256 not available on this platform"); + #endif + +sha256: diff --git a/tests/components/ble_presence/test.esp32-c3-ard.yaml b/tests/components/sha256/test.bk72xx-ard.yaml similarity index 100% rename from tests/components/ble_presence/test.esp32-c3-ard.yaml rename to tests/components/sha256/test.bk72xx-ard.yaml diff --git a/tests/components/inkplate6/test.esp32-idf.yaml b/tests/components/sha256/test.esp32-idf.yaml similarity index 100% rename from tests/components/inkplate6/test.esp32-idf.yaml rename to tests/components/sha256/test.esp32-idf.yaml diff --git a/tests/components/ble_presence/test.esp32-c3-idf.yaml b/tests/components/sha256/test.esp8266-ard.yaml similarity index 100% rename from tests/components/ble_presence/test.esp32-c3-idf.yaml rename to tests/components/sha256/test.esp8266-ard.yaml diff --git a/tests/components/ble_rssi/test.esp32-ard.yaml b/tests/components/sha256/test.host.yaml similarity index 100% rename from tests/components/ble_rssi/test.esp32-ard.yaml rename to tests/components/sha256/test.host.yaml diff --git a/tests/components/ble_rssi/test.esp32-c3-ard.yaml b/tests/components/sha256/test.rp2040-ard.yaml similarity index 100% rename from tests/components/ble_rssi/test.esp32-c3-ard.yaml rename to tests/components/sha256/test.rp2040-ard.yaml diff --git a/tests/components/shelly_dimmer/common.yaml b/tests/components/shelly_dimmer/common.yaml index 3acd0260d5..ba36fa995d 100644 --- a/tests/components/shelly_dimmer/common.yaml +++ b/tests/components/shelly_dimmer/common.yaml @@ -1,9 +1,3 @@ -uart: - - id: uart_shelly_dimmer - tx_pin: 4 - rx_pin: 5 - baud_rate: 9600 - light: - platform: shelly_dimmer name: Shelly Dimmer Light diff --git a/tests/components/shelly_dimmer/test.esp8266-ard.yaml b/tests/components/shelly_dimmer/test.esp8266-ard.yaml index dade44d145..80a2cb2fc0 100644 --- a/tests/components/shelly_dimmer/test.esp8266-ard.yaml +++ b/tests/components/shelly_dimmer/test.esp8266-ard.yaml @@ -1 +1,4 @@ +packages: + uart_115200: !include ../../test_build_components/common/uart_115200/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/sht3xd/common.yaml b/tests/components/sht3xd/common.yaml index 2426ebfbb9..02ce521747 100644 --- a/tests/components/sht3xd/common.yaml +++ b/tests/components/sht3xd/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_sht3xd - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: sht3xd + i2c_id: i2c_bus temperature: name: SHT3XD Temperature humidity: diff --git a/tests/components/sht3xd/test.esp32-ard.yaml b/tests/components/sht3xd/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/sht3xd/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/sht3xd/test.esp32-c3-ard.yaml b/tests/components/sht3xd/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/sht3xd/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/sht3xd/test.esp32-c3-idf.yaml b/tests/components/sht3xd/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/sht3xd/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/sht3xd/test.esp32-idf.yaml b/tests/components/sht3xd/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/sht3xd/test.esp32-idf.yaml +++ b/tests/components/sht3xd/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/sht3xd/test.esp8266-ard.yaml b/tests/components/sht3xd/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/sht3xd/test.esp8266-ard.yaml +++ b/tests/components/sht3xd/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/sht3xd/test.rp2040-ard.yaml b/tests/components/sht3xd/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/sht3xd/test.rp2040-ard.yaml +++ b/tests/components/sht3xd/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/sht4x/common.yaml b/tests/components/sht4x/common.yaml index 703a8fa32b..50d5ad8ca4 100644 --- a/tests/components/sht4x/common.yaml +++ b/tests/components/sht4x/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_sht4x - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: sht4x + i2c_id: i2c_bus temperature: name: SHT4X Temperature humidity: diff --git a/tests/components/sht4x/test.esp32-ard.yaml b/tests/components/sht4x/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/sht4x/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/sht4x/test.esp32-c3-ard.yaml b/tests/components/sht4x/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/sht4x/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/sht4x/test.esp32-c3-idf.yaml b/tests/components/sht4x/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/sht4x/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/sht4x/test.esp32-idf.yaml b/tests/components/sht4x/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/sht4x/test.esp32-idf.yaml +++ b/tests/components/sht4x/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/sht4x/test.esp8266-ard.yaml b/tests/components/sht4x/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/sht4x/test.esp8266-ard.yaml +++ b/tests/components/sht4x/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/sht4x/test.rp2040-ard.yaml b/tests/components/sht4x/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/sht4x/test.rp2040-ard.yaml +++ b/tests/components/sht4x/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/shtcx/common.yaml b/tests/components/shtcx/common.yaml index 0211319124..a326a1b4d6 100644 --- a/tests/components/shtcx/common.yaml +++ b/tests/components/shtcx/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_shtcx - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: shtcx + i2c_id: i2c_bus temperature: name: SHTCX Temperature humidity: diff --git a/tests/components/shtcx/test.esp32-ard.yaml b/tests/components/shtcx/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/shtcx/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/shtcx/test.esp32-c3-ard.yaml b/tests/components/shtcx/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/shtcx/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/shtcx/test.esp32-c3-idf.yaml b/tests/components/shtcx/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/shtcx/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/shtcx/test.esp32-idf.yaml b/tests/components/shtcx/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/shtcx/test.esp32-idf.yaml +++ b/tests/components/shtcx/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/shtcx/test.esp8266-ard.yaml b/tests/components/shtcx/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/shtcx/test.esp8266-ard.yaml +++ b/tests/components/shtcx/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/shtcx/test.rp2040-ard.yaml b/tests/components/shtcx/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/shtcx/test.rp2040-ard.yaml +++ b/tests/components/shtcx/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/shutdown/test.esp32-ard.yaml b/tests/components/shutdown/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/shutdown/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/shutdown/test.esp32-c3-ard.yaml b/tests/components/shutdown/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/shutdown/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/shutdown/test.esp32-c3-idf.yaml b/tests/components/shutdown/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/shutdown/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/sigma_delta_output/test.esp32-ard.yaml b/tests/components/sigma_delta_output/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/sigma_delta_output/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/sigma_delta_output/test.esp32-c3-ard.yaml b/tests/components/sigma_delta_output/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/sigma_delta_output/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/sim800l/common.yaml b/tests/components/sim800l/common.yaml index 1b4e2e1af6..503637b35f 100644 --- a/tests/components/sim800l/common.yaml +++ b/tests/components/sim800l/common.yaml @@ -11,12 +11,6 @@ esphome: - sim800l.send_ussd: ussd: test_ussd -uart: - - id: uart_sim800l - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 9600 - sim800l: on_sms_received: - lambda: |- diff --git a/tests/components/sim800l/test.esp32-ard.yaml b/tests/components/sim800l/test.esp32-ard.yaml deleted file mode 100644 index f486544afa..0000000000 --- a/tests/components/sim800l/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 - -<<: !include common.yaml diff --git a/tests/components/sim800l/test.esp32-c3-ard.yaml b/tests/components/sim800l/test.esp32-c3-ard.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/sim800l/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/sim800l/test.esp32-c3-idf.yaml b/tests/components/sim800l/test.esp32-c3-idf.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/sim800l/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/sim800l/test.esp32-idf.yaml b/tests/components/sim800l/test.esp32-idf.yaml index f486544afa..b415125e84 100644 --- a/tests/components/sim800l/test.esp32-idf.yaml +++ b/tests/components/sim800l/test.esp32-idf.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 + tx_pin: GPIO4 + rx_pin: GPIO5 + +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/sim800l/test.esp8266-ard.yaml b/tests/components/sim800l/test.esp8266-ard.yaml index b516342f3b..96ab4ef6ac 100644 --- a/tests/components/sim800l/test.esp8266-ard.yaml +++ b/tests/components/sim800l/test.esp8266-ard.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: GPIO0 + rx_pin: GPIO2 + +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/sim800l/test.rp2040-ard.yaml b/tests/components/sim800l/test.rp2040-ard.yaml index b516342f3b..b28f2b5e05 100644 --- a/tests/components/sim800l/test.rp2040-ard.yaml +++ b/tests/components/sim800l/test.rp2040-ard.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO4 rx_pin: GPIO5 +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/slow_pwm/test.esp32-ard.yaml b/tests/components/slow_pwm/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/slow_pwm/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/slow_pwm/test.esp32-c3-ard.yaml b/tests/components/slow_pwm/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/slow_pwm/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/slow_pwm/test.esp32-c3-idf.yaml b/tests/components/slow_pwm/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/slow_pwm/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/sm16716/test.esp32-ard.yaml b/tests/components/sm16716/test.esp32-ard.yaml deleted file mode 100644 index d295973e3f..0000000000 --- a/tests/components/sm16716/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - clock_pin: GPIO16 - data_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/sm16716/test.esp32-c3-ard.yaml b/tests/components/sm16716/test.esp32-c3-ard.yaml deleted file mode 100644 index 7808481215..0000000000 --- a/tests/components/sm16716/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - clock_pin: GPIO5 - data_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/sm16716/test.esp32-c3-idf.yaml b/tests/components/sm16716/test.esp32-c3-idf.yaml deleted file mode 100644 index 7808481215..0000000000 --- a/tests/components/sm16716/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - clock_pin: GPIO5 - data_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/sm16716/test.esp32-idf.yaml b/tests/components/sm16716/test.esp32-idf.yaml index d295973e3f..a4ecdb6c49 100644 --- a/tests/components/sm16716/test.esp32-idf.yaml +++ b/tests/components/sm16716/test.esp32-idf.yaml @@ -1,5 +1,5 @@ substitutions: - clock_pin: GPIO16 - data_pin: GPIO17 + clock_pin: GPIO4 + data_pin: GPIO5 <<: !include common.yaml diff --git a/tests/components/sm16716/test.esp8266-ard.yaml b/tests/components/sm16716/test.esp8266-ard.yaml index 7808481215..7c7f1e1a11 100644 --- a/tests/components/sm16716/test.esp8266-ard.yaml +++ b/tests/components/sm16716/test.esp8266-ard.yaml @@ -1,5 +1,5 @@ substitutions: - clock_pin: GPIO5 - data_pin: GPIO4 + clock_pin: GPIO0 + data_pin: GPIO2 <<: !include common.yaml diff --git a/tests/components/sm2135/test.esp32-ard.yaml b/tests/components/sm2135/test.esp32-ard.yaml deleted file mode 100644 index d295973e3f..0000000000 --- a/tests/components/sm2135/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - clock_pin: GPIO16 - data_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/sm2135/test.esp32-c3-ard.yaml b/tests/components/sm2135/test.esp32-c3-ard.yaml deleted file mode 100644 index 7808481215..0000000000 --- a/tests/components/sm2135/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - clock_pin: GPIO5 - data_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/sm2135/test.esp32-c3-idf.yaml b/tests/components/sm2135/test.esp32-c3-idf.yaml deleted file mode 100644 index 7808481215..0000000000 --- a/tests/components/sm2135/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - clock_pin: GPIO5 - data_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/sm2135/test.esp32-idf.yaml b/tests/components/sm2135/test.esp32-idf.yaml index d295973e3f..a4ecdb6c49 100644 --- a/tests/components/sm2135/test.esp32-idf.yaml +++ b/tests/components/sm2135/test.esp32-idf.yaml @@ -1,5 +1,5 @@ substitutions: - clock_pin: GPIO16 - data_pin: GPIO17 + clock_pin: GPIO4 + data_pin: GPIO5 <<: !include common.yaml diff --git a/tests/components/sm2135/test.esp8266-ard.yaml b/tests/components/sm2135/test.esp8266-ard.yaml index 7808481215..7c7f1e1a11 100644 --- a/tests/components/sm2135/test.esp8266-ard.yaml +++ b/tests/components/sm2135/test.esp8266-ard.yaml @@ -1,5 +1,5 @@ substitutions: - clock_pin: GPIO5 - data_pin: GPIO4 + clock_pin: GPIO0 + data_pin: GPIO2 <<: !include common.yaml diff --git a/tests/components/sm2235/test.esp32-ard.yaml b/tests/components/sm2235/test.esp32-ard.yaml deleted file mode 100644 index d295973e3f..0000000000 --- a/tests/components/sm2235/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - clock_pin: GPIO16 - data_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/sm2235/test.esp32-c3-ard.yaml b/tests/components/sm2235/test.esp32-c3-ard.yaml deleted file mode 100644 index 7808481215..0000000000 --- a/tests/components/sm2235/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - clock_pin: GPIO5 - data_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/sm2235/test.esp32-c3-idf.yaml b/tests/components/sm2235/test.esp32-c3-idf.yaml deleted file mode 100644 index 7808481215..0000000000 --- a/tests/components/sm2235/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - clock_pin: GPIO5 - data_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/sm2235/test.esp32-idf.yaml b/tests/components/sm2235/test.esp32-idf.yaml index d295973e3f..a4ecdb6c49 100644 --- a/tests/components/sm2235/test.esp32-idf.yaml +++ b/tests/components/sm2235/test.esp32-idf.yaml @@ -1,5 +1,5 @@ substitutions: - clock_pin: GPIO16 - data_pin: GPIO17 + clock_pin: GPIO4 + data_pin: GPIO5 <<: !include common.yaml diff --git a/tests/components/sm2235/test.esp8266-ard.yaml b/tests/components/sm2235/test.esp8266-ard.yaml index 7808481215..7c7f1e1a11 100644 --- a/tests/components/sm2235/test.esp8266-ard.yaml +++ b/tests/components/sm2235/test.esp8266-ard.yaml @@ -1,5 +1,5 @@ substitutions: - clock_pin: GPIO5 - data_pin: GPIO4 + clock_pin: GPIO0 + data_pin: GPIO2 <<: !include common.yaml diff --git a/tests/components/sm2335/test.esp32-ard.yaml b/tests/components/sm2335/test.esp32-ard.yaml deleted file mode 100644 index d295973e3f..0000000000 --- a/tests/components/sm2335/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - clock_pin: GPIO16 - data_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/sm2335/test.esp32-c3-ard.yaml b/tests/components/sm2335/test.esp32-c3-ard.yaml deleted file mode 100644 index 7808481215..0000000000 --- a/tests/components/sm2335/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - clock_pin: GPIO5 - data_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/sm2335/test.esp32-c3-idf.yaml b/tests/components/sm2335/test.esp32-c3-idf.yaml deleted file mode 100644 index 7808481215..0000000000 --- a/tests/components/sm2335/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - clock_pin: GPIO5 - data_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/sm2335/test.esp32-idf.yaml b/tests/components/sm2335/test.esp32-idf.yaml index d295973e3f..a4ecdb6c49 100644 --- a/tests/components/sm2335/test.esp32-idf.yaml +++ b/tests/components/sm2335/test.esp32-idf.yaml @@ -1,5 +1,5 @@ substitutions: - clock_pin: GPIO16 - data_pin: GPIO17 + clock_pin: GPIO4 + data_pin: GPIO5 <<: !include common.yaml diff --git a/tests/components/sm2335/test.esp8266-ard.yaml b/tests/components/sm2335/test.esp8266-ard.yaml index 7808481215..7c7f1e1a11 100644 --- a/tests/components/sm2335/test.esp8266-ard.yaml +++ b/tests/components/sm2335/test.esp8266-ard.yaml @@ -1,5 +1,5 @@ substitutions: - clock_pin: GPIO5 - data_pin: GPIO4 + clock_pin: GPIO0 + data_pin: GPIO2 <<: !include common.yaml diff --git a/tests/components/sm300d2/common.yaml b/tests/components/sm300d2/common.yaml index a231b63816..729f243a65 100644 --- a/tests/components/sm300d2/common.yaml +++ b/tests/components/sm300d2/common.yaml @@ -1,9 +1,3 @@ -uart: - - id: uart_sm300d2 - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 9600 - sensor: - platform: sm300d2 co2: diff --git a/tests/components/sm300d2/test.esp32-ard.yaml b/tests/components/sm300d2/test.esp32-ard.yaml deleted file mode 100644 index f486544afa..0000000000 --- a/tests/components/sm300d2/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 - -<<: !include common.yaml diff --git a/tests/components/sm300d2/test.esp32-c3-ard.yaml b/tests/components/sm300d2/test.esp32-c3-ard.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/sm300d2/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/sm300d2/test.esp32-c3-idf.yaml b/tests/components/sm300d2/test.esp32-c3-idf.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/sm300d2/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/sm300d2/test.esp32-idf.yaml b/tests/components/sm300d2/test.esp32-idf.yaml index f486544afa..b415125e84 100644 --- a/tests/components/sm300d2/test.esp32-idf.yaml +++ b/tests/components/sm300d2/test.esp32-idf.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 + tx_pin: GPIO4 + rx_pin: GPIO5 + +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/sm300d2/test.esp8266-ard.yaml b/tests/components/sm300d2/test.esp8266-ard.yaml index b516342f3b..96ab4ef6ac 100644 --- a/tests/components/sm300d2/test.esp8266-ard.yaml +++ b/tests/components/sm300d2/test.esp8266-ard.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: GPIO0 + rx_pin: GPIO2 + +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/sm300d2/test.rp2040-ard.yaml b/tests/components/sm300d2/test.rp2040-ard.yaml index b516342f3b..b28f2b5e05 100644 --- a/tests/components/sm300d2/test.rp2040-ard.yaml +++ b/tests/components/sm300d2/test.rp2040-ard.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO4 rx_pin: GPIO5 +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/sml/common.yaml b/tests/components/sml/common.yaml index a50d25eeee..b1bd5949b6 100644 --- a/tests/components/sml/common.yaml +++ b/tests/components/sml/common.yaml @@ -1,9 +1,3 @@ -uart: - - id: uart_sml - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 9600 - sml: id: mysml on_data: diff --git a/tests/components/sml/test.esp32-ard.yaml b/tests/components/sml/test.esp32-ard.yaml deleted file mode 100644 index f486544afa..0000000000 --- a/tests/components/sml/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 - -<<: !include common.yaml diff --git a/tests/components/sml/test.esp32-c3-ard.yaml b/tests/components/sml/test.esp32-c3-ard.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/sml/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/sml/test.esp32-c3-idf.yaml b/tests/components/sml/test.esp32-c3-idf.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/sml/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/sml/test.esp32-idf.yaml b/tests/components/sml/test.esp32-idf.yaml index f486544afa..b415125e84 100644 --- a/tests/components/sml/test.esp32-idf.yaml +++ b/tests/components/sml/test.esp32-idf.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 + tx_pin: GPIO4 + rx_pin: GPIO5 + +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/sml/test.esp8266-ard.yaml b/tests/components/sml/test.esp8266-ard.yaml index b516342f3b..96ab4ef6ac 100644 --- a/tests/components/sml/test.esp8266-ard.yaml +++ b/tests/components/sml/test.esp8266-ard.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: GPIO0 + rx_pin: GPIO2 + +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/sml/test.rp2040-ard.yaml b/tests/components/sml/test.rp2040-ard.yaml index b516342f3b..b28f2b5e05 100644 --- a/tests/components/sml/test.rp2040-ard.yaml +++ b/tests/components/sml/test.rp2040-ard.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO4 rx_pin: GPIO5 +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/smt100/common.yaml b/tests/components/smt100/common.yaml index b12d7198fd..86c5636b94 100644 --- a/tests/components/smt100/common.yaml +++ b/tests/components/smt100/common.yaml @@ -1,9 +1,3 @@ -uart: - - id: uart_smt100 - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 9600 - sensor: - platform: smt100 counts: diff --git a/tests/components/smt100/test.esp32-ard.yaml b/tests/components/smt100/test.esp32-ard.yaml deleted file mode 100644 index f486544afa..0000000000 --- a/tests/components/smt100/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 - -<<: !include common.yaml diff --git a/tests/components/smt100/test.esp32-c3-ard.yaml b/tests/components/smt100/test.esp32-c3-ard.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/smt100/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/smt100/test.esp32-c3-idf.yaml b/tests/components/smt100/test.esp32-c3-idf.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/smt100/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/smt100/test.esp32-idf.yaml b/tests/components/smt100/test.esp32-idf.yaml index f486544afa..b415125e84 100644 --- a/tests/components/smt100/test.esp32-idf.yaml +++ b/tests/components/smt100/test.esp32-idf.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 + tx_pin: GPIO4 + rx_pin: GPIO5 + +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/smt100/test.esp8266-ard.yaml b/tests/components/smt100/test.esp8266-ard.yaml index b516342f3b..96ab4ef6ac 100644 --- a/tests/components/smt100/test.esp8266-ard.yaml +++ b/tests/components/smt100/test.esp8266-ard.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: GPIO0 + rx_pin: GPIO2 + +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/smt100/test.rp2040-ard.yaml b/tests/components/smt100/test.rp2040-ard.yaml index b516342f3b..b28f2b5e05 100644 --- a/tests/components/smt100/test.rp2040-ard.yaml +++ b/tests/components/smt100/test.rp2040-ard.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO4 rx_pin: GPIO5 +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/sn74hc165/test.esp32-ard.yaml b/tests/components/sn74hc165/test.esp32-ard.yaml deleted file mode 100644 index 27f963312f..0000000000 --- a/tests/components/sn74hc165/test.esp32-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clock_pin: GPIO13 - data_pin: GPIO14 - load_pin: GPIO15 - clock_inhibit_pin: GPIO16 - -<<: !include common.yaml diff --git a/tests/components/sn74hc165/test.esp32-c3-ard.yaml b/tests/components/sn74hc165/test.esp32-c3-ard.yaml deleted file mode 100644 index 0a3db917b7..0000000000 --- a/tests/components/sn74hc165/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clock_pin: GPIO3 - data_pin: GPIO4 - load_pin: GPIO5 - clock_inhibit_pin: GPIO6 - -<<: !include common.yaml diff --git a/tests/components/sn74hc165/test.esp32-c3-idf.yaml b/tests/components/sn74hc165/test.esp32-c3-idf.yaml deleted file mode 100644 index 0a3db917b7..0000000000 --- a/tests/components/sn74hc165/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clock_pin: GPIO3 - data_pin: GPIO4 - load_pin: GPIO5 - clock_inhibit_pin: GPIO6 - -<<: !include common.yaml diff --git a/tests/components/sn74hc165/test.esp32-idf.yaml b/tests/components/sn74hc165/test.esp32-idf.yaml index 27f963312f..8a997e16e1 100644 --- a/tests/components/sn74hc165/test.esp32-idf.yaml +++ b/tests/components/sn74hc165/test.esp32-idf.yaml @@ -2,6 +2,6 @@ substitutions: clock_pin: GPIO13 data_pin: GPIO14 load_pin: GPIO15 - clock_inhibit_pin: GPIO16 + clock_inhibit_pin: GPIO4 <<: !include common.yaml diff --git a/tests/components/sn74hc165/test.esp8266-ard.yaml b/tests/components/sn74hc165/test.esp8266-ard.yaml index 27f963312f..00ffc10a9e 100644 --- a/tests/components/sn74hc165/test.esp8266-ard.yaml +++ b/tests/components/sn74hc165/test.esp8266-ard.yaml @@ -1,6 +1,6 @@ substitutions: - clock_pin: GPIO13 - data_pin: GPIO14 + clock_pin: GPIO0 + data_pin: GPIO2 load_pin: GPIO15 clock_inhibit_pin: GPIO16 diff --git a/tests/components/sn74hc595/common.yaml b/tests/components/sn74hc595/common.yaml index fc297909f5..3892c0564b 100644 --- a/tests/components/sn74hc595/common.yaml +++ b/tests/components/sn74hc595/common.yaml @@ -1,9 +1,3 @@ -spi: - - id: spi_sn74hc595 - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - miso_pin: ${miso_pin} - sn74hc595: - id: sn74hc595_hub clock_pin: ${clock_pin} diff --git a/tests/components/sn74hc595/test.esp32-ard.yaml b/tests/components/sn74hc595/test.esp32-ard.yaml deleted file mode 100644 index a4bab64862..0000000000 --- a/tests/components/sn74hc595/test.esp32-ard.yaml +++ /dev/null @@ -1,12 +0,0 @@ -substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO18 - clock_pin: GPIO15 - data_pin: GPIO14 - latch_pin1: GPIO21 - oe_pin1: GPIO22 - latch_pin2: GPIO23 - oe_pin2: GPIO25 - -<<: !include common.yaml diff --git a/tests/components/sn74hc595/test.esp32-c3-ard.yaml b/tests/components/sn74hc595/test.esp32-c3-ard.yaml deleted file mode 100644 index 14c928be88..0000000000 --- a/tests/components/sn74hc595/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,12 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO8 - clock_pin: GPIO5 - data_pin: GPIO4 - latch_pin1: GPIO1 - oe_pin1: GPIO2 - latch_pin2: GPIO3 - oe_pin2: GPIO9 - -<<: !include common.yaml diff --git a/tests/components/sn74hc595/test.esp32-c3-idf.yaml b/tests/components/sn74hc595/test.esp32-c3-idf.yaml deleted file mode 100644 index 14c928be88..0000000000 --- a/tests/components/sn74hc595/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,12 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO8 - clock_pin: GPIO5 - data_pin: GPIO4 - latch_pin1: GPIO1 - oe_pin1: GPIO2 - latch_pin2: GPIO3 - oe_pin2: GPIO9 - -<<: !include common.yaml diff --git a/tests/components/sn74hc595/test.esp32-idf.yaml b/tests/components/sn74hc595/test.esp32-idf.yaml index a4bab64862..4b47d2aeaa 100644 --- a/tests/components/sn74hc595/test.esp32-idf.yaml +++ b/tests/components/sn74hc595/test.esp32-idf.yaml @@ -1,12 +1,12 @@ substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO18 clock_pin: GPIO15 data_pin: GPIO14 latch_pin1: GPIO21 oe_pin1: GPIO22 - latch_pin2: GPIO23 - oe_pin2: GPIO25 + latch_pin2: GPIO25 + oe_pin2: GPIO26 + +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/sn74hc595/test.esp8266-ard.yaml b/tests/components/sn74hc595/test.esp8266-ard.yaml index cad11feca8..e0de8bb0a3 100644 --- a/tests/components/sn74hc595/test.esp8266-ard.yaml +++ b/tests/components/sn74hc595/test.esp8266-ard.yaml @@ -1,9 +1,9 @@ +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + substitutions: - clk_pin: GPIO14 - mosi_pin: GPIO13 - miso_pin: GPIO12 - clock_pin: GPIO5 - data_pin: GPIO4 + clock_pin: GPIO15 + data_pin: GPIO16 latch_pin1: GPIO2 oe_pin1: GPIO0 latch_pin2: GPIO3 diff --git a/tests/components/sn74hc595/test.rp2040-ard.yaml b/tests/components/sn74hc595/test.rp2040-ard.yaml index 14c928be88..93cf5efb0c 100644 --- a/tests/components/sn74hc595/test.rp2040-ard.yaml +++ b/tests/components/sn74hc595/test.rp2040-ard.yaml @@ -1,12 +1,12 @@ +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml + substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO8 clock_pin: GPIO5 - data_pin: GPIO4 + data_pin: GPIO0 latch_pin1: GPIO1 - oe_pin1: GPIO2 - latch_pin2: GPIO3 + oe_pin1: GPIO6 + latch_pin2: GPIO7 oe_pin2: GPIO9 <<: !include common.yaml diff --git a/tests/components/sntp/test.esp32-ard.yaml b/tests/components/sntp/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/sntp/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/sntp/test.esp32-c3-ard.yaml b/tests/components/sntp/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/sntp/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/sntp/test.esp32-c3-idf.yaml b/tests/components/sntp/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/sntp/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/socket/conftest.py b/tests/components/socket/conftest.py new file mode 100644 index 0000000000..5d93cac232 --- /dev/null +++ b/tests/components/socket/conftest.py @@ -0,0 +1,12 @@ +"""Configuration file for socket component tests.""" + +import pytest + +from esphome.core import CORE + + +@pytest.fixture(autouse=True) +def reset_core(): + """Reset CORE after each test.""" + yield + CORE.reset() diff --git a/tests/components/socket/test_wake_loop_threadsafe.py b/tests/components/socket/test_wake_loop_threadsafe.py new file mode 100644 index 0000000000..45e5ea2211 --- /dev/null +++ b/tests/components/socket/test_wake_loop_threadsafe.py @@ -0,0 +1,42 @@ +from esphome.components import socket +from esphome.core import CORE + + +def test_require_wake_loop_threadsafe__first_call() -> None: + """Test that first call sets up define and consumes socket.""" + socket.require_wake_loop_threadsafe() + + # Verify CORE.data was updated + assert CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True + + # Verify the define was added + assert any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines) + + +def test_require_wake_loop_threadsafe__idempotent() -> None: + """Test that subsequent calls are idempotent.""" + # Set up initial state as if already called + CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] = True + + # Call again - should not raise or fail + socket.require_wake_loop_threadsafe() + + # Verify state is still True + assert CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True + + # Define should not be added since flag was already True + assert not any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines) + + +def test_require_wake_loop_threadsafe__multiple_calls() -> None: + """Test that multiple calls only set up once.""" + # Call three times + socket.require_wake_loop_threadsafe() + socket.require_wake_loop_threadsafe() + socket.require_wake_loop_threadsafe() + + # Verify CORE.data was set + assert CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True + + # Verify the define was added (only once, but we can just check it exists) + assert any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines) diff --git a/tests/components/sonoff_d1/common.yaml b/tests/components/sonoff_d1/common.yaml index d2d4043b95..dc08380683 100644 --- a/tests/components/sonoff_d1/common.yaml +++ b/tests/components/sonoff_d1/common.yaml @@ -1,9 +1,3 @@ -uart: - - id: uart_sonoff_d1 - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 9600 - light: - platform: sonoff_d1 id: d1_light diff --git a/tests/components/sonoff_d1/test.esp32-ard.yaml b/tests/components/sonoff_d1/test.esp32-ard.yaml deleted file mode 100644 index f486544afa..0000000000 --- a/tests/components/sonoff_d1/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 - -<<: !include common.yaml diff --git a/tests/components/sonoff_d1/test.esp32-c3-ard.yaml b/tests/components/sonoff_d1/test.esp32-c3-ard.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/sonoff_d1/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/sonoff_d1/test.esp32-c3-idf.yaml b/tests/components/sonoff_d1/test.esp32-c3-idf.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/sonoff_d1/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/sonoff_d1/test.esp32-idf.yaml b/tests/components/sonoff_d1/test.esp32-idf.yaml index f486544afa..2d29656c94 100644 --- a/tests/components/sonoff_d1/test.esp32-idf.yaml +++ b/tests/components/sonoff_d1/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/sonoff_d1/test.esp8266-ard.yaml b/tests/components/sonoff_d1/test.esp8266-ard.yaml index b516342f3b..5a05efa259 100644 --- a/tests/components/sonoff_d1/test.esp8266-ard.yaml +++ b/tests/components/sonoff_d1/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/sonoff_d1/test.rp2040-ard.yaml b/tests/components/sonoff_d1/test.rp2040-ard.yaml index b516342f3b..f1df2daf83 100644 --- a/tests/components/sonoff_d1/test.rp2040-ard.yaml +++ b/tests/components/sonoff_d1/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/sound_level/test.esp32-ard.yaml b/tests/components/sound_level/test.esp32-ard.yaml deleted file mode 100644 index c6d1bfa330..0000000000 --- a/tests/components/sound_level/test.esp32-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - i2s_bclk_pin: GPIO25 - i2s_lrclk_pin: GPIO26 - i2s_dout_pin: GPIO27 - -<<: !include common.yaml diff --git a/tests/components/sound_level/test.esp32-c3-ard.yaml b/tests/components/sound_level/test.esp32-c3-ard.yaml deleted file mode 100644 index aeb7d9f0af..0000000000 --- a/tests/components/sound_level/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - i2s_bclk_pin: GPIO6 - i2s_lrclk_pin: GPIO7 - i2s_dout_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/sound_level/test.esp32-c3-idf.yaml b/tests/components/sound_level/test.esp32-c3-idf.yaml deleted file mode 100644 index aeb7d9f0af..0000000000 --- a/tests/components/sound_level/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - i2s_bclk_pin: GPIO6 - i2s_lrclk_pin: GPIO7 - i2s_dout_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/sound_level/test.esp32-idf.yaml b/tests/components/sound_level/test.esp32-idf.yaml index c6d1bfa330..20e38e8df8 100644 --- a/tests/components/sound_level/test.esp32-idf.yaml +++ b/tests/components/sound_level/test.esp32-idf.yaml @@ -3,4 +3,7 @@ substitutions: i2s_lrclk_pin: GPIO26 i2s_dout_pin: GPIO27 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/sound_level/test.esp32-s3-ard.yaml b/tests/components/sound_level/test.esp32-s3-ard.yaml deleted file mode 100644 index 9c1f32d5bd..0000000000 --- a/tests/components/sound_level/test.esp32-s3-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - i2s_bclk_pin: GPIO4 - i2s_lrclk_pin: GPIO5 - i2s_dout_pin: GPIO6 - -<<: !include common.yaml diff --git a/tests/components/speaker/audio_dac.esp32-ard.yaml b/tests/components/speaker/audio_dac.esp32-ard.yaml index 75d9ddf92b..3f5d1bba7c 100644 --- a/tests/components/speaker/audio_dac.esp32-ard.yaml +++ b/tests/components/speaker/audio_dac.esp32-ard.yaml @@ -1,9 +1,10 @@ substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 i2s_bclk_pin: GPIO27 i2s_lrclk_pin: GPIO26 i2s_mclk_pin: GPIO25 i2s_dout_pin: GPIO23 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-ard.yaml + <<: !include common-audio_dac.yaml diff --git a/tests/components/speaker/audio_dac.esp32-c3-ard.yaml b/tests/components/speaker/audio_dac.esp32-c3-ard.yaml deleted file mode 100644 index 1004d2143e..0000000000 --- a/tests/components/speaker/audio_dac.esp32-c3-ard.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - i2s_bclk_pin: GPIO7 - i2s_lrclk_pin: GPIO6 - i2s_mclk_pin: GPIO9 - i2s_dout_pin: GPIO8 - -<<: !include common-audio_dac.yaml diff --git a/tests/components/speaker/audio_dac.esp32-c3-idf.yaml b/tests/components/speaker/audio_dac.esp32-c3-idf.yaml deleted file mode 100644 index 1004d2143e..0000000000 --- a/tests/components/speaker/audio_dac.esp32-c3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - i2s_bclk_pin: GPIO7 - i2s_lrclk_pin: GPIO6 - i2s_mclk_pin: GPIO9 - i2s_dout_pin: GPIO8 - -<<: !include common-audio_dac.yaml diff --git a/tests/components/speaker/audio_dac.esp32-idf.yaml b/tests/components/speaker/audio_dac.esp32-idf.yaml index 75d9ddf92b..71c8b06e24 100644 --- a/tests/components/speaker/audio_dac.esp32-idf.yaml +++ b/tests/components/speaker/audio_dac.esp32-idf.yaml @@ -1,9 +1,10 @@ substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 i2s_bclk_pin: GPIO27 i2s_lrclk_pin: GPIO26 i2s_mclk_pin: GPIO25 i2s_dout_pin: GPIO23 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + <<: !include common-audio_dac.yaml diff --git a/tests/components/speaker/common-audio_dac.yaml b/tests/components/speaker/common-audio_dac.yaml index 41b994d4d4..67bd6c28ef 100644 --- a/tests/components/speaker/common-audio_dac.yaml +++ b/tests/components/speaker/common-audio_dac.yaml @@ -14,11 +14,6 @@ esphome: - speaker.finish: - speaker.stop: -i2c: - - id: i2c_audio_dac - scl: ${scl_pin} - sda: ${sda_pin} - i2s_audio: i2s_lrclk_pin: ${i2s_bclk_pin} i2s_bclk_pin: ${i2s_lrclk_pin} @@ -26,6 +21,7 @@ i2s_audio: audio_dac: - platform: aic3204 + i2c_id: i2c_bus id: internal_dac speaker: diff --git a/tests/components/speaker/common.yaml b/tests/components/speaker/common.yaml index c04674ee29..9aaf639162 100644 --- a/tests/components/speaker/common.yaml +++ b/tests/components/speaker/common.yaml @@ -1,18 +1,52 @@ +number: + - platform: template + name: "Speaker Number" + id: my_number + optimistic: true + min_value: 0 + max_value: 100 + step: 1 + esphome: on_boot: then: - speaker.mute_on: + id: speaker_id - speaker.mute_off: + id: speaker_id - if: - condition: speaker.is_stopped + condition: + speaker.is_stopped: + id: speaker_id then: - - speaker.play: [0, 1, 2, 3] - - speaker.volume_set: 0.9 + - speaker.play: + id: speaker_id + data: [0, 1, 2, 3] + - speaker.volume_set: + id: speaker_id + volume: 0.9 - if: - condition: speaker.is_playing + condition: + speaker.is_playing: + id: speaker_id then: - speaker.finish: + id: speaker_id - speaker.stop: + id: speaker_id + +button: + - platform: template + name: "Speaker Button" + on_press: + then: + - speaker.play: + id: speaker_id + data: [0x10, 0x20, 0x30, 0x40] + - speaker.play: + id: speaker_id + data: !lambda |- + return {0x01, 0x02, (uint8_t)id(my_number).state}; i2s_audio: i2s_lrclk_pin: ${i2s_bclk_pin} diff --git a/tests/components/speaker/test.esp32-ard.yaml b/tests/components/speaker/test.esp32-ard.yaml index e2439ebdf2..13350cd097 100644 --- a/tests/components/speaker/test.esp32-ard.yaml +++ b/tests/components/speaker/test.esp32-ard.yaml @@ -1,9 +1,10 @@ substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 i2s_bclk_pin: GPIO27 i2s_lrclk_pin: GPIO26 i2s_mclk_pin: GPIO25 - i2s_dout_pin: GPIO23 + i2s_dout_pin: GPIO4 + +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-ard.yaml <<: !include common.yaml diff --git a/tests/components/speaker/test.esp32-c3-ard.yaml b/tests/components/speaker/test.esp32-c3-ard.yaml deleted file mode 100644 index ddcf051fab..0000000000 --- a/tests/components/speaker/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - i2s_bclk_pin: GPIO7 - i2s_lrclk_pin: GPIO6 - i2s_mclk_pin: GPIO9 - i2s_dout_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/speaker/test.esp32-c3-idf.yaml b/tests/components/speaker/test.esp32-c3-idf.yaml deleted file mode 100644 index ddcf051fab..0000000000 --- a/tests/components/speaker/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - i2s_bclk_pin: GPIO7 - i2s_lrclk_pin: GPIO6 - i2s_mclk_pin: GPIO9 - i2s_dout_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/speaker/test.esp32-idf.yaml b/tests/components/speaker/test.esp32-idf.yaml index e2439ebdf2..27b8604656 100644 --- a/tests/components/speaker/test.esp32-idf.yaml +++ b/tests/components/speaker/test.esp32-idf.yaml @@ -1,9 +1,10 @@ substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 i2s_bclk_pin: GPIO27 i2s_lrclk_pin: GPIO26 i2s_mclk_pin: GPIO25 - i2s_dout_pin: GPIO23 + i2s_dout_pin: GPIO12 + +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/speed/test.esp32-ard.yaml b/tests/components/speed/test.esp32-ard.yaml deleted file mode 100644 index 26da1ce1d6..0000000000 --- a/tests/components/speed/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - output_platform: ledc - pin: GPIO14 - -<<: !include common.yaml diff --git a/tests/components/speed/test.esp32-c3-ard.yaml b/tests/components/speed/test.esp32-c3-ard.yaml deleted file mode 100644 index 7476963591..0000000000 --- a/tests/components/speed/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - output_platform: ledc - pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/speed/test.esp32-c3-idf.yaml b/tests/components/speed/test.esp32-c3-idf.yaml deleted file mode 100644 index 7476963591..0000000000 --- a/tests/components/speed/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - output_platform: ledc - pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/spi/common.yaml b/tests/components/spi/common.yaml index 04b4779957..e69de29bb2 100644 --- a/tests/components/spi/common.yaml +++ b/tests/components/spi/common.yaml @@ -1,5 +0,0 @@ -spi: - - id: spi_spi - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - miso_pin: ${miso_pin} diff --git a/tests/components/spi/test.esp32-c3-ard.yaml b/tests/components/spi/test.esp32-c3-ard.yaml deleted file mode 100644 index bfa12b1755..0000000000 --- a/tests/components/spi/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/spi/test.esp32-c3-idf.yaml b/tests/components/spi/test.esp32-c3-idf.yaml index bfa12b1755..54463271a9 100644 --- a/tests/components/spi/test.esp32-c3-idf.yaml +++ b/tests/components/spi/test.esp32-c3-idf.yaml @@ -1,6 +1,5 @@ substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 +packages: + spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml <<: !include common.yaml diff --git a/tests/components/spi/test.esp32-idf.yaml b/tests/components/spi/test.esp32-idf.yaml index 448e54fea6..a8e18ca503 100644 --- a/tests/components/spi/test.esp32-idf.yaml +++ b/tests/components/spi/test.esp32-idf.yaml @@ -1,6 +1,4 @@ -substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/spi/test.esp32-s3-ard.yaml b/tests/components/spi/test.esp32-s3-ard.yaml new file mode 100644 index 0000000000..e4d4f20586 --- /dev/null +++ b/tests/components/spi/test.esp32-s3-ard.yaml @@ -0,0 +1,13 @@ +spi: + - id: three_spi + interface: spi3 + clk_pin: + number: 47 + mosi_pin: + number: 40 + - id: hw_spi + interface: hardware + clk_pin: + number: 0 + miso_pin: + number: 41 diff --git a/tests/components/spi/test.esp8266-ard.yaml b/tests/components/spi/test.esp8266-ard.yaml index b9545d4f6a..4f9c816740 100644 --- a/tests/components/spi/test.esp8266-ard.yaml +++ b/tests/components/spi/test.esp8266-ard.yaml @@ -1,6 +1,6 @@ substitutions: - clk_pin: GPIO14 - mosi_pin: GPIO13 - miso_pin: GPIO12 + clk_pin: GPIO0 + mosi_pin: GPIO2 + miso_pin: GPIO15 <<: !include common.yaml diff --git a/tests/components/spi_device/common.yaml b/tests/components/spi_device/common.yaml index 0f6a5038fb..8511603bc2 100644 --- a/tests/components/spi_device/common.yaml +++ b/tests/components/spi_device/common.yaml @@ -1,9 +1,3 @@ -spi: - - id: spi_device1 - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - miso_pin: ${miso_pin} - spi_device: - id: spi_device_test data_rate: 2MHz diff --git a/tests/components/spi_device/test.esp32-ard.yaml b/tests/components/spi_device/test.esp32-ard.yaml deleted file mode 100644 index 448e54fea6..0000000000 --- a/tests/components/spi_device/test.esp32-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 - -<<: !include common.yaml diff --git a/tests/components/spi_device/test.esp32-c3-ard.yaml b/tests/components/spi_device/test.esp32-c3-ard.yaml deleted file mode 100644 index bfa12b1755..0000000000 --- a/tests/components/spi_device/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/spi_device/test.esp32-c3-idf.yaml b/tests/components/spi_device/test.esp32-c3-idf.yaml deleted file mode 100644 index bfa12b1755..0000000000 --- a/tests/components/spi_device/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/spi_device/test.esp32-idf.yaml b/tests/components/spi_device/test.esp32-idf.yaml index c4989cccbf..aace9192b1 100644 --- a/tests/components/spi_device/test.esp32-idf.yaml +++ b/tests/components/spi_device/test.esp32-idf.yaml @@ -1,7 +1,5 @@ -substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - miso_pin: GPIO15 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml <<: !include common.yaml spi_device: diff --git a/tests/components/spi_device/test.esp8266-ard.yaml b/tests/components/spi_device/test.esp8266-ard.yaml index b9545d4f6a..433a931284 100644 --- a/tests/components/spi_device/test.esp8266-ard.yaml +++ b/tests/components/spi_device/test.esp8266-ard.yaml @@ -1,6 +1,4 @@ -substitutions: - clk_pin: GPIO14 - mosi_pin: GPIO13 - miso_pin: GPIO12 +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/spi_device/test.rp2040-ard.yaml b/tests/components/spi_device/test.rp2040-ard.yaml index 81a8acafd8..149d530753 100644 --- a/tests/components/spi_device/test.rp2040-ard.yaml +++ b/tests/components/spi_device/test.rp2040-ard.yaml @@ -1,6 +1,4 @@ -substitutions: - clk_pin: GPIO2 - mosi_pin: GPIO3 - miso_pin: GPIO4 +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/spi_led_strip/common.yaml b/tests/components/spi_led_strip/common.yaml index 80b98a63a4..30e2ef22f7 100644 --- a/tests/components/spi_led_strip/common.yaml +++ b/tests/components/spi_led_strip/common.yaml @@ -1,8 +1,3 @@ -spi: - - id: spi_spi_led_strip - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - light: - platform: spi_led_strip num_leds: 4 diff --git a/tests/components/spi_led_strip/test.esp32-ard.yaml b/tests/components/spi_led_strip/test.esp32-ard.yaml deleted file mode 100644 index 8906602ef4..0000000000 --- a/tests/components/spi_led_strip/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/spi_led_strip/test.esp32-c3-ard.yaml b/tests/components/spi_led_strip/test.esp32-c3-ard.yaml deleted file mode 100644 index a85b587070..0000000000 --- a/tests/components/spi_led_strip/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - -<<: !include common.yaml diff --git a/tests/components/spi_led_strip/test.esp32-c3-idf.yaml b/tests/components/spi_led_strip/test.esp32-c3-idf.yaml deleted file mode 100644 index a85b587070..0000000000 --- a/tests/components/spi_led_strip/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - -<<: !include common.yaml diff --git a/tests/components/spi_led_strip/test.esp32-idf.yaml b/tests/components/spi_led_strip/test.esp32-idf.yaml index 8906602ef4..a8e18ca503 100644 --- a/tests/components/spi_led_strip/test.esp32-idf.yaml +++ b/tests/components/spi_led_strip/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/spi_led_strip/test.esp8266-ard.yaml b/tests/components/spi_led_strip/test.esp8266-ard.yaml index 7baaa62ed5..433a931284 100644 --- a/tests/components/spi_led_strip/test.esp8266-ard.yaml +++ b/tests/components/spi_led_strip/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - clk_pin: GPIO14 - mosi_pin: GPIO13 +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/spi_led_strip/test.rp2040-ard.yaml b/tests/components/spi_led_strip/test.rp2040-ard.yaml index 411cfbe00e..149d530753 100644 --- a/tests/components/spi_led_strip/test.rp2040-ard.yaml +++ b/tests/components/spi_led_strip/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - clk_pin: GPIO2 - mosi_pin: GPIO3 +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/sprinkler/test.esp32-ard.yaml b/tests/components/sprinkler/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/sprinkler/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/sprinkler/test.esp32-c3-ard.yaml b/tests/components/sprinkler/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/sprinkler/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/sprinkler/test.esp32-c3-idf.yaml b/tests/components/sprinkler/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/sprinkler/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/ble_rssi/test.esp32-c3-idf.yaml b/tests/components/sprinkler/test.nrf52-adafruit.yaml similarity index 100% rename from tests/components/ble_rssi/test.esp32-c3-idf.yaml rename to tests/components/sprinkler/test.nrf52-adafruit.yaml diff --git a/tests/components/ble_scanner/test.esp32-ard.yaml b/tests/components/sprinkler/test.nrf52-mcumgr.yaml similarity index 100% rename from tests/components/ble_scanner/test.esp32-ard.yaml rename to tests/components/sprinkler/test.nrf52-mcumgr.yaml diff --git a/tests/components/sps30/common.yaml b/tests/components/sps30/common.yaml index 2fbe2c747a..d40cd16b6d 100644 --- a/tests/components/sps30/common.yaml +++ b/tests/components/sps30/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_sps30 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: sps30 + i2c_id: i2c_bus pm_1_0: name: Workshop PM <1µm Weight concentration id: workshop_PM_1_0 diff --git a/tests/components/sps30/test.esp32-ard.yaml b/tests/components/sps30/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/sps30/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/sps30/test.esp32-c3-ard.yaml b/tests/components/sps30/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/sps30/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/sps30/test.esp32-c3-idf.yaml b/tests/components/sps30/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/sps30/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/sps30/test.esp32-idf.yaml b/tests/components/sps30/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/sps30/test.esp32-idf.yaml +++ b/tests/components/sps30/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/sps30/test.esp8266-ard.yaml b/tests/components/sps30/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/sps30/test.esp8266-ard.yaml +++ b/tests/components/sps30/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/sps30/test.rp2040-ard.yaml b/tests/components/sps30/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/sps30/test.rp2040-ard.yaml +++ b/tests/components/sps30/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/ssd1306_i2c/common.yaml b/tests/components/ssd1306_i2c/common.yaml index d17f83f03a..09eb569a8e 100644 --- a/tests/components/ssd1306_i2c/common.yaml +++ b/tests/components/ssd1306_i2c/common.yaml @@ -1,25 +1,21 @@ -i2c: - - id: i2c_ssd1306_i2c - scl: ${scl_pin} - sda: ${sda_pin} - display: - platform: ssd1306_i2c + i2c_id: i2c_bus model: SSD1306_128X64 reset_pin: ${reset_pin} address: 0x3C - id: display1 + id: ssd1306_i2c_display contrast: 60% pages: - - id: page1 + - id: ssd1306_i2c_page1 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - - id: page2 + - id: ssd1306_i2c_page2 lambda: |- it.rectangle(0, 0, 10, 10); on_page_change: - from: page1 - to: page2 + from: ssd1306_i2c_page1 + to: ssd1306_i2c_page2 then: lambda: |- ESP_LOGD("display", "1 -> 2"); diff --git a/tests/components/ssd1306_i2c/test.esp32-ard.yaml b/tests/components/ssd1306_i2c/test.esp32-ard.yaml deleted file mode 100644 index 1ca773e06c..0000000000 --- a/tests/components/ssd1306_i2c/test.esp32-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - reset_pin: GPIO15 - -<<: !include common.yaml diff --git a/tests/components/ssd1306_i2c/test.esp32-c3-ard.yaml b/tests/components/ssd1306_i2c/test.esp32-c3-ard.yaml deleted file mode 100644 index 4eaff7fa4a..0000000000 --- a/tests/components/ssd1306_i2c/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - reset_pin: GPIO3 - -<<: !include common.yaml diff --git a/tests/components/ssd1306_i2c/test.esp32-c3-idf.yaml b/tests/components/ssd1306_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index 4eaff7fa4a..0000000000 --- a/tests/components/ssd1306_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - reset_pin: GPIO3 - -<<: !include common.yaml diff --git a/tests/components/ssd1306_i2c/test.esp32-idf.yaml b/tests/components/ssd1306_i2c/test.esp32-idf.yaml index 1ca773e06c..4ff2241ec9 100644 --- a/tests/components/ssd1306_i2c/test.esp32-idf.yaml +++ b/tests/components/ssd1306_i2c/test.esp32-idf.yaml @@ -1,6 +1,7 @@ substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 reset_pin: GPIO15 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/ssd1306_i2c/test.esp8266-ard.yaml b/tests/components/ssd1306_i2c/test.esp8266-ard.yaml index af91c21a0d..352cc8cda8 100644 --- a/tests/components/ssd1306_i2c/test.esp8266-ard.yaml +++ b/tests/components/ssd1306_i2c/test.esp8266-ard.yaml @@ -1,6 +1,7 @@ substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 reset_pin: GPIO2 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/ssd1306_i2c/test.nrf52-xiao-ble.yaml b/tests/components/ssd1306_i2c/test.nrf52-xiao-ble.yaml new file mode 100644 index 0000000000..28254e4af5 --- /dev/null +++ b/tests/components/ssd1306_i2c/test.nrf52-xiao-ble.yaml @@ -0,0 +1,7 @@ +substitutions: + reset_pin: P0.10 + +packages: + i2c: !include ../../test_build_components/common/i2c/nrf52.yaml + +<<: !include common.yaml diff --git a/tests/components/ssd1306_i2c/test.rp2040-ard.yaml b/tests/components/ssd1306_i2c/test.rp2040-ard.yaml index 4eaff7fa4a..2972fde8a5 100644 --- a/tests/components/ssd1306_i2c/test.rp2040-ard.yaml +++ b/tests/components/ssd1306_i2c/test.rp2040-ard.yaml @@ -1,6 +1,7 @@ substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 reset_pin: GPIO3 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/ssd1306_spi/common.yaml b/tests/components/ssd1306_spi/common.yaml index 71705f32d2..0297abc192 100644 --- a/tests/components/ssd1306_spi/common.yaml +++ b/tests/components/ssd1306_spi/common.yaml @@ -1,24 +1,20 @@ -spi: - - id: spi_ssd1306_spi - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - display: - platform: ssd1306_spi + id: ssd1306_spi_display model: SSD1306 128x64 cs_pin: ${cs_pin} dc_pin: ${dc_pin} reset_pin: ${reset_pin} pages: - - id: page1 + - id: ssd1306_spi_page1 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - - id: page2 + - id: ssd1306_spi_page2 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); on_page_change: - from: page1 - to: page2 + from: ssd1306_spi_page1 + to: ssd1306_spi_page2 then: lambda: |- ESP_LOGD("display", "1 -> 2"); diff --git a/tests/components/ssd1306_spi/test.esp32-ard.yaml b/tests/components/ssd1306_spi/test.esp32-ard.yaml deleted file mode 100644 index bad5241f79..0000000000 --- a/tests/components/ssd1306_spi/test.esp32-ard.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - cs_pin: GPIO12 - dc_pin: GPIO13 - reset_pin: GPIO14 - -<<: !include common.yaml diff --git a/tests/components/ssd1306_spi/test.esp32-c3-ard.yaml b/tests/components/ssd1306_spi/test.esp32-c3-ard.yaml deleted file mode 100644 index c5c932c92c..0000000000 --- a/tests/components/ssd1306_spi/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - dc_pin: GPIO9 - reset_pin: GPIO10 - -<<: !include common.yaml diff --git a/tests/components/ssd1306_spi/test.esp32-c3-idf.yaml b/tests/components/ssd1306_spi/test.esp32-c3-idf.yaml deleted file mode 100644 index c5c932c92c..0000000000 --- a/tests/components/ssd1306_spi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - dc_pin: GPIO9 - reset_pin: GPIO10 - -<<: !include common.yaml diff --git a/tests/components/ssd1306_spi/test.esp32-idf.yaml b/tests/components/ssd1306_spi/test.esp32-idf.yaml index bad5241f79..ff174a4656 100644 --- a/tests/components/ssd1306_spi/test.esp32-idf.yaml +++ b/tests/components/ssd1306_spi/test.esp32-idf.yaml @@ -1,8 +1,9 @@ substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 cs_pin: GPIO12 dc_pin: GPIO13 reset_pin: GPIO14 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/ssd1306_spi/test.esp8266-ard.yaml b/tests/components/ssd1306_spi/test.esp8266-ard.yaml index 3f023a60eb..56cb29f29e 100644 --- a/tests/components/ssd1306_spi/test.esp8266-ard.yaml +++ b/tests/components/ssd1306_spi/test.esp8266-ard.yaml @@ -1,9 +1,12 @@ substitutions: - clk_pin: GPIO14 - mosi_pin: GPIO13 + clk_pin: GPIO0 + mosi_pin: GPIO2 miso_pin: GPIO12 cs_pin: GPIO5 dc_pin: GPIO15 reset_pin: GPIO16 +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/ssd1306_spi/test.rp2040-ard.yaml b/tests/components/ssd1306_spi/test.rp2040-ard.yaml index d7fd6ee294..66caa956f7 100644 --- a/tests/components/ssd1306_spi/test.rp2040-ard.yaml +++ b/tests/components/ssd1306_spi/test.rp2040-ard.yaml @@ -6,4 +6,7 @@ substitutions: dc_pin: GPIO15 reset_pin: GPIO16 +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/ssd1322_spi/common.yaml b/tests/components/ssd1322_spi/common.yaml index b67392794c..c0e16b6f09 100644 --- a/tests/components/ssd1322_spi/common.yaml +++ b/tests/components/ssd1322_spi/common.yaml @@ -1,8 +1,3 @@ -spi: - - id: spi_ssd1322_spi - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - display: - platform: ssd1322_spi model: SSD1322 256x64 @@ -10,15 +5,15 @@ display: dc_pin: ${dc_pin} reset_pin: ${reset_pin} pages: - - id: page1 + - id: ssd1322_spi_page1 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - - id: page2 + - id: ssd1322_spi_page2 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); on_page_change: - from: page1 - to: page2 + from: ssd1322_spi_page1 + to: ssd1322_spi_page2 then: lambda: |- ESP_LOGD("display", "1 -> 2"); diff --git a/tests/components/ssd1322_spi/test.esp32-ard.yaml b/tests/components/ssd1322_spi/test.esp32-ard.yaml deleted file mode 100644 index bad5241f79..0000000000 --- a/tests/components/ssd1322_spi/test.esp32-ard.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - cs_pin: GPIO12 - dc_pin: GPIO13 - reset_pin: GPIO14 - -<<: !include common.yaml diff --git a/tests/components/ssd1322_spi/test.esp32-c3-ard.yaml b/tests/components/ssd1322_spi/test.esp32-c3-ard.yaml deleted file mode 100644 index c5c932c92c..0000000000 --- a/tests/components/ssd1322_spi/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - dc_pin: GPIO9 - reset_pin: GPIO10 - -<<: !include common.yaml diff --git a/tests/components/ssd1322_spi/test.esp32-c3-idf.yaml b/tests/components/ssd1322_spi/test.esp32-c3-idf.yaml deleted file mode 100644 index c5c932c92c..0000000000 --- a/tests/components/ssd1322_spi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - dc_pin: GPIO9 - reset_pin: GPIO10 - -<<: !include common.yaml diff --git a/tests/components/ssd1322_spi/test.esp32-idf.yaml b/tests/components/ssd1322_spi/test.esp32-idf.yaml index bad5241f79..ff174a4656 100644 --- a/tests/components/ssd1322_spi/test.esp32-idf.yaml +++ b/tests/components/ssd1322_spi/test.esp32-idf.yaml @@ -1,8 +1,9 @@ substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 cs_pin: GPIO12 dc_pin: GPIO13 reset_pin: GPIO14 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/ssd1322_spi/test.esp8266-ard.yaml b/tests/components/ssd1322_spi/test.esp8266-ard.yaml index 3f023a60eb..56cb29f29e 100644 --- a/tests/components/ssd1322_spi/test.esp8266-ard.yaml +++ b/tests/components/ssd1322_spi/test.esp8266-ard.yaml @@ -1,9 +1,12 @@ substitutions: - clk_pin: GPIO14 - mosi_pin: GPIO13 + clk_pin: GPIO0 + mosi_pin: GPIO2 miso_pin: GPIO12 cs_pin: GPIO5 dc_pin: GPIO15 reset_pin: GPIO16 +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/ssd1322_spi/test.rp2040-ard.yaml b/tests/components/ssd1322_spi/test.rp2040-ard.yaml index d7fd6ee294..66caa956f7 100644 --- a/tests/components/ssd1322_spi/test.rp2040-ard.yaml +++ b/tests/components/ssd1322_spi/test.rp2040-ard.yaml @@ -6,4 +6,7 @@ substitutions: dc_pin: GPIO15 reset_pin: GPIO16 +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/ssd1325_spi/common.yaml b/tests/components/ssd1325_spi/common.yaml index eaa8b619ca..dea8502538 100644 --- a/tests/components/ssd1325_spi/common.yaml +++ b/tests/components/ssd1325_spi/common.yaml @@ -1,8 +1,3 @@ -spi: - - id: spi_ssd1325_spi - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - display: - platform: ssd1325_spi model: SSD1325 128x64 @@ -10,15 +5,15 @@ display: dc_pin: ${dc_pin} reset_pin: ${reset_pin} pages: - - id: page1 + - id: ssd1325_spi_page1 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - - id: page2 + - id: ssd1325_spi_page2 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); on_page_change: - from: page1 - to: page2 + from: ssd1325_spi_page1 + to: ssd1325_spi_page2 then: lambda: |- ESP_LOGD("display", "1 -> 2"); diff --git a/tests/components/ssd1325_spi/test.esp32-ard.yaml b/tests/components/ssd1325_spi/test.esp32-ard.yaml deleted file mode 100644 index bad5241f79..0000000000 --- a/tests/components/ssd1325_spi/test.esp32-ard.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - cs_pin: GPIO12 - dc_pin: GPIO13 - reset_pin: GPIO14 - -<<: !include common.yaml diff --git a/tests/components/ssd1325_spi/test.esp32-c3-ard.yaml b/tests/components/ssd1325_spi/test.esp32-c3-ard.yaml deleted file mode 100644 index c5c932c92c..0000000000 --- a/tests/components/ssd1325_spi/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - dc_pin: GPIO9 - reset_pin: GPIO10 - -<<: !include common.yaml diff --git a/tests/components/ssd1325_spi/test.esp32-c3-idf.yaml b/tests/components/ssd1325_spi/test.esp32-c3-idf.yaml deleted file mode 100644 index c5c932c92c..0000000000 --- a/tests/components/ssd1325_spi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - dc_pin: GPIO9 - reset_pin: GPIO10 - -<<: !include common.yaml diff --git a/tests/components/ssd1325_spi/test.esp32-idf.yaml b/tests/components/ssd1325_spi/test.esp32-idf.yaml index bad5241f79..ff174a4656 100644 --- a/tests/components/ssd1325_spi/test.esp32-idf.yaml +++ b/tests/components/ssd1325_spi/test.esp32-idf.yaml @@ -1,8 +1,9 @@ substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 cs_pin: GPIO12 dc_pin: GPIO13 reset_pin: GPIO14 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/ssd1325_spi/test.esp8266-ard.yaml b/tests/components/ssd1325_spi/test.esp8266-ard.yaml index 3f023a60eb..56cb29f29e 100644 --- a/tests/components/ssd1325_spi/test.esp8266-ard.yaml +++ b/tests/components/ssd1325_spi/test.esp8266-ard.yaml @@ -1,9 +1,12 @@ substitutions: - clk_pin: GPIO14 - mosi_pin: GPIO13 + clk_pin: GPIO0 + mosi_pin: GPIO2 miso_pin: GPIO12 cs_pin: GPIO5 dc_pin: GPIO15 reset_pin: GPIO16 +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/ssd1325_spi/test.rp2040-ard.yaml b/tests/components/ssd1325_spi/test.rp2040-ard.yaml index d7fd6ee294..66caa956f7 100644 --- a/tests/components/ssd1325_spi/test.rp2040-ard.yaml +++ b/tests/components/ssd1325_spi/test.rp2040-ard.yaml @@ -6,4 +6,7 @@ substitutions: dc_pin: GPIO15 reset_pin: GPIO16 +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/ssd1327_i2c/common.yaml b/tests/components/ssd1327_i2c/common.yaml index 72a122c3d7..c5f2123d9a 100644 --- a/tests/components/ssd1327_i2c/common.yaml +++ b/tests/components/ssd1327_i2c/common.yaml @@ -1,24 +1,20 @@ -i2c: - - id: i2c_ssd1327_i2c - scl: ${scl_pin} - sda: ${sda_pin} - display: - platform: ssd1327_i2c + i2c_id: i2c_bus model: SSD1327_128x128 reset_pin: ${reset_pin} address: 0x3C - id: display1 + id: ssd1327_i2c_display pages: - - id: page1 + - id: ssd1327_i2c_page1 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - - id: page2 + - id: ssd1327_i2c_page2 lambda: |- it.rectangle(0, 0, 10, 10); on_page_change: - from: page1 - to: page2 + from: ssd1327_i2c_page1 + to: ssd1327_i2c_page2 then: lambda: |- ESP_LOGD("display", "1 -> 2"); diff --git a/tests/components/ssd1327_i2c/test.esp32-ard.yaml b/tests/components/ssd1327_i2c/test.esp32-ard.yaml deleted file mode 100644 index 1ca773e06c..0000000000 --- a/tests/components/ssd1327_i2c/test.esp32-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - reset_pin: GPIO15 - -<<: !include common.yaml diff --git a/tests/components/ssd1327_i2c/test.esp32-c3-ard.yaml b/tests/components/ssd1327_i2c/test.esp32-c3-ard.yaml deleted file mode 100644 index 4eaff7fa4a..0000000000 --- a/tests/components/ssd1327_i2c/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - reset_pin: GPIO3 - -<<: !include common.yaml diff --git a/tests/components/ssd1327_i2c/test.esp32-c3-idf.yaml b/tests/components/ssd1327_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index 4eaff7fa4a..0000000000 --- a/tests/components/ssd1327_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - reset_pin: GPIO3 - -<<: !include common.yaml diff --git a/tests/components/ssd1327_i2c/test.esp32-idf.yaml b/tests/components/ssd1327_i2c/test.esp32-idf.yaml index 1ca773e06c..4ff2241ec9 100644 --- a/tests/components/ssd1327_i2c/test.esp32-idf.yaml +++ b/tests/components/ssd1327_i2c/test.esp32-idf.yaml @@ -1,6 +1,7 @@ substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 reset_pin: GPIO15 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/ssd1327_i2c/test.esp8266-ard.yaml b/tests/components/ssd1327_i2c/test.esp8266-ard.yaml index af91c21a0d..352cc8cda8 100644 --- a/tests/components/ssd1327_i2c/test.esp8266-ard.yaml +++ b/tests/components/ssd1327_i2c/test.esp8266-ard.yaml @@ -1,6 +1,7 @@ substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 reset_pin: GPIO2 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/ssd1327_i2c/test.rp2040-ard.yaml b/tests/components/ssd1327_i2c/test.rp2040-ard.yaml index 4eaff7fa4a..2972fde8a5 100644 --- a/tests/components/ssd1327_i2c/test.rp2040-ard.yaml +++ b/tests/components/ssd1327_i2c/test.rp2040-ard.yaml @@ -1,6 +1,7 @@ substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 reset_pin: GPIO3 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/ssd1327_spi/common.yaml b/tests/components/ssd1327_spi/common.yaml index 85f4d4736b..b46e61e080 100644 --- a/tests/components/ssd1327_spi/common.yaml +++ b/tests/components/ssd1327_spi/common.yaml @@ -1,24 +1,20 @@ -spi: - - id: spi_ssd1327_spi - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - display: - platform: ssd1327_spi + id: ssd1327_spi_display model: SSD1327 128x128 cs_pin: ${cs_pin} dc_pin: ${dc_pin} reset_pin: ${reset_pin} pages: - - id: page1 + - id: ssd1327_spi_page1 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - - id: page2 + - id: ssd1327_spi_page2 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); on_page_change: - from: page1 - to: page2 + from: ssd1327_spi_page1 + to: ssd1327_spi_page2 then: lambda: |- ESP_LOGD("display", "1 -> 2"); diff --git a/tests/components/ssd1327_spi/test.esp32-ard.yaml b/tests/components/ssd1327_spi/test.esp32-ard.yaml deleted file mode 100644 index bad5241f79..0000000000 --- a/tests/components/ssd1327_spi/test.esp32-ard.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - cs_pin: GPIO12 - dc_pin: GPIO13 - reset_pin: GPIO14 - -<<: !include common.yaml diff --git a/tests/components/ssd1327_spi/test.esp32-c3-ard.yaml b/tests/components/ssd1327_spi/test.esp32-c3-ard.yaml deleted file mode 100644 index c5c932c92c..0000000000 --- a/tests/components/ssd1327_spi/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - dc_pin: GPIO9 - reset_pin: GPIO10 - -<<: !include common.yaml diff --git a/tests/components/ssd1327_spi/test.esp32-c3-idf.yaml b/tests/components/ssd1327_spi/test.esp32-c3-idf.yaml deleted file mode 100644 index c5c932c92c..0000000000 --- a/tests/components/ssd1327_spi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - dc_pin: GPIO9 - reset_pin: GPIO10 - -<<: !include common.yaml diff --git a/tests/components/ssd1327_spi/test.esp32-idf.yaml b/tests/components/ssd1327_spi/test.esp32-idf.yaml index bad5241f79..ff174a4656 100644 --- a/tests/components/ssd1327_spi/test.esp32-idf.yaml +++ b/tests/components/ssd1327_spi/test.esp32-idf.yaml @@ -1,8 +1,9 @@ substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 cs_pin: GPIO12 dc_pin: GPIO13 reset_pin: GPIO14 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/ssd1327_spi/test.esp8266-ard.yaml b/tests/components/ssd1327_spi/test.esp8266-ard.yaml index 3f023a60eb..56cb29f29e 100644 --- a/tests/components/ssd1327_spi/test.esp8266-ard.yaml +++ b/tests/components/ssd1327_spi/test.esp8266-ard.yaml @@ -1,9 +1,12 @@ substitutions: - clk_pin: GPIO14 - mosi_pin: GPIO13 + clk_pin: GPIO0 + mosi_pin: GPIO2 miso_pin: GPIO12 cs_pin: GPIO5 dc_pin: GPIO15 reset_pin: GPIO16 +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/ssd1327_spi/test.rp2040-ard.yaml b/tests/components/ssd1327_spi/test.rp2040-ard.yaml index d7fd6ee294..66caa956f7 100644 --- a/tests/components/ssd1327_spi/test.rp2040-ard.yaml +++ b/tests/components/ssd1327_spi/test.rp2040-ard.yaml @@ -6,4 +6,7 @@ substitutions: dc_pin: GPIO15 reset_pin: GPIO16 +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/ssd1331_spi/common.yaml b/tests/components/ssd1331_spi/common.yaml index 40726101e8..2b708e9bd9 100644 --- a/tests/components/ssd1331_spi/common.yaml +++ b/tests/components/ssd1331_spi/common.yaml @@ -1,23 +1,18 @@ -spi: - - id: spi_ssd1331_spi - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - display: - platform: ssd1331_spi cs_pin: ${cs_pin} dc_pin: ${dc_pin} reset_pin: ${reset_pin} pages: - - id: page1 + - id: ssd1331_spi_page1 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - - id: page2 + - id: ssd1331_spi_page2 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); on_page_change: - from: page1 - to: page2 + from: ssd1331_spi_page1 + to: ssd1331_spi_page2 then: lambda: |- ESP_LOGD("display", "1 -> 2"); diff --git a/tests/components/ssd1331_spi/test.esp32-ard.yaml b/tests/components/ssd1331_spi/test.esp32-ard.yaml deleted file mode 100644 index bad5241f79..0000000000 --- a/tests/components/ssd1331_spi/test.esp32-ard.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - cs_pin: GPIO12 - dc_pin: GPIO13 - reset_pin: GPIO14 - -<<: !include common.yaml diff --git a/tests/components/ssd1331_spi/test.esp32-c3-ard.yaml b/tests/components/ssd1331_spi/test.esp32-c3-ard.yaml deleted file mode 100644 index c5c932c92c..0000000000 --- a/tests/components/ssd1331_spi/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - dc_pin: GPIO9 - reset_pin: GPIO10 - -<<: !include common.yaml diff --git a/tests/components/ssd1331_spi/test.esp32-c3-idf.yaml b/tests/components/ssd1331_spi/test.esp32-c3-idf.yaml deleted file mode 100644 index c5c932c92c..0000000000 --- a/tests/components/ssd1331_spi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - dc_pin: GPIO9 - reset_pin: GPIO10 - -<<: !include common.yaml diff --git a/tests/components/ssd1331_spi/test.esp32-idf.yaml b/tests/components/ssd1331_spi/test.esp32-idf.yaml index bad5241f79..ff174a4656 100644 --- a/tests/components/ssd1331_spi/test.esp32-idf.yaml +++ b/tests/components/ssd1331_spi/test.esp32-idf.yaml @@ -1,8 +1,9 @@ substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 cs_pin: GPIO12 dc_pin: GPIO13 reset_pin: GPIO14 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/ssd1331_spi/test.esp8266-ard.yaml b/tests/components/ssd1331_spi/test.esp8266-ard.yaml index 3f023a60eb..56cb29f29e 100644 --- a/tests/components/ssd1331_spi/test.esp8266-ard.yaml +++ b/tests/components/ssd1331_spi/test.esp8266-ard.yaml @@ -1,9 +1,12 @@ substitutions: - clk_pin: GPIO14 - mosi_pin: GPIO13 + clk_pin: GPIO0 + mosi_pin: GPIO2 miso_pin: GPIO12 cs_pin: GPIO5 dc_pin: GPIO15 reset_pin: GPIO16 +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/ssd1331_spi/test.rp2040-ard.yaml b/tests/components/ssd1331_spi/test.rp2040-ard.yaml index d7fd6ee294..66caa956f7 100644 --- a/tests/components/ssd1331_spi/test.rp2040-ard.yaml +++ b/tests/components/ssd1331_spi/test.rp2040-ard.yaml @@ -6,4 +6,7 @@ substitutions: dc_pin: GPIO15 reset_pin: GPIO16 +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/ssd1351_spi/common.yaml b/tests/components/ssd1351_spi/common.yaml index fa495ff0c3..e4d3d55f3f 100644 --- a/tests/components/ssd1351_spi/common.yaml +++ b/tests/components/ssd1351_spi/common.yaml @@ -1,8 +1,3 @@ -spi: - - id: spi_ssd1351_spi - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - display: - platform: ssd1351_spi model: "SSD1351 128x128" @@ -10,15 +5,15 @@ display: dc_pin: ${dc_pin} reset_pin: ${reset_pin} pages: - - id: page1 + - id: ssd1351_spi_page1 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - - id: page2 + - id: ssd1351_spi_page2 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); on_page_change: - from: page1 - to: page2 + from: ssd1351_spi_page1 + to: ssd1351_spi_page2 then: lambda: |- ESP_LOGD("display", "1 -> 2"); diff --git a/tests/components/ssd1351_spi/test.esp32-ard.yaml b/tests/components/ssd1351_spi/test.esp32-ard.yaml deleted file mode 100644 index bad5241f79..0000000000 --- a/tests/components/ssd1351_spi/test.esp32-ard.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - cs_pin: GPIO12 - dc_pin: GPIO13 - reset_pin: GPIO14 - -<<: !include common.yaml diff --git a/tests/components/ssd1351_spi/test.esp32-c3-ard.yaml b/tests/components/ssd1351_spi/test.esp32-c3-ard.yaml deleted file mode 100644 index c5c932c92c..0000000000 --- a/tests/components/ssd1351_spi/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - dc_pin: GPIO9 - reset_pin: GPIO10 - -<<: !include common.yaml diff --git a/tests/components/ssd1351_spi/test.esp32-c3-idf.yaml b/tests/components/ssd1351_spi/test.esp32-c3-idf.yaml deleted file mode 100644 index c5c932c92c..0000000000 --- a/tests/components/ssd1351_spi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - dc_pin: GPIO9 - reset_pin: GPIO10 - -<<: !include common.yaml diff --git a/tests/components/ssd1351_spi/test.esp32-idf.yaml b/tests/components/ssd1351_spi/test.esp32-idf.yaml index bad5241f79..ff174a4656 100644 --- a/tests/components/ssd1351_spi/test.esp32-idf.yaml +++ b/tests/components/ssd1351_spi/test.esp32-idf.yaml @@ -1,8 +1,9 @@ substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 cs_pin: GPIO12 dc_pin: GPIO13 reset_pin: GPIO14 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/ssd1351_spi/test.esp8266-ard.yaml b/tests/components/ssd1351_spi/test.esp8266-ard.yaml index 3f023a60eb..56cb29f29e 100644 --- a/tests/components/ssd1351_spi/test.esp8266-ard.yaml +++ b/tests/components/ssd1351_spi/test.esp8266-ard.yaml @@ -1,9 +1,12 @@ substitutions: - clk_pin: GPIO14 - mosi_pin: GPIO13 + clk_pin: GPIO0 + mosi_pin: GPIO2 miso_pin: GPIO12 cs_pin: GPIO5 dc_pin: GPIO15 reset_pin: GPIO16 +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/ssd1351_spi/test.rp2040-ard.yaml b/tests/components/ssd1351_spi/test.rp2040-ard.yaml index d7fd6ee294..66caa956f7 100644 --- a/tests/components/ssd1351_spi/test.rp2040-ard.yaml +++ b/tests/components/ssd1351_spi/test.rp2040-ard.yaml @@ -6,4 +6,7 @@ substitutions: dc_pin: GPIO15 reset_pin: GPIO16 +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/st7567_i2c/common.yaml b/tests/components/st7567_i2c/common.yaml index 41c65e5110..c81d6825e3 100644 --- a/tests/components/st7567_i2c/common.yaml +++ b/tests/components/st7567_i2c/common.yaml @@ -1,23 +1,19 @@ -i2c: - - id: i2c_st7567_i2c - scl: ${scl_pin} - sda: ${sda_pin} - display: - platform: st7567_i2c + i2c_id: i2c_bus reset_pin: ${reset_pin} address: 0x3C - id: display1 + id: st7567_i2c_display pages: - - id: page1 + - id: st7567_i2c_page1 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - - id: page2 + - id: st7567_i2c_page2 lambda: |- it.rectangle(0, 0, 10, 10); on_page_change: - from: page1 - to: page2 + from: st7567_i2c_page1 + to: st7567_i2c_page2 then: lambda: |- ESP_LOGD("display", "1 -> 2"); diff --git a/tests/components/st7567_i2c/test.esp32-ard.yaml b/tests/components/st7567_i2c/test.esp32-ard.yaml deleted file mode 100644 index 1ca773e06c..0000000000 --- a/tests/components/st7567_i2c/test.esp32-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - reset_pin: GPIO15 - -<<: !include common.yaml diff --git a/tests/components/st7567_i2c/test.esp32-c3-ard.yaml b/tests/components/st7567_i2c/test.esp32-c3-ard.yaml deleted file mode 100644 index 4eaff7fa4a..0000000000 --- a/tests/components/st7567_i2c/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - reset_pin: GPIO3 - -<<: !include common.yaml diff --git a/tests/components/st7567_i2c/test.esp32-c3-idf.yaml b/tests/components/st7567_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index 4eaff7fa4a..0000000000 --- a/tests/components/st7567_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - reset_pin: GPIO3 - -<<: !include common.yaml diff --git a/tests/components/st7567_i2c/test.esp32-idf.yaml b/tests/components/st7567_i2c/test.esp32-idf.yaml index 1ca773e06c..4ff2241ec9 100644 --- a/tests/components/st7567_i2c/test.esp32-idf.yaml +++ b/tests/components/st7567_i2c/test.esp32-idf.yaml @@ -1,6 +1,7 @@ substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 reset_pin: GPIO15 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/st7567_i2c/test.esp8266-ard.yaml b/tests/components/st7567_i2c/test.esp8266-ard.yaml index af91c21a0d..352cc8cda8 100644 --- a/tests/components/st7567_i2c/test.esp8266-ard.yaml +++ b/tests/components/st7567_i2c/test.esp8266-ard.yaml @@ -1,6 +1,7 @@ substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 reset_pin: GPIO2 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/st7567_i2c/test.rp2040-ard.yaml b/tests/components/st7567_i2c/test.rp2040-ard.yaml index 4eaff7fa4a..2972fde8a5 100644 --- a/tests/components/st7567_i2c/test.rp2040-ard.yaml +++ b/tests/components/st7567_i2c/test.rp2040-ard.yaml @@ -1,6 +1,7 @@ substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 reset_pin: GPIO3 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/st7567_spi/common.yaml b/tests/components/st7567_spi/common.yaml index 037a700239..25a8932ee1 100644 --- a/tests/components/st7567_spi/common.yaml +++ b/tests/components/st7567_spi/common.yaml @@ -1,23 +1,19 @@ -spi: - - id: spi_st7567_spi - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - display: - platform: st7567_spi + id: st7567_spi_display cs_pin: ${cs_pin} dc_pin: ${dc_pin} reset_pin: ${reset_pin} pages: - - id: page1 + - id: st7567_spi_page1 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - - id: page2 + - id: st7567_spi_page2 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); on_page_change: - from: page1 - to: page2 + from: st7567_spi_page1 + to: st7567_spi_page2 then: lambda: |- ESP_LOGD("display", "1 -> 2"); diff --git a/tests/components/st7567_spi/test.esp32-ard.yaml b/tests/components/st7567_spi/test.esp32-ard.yaml deleted file mode 100644 index bad5241f79..0000000000 --- a/tests/components/st7567_spi/test.esp32-ard.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - cs_pin: GPIO12 - dc_pin: GPIO13 - reset_pin: GPIO14 - -<<: !include common.yaml diff --git a/tests/components/st7567_spi/test.esp32-c3-ard.yaml b/tests/components/st7567_spi/test.esp32-c3-ard.yaml deleted file mode 100644 index c5c932c92c..0000000000 --- a/tests/components/st7567_spi/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - dc_pin: GPIO9 - reset_pin: GPIO10 - -<<: !include common.yaml diff --git a/tests/components/st7567_spi/test.esp32-c3-idf.yaml b/tests/components/st7567_spi/test.esp32-c3-idf.yaml deleted file mode 100644 index c5c932c92c..0000000000 --- a/tests/components/st7567_spi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - dc_pin: GPIO9 - reset_pin: GPIO10 - -<<: !include common.yaml diff --git a/tests/components/st7567_spi/test.esp32-idf.yaml b/tests/components/st7567_spi/test.esp32-idf.yaml index bad5241f79..ff174a4656 100644 --- a/tests/components/st7567_spi/test.esp32-idf.yaml +++ b/tests/components/st7567_spi/test.esp32-idf.yaml @@ -1,8 +1,9 @@ substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 cs_pin: GPIO12 dc_pin: GPIO13 reset_pin: GPIO14 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/st7567_spi/test.esp8266-ard.yaml b/tests/components/st7567_spi/test.esp8266-ard.yaml index 3f023a60eb..56cb29f29e 100644 --- a/tests/components/st7567_spi/test.esp8266-ard.yaml +++ b/tests/components/st7567_spi/test.esp8266-ard.yaml @@ -1,9 +1,12 @@ substitutions: - clk_pin: GPIO14 - mosi_pin: GPIO13 + clk_pin: GPIO0 + mosi_pin: GPIO2 miso_pin: GPIO12 cs_pin: GPIO5 dc_pin: GPIO15 reset_pin: GPIO16 +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/st7567_spi/test.rp2040-ard.yaml b/tests/components/st7567_spi/test.rp2040-ard.yaml index d7fd6ee294..66caa956f7 100644 --- a/tests/components/st7567_spi/test.rp2040-ard.yaml +++ b/tests/components/st7567_spi/test.rp2040-ard.yaml @@ -6,4 +6,7 @@ substitutions: dc_pin: GPIO15 reset_pin: GPIO16 +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/st7701s/common.yaml b/tests/components/st7701s/common.yaml index b94fadfe52..751862dea5 100644 --- a/tests/components/st7701s/common.yaml +++ b/tests/components/st7701s/common.yaml @@ -1,8 +1,3 @@ -spi: - - id: spi_st7701s - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - display: - platform: st7701s spi_mode: MODE3 diff --git a/tests/components/st7701s/test.esp32-s3-idf.yaml b/tests/components/st7701s/test.esp32-s3-idf.yaml index cd09b31f6e..87e5248888 100644 --- a/tests/components/st7701s/test.esp32-s3-idf.yaml +++ b/tests/components/st7701s/test.esp32-s3-idf.yaml @@ -1,5 +1,7 @@ +packages: + spi: !include ../../test_build_components/common/spi/esp32-s3-idf.yaml + substitutions: - clk_pin: GPIO41 mosi_pin: GPIO48 cs_pin: GPIO44 de_pin: GPIO18 diff --git a/tests/components/st7735/common.yaml b/tests/components/st7735/common.yaml index c140652eda..242c4bccd6 100644 --- a/tests/components/st7735/common.yaml +++ b/tests/components/st7735/common.yaml @@ -1,8 +1,3 @@ -spi: - - id: spi_st7735 - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - display: - platform: st7735 model: INITR_18BLACKTAB @@ -14,15 +9,15 @@ display: col_start: 0 row_start: 0 pages: - - id: page1 + - id: st7735_page1 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - - id: page2 + - id: st7735_page2 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); on_page_change: - from: page1 - to: page2 + from: st7735_page1 + to: st7735_page2 then: lambda: |- ESP_LOGD("display", "1 -> 2"); diff --git a/tests/components/st7735/test.esp32-ard.yaml b/tests/components/st7735/test.esp32-ard.yaml deleted file mode 100644 index bad5241f79..0000000000 --- a/tests/components/st7735/test.esp32-ard.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - cs_pin: GPIO12 - dc_pin: GPIO13 - reset_pin: GPIO14 - -<<: !include common.yaml diff --git a/tests/components/st7735/test.esp32-c3-ard.yaml b/tests/components/st7735/test.esp32-c3-ard.yaml deleted file mode 100644 index c5c932c92c..0000000000 --- a/tests/components/st7735/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - dc_pin: GPIO9 - reset_pin: GPIO10 - -<<: !include common.yaml diff --git a/tests/components/st7735/test.esp32-c3-idf.yaml b/tests/components/st7735/test.esp32-c3-idf.yaml deleted file mode 100644 index c5c932c92c..0000000000 --- a/tests/components/st7735/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - dc_pin: GPIO9 - reset_pin: GPIO10 - -<<: !include common.yaml diff --git a/tests/components/st7735/test.esp32-idf.yaml b/tests/components/st7735/test.esp32-idf.yaml index bad5241f79..ff174a4656 100644 --- a/tests/components/st7735/test.esp32-idf.yaml +++ b/tests/components/st7735/test.esp32-idf.yaml @@ -1,8 +1,9 @@ substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 cs_pin: GPIO12 dc_pin: GPIO13 reset_pin: GPIO14 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/st7735/test.esp8266-ard.yaml b/tests/components/st7735/test.esp8266-ard.yaml index 3f023a60eb..56cb29f29e 100644 --- a/tests/components/st7735/test.esp8266-ard.yaml +++ b/tests/components/st7735/test.esp8266-ard.yaml @@ -1,9 +1,12 @@ substitutions: - clk_pin: GPIO14 - mosi_pin: GPIO13 + clk_pin: GPIO0 + mosi_pin: GPIO2 miso_pin: GPIO12 cs_pin: GPIO5 dc_pin: GPIO15 reset_pin: GPIO16 +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/st7735/test.rp2040-ard.yaml b/tests/components/st7735/test.rp2040-ard.yaml index d7fd6ee294..66caa956f7 100644 --- a/tests/components/st7735/test.rp2040-ard.yaml +++ b/tests/components/st7735/test.rp2040-ard.yaml @@ -6,4 +6,7 @@ substitutions: dc_pin: GPIO15 reset_pin: GPIO16 +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/st7789v/common.yaml b/tests/components/st7789v/common.yaml index d5f74809e0..19cef5656a 100644 --- a/tests/components/st7789v/common.yaml +++ b/tests/components/st7789v/common.yaml @@ -1,24 +1,20 @@ -spi: - - id: spi_st7789v - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - display: - platform: st7789v model: TTGO TDisplay 135x240 cs_pin: ${cs_pin} dc_pin: ${dc_pin} reset_pin: ${reset_pin} + backlight_pin: ${backlight_pin} pages: - - id: page1 + - id: st7789v_page1 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - - id: page2 + - id: st7789v_page2 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); on_page_change: - from: page1 - to: page2 + from: st7789v_page1 + to: st7789v_page2 then: lambda: |- ESP_LOGD("display", "1 -> 2"); diff --git a/tests/components/st7789v/test.esp32-ard.yaml b/tests/components/st7789v/test.esp32-ard.yaml deleted file mode 100644 index bad5241f79..0000000000 --- a/tests/components/st7789v/test.esp32-ard.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - cs_pin: GPIO12 - dc_pin: GPIO13 - reset_pin: GPIO14 - -<<: !include common.yaml diff --git a/tests/components/st7789v/test.esp32-c3-ard.yaml b/tests/components/st7789v/test.esp32-c3-ard.yaml deleted file mode 100644 index c5c932c92c..0000000000 --- a/tests/components/st7789v/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - dc_pin: GPIO9 - reset_pin: GPIO10 - -<<: !include common.yaml diff --git a/tests/components/st7789v/test.esp32-c3-idf.yaml b/tests/components/st7789v/test.esp32-c3-idf.yaml deleted file mode 100644 index c5c932c92c..0000000000 --- a/tests/components/st7789v/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - dc_pin: GPIO9 - reset_pin: GPIO10 - -<<: !include common.yaml diff --git a/tests/components/st7789v/test.esp32-idf.yaml b/tests/components/st7789v/test.esp32-idf.yaml index bad5241f79..3b6d584e5c 100644 --- a/tests/components/st7789v/test.esp32-idf.yaml +++ b/tests/components/st7789v/test.esp32-idf.yaml @@ -1,8 +1,10 @@ substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 cs_pin: GPIO12 dc_pin: GPIO13 reset_pin: GPIO14 + backlight_pin: GPIO15 + +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/st7789v/test.esp8266-ard.yaml b/tests/components/st7789v/test.esp8266-ard.yaml index 3f023a60eb..0ddca66ef4 100644 --- a/tests/components/st7789v/test.esp8266-ard.yaml +++ b/tests/components/st7789v/test.esp8266-ard.yaml @@ -1,9 +1,13 @@ substitutions: - clk_pin: GPIO14 - mosi_pin: GPIO13 + clk_pin: GPIO0 + mosi_pin: GPIO2 miso_pin: GPIO12 cs_pin: GPIO5 dc_pin: GPIO15 reset_pin: GPIO16 + backlight_pin: GPIO4 + +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/st7789v/test.rp2040-ard.yaml b/tests/components/st7789v/test.rp2040-ard.yaml index d7fd6ee294..464e720549 100644 --- a/tests/components/st7789v/test.rp2040-ard.yaml +++ b/tests/components/st7789v/test.rp2040-ard.yaml @@ -5,5 +5,9 @@ substitutions: cs_pin: GPIO5 dc_pin: GPIO15 reset_pin: GPIO16 + backlight_pin: GPIO6 + +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/st7920/common.yaml b/tests/components/st7920/common.yaml index 9ede271f01..477ac14bd4 100644 --- a/tests/components/st7920/common.yaml +++ b/tests/components/st7920/common.yaml @@ -1,23 +1,18 @@ -spi: - - id: spi_st7920 - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - display: - platform: st7920 cs_pin: ${cs_pin} height: 128 width: 64 pages: - - id: page1 + - id: st7920_page1 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - - id: page2 + - id: st7920_page2 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); on_page_change: - from: page1 - to: page2 + from: st7920_page1 + to: st7920_page2 then: lambda: |- ESP_LOGD("display", "1 -> 2"); diff --git a/tests/components/st7920/test.esp32-ard.yaml b/tests/components/st7920/test.esp32-ard.yaml deleted file mode 100644 index 04d2633d2b..0000000000 --- a/tests/components/st7920/test.esp32-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 - cs_pin: GPIO12 - -<<: !include common.yaml diff --git a/tests/components/st7920/test.esp32-c3-ard.yaml b/tests/components/st7920/test.esp32-c3-ard.yaml deleted file mode 100644 index 2415ba5dc6..0000000000 --- a/tests/components/st7920/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/st7920/test.esp32-c3-idf.yaml b/tests/components/st7920/test.esp32-c3-idf.yaml deleted file mode 100644 index 2415ba5dc6..0000000000 --- a/tests/components/st7920/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - miso_pin: GPIO5 - cs_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/st7920/test.esp32-idf.yaml b/tests/components/st7920/test.esp32-idf.yaml index 04d2633d2b..9bb524aa65 100644 --- a/tests/components/st7920/test.esp32-idf.yaml +++ b/tests/components/st7920/test.esp32-idf.yaml @@ -1,6 +1,7 @@ substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 cs_pin: GPIO12 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/st7920/test.esp8266-ard.yaml b/tests/components/st7920/test.esp8266-ard.yaml index bd5c203e35..1aac800592 100644 --- a/tests/components/st7920/test.esp8266-ard.yaml +++ b/tests/components/st7920/test.esp8266-ard.yaml @@ -1,7 +1,10 @@ substitutions: - clk_pin: GPIO14 - mosi_pin: GPIO13 - miso_pin: GPIO12 - cs_pin: GPIO5 + clk_pin: GPIO0 + mosi_pin: GPIO2 + miso_pin: GPIO15 + cs_pin: GPIO16 + +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/st7920/test.rp2040-ard.yaml b/tests/components/st7920/test.rp2040-ard.yaml index f6c3f1eeca..1ded24de1c 100644 --- a/tests/components/st7920/test.rp2040-ard.yaml +++ b/tests/components/st7920/test.rp2040-ard.yaml @@ -4,4 +4,7 @@ substitutions: miso_pin: GPIO4 cs_pin: GPIO5 +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/statsD/test.esp32-ard.yaml b/tests/components/statsD/test.esp32-ard.yaml deleted file mode 100644 index 25cb37a0b4..0000000000 --- a/tests/components/statsD/test.esp32-ard.yaml +++ /dev/null @@ -1,2 +0,0 @@ -packages: - common: !include common.yaml diff --git a/tests/components/statsD/test.esp32-c3-ard.yaml b/tests/components/statsD/test.esp32-c3-ard.yaml deleted file mode 100644 index 25cb37a0b4..0000000000 --- a/tests/components/statsD/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,2 +0,0 @@ -packages: - common: !include common.yaml diff --git a/tests/components/statsD/test.esp32-c3-idf.yaml b/tests/components/statsD/test.esp32-c3-idf.yaml deleted file mode 100644 index 25cb37a0b4..0000000000 --- a/tests/components/statsD/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,2 +0,0 @@ -packages: - common: !include common.yaml diff --git a/tests/components/status/test.esp32-ard.yaml b/tests/components/status/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/status/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/status/test.esp32-c3-ard.yaml b/tests/components/status/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/status/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/status/test.esp32-c3-idf.yaml b/tests/components/status/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/status/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/status_led/test.esp32-ard.yaml b/tests/components/status_led/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/status_led/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/status_led/test.esp32-c3-ard.yaml b/tests/components/status_led/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/status_led/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/status_led/test.esp32-c3-idf.yaml b/tests/components/status_led/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/status_led/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/stepper/test.esp32-ard.yaml b/tests/components/stepper/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/stepper/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/stepper/test.esp32-c3-ard.yaml b/tests/components/stepper/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/stepper/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/stepper/test.esp32-c3-idf.yaml b/tests/components/stepper/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/stepper/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/sts3x/common.yaml b/tests/components/sts3x/common.yaml index 1feac4bc3f..1a61fa9212 100644 --- a/tests/components/sts3x/common.yaml +++ b/tests/components/sts3x/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_sts3x - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: sts3x + i2c_id: i2c_bus id: sts3x_sensor name: STS3X Temperature address: 0x4A diff --git a/tests/components/sts3x/test.esp32-ard.yaml b/tests/components/sts3x/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/sts3x/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/sts3x/test.esp32-c3-ard.yaml b/tests/components/sts3x/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/sts3x/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/sts3x/test.esp32-c3-idf.yaml b/tests/components/sts3x/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/sts3x/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/sts3x/test.esp32-idf.yaml b/tests/components/sts3x/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/sts3x/test.esp32-idf.yaml +++ b/tests/components/sts3x/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/sts3x/test.esp8266-ard.yaml b/tests/components/sts3x/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/sts3x/test.esp8266-ard.yaml +++ b/tests/components/sts3x/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/sts3x/test.rp2040-ard.yaml b/tests/components/sts3x/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/sts3x/test.rp2040-ard.yaml +++ b/tests/components/sts3x/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/stts22h/common.yaml b/tests/components/stts22h/common.yaml new file mode 100644 index 0000000000..2e332f9276 --- /dev/null +++ b/tests/components/stts22h/common.yaml @@ -0,0 +1,5 @@ +sensor: + - platform: stts22h + i2c_id: i2c_bus + name: Temperature + update_interval: 15s diff --git a/tests/components/stts22h/test.esp32-idf.yaml b/tests/components/stts22h/test.esp32-idf.yaml new file mode 100644 index 0000000000..b47e39c389 --- /dev/null +++ b/tests/components/stts22h/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/stts22h/test.esp8266-ard.yaml b/tests/components/stts22h/test.esp8266-ard.yaml new file mode 100644 index 0000000000..4a98b9388a --- /dev/null +++ b/tests/components/stts22h/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/stts22h/test.nrf52-adafruit.yaml b/tests/components/stts22h/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..2a0de6241c --- /dev/null +++ b/tests/components/stts22h/test.nrf52-adafruit.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/nrf52.yaml + +<<: !include common.yaml diff --git a/tests/components/stts22h/test.rp2040-ard.yaml b/tests/components/stts22h/test.rp2040-ard.yaml new file mode 100644 index 0000000000..319a7c71a6 --- /dev/null +++ b/tests/components/stts22h/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/sun/test.esp32-ard.yaml b/tests/components/sun/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/sun/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/sun/test.esp32-c3-ard.yaml b/tests/components/sun/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/sun/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/sun/test.esp32-c3-idf.yaml b/tests/components/sun/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/sun/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/sun_gtil2/common.yaml b/tests/components/sun_gtil2/common.yaml index 15cc892d90..37fd4b056f 100644 --- a/tests/components/sun_gtil2/common.yaml +++ b/tests/components/sun_gtil2/common.yaml @@ -1,8 +1,3 @@ -uart: - - id: uart_sun_gtil2 - rx_pin: ${rx_pin} - baud_rate: 9600 - sun_gtil2: sensor: diff --git a/tests/components/sun_gtil2/test.esp32-ard.yaml b/tests/components/sun_gtil2/test.esp32-ard.yaml deleted file mode 100644 index ad420099ff..0000000000 --- a/tests/components/sun_gtil2/test.esp32-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - rx_pin: GPIO16 - -<<: !include common.yaml diff --git a/tests/components/sun_gtil2/test.esp32-c3-ard.yaml b/tests/components/sun_gtil2/test.esp32-c3-ard.yaml deleted file mode 100644 index b8a6b85616..0000000000 --- a/tests/components/sun_gtil2/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/sun_gtil2/test.esp32-c3-idf.yaml b/tests/components/sun_gtil2/test.esp32-c3-idf.yaml deleted file mode 100644 index b8a6b85616..0000000000 --- a/tests/components/sun_gtil2/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/sun_gtil2/test.esp32-idf.yaml b/tests/components/sun_gtil2/test.esp32-idf.yaml index ad420099ff..29c7835b4e 100644 --- a/tests/components/sun_gtil2/test.esp32-idf.yaml +++ b/tests/components/sun_gtil2/test.esp32-idf.yaml @@ -1,4 +1,7 @@ substitutions: - rx_pin: GPIO16 + rx_pin: GPIO4 + +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/sun_gtil2/test.esp8266-ard.yaml b/tests/components/sun_gtil2/test.esp8266-ard.yaml index b8a6b85616..a591df42cb 100644 --- a/tests/components/sun_gtil2/test.esp8266-ard.yaml +++ b/tests/components/sun_gtil2/test.esp8266-ard.yaml @@ -1,4 +1,7 @@ substitutions: - rx_pin: GPIO5 + rx_pin: GPIO0 + +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/sun_gtil2/test.rp2040-ard.yaml b/tests/components/sun_gtil2/test.rp2040-ard.yaml index b8a6b85616..281ea9480e 100644 --- a/tests/components/sun_gtil2/test.rp2040-ard.yaml +++ b/tests/components/sun_gtil2/test.rp2040-ard.yaml @@ -1,4 +1,7 @@ substitutions: rx_pin: GPIO5 +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/switch/common.yaml b/tests/components/switch/common.yaml index 8d6972f91b..afdf26c150 100644 --- a/tests/components/switch/common.yaml +++ b/tests/components/switch/common.yaml @@ -9,3 +9,23 @@ switch: name: "Template Switch" id: the_switch optimistic: true + on_state: + - if: + condition: + - lambda: return x; + then: + - logger.log: "Switch turned ON" + else: + - logger.log: "Switch turned OFF" + on_turn_on: + - logger.log: "Switch is now ON" + on_turn_off: + - logger.log: "Switch is now OFF" + +esphome: + on_boot: + - switch.turn_on: the_switch + - switch.turn_off: the_switch + - switch.control: + id: the_switch + state: !lambda return (1 > 2); diff --git a/tests/components/switch/test.esp32-ard.yaml b/tests/components/switch/test.esp32-ard.yaml deleted file mode 100644 index 25cb37a0b4..0000000000 --- a/tests/components/switch/test.esp32-ard.yaml +++ /dev/null @@ -1,2 +0,0 @@ -packages: - common: !include common.yaml diff --git a/tests/components/switch/test.esp32-c3-ard.yaml b/tests/components/switch/test.esp32-c3-ard.yaml deleted file mode 100644 index 25cb37a0b4..0000000000 --- a/tests/components/switch/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,2 +0,0 @@ -packages: - common: !include common.yaml diff --git a/tests/components/switch/test.esp32-c3-idf.yaml b/tests/components/switch/test.esp32-c3-idf.yaml deleted file mode 100644 index 25cb37a0b4..0000000000 --- a/tests/components/switch/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,2 +0,0 @@ -packages: - common: !include common.yaml diff --git a/tests/components/switch/test.esp32-s3-idf.yaml b/tests/components/switch/test.esp32-s3-idf.yaml deleted file mode 100644 index 25cb37a0b4..0000000000 --- a/tests/components/switch/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,2 +0,0 @@ -packages: - common: !include common.yaml diff --git a/tests/components/ble_scanner/test.esp32-c3-ard.yaml b/tests/components/switch/test.nrf52-adafruit.yaml similarity index 100% rename from tests/components/ble_scanner/test.esp32-c3-ard.yaml rename to tests/components/switch/test.nrf52-adafruit.yaml diff --git a/tests/components/ble_scanner/test.esp32-c3-idf.yaml b/tests/components/switch/test.nrf52-mcumgr.yaml similarity index 100% rename from tests/components/ble_scanner/test.esp32-c3-idf.yaml rename to tests/components/switch/test.nrf52-mcumgr.yaml diff --git a/tests/components/sx126x/common.yaml b/tests/components/sx126x/common.yaml index 3f888c3ce4..659550cc01 100644 --- a/tests/components/sx126x/common.yaml +++ b/tests/components/sx126x/common.yaml @@ -1,8 +1,3 @@ -spi: - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - miso_pin: ${miso_pin} - sx126x: dio1_pin: ${dio1_pin} cs_pin: ${cs_pin} @@ -11,6 +6,10 @@ sx126x: pa_power: 3 bandwidth: 125_0kHz crc_enable: true + crc_initial: 0x1D0F + crc_polynomial: 0x1021 + crc_size: 2 + crc_inverted: true frequency: 433920000 modulation: LORA rx_start: true @@ -27,6 +26,15 @@ sx126x: - lambda: |- ESP_LOGD("lambda", "packet %.2f %.2f %s", rssi, snr, format_hex(x).c_str()); +number: + - platform: template + name: "SX126x Number" + id: my_number + optimistic: true + min_value: 0 + max_value: 100 + step: 1 + button: - platform: template name: "SX126x Button" @@ -38,3 +46,5 @@ button: - sx126x.set_mode_rx - sx126x.send_packet: data: [0xC5, 0x51, 0x78, 0x82, 0xB7, 0xF9, 0x9C, 0x5C] + - sx126x.send_packet: !lambda |- + return {0x01, 0x02, (uint8_t)id(my_number).state}; diff --git a/tests/components/sx126x/test.esp32-ard.yaml b/tests/components/sx126x/test.esp32-ard.yaml deleted file mode 100644 index 9770f52229..0000000000 --- a/tests/components/sx126x/test.esp32-ard.yaml +++ /dev/null @@ -1,10 +0,0 @@ -substitutions: - clk_pin: GPIO5 - mosi_pin: GPIO27 - miso_pin: GPIO19 - cs_pin: GPIO18 - rst_pin: GPIO23 - busy_pin: GPIO25 - dio1_pin: GPIO26 - -<<: !include common.yaml diff --git a/tests/components/sx126x/test.esp32-c3-ard.yaml b/tests/components/sx126x/test.esp32-c3-ard.yaml deleted file mode 100644 index 91450e24ce..0000000000 --- a/tests/components/sx126x/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,10 +0,0 @@ -substitutions: - clk_pin: GPIO5 - mosi_pin: GPIO18 - miso_pin: GPIO19 - cs_pin: GPIO1 - rst_pin: GPIO2 - busy_pin: GPIO4 - dio1_pin: GPIO3 - -<<: !include common.yaml diff --git a/tests/components/sx126x/test.esp32-c3-idf.yaml b/tests/components/sx126x/test.esp32-c3-idf.yaml deleted file mode 100644 index 91450e24ce..0000000000 --- a/tests/components/sx126x/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,10 +0,0 @@ -substitutions: - clk_pin: GPIO5 - mosi_pin: GPIO18 - miso_pin: GPIO19 - cs_pin: GPIO1 - rst_pin: GPIO2 - busy_pin: GPIO4 - dio1_pin: GPIO3 - -<<: !include common.yaml diff --git a/tests/components/sx126x/test.esp32-idf.yaml b/tests/components/sx126x/test.esp32-idf.yaml index 9770f52229..854638ea9c 100644 --- a/tests/components/sx126x/test.esp32-idf.yaml +++ b/tests/components/sx126x/test.esp32-idf.yaml @@ -1,10 +1,10 @@ substitutions: - clk_pin: GPIO5 - mosi_pin: GPIO27 - miso_pin: GPIO19 - cs_pin: GPIO18 - rst_pin: GPIO23 + cs_pin: GPIO12 + rst_pin: GPIO13 busy_pin: GPIO25 dio1_pin: GPIO26 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/sx126x/test.esp8266-ard.yaml b/tests/components/sx126x/test.esp8266-ard.yaml index d2c07c5bb7..98a3fbbd67 100644 --- a/tests/components/sx126x/test.esp8266-ard.yaml +++ b/tests/components/sx126x/test.esp8266-ard.yaml @@ -1,10 +1,13 @@ substitutions: - clk_pin: GPIO5 - mosi_pin: GPIO13 - miso_pin: GPIO12 + clk_pin: GPIO0 + mosi_pin: GPIO15 + miso_pin: GPIO16 cs_pin: GPIO1 rst_pin: GPIO2 busy_pin: GPIO4 dio1_pin: GPIO3 +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/sx126x/test.rp2040-ard.yaml b/tests/components/sx126x/test.rp2040-ard.yaml index 8881e96971..9bc12a370d 100644 --- a/tests/components/sx126x/test.rp2040-ard.yaml +++ b/tests/components/sx126x/test.rp2040-ard.yaml @@ -7,4 +7,7 @@ substitutions: busy_pin: GPIO8 dio1_pin: GPIO7 +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/sx127x/common.yaml b/tests/components/sx127x/common.yaml index 63adc2e91c..6e48952fcc 100644 --- a/tests/components/sx127x/common.yaml +++ b/tests/components/sx127x/common.yaml @@ -1,8 +1,3 @@ -spi: - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - miso_pin: ${miso_pin} - sx127x: cs_pin: ${cs_pin} rst_pin: ${rst_pin} @@ -31,6 +26,15 @@ sx127x: - sx127x.send_packet: data: [0xC5, 0x51, 0x78, 0x82, 0xB7, 0xF9, 0x9C, 0x5C] +number: + - platform: template + name: "SX127x Number" + id: my_number + optimistic: true + min_value: 0 + max_value: 100 + step: 1 + button: - platform: template name: "SX127x Button" @@ -43,3 +47,5 @@ button: - sx127x.set_mode_rx - sx127x.send_packet: data: [0xC5, 0x51, 0x78, 0x82, 0xB7, 0xF9, 0x9C, 0x5C] + - sx127x.send_packet: !lambda |- + return {0x01, 0x02, (uint8_t)id(my_number).state}; diff --git a/tests/components/sx127x/test.esp32-ard.yaml b/tests/components/sx127x/test.esp32-ard.yaml deleted file mode 100644 index 71270462a2..0000000000 --- a/tests/components/sx127x/test.esp32-ard.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - clk_pin: GPIO5 - mosi_pin: GPIO27 - miso_pin: GPIO19 - cs_pin: GPIO18 - rst_pin: GPIO23 - dio0_pin: GPIO26 - -<<: !include common.yaml diff --git a/tests/components/sx127x/test.esp32-c3-ard.yaml b/tests/components/sx127x/test.esp32-c3-ard.yaml deleted file mode 100644 index 36535a950d..0000000000 --- a/tests/components/sx127x/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - clk_pin: GPIO5 - mosi_pin: GPIO18 - miso_pin: GPIO19 - cs_pin: GPIO1 - rst_pin: GPIO2 - dio0_pin: GPIO3 - -<<: !include common.yaml diff --git a/tests/components/sx127x/test.esp32-c3-idf.yaml b/tests/components/sx127x/test.esp32-c3-idf.yaml deleted file mode 100644 index 36535a950d..0000000000 --- a/tests/components/sx127x/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - clk_pin: GPIO5 - mosi_pin: GPIO18 - miso_pin: GPIO19 - cs_pin: GPIO1 - rst_pin: GPIO2 - dio0_pin: GPIO3 - -<<: !include common.yaml diff --git a/tests/components/sx127x/test.esp32-idf.yaml b/tests/components/sx127x/test.esp32-idf.yaml index 71270462a2..c9d58bb27e 100644 --- a/tests/components/sx127x/test.esp32-idf.yaml +++ b/tests/components/sx127x/test.esp32-idf.yaml @@ -1,9 +1,9 @@ substitutions: - clk_pin: GPIO5 - mosi_pin: GPIO27 - miso_pin: GPIO19 - cs_pin: GPIO18 - rst_pin: GPIO23 + cs_pin: GPIO12 + rst_pin: GPIO13 dio0_pin: GPIO26 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/sx127x/test.esp8266-ard.yaml b/tests/components/sx127x/test.esp8266-ard.yaml index 64c01edd44..d58166137c 100644 --- a/tests/components/sx127x/test.esp8266-ard.yaml +++ b/tests/components/sx127x/test.esp8266-ard.yaml @@ -1,9 +1,12 @@ substitutions: - clk_pin: GPIO5 - mosi_pin: GPIO13 - miso_pin: GPIO12 + clk_pin: GPIO0 + mosi_pin: GPIO15 + miso_pin: GPIO16 cs_pin: GPIO1 rst_pin: GPIO2 dio0_pin: GPIO3 +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/sx127x/test.rp2040-ard.yaml b/tests/components/sx127x/test.rp2040-ard.yaml index 0af7b29790..09a9b3203b 100644 --- a/tests/components/sx127x/test.rp2040-ard.yaml +++ b/tests/components/sx127x/test.rp2040-ard.yaml @@ -6,4 +6,7 @@ substitutions: rst_pin: GPIO6 dio0_pin: GPIO7 +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/sx1509/common.yaml b/tests/components/sx1509/common.yaml index a83217e579..cf7e234f09 100644 --- a/tests/components/sx1509/common.yaml +++ b/tests/components/sx1509/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_sx1509 - scl: ${scl_pin} - sda: ${sda_pin} - sx1509: - id: sx1509_hub + i2c_id: i2c_bus address: 0x3E keypad: key_rows: 2 diff --git a/tests/components/sx1509/test.esp32-ard.yaml b/tests/components/sx1509/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/sx1509/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/sx1509/test.esp32-c3-ard.yaml b/tests/components/sx1509/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/sx1509/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/sx1509/test.esp32-c3-idf.yaml b/tests/components/sx1509/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/sx1509/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/sx1509/test.esp32-idf.yaml b/tests/components/sx1509/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/sx1509/test.esp32-idf.yaml +++ b/tests/components/sx1509/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/sx1509/test.esp8266-ard.yaml b/tests/components/sx1509/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/sx1509/test.esp8266-ard.yaml +++ b/tests/components/sx1509/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/sx1509/test.rp2040-ard.yaml b/tests/components/sx1509/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/sx1509/test.rp2040-ard.yaml +++ b/tests/components/sx1509/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/syslog/common.yaml b/tests/components/syslog/common.yaml index cd6e63c9ec..daa913f009 100644 --- a/tests/components/syslog/common.yaml +++ b/tests/components/syslog/common.yaml @@ -6,7 +6,8 @@ udp: addresses: ["239.0.60.53"] time: - platform: host + - platform: host + id: host_time syslog: port: 514 diff --git a/tests/components/syslog/test.esp32-ard.yaml b/tests/components/syslog/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/syslog/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/syslog/test.esp32-c3-ard.yaml b/tests/components/syslog/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/syslog/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/syslog/test.esp32-c3-idf.yaml b/tests/components/syslog/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/syslog/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/syslog/test.host.yaml b/tests/components/syslog/test.host.yaml index e735c37e4d..31122437d5 100644 --- a/tests/components/syslog/test.host.yaml +++ b/tests/components/syslog/test.host.yaml @@ -1,4 +1,11 @@ -packages: - common: !include common.yaml +udp: + addresses: ["239.0.60.53"] -wifi: !remove +time: + platform: host + +syslog: + port: 514 + strip: true + level: info + facility: 16 diff --git a/tests/components/t6615/common.yaml b/tests/components/t6615/common.yaml index 3ad715ae2b..4317130461 100644 --- a/tests/components/t6615/common.yaml +++ b/tests/components/t6615/common.yaml @@ -1,9 +1,3 @@ -uart: - - id: uart_t6615 - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 19200 - sensor: - platform: t6615 co2: diff --git a/tests/components/t6615/test.esp32-ard.yaml b/tests/components/t6615/test.esp32-ard.yaml deleted file mode 100644 index f486544afa..0000000000 --- a/tests/components/t6615/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 - -<<: !include common.yaml diff --git a/tests/components/t6615/test.esp32-c3-ard.yaml b/tests/components/t6615/test.esp32-c3-ard.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/t6615/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/t6615/test.esp32-c3-idf.yaml b/tests/components/t6615/test.esp32-c3-idf.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/t6615/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/t6615/test.esp32-idf.yaml b/tests/components/t6615/test.esp32-idf.yaml index f486544afa..4bb9e7fff6 100644 --- a/tests/components/t6615/test.esp32-idf.yaml +++ b/tests/components/t6615/test.esp32-idf.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 + tx_pin: GPIO4 + rx_pin: GPIO5 + +packages: + uart_19200: !include ../../test_build_components/common/uart_19200/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/t6615/test.esp8266-ard.yaml b/tests/components/t6615/test.esp8266-ard.yaml index b516342f3b..1dd8409590 100644 --- a/tests/components/t6615/test.esp8266-ard.yaml +++ b/tests/components/t6615/test.esp8266-ard.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: GPIO0 + rx_pin: GPIO2 + +packages: + uart_19200: !include ../../test_build_components/common/uart_19200/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/t6615/test.rp2040-ard.yaml b/tests/components/t6615/test.rp2040-ard.yaml index b516342f3b..f4dada6605 100644 --- a/tests/components/t6615/test.rp2040-ard.yaml +++ b/tests/components/t6615/test.rp2040-ard.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO4 rx_pin: GPIO5 +packages: + uart_19200: !include ../../test_build_components/common/uart_19200/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/tc74/common.yaml b/tests/components/tc74/common.yaml index 88f5c91e12..c2430ee2d4 100644 --- a/tests/components/tc74/common.yaml +++ b/tests/components/tc74/common.yaml @@ -1,8 +1,4 @@ -i2c: - - id: i2c_tc74 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: tc74 + i2c_id: i2c_bus name: TC74 Temperature diff --git a/tests/components/tc74/test.esp32-ard.yaml b/tests/components/tc74/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/tc74/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/tc74/test.esp32-c3-ard.yaml b/tests/components/tc74/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/tc74/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/tc74/test.esp32-c3-idf.yaml b/tests/components/tc74/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/tc74/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/tc74/test.esp32-idf.yaml b/tests/components/tc74/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/tc74/test.esp32-idf.yaml +++ b/tests/components/tc74/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/tc74/test.esp8266-ard.yaml b/tests/components/tc74/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/tc74/test.esp8266-ard.yaml +++ b/tests/components/tc74/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/tc74/test.rp2040-ard.yaml b/tests/components/tc74/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/tc74/test.rp2040-ard.yaml +++ b/tests/components/tc74/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/tca9548a/common.yaml b/tests/components/tca9548a/common.yaml index 67e812e08b..bfaf1e26fb 100644 --- a/tests/components/tca9548a/common.yaml +++ b/tests/components/tca9548a/common.yaml @@ -1,15 +1,10 @@ -i2c: - - id: i2c_tca9548a - scl: ${scl_pin} - sda: ${sda_pin} - tca9548a: - id: multiplex0 + i2c_id: i2c_bus address: 0x70 channels: - bus_id: multiplex0_chan0 channel: 0 - i2c_id: i2c_tca9548a - id: multiplex1 + i2c_id: i2c_bus address: 0x71 - i2c_id: multiplex0_chan0 diff --git a/tests/components/tca9548a/test.esp32-ard.yaml b/tests/components/tca9548a/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/tca9548a/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/tca9548a/test.esp32-c3-ard.yaml b/tests/components/tca9548a/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/tca9548a/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/tca9548a/test.esp32-c3-idf.yaml b/tests/components/tca9548a/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/tca9548a/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/tca9548a/test.esp32-idf.yaml b/tests/components/tca9548a/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/tca9548a/test.esp32-idf.yaml +++ b/tests/components/tca9548a/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/tca9548a/test.esp8266-ard.yaml b/tests/components/tca9548a/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/tca9548a/test.esp8266-ard.yaml +++ b/tests/components/tca9548a/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/tca9548a/test.rp2040-ard.yaml b/tests/components/tca9548a/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/tca9548a/test.rp2040-ard.yaml +++ b/tests/components/tca9548a/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/tca9555/common.yaml b/tests/components/tca9555/common.yaml index 0fc3086786..82b4c959d8 100644 --- a/tests/components/tca9555/common.yaml +++ b/tests/components/tca9555/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_tca9555 - scl: ${scl_pin} - sda: ${sda_pin} - tca9555: - id: tca9555_hub + i2c_id: i2c_bus address: 0x21 binary_sensor: diff --git a/tests/components/tca9555/test.esp32-ard.yaml b/tests/components/tca9555/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/tca9555/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/tca9555/test.esp32-c3-ard.yaml b/tests/components/tca9555/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/tca9555/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/tca9555/test.esp32-c3-idf.yaml b/tests/components/tca9555/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/tca9555/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/tca9555/test.esp32-idf.yaml b/tests/components/tca9555/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/tca9555/test.esp32-idf.yaml +++ b/tests/components/tca9555/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/tca9555/test.esp8266-ard.yaml b/tests/components/tca9555/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/tca9555/test.esp8266-ard.yaml +++ b/tests/components/tca9555/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/tca9555/test.rp2040-ard.yaml b/tests/components/tca9555/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/tca9555/test.rp2040-ard.yaml +++ b/tests/components/tca9555/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/tcl112/common.yaml b/tests/components/tcl112/common.yaml index 0e43de4a4a..1074712f94 100644 --- a/tests/components/tcl112/common.yaml +++ b/tests/components/tcl112/common.yaml @@ -1,7 +1,3 @@ -remote_transmitter: - pin: ${pin} - carrier_duty_percent: 50% - sensor: - platform: template id: tcl112_sensor @@ -13,3 +9,4 @@ climate: supports_heat: true supports_cool: true sensor: tcl112_sensor + transmitter_id: xmitr diff --git a/tests/components/tcl112/test.esp32-ard.yaml b/tests/components/tcl112/test.esp32-ard.yaml deleted file mode 100644 index 7b012aa64c..0000000000 --- a/tests/components/tcl112/test.esp32-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/tcl112/test.esp32-c3-ard.yaml b/tests/components/tcl112/test.esp32-c3-ard.yaml deleted file mode 100644 index 7b012aa64c..0000000000 --- a/tests/components/tcl112/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/tcl112/test.esp32-c3-idf.yaml b/tests/components/tcl112/test.esp32-c3-idf.yaml deleted file mode 100644 index 7b012aa64c..0000000000 --- a/tests/components/tcl112/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/tcl112/test.esp32-idf.yaml b/tests/components/tcl112/test.esp32-idf.yaml index 7b012aa64c..e891f9dc85 100644 --- a/tests/components/tcl112/test.esp32-idf.yaml +++ b/tests/components/tcl112/test.esp32-idf.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO2 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/tcl112/test.esp8266-ard.yaml b/tests/components/tcl112/test.esp8266-ard.yaml index f5097fcf5f..4bed2f03e5 100644 --- a/tests/components/tcl112/test.esp8266-ard.yaml +++ b/tests/components/tcl112/test.esp8266-ard.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO5 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/tcs34725/common.yaml b/tests/components/tcs34725/common.yaml index 5296988fa5..e16862035e 100644 --- a/tests/components/tcs34725/common.yaml +++ b/tests/components/tcs34725/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_tcs34725 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: tcs34725 + i2c_id: i2c_bus red_channel: name: Red Channel green_channel: diff --git a/tests/components/tcs34725/test.esp32-ard.yaml b/tests/components/tcs34725/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/tcs34725/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/tcs34725/test.esp32-c3-ard.yaml b/tests/components/tcs34725/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/tcs34725/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/tcs34725/test.esp32-c3-idf.yaml b/tests/components/tcs34725/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/tcs34725/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/tcs34725/test.esp32-idf.yaml b/tests/components/tcs34725/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/tcs34725/test.esp32-idf.yaml +++ b/tests/components/tcs34725/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/tcs34725/test.esp8266-ard.yaml b/tests/components/tcs34725/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/tcs34725/test.esp8266-ard.yaml +++ b/tests/components/tcs34725/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/tcs34725/test.rp2040-ard.yaml b/tests/components/tcs34725/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/tcs34725/test.rp2040-ard.yaml +++ b/tests/components/tcs34725/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/tee501/common.yaml b/tests/components/tee501/common.yaml index c01ab7e37a..82091faccf 100644 --- a/tests/components/tee501/common.yaml +++ b/tests/components/tee501/common.yaml @@ -1,9 +1,5 @@ -i2c: - - id: i2c_tee501 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: tee501 + i2c_id: i2c_bus name: TEE501 Temperature address: 0x48 diff --git a/tests/components/tee501/test.esp32-ard.yaml b/tests/components/tee501/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/tee501/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/tee501/test.esp32-c3-ard.yaml b/tests/components/tee501/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/tee501/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/tee501/test.esp32-c3-idf.yaml b/tests/components/tee501/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/tee501/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/tee501/test.esp32-idf.yaml b/tests/components/tee501/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/tee501/test.esp32-idf.yaml +++ b/tests/components/tee501/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/tee501/test.esp8266-ard.yaml b/tests/components/tee501/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/tee501/test.esp8266-ard.yaml +++ b/tests/components/tee501/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/tee501/test.rp2040-ard.yaml b/tests/components/tee501/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/tee501/test.rp2040-ard.yaml +++ b/tests/components/tee501/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/teleinfo/common.yaml b/tests/components/teleinfo/common.yaml index 90b684e977..dcb2cb14bd 100644 --- a/tests/components/teleinfo/common.yaml +++ b/tests/components/teleinfo/common.yaml @@ -1,10 +1,3 @@ -uart: - - id: uart_teleinfo - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 1200 - parity: EVEN - button: - platform: template name: Poller component suspend test diff --git a/tests/components/teleinfo/test.esp32-ard.yaml b/tests/components/teleinfo/test.esp32-ard.yaml deleted file mode 100644 index f486544afa..0000000000 --- a/tests/components/teleinfo/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 - -<<: !include common.yaml diff --git a/tests/components/teleinfo/test.esp32-c3-ard.yaml b/tests/components/teleinfo/test.esp32-c3-ard.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/teleinfo/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/teleinfo/test.esp32-c3-idf.yaml b/tests/components/teleinfo/test.esp32-c3-idf.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/teleinfo/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/teleinfo/test.esp32-idf.yaml b/tests/components/teleinfo/test.esp32-idf.yaml index f486544afa..b415125e84 100644 --- a/tests/components/teleinfo/test.esp32-idf.yaml +++ b/tests/components/teleinfo/test.esp32-idf.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 + tx_pin: GPIO4 + rx_pin: GPIO5 + +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/teleinfo/test.esp8266-ard.yaml b/tests/components/teleinfo/test.esp8266-ard.yaml index b516342f3b..96ab4ef6ac 100644 --- a/tests/components/teleinfo/test.esp8266-ard.yaml +++ b/tests/components/teleinfo/test.esp8266-ard.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: GPIO0 + rx_pin: GPIO2 + +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/teleinfo/test.rp2040-ard.yaml b/tests/components/teleinfo/test.rp2040-ard.yaml index b516342f3b..b28f2b5e05 100644 --- a/tests/components/teleinfo/test.rp2040-ard.yaml +++ b/tests/components/teleinfo/test.rp2040-ard.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO4 rx_pin: GPIO5 +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/tem3200/common.yaml b/tests/components/tem3200/common.yaml index 392c853bf4..cef7317450 100644 --- a/tests/components/tem3200/common.yaml +++ b/tests/components/tem3200/common.yaml @@ -1,13 +1,7 @@ -i2c: - id: i2c_bus - scl: ${scl_pin} - sda: ${sda_pin} - frequency: 200kHz - sensor: - platform: tem3200 - update_interval: 1s i2c_id: i2c_bus + update_interval: 1s temperature: name: water temperature diff --git a/tests/components/tem3200/test.esp32-ard.yaml b/tests/components/tem3200/test.esp32-ard.yaml deleted file mode 100644 index 3b761d3fc1..0000000000 --- a/tests/components/tem3200/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO22 - sda_pin: GPIO21 - -<<: !include common.yaml diff --git a/tests/components/tem3200/test.esp32-idf.yaml b/tests/components/tem3200/test.esp32-idf.yaml index 3b761d3fc1..b47e39c389 100644 --- a/tests/components/tem3200/test.esp32-idf.yaml +++ b/tests/components/tem3200/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO22 - sda_pin: GPIO21 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/tem3200/test.esp32-s3-ard.yaml b/tests/components/tem3200/test.esp32-s3-ard.yaml deleted file mode 100644 index 4942e3c2b3..0000000000 --- a/tests/components/tem3200/test.esp32-s3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO40 - sda_pin: GPIO41 - -<<: !include common.yaml diff --git a/tests/components/tem3200/test.esp32-s3-idf.yaml b/tests/components/tem3200/test.esp32-s3-idf.yaml deleted file mode 100644 index 4942e3c2b3..0000000000 --- a/tests/components/tem3200/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO40 - sda_pin: GPIO41 - -<<: !include common.yaml diff --git a/tests/components/tem3200/test.esp8266-ard.yaml b/tests/components/tem3200/test.esp8266-ard.yaml index 3be5e53dcb..4a98b9388a 100644 --- a/tests/components/tem3200/test.esp8266-ard.yaml +++ b/tests/components/tem3200/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO05 - sda_pin: GPIO04 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/template/common-base.yaml b/tests/components/template/common-base.yaml new file mode 100644 index 0000000000..f101eea942 --- /dev/null +++ b/tests/components/template/common-base.yaml @@ -0,0 +1,347 @@ +esphome: + on_boot: + - sensor.template.publish: + id: template_sens + state: 42.0 + + # Templated + - sensor.template.publish: + id: template_sens + state: !lambda "return 42.0;" + + # Test C++ API: set_template() with stateless lambda (no captures) + # NOTE: set_template() is not intended to be a public API, but we test it to ensure it doesn't break. + - lambda: |- + id(template_sens).set_template([]() -> esphome::optional { + return 123.0f; + }); + + - datetime.date.set: + id: test_date + date: + year: 2021 + month: 1 + day: 1 + - datetime.date.set: + id: test_date + date: !lambda "return {.day_of_month = 1, .month = 1, .year = 2021};" + - datetime.date.set: + id: test_date + date: "2021-01-01" + +binary_sensor: + - platform: template + id: some_binary_sensor + name: "Garage Door Open" + lambda: |- + if (id(template_sens).state > 30) { + // Garage Door is open. + return true; + } else { + // Garage Door is closed. + return false; + } + - platform: template + id: other_binary_sensor + name: "Garage Door Closed" + condition: + sensor.in_range: + id: template_sens + below: 30.0 + filters: + - invert: + - delayed_on: 100ms + - delayed_off: 100ms + - delayed_on_off: !lambda "if (id(test_switch).state) return 1000; else return 0;" + - delayed_on_off: + time_on: 10s + time_off: !lambda "if (id(test_switch).state) return 1000; else return 0;" + - autorepeat: + - delay: 1s + time_off: 100ms + time_on: 900ms + - delay: 5s + time_off: 100ms + time_on: 400ms + - lambda: |- + if (id(other_binary_sensor).state) { + return x; + } + return {}; + - settle: 500ms + - timeout: 5s + +sensor: + - platform: template + name: "Template Sensor" + id: template_sens + lambda: |- + if (id(some_binary_sensor).state) { + return 42.0; + } + return 0.0; + update_interval: 60s + filters: + - calibrate_linear: + - 0.0 -> 0.0 + - 40.0 -> 45.0 + - 100.0 -> 102.5 + - calibrate_polynomial: + degree: 2 + datapoints: + # Map 0.0 (from sensor) to 0.0 (true value) + - 0.0 -> 0.0 + - 10.0 -> 12.1 + - 13.0 -> 14.0 + - clamp: + max_value: 10.0 + min_value: -10.0 + - debounce: 0.1s + - delta: 5.0 + - exponential_moving_average: + alpha: 0.1 + send_every: 15 + - filter_out: + - 10 + - 20 + - !lambda return 10; + - filter_out: 10 + - filter_out: !lambda return NAN; + - heartbeat: 5s + - heartbeat: + period: 5s + optimistic: true + - lambda: return x * (9.0/5.0) + 32.0; + - max: + window_size: 10 + send_every: 2 + send_first_at: 1 + - median: + window_size: 7 + send_every: 4 + send_first_at: 3 + - min: + window_size: 10 + send_every: 2 + send_first_at: 1 + - multiply: 1 + - multiply: !lambda return 2; + - offset: 10 + - offset: !lambda return 10; + - or: + - quantile: + window_size: 7 + send_every: 4 + send_first_at: 3 + quantile: .9 + - round: 1 + - round_to_multiple_of: 0.25 + - skip_initial: 3 + - sliding_window_moving_average: + window_size: 15 + send_every: 15 + - throttle: 1s + - throttle_average: 2s + - throttle_with_priority: 5s + - throttle_with_priority: + timeout: 3s + value: 42.0 + - throttle_with_priority: + timeout: 3s + value: !lambda return 1.0f / 2.0f; + - throttle_with_priority: + timeout: 3s + value: + - 42.0 + - !lambda return 2.0f / 2.0f; + - nan + - timeout: + timeout: 10s + value: !lambda return 10; + - timeout: + timeout: 1h + value: 20.0 + - timeout: + timeout: 1min + value: last + - timeout: + timeout: 1d + - to_ntc_resistance: + calibration: + - 10.0kOhm -> 25°C + - 27.219kOhm -> 0°C + - 14.674kOhm -> 15°C + - to_ntc_temperature: + calibration: + - 10.0kOhm -> 25°C + - 27.219kOhm -> 0°C + - 14.674kOhm -> 15°C + +output: + - platform: template + id: outputsplit + type: float + write_action: + - logger.log: "write_action" + +switch: + - platform: template + id: test_switch + name: "Template Switch" + lambda: |- + if (id(some_binary_sensor).state) { + return true; + } + return false; + turn_on_action: + - logger.log: "turn_on_action" + turn_off_action: + - logger.log: "turn_off_action" + +button: + - platform: template + name: "Template Button" + on_press: + - logger.log: Button Pressed + +cover: + - platform: template + name: "Template Cover" + lambda: |- + if (id(some_binary_sensor).state) { + return COVER_OPEN; + } + return COVER_CLOSED; + open_action: + - logger.log: open_action + close_action: + - logger.log: close_action + stop_action: + - logger.log: stop_action + optimistic: true + +number: + - platform: template + id: template_number + name: "Template number" + optimistic: true + min_value: 0 + max_value: 100 + step: 1 + +select: + - platform: template + name: "Template select" + optimistic: true + options: + - one + - two + - three + initial_option: two + +lock: + - platform: template + name: "Template Lock" + lambda: |- + if (id(some_binary_sensor).state) { + return LOCK_STATE_LOCKED; + } + return LOCK_STATE_UNLOCKED; + lock_action: + - logger.log: lock_action + unlock_action: + - logger.log: unlock_action + open_action: + - logger.log: open_action + +valve: + - platform: template + id: template_valve + name: "Template Valve" + lambda: |- + if (id(some_binary_sensor).state) { + return VALVE_OPEN; + } + return VALVE_CLOSED; + open_action: + - logger.log: open_action + close_action: + - logger.log: close_action + - valve.template.publish: + id: template_valve + state: CLOSED + stop_action: + - logger.log: stop_action + optimistic: true + +text: + - platform: template + name: "Template text" + optimistic: true + min_length: 0 + max_length: 100 + mode: text + - platform: template + name: "Template text lambda" + mode: text + update_interval: 1s + lambda: | + return std::string{"Hello!"}; + set_action: + then: + - logger.log: + format: Template Text set to %s + args: ["x.c_str()"] + +alarm_control_panel: + - platform: template + name: Alarm Panel + codes: + - "1234" + +datetime: + - platform: template + name: Date + id: test_date + type: date + initial_value: "2000-1-2" + set_action: + - logger.log: "set_value" + on_value: + - logger.log: + format: "Date: %04d-%02d-%02d" + args: + - x.year + - x.month + - x.day_of_month + - platform: template + name: Time + id: test_time + type: time + initial_value: "12:34:56am" + set_action: + - logger.log: "set_value" + on_value: + - logger.log: + format: "Time: %02d:%02d:%02d" + args: + - x.hour + - x.minute + - x.second + - platform: template + name: DateTime + id: test_datetime + type: datetime + initial_value: "2000-1-2 12:34:56" + set_action: + - logger.log: "set_value" + on_value: + - logger.log: + format: "DateTime: %04d-%02d-%02d %02d:%02d:%02d" + args: + - x.year + - x.month + - x.day_of_month + - x.hour + - x.minute + - x.second diff --git a/tests/components/template/common.yaml b/tests/components/template/common.yaml index 6b7c7ddea1..d06f3ce131 100644 --- a/tests/components/template/common.yaml +++ b/tests/components/template/common.yaml @@ -1,343 +1,8 @@ -esphome: - on_boot: - - sensor.template.publish: - id: template_sens - state: 42.0 - - # Templated - - sensor.template.publish: - id: template_sens - state: !lambda "return 42.0;" - - - datetime.date.set: - id: test_date - date: - year: 2021 - month: 1 - day: 1 - - datetime.date.set: - id: test_date - date: !lambda "return {.day_of_month = 1, .month = 1, .year = 2021};" - - datetime.date.set: - id: test_date - date: "2021-01-01" - -binary_sensor: - - platform: template - id: some_binary_sensor - name: "Garage Door Open" - lambda: |- - if (id(template_sens).state > 30) { - // Garage Door is open. - return true; - } else { - // Garage Door is closed. - return false; - } - - platform: template - id: other_binary_sensor - name: "Garage Door Closed" - condition: - sensor.in_range: - id: template_sens - below: 30.0 - filters: - - invert: - - delayed_on: 100ms - - delayed_off: 100ms - - delayed_on_off: !lambda "if (id(test_switch).state) return 1000; else return 0;" - - delayed_on_off: - time_on: 10s - time_off: !lambda "if (id(test_switch).state) return 1000; else return 0;" - - autorepeat: - - delay: 1s - time_off: 100ms - time_on: 900ms - - delay: 5s - time_off: 100ms - time_on: 400ms - - lambda: |- - if (id(other_binary_sensor).state) { - return x; - } else { - return {}; - } - - settle: 500ms - - timeout: 5s - -sensor: - - platform: template - name: "Template Sensor" - id: template_sens - lambda: |- - if (id(some_binary_sensor).state) { - return 42.0; - } else { - return 0.0; - } - update_interval: 60s - filters: - - calibrate_linear: - - 0.0 -> 0.0 - - 40.0 -> 45.0 - - 100.0 -> 102.5 - - calibrate_polynomial: - degree: 2 - datapoints: - # Map 0.0 (from sensor) to 0.0 (true value) - - 0.0 -> 0.0 - - 10.0 -> 12.1 - - 13.0 -> 14.0 - - clamp: - max_value: 10.0 - min_value: -10.0 - - debounce: 0.1s - - delta: 5.0 - - exponential_moving_average: - alpha: 0.1 - send_every: 15 - - filter_out: - - 10 - - 20 - - !lambda return 10; - - filter_out: 10 - - filter_out: !lambda return NAN; - - heartbeat: 5s - - lambda: return x * (9.0/5.0) + 32.0; - - max: - window_size: 10 - send_every: 2 - send_first_at: 1 - - median: - window_size: 7 - send_every: 4 - send_first_at: 3 - - min: - window_size: 10 - send_every: 2 - send_first_at: 1 - - multiply: 1 - - multiply: !lambda return 2; - - offset: 10 - - offset: !lambda return 10; - - or: - - quantile: - window_size: 7 - send_every: 4 - send_first_at: 3 - quantile: .9 - - round: 1 - - round_to_multiple_of: 0.25 - - skip_initial: 3 - - sliding_window_moving_average: - window_size: 15 - send_every: 15 - - throttle: 1s - - throttle_average: 2s - - throttle_with_priority: 5s - - throttle_with_priority: - timeout: 3s - value: 42.0 - - throttle_with_priority: - timeout: 3s - value: !lambda return 1.0f / 2.0f; - - throttle_with_priority: - timeout: 3s - value: - - 42.0 - - !lambda return 2.0f / 2.0f; - - nan - - timeout: - timeout: 10s - value: !lambda return 10; - - timeout: - timeout: 1h - value: 20.0 - - timeout: - timeout: 1d - - to_ntc_resistance: - calibration: - - 10.0kOhm -> 25°C - - 27.219kOhm -> 0°C - - 14.674kOhm -> 15°C - - to_ntc_temperature: - calibration: - - 10.0kOhm -> 25°C - - 27.219kOhm -> 0°C - - 14.674kOhm -> 15°C - -output: - - platform: template - id: outputsplit - type: float - write_action: - - logger.log: "write_action" - -switch: - - platform: template - id: test_switch - name: "Template Switch" - lambda: |- - if (id(some_binary_sensor).state) { - return true; - } else { - return false; - } - turn_on_action: - - logger.log: "turn_on_action" - turn_off_action: - - logger.log: "turn_off_action" - -button: - - platform: template - name: "Template Button" - on_press: - - logger.log: Button Pressed - -cover: - - platform: template - name: "Template Cover" - lambda: |- - if (id(some_binary_sensor).state) { - return COVER_OPEN; - } else { - return COVER_CLOSED; - } - open_action: - - logger.log: open_action - close_action: - - logger.log: close_action - stop_action: - - logger.log: stop_action - optimistic: true - -number: - - platform: template - name: "Template number" - optimistic: true - min_value: 0 - max_value: 100 - step: 1 - -select: - - platform: template - name: "Template select" - optimistic: true - options: - - one - - two - - three - initial_option: two - -lock: - - platform: template - name: "Template Lock" - lambda: |- - if (id(some_binary_sensor).state) { - return LOCK_STATE_LOCKED; - } else { - return LOCK_STATE_UNLOCKED; - } - lock_action: - - logger.log: lock_action - unlock_action: - - logger.log: unlock_action - open_action: - - logger.log: open_action - -valve: - - platform: template - name: "Template Valve" - lambda: |- - if (id(some_binary_sensor).state) { - return VALVE_OPEN; - } else { - return VALVE_CLOSED; - } - open_action: - - logger.log: open_action - close_action: - - logger.log: close_action - - valve.template.publish: - state: CLOSED - stop_action: - - logger.log: stop_action - optimistic: true - -text: - - platform: template - name: "Template text" - optimistic: true - min_length: 0 - max_length: 100 - mode: text - - platform: template - name: "Template text lambda" - mode: text - update_interval: 1s - lambda: | - return std::string{"Hello!"}; - set_action: - then: - - logger.log: - format: Template Text set to %s - args: ["x.c_str()"] - -alarm_control_panel: - - platform: template - name: Alarm Panel - codes: - - "1234" - -datetime: - - platform: template - name: Date - id: test_date - type: date - initial_value: "2000-1-2" - set_action: - - logger.log: "set_value" - on_value: - - logger.log: - format: "Date: %04d-%02d-%02d" - args: - - x.year - - x.month - - x.day_of_month - - platform: template - name: Time - id: test_time - type: time - initial_value: "12:34:56am" - set_action: - - logger.log: "set_value" - on_value: - - logger.log: - format: "Time: %02d:%02d:%02d" - args: - - x.hour - - x.minute - - x.second - - platform: template - name: DateTime - id: test_datetime - type: datetime - initial_value: "2000-1-2 12:34:56" - set_action: - - logger.log: "set_value" - on_value: - - logger.log: - format: "DateTime: %04d-%02d-%02d %02d:%02d:%02d" - args: - - x.year - - x.month - - x.day_of_month - - x.hour - - x.minute - - x.second +<<: !include common-base.yaml time: - platform: sntp # Required for datetime + id: sntp_time wifi: # Required for sntp time ap: diff --git a/tests/components/template/test.esp32-ard.yaml b/tests/components/template/test.esp32-ard.yaml deleted file mode 100644 index 25cb37a0b4..0000000000 --- a/tests/components/template/test.esp32-ard.yaml +++ /dev/null @@ -1,2 +0,0 @@ -packages: - common: !include common.yaml diff --git a/tests/components/template/test.esp32-c3-ard.yaml b/tests/components/template/test.esp32-c3-ard.yaml deleted file mode 100644 index 25cb37a0b4..0000000000 --- a/tests/components/template/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,2 +0,0 @@ -packages: - common: !include common.yaml diff --git a/tests/components/template/test.esp32-c3-idf.yaml b/tests/components/template/test.esp32-c3-idf.yaml deleted file mode 100644 index 25cb37a0b4..0000000000 --- a/tests/components/template/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,2 +0,0 @@ -packages: - common: !include common.yaml diff --git a/tests/components/template/test.esp32-s3-idf.yaml b/tests/components/template/test.esp32-s3-idf.yaml deleted file mode 100644 index 25cb37a0b4..0000000000 --- a/tests/components/template/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,2 +0,0 @@ -packages: - common: !include common.yaml diff --git a/tests/components/template/test.nrf52-adafruit.yaml b/tests/components/template/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..45751c6398 --- /dev/null +++ b/tests/components/template/test.nrf52-adafruit.yaml @@ -0,0 +1 @@ +<<: !include common-base.yaml diff --git a/tests/components/template/test.nrf52-mcumgr.yaml b/tests/components/template/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..45751c6398 --- /dev/null +++ b/tests/components/template/test.nrf52-mcumgr.yaml @@ -0,0 +1 @@ +<<: !include common-base.yaml diff --git a/tests/components/text_sensor/common.yaml b/tests/components/text_sensor/common.yaml new file mode 100644 index 0000000000..4459c0fa44 --- /dev/null +++ b/tests/components/text_sensor/common.yaml @@ -0,0 +1,66 @@ +text_sensor: + - platform: template + name: "Test Substitute Single" + id: test_substitute_single + filters: + - substitute: + - ERROR -> Error + + - platform: template + name: "Test Substitute Multiple" + id: test_substitute_multiple + filters: + - substitute: + - ERROR -> Error + - WARN -> Warning + - INFO -> Information + - DEBUG -> Debug + + - platform: template + name: "Test Substitute Chained" + id: test_substitute_chained + filters: + - substitute: + - foo -> bar + - to_upper + - substitute: + - BAR -> baz + + - platform: template + name: "Test Map Single" + id: test_map_single + filters: + - map: + - ON -> Active + + - platform: template + name: "Test Map Multiple" + id: test_map_multiple + filters: + - map: + - ON -> Active + - OFF -> Inactive + - UNKNOWN -> Error + - IDLE -> Standby + + - platform: template + name: "Test Map Passthrough" + id: test_map_passthrough + filters: + - map: + - Good -> Excellent + - Bad -> Poor + + - platform: template + name: "Test All Filters" + id: test_all_filters + filters: + - to_upper + - to_lower + - append: " suffix" + - prepend: "prefix " + - substitute: + - prefix -> PREFIX + - suffix -> SUFFIX + - map: + - PREFIX text SUFFIX -> mapped diff --git a/tests/components/button/test.esp32-ard.yaml b/tests/components/text_sensor/test.esp8266-ard.yaml similarity index 100% rename from tests/components/button/test.esp32-ard.yaml rename to tests/components/text_sensor/test.esp8266-ard.yaml diff --git a/tests/components/thermopro_ble/common.yaml b/tests/components/thermopro_ble/common.yaml new file mode 100644 index 0000000000..297725e1c3 --- /dev/null +++ b/tests/components/thermopro_ble/common.yaml @@ -0,0 +1,13 @@ +esp32_ble_tracker: + +sensor: + - platform: thermopro_ble + mac_address: FE:74:B8:6A:97:B7 + temperature: + name: "ThermoPro Temperature" + humidity: + name: "ThermoPro Humidity" + battery_level: + name: "ThermoPro Battery Level" + signal_strength: + name: "ThermoPro Signal Strength" diff --git a/tests/components/thermopro_ble/test.esp32-idf.yaml b/tests/components/thermopro_ble/test.esp32-idf.yaml new file mode 100644 index 0000000000..7a6541ae76 --- /dev/null +++ b/tests/components/thermopro_ble/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/thermostat/common.yaml b/tests/components/thermostat/common.yaml index d630a93efc..4aa87c0ac3 100644 --- a/tests/components/thermostat/common.yaml +++ b/tests/components/thermostat/common.yaml @@ -69,6 +69,11 @@ climate: - logger.log: swing_vertical_action swing_both_action: - logger.log: swing_both_action + humidity_control_humidify_action: + - logger.log: humidity_control_humidify_action + humidity_control_off_action: + - logger.log: humidity_control_off_action + humidity_hysteresis: 1.0 startup_delay: true supplemental_cooling_delta: 2.0 cool_deadband: 0.5 diff --git a/tests/components/thermostat/test.esp32-ard.yaml b/tests/components/thermostat/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/thermostat/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/thermostat/test.esp32-c3-ard.yaml b/tests/components/thermostat/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/thermostat/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/thermostat/test.esp32-c3-idf.yaml b/tests/components/thermostat/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/thermostat/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/button/test.esp32-c3-ard.yaml b/tests/components/thermostat/test.nrf52-adafruit.yaml similarity index 100% rename from tests/components/button/test.esp32-c3-ard.yaml rename to tests/components/thermostat/test.nrf52-adafruit.yaml diff --git a/tests/components/button/test.esp32-c3-idf.yaml b/tests/components/thermostat/test.nrf52-mcumgr.yaml similarity index 100% rename from tests/components/button/test.esp32-c3-idf.yaml rename to tests/components/thermostat/test.nrf52-mcumgr.yaml diff --git a/tests/components/time/test.esp32-ard.yaml b/tests/components/time/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/time/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/time/test.esp32-c3-ard.yaml b/tests/components/time/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/time/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/time/test.esp32-c3-idf.yaml b/tests/components/time/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/time/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/time_based/test.esp32-ard.yaml b/tests/components/time_based/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/time_based/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/time_based/test.esp32-c3-ard.yaml b/tests/components/time_based/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/time_based/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/time_based/test.esp32-c3-idf.yaml b/tests/components/time_based/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/time_based/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/tinyusb/common.yaml b/tests/components/tinyusb/common.yaml new file mode 100644 index 0000000000..cb3f48836a --- /dev/null +++ b/tests/components/tinyusb/common.yaml @@ -0,0 +1,8 @@ +tinyusb: + id: tinyusb_test + usb_lang_id: 0x0123 + usb_manufacturer_str: ESPHomeTestManufacturer + usb_product_id: 0x1234 + usb_product_str: ESPHomeTestProduct + usb_serial_str: ESPHomeTestSerialNumber + usb_vendor_id: 0x2345 diff --git a/tests/components/camera/test.esp32-ard.yaml b/tests/components/tinyusb/test.esp32-p4-idf.yaml similarity index 100% rename from tests/components/camera/test.esp32-ard.yaml rename to tests/components/tinyusb/test.esp32-p4-idf.yaml diff --git a/tests/components/canbus/test.esp32-ard.yaml b/tests/components/tinyusb/test.esp32-s2-idf.yaml similarity index 100% rename from tests/components/canbus/test.esp32-ard.yaml rename to tests/components/tinyusb/test.esp32-s2-idf.yaml diff --git a/tests/components/debug/test.esp32-s3-idf.yaml b/tests/components/tinyusb/test.esp32-s3-idf.yaml similarity index 100% rename from tests/components/debug/test.esp32-s3-idf.yaml rename to tests/components/tinyusb/test.esp32-s3-idf.yaml diff --git a/tests/components/tlc59208f/common.yaml b/tests/components/tlc59208f/common.yaml index 49460dcefc..1943063347 100644 --- a/tests/components/tlc59208f/common.yaml +++ b/tests/components/tlc59208f/common.yaml @@ -1,15 +1,13 @@ -i2c: - - id: i2c_tlc59208f - scl: ${scl_pin} - sda: ${sda_pin} - tlc59208f: - address: 0x20 id: tlc59208f_1 + i2c_id: i2c_bus - address: 0x22 id: tlc59208f_2 + i2c_id: i2c_bus - address: 0x24 id: tlc59208f_3 + i2c_id: i2c_bus output: - platform: tlc59208f diff --git a/tests/components/tlc59208f/test.esp32-ard.yaml b/tests/components/tlc59208f/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/tlc59208f/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/tlc59208f/test.esp32-c3-ard.yaml b/tests/components/tlc59208f/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/tlc59208f/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/tlc59208f/test.esp32-c3-idf.yaml b/tests/components/tlc59208f/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/tlc59208f/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/tlc59208f/test.esp32-idf.yaml b/tests/components/tlc59208f/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/tlc59208f/test.esp32-idf.yaml +++ b/tests/components/tlc59208f/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/tlc59208f/test.esp8266-ard.yaml b/tests/components/tlc59208f/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/tlc59208f/test.esp8266-ard.yaml +++ b/tests/components/tlc59208f/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/tlc59208f/test.rp2040-ard.yaml b/tests/components/tlc59208f/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/tlc59208f/test.rp2040-ard.yaml +++ b/tests/components/tlc59208f/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/tlc5947/test.esp32-ard.yaml b/tests/components/tlc5947/test.esp32-ard.yaml deleted file mode 100644 index 52411bc1e9..0000000000 --- a/tests/components/tlc5947/test.esp32-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clock_pin: GPIO15 - data_pin: GPIO14 - lat_pin: GPIO13 - -packages: - common: !include common.yaml diff --git a/tests/components/tlc5947/test.esp32-c3-ard.yaml b/tests/components/tlc5947/test.esp32-c3-ard.yaml deleted file mode 100644 index 4694c43642..0000000000 --- a/tests/components/tlc5947/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clock_pin: GPIO5 - data_pin: GPIO4 - lat_pin: GPIO3 - -packages: - common: !include common.yaml diff --git a/tests/components/tlc5947/test.esp32-c3-idf.yaml b/tests/components/tlc5947/test.esp32-c3-idf.yaml deleted file mode 100644 index 4694c43642..0000000000 --- a/tests/components/tlc5947/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clock_pin: GPIO5 - data_pin: GPIO4 - lat_pin: GPIO3 - -packages: - common: !include common.yaml diff --git a/tests/components/tlc5947/test.esp8266-ard.yaml b/tests/components/tlc5947/test.esp8266-ard.yaml index 44da5a07b3..1eb9bcfae2 100644 --- a/tests/components/tlc5947/test.esp8266-ard.yaml +++ b/tests/components/tlc5947/test.esp8266-ard.yaml @@ -1,7 +1,7 @@ substitutions: - clock_pin: GPIO5 - data_pin: GPIO4 - lat_pin: GPIO13 + clock_pin: GPIO0 + data_pin: GPIO2 + lat_pin: GPIO15 packages: common: !include common.yaml diff --git a/tests/components/tlc5971/test.esp32-ard.yaml b/tests/components/tlc5971/test.esp32-ard.yaml deleted file mode 100644 index 52411bc1e9..0000000000 --- a/tests/components/tlc5971/test.esp32-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clock_pin: GPIO15 - data_pin: GPIO14 - lat_pin: GPIO13 - -packages: - common: !include common.yaml diff --git a/tests/components/tlc5971/test.esp32-c3-ard.yaml b/tests/components/tlc5971/test.esp32-c3-ard.yaml deleted file mode 100644 index d898a21d46..0000000000 --- a/tests/components/tlc5971/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - clock_pin: GPIO5 - data_pin: GPIO4 - -packages: - common: !include common.yaml diff --git a/tests/components/tlc5971/test.esp32-c3-idf.yaml b/tests/components/tlc5971/test.esp32-c3-idf.yaml deleted file mode 100644 index d898a21d46..0000000000 --- a/tests/components/tlc5971/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - clock_pin: GPIO5 - data_pin: GPIO4 - -packages: - common: !include common.yaml diff --git a/tests/components/tlc5971/test.esp32-s2-ard.yaml b/tests/components/tlc5971/test.esp32-s2-ard.yaml deleted file mode 100644 index 7bba0b0117..0000000000 --- a/tests/components/tlc5971/test.esp32-s2-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - clock_pin: GPIO36 - data_pin: GPIO35 - -packages: - common: !include common.yaml diff --git a/tests/components/tlc5971/test.esp8266-ard.yaml b/tests/components/tlc5971/test.esp8266-ard.yaml index 52411bc1e9..7923874f96 100644 --- a/tests/components/tlc5971/test.esp8266-ard.yaml +++ b/tests/components/tlc5971/test.esp8266-ard.yaml @@ -1,7 +1,7 @@ substitutions: clock_pin: GPIO15 - data_pin: GPIO14 - lat_pin: GPIO13 + data_pin: GPIO0 + lat_pin: GPIO2 packages: common: !include common.yaml diff --git a/tests/components/tm1621/test.esp32-ard.yaml b/tests/components/tm1621/test.esp32-ard.yaml deleted file mode 100644 index 0441e4bffe..0000000000 --- a/tests/components/tm1621/test.esp32-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - cs_pin: GPIO16 - data_pin: GPIO17 - read_pin: GPIO12 - write_pin: GPIO13 - -<<: !include common.yaml diff --git a/tests/components/tm1621/test.esp32-c3-ard.yaml b/tests/components/tm1621/test.esp32-c3-ard.yaml deleted file mode 100644 index 562ced7485..0000000000 --- a/tests/components/tm1621/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - cs_pin: GPIO6 - data_pin: GPIO7 - read_pin: GPIO2 - write_pin: GPIO3 - -<<: !include common.yaml diff --git a/tests/components/tm1621/test.esp32-c3-idf.yaml b/tests/components/tm1621/test.esp32-c3-idf.yaml deleted file mode 100644 index 562ced7485..0000000000 --- a/tests/components/tm1621/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - cs_pin: GPIO6 - data_pin: GPIO7 - read_pin: GPIO2 - write_pin: GPIO3 - -<<: !include common.yaml diff --git a/tests/components/tm1621/test.esp32-idf.yaml b/tests/components/tm1621/test.esp32-idf.yaml index 0441e4bffe..21402f3216 100644 --- a/tests/components/tm1621/test.esp32-idf.yaml +++ b/tests/components/tm1621/test.esp32-idf.yaml @@ -1,7 +1,10 @@ substitutions: - cs_pin: GPIO16 - data_pin: GPIO17 + cs_pin: GPIO4 + data_pin: GPIO5 read_pin: GPIO12 write_pin: GPIO13 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/tm1621/test.esp8266-ard.yaml b/tests/components/tm1621/test.esp8266-ard.yaml index ee7b62ce35..304ca71eb2 100644 --- a/tests/components/tm1621/test.esp8266-ard.yaml +++ b/tests/components/tm1621/test.esp8266-ard.yaml @@ -1,7 +1,10 @@ substitutions: cs_pin: GPIO15 - data_pin: GPIO14 - read_pin: GPIO12 - write_pin: GPIO13 + data_pin: GPIO0 + read_pin: GPIO2 + write_pin: GPIO16 + +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/tm1621/test.rp2040-ard.yaml b/tests/components/tm1621/test.rp2040-ard.yaml index 562ced7485..9a880da876 100644 --- a/tests/components/tm1621/test.rp2040-ard.yaml +++ b/tests/components/tm1621/test.rp2040-ard.yaml @@ -1,7 +1,10 @@ substitutions: cs_pin: GPIO6 data_pin: GPIO7 - read_pin: GPIO2 - write_pin: GPIO3 + read_pin: GPIO8 + write_pin: GPIO9 + +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/tm1637/test.esp32-ard.yaml b/tests/components/tm1637/test.esp32-ard.yaml deleted file mode 100644 index 2c5786c47c..0000000000 --- a/tests/components/tm1637/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - clk_pin: GPIO14 - dio_pin: GPIO13 - -<<: !include common.yaml diff --git a/tests/components/tm1637/test.esp32-c3-ard.yaml b/tests/components/tm1637/test.esp32-c3-ard.yaml deleted file mode 100644 index 96f6708a3b..0000000000 --- a/tests/components/tm1637/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - clk_pin: GPIO4 - dio_pin: GPIO3 - -<<: !include common.yaml diff --git a/tests/components/tm1637/test.esp32-c3-idf.yaml b/tests/components/tm1637/test.esp32-c3-idf.yaml deleted file mode 100644 index 96f6708a3b..0000000000 --- a/tests/components/tm1637/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - clk_pin: GPIO4 - dio_pin: GPIO3 - -<<: !include common.yaml diff --git a/tests/components/tm1637/test.esp8266-ard.yaml b/tests/components/tm1637/test.esp8266-ard.yaml index 2c5786c47c..0a4221cbc0 100644 --- a/tests/components/tm1637/test.esp8266-ard.yaml +++ b/tests/components/tm1637/test.esp8266-ard.yaml @@ -1,5 +1,5 @@ substitutions: - clk_pin: GPIO14 - dio_pin: GPIO13 + clk_pin: GPIO0 + dio_pin: GPIO2 <<: !include common.yaml diff --git a/tests/components/tm1638/test.esp32-ard.yaml b/tests/components/tm1638/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/tm1638/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/tm1638/test.esp32-c3-ard.yaml b/tests/components/tm1638/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/tm1638/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/tm1638/test.esp32-c3-idf.yaml b/tests/components/tm1638/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/tm1638/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/tm1651/test.esp32-ard.yaml b/tests/components/tm1651/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/tm1651/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/tm1651/test.esp32-c3-ard.yaml b/tests/components/tm1651/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/tm1651/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/tm1651/test.esp32-c3-idf.yaml b/tests/components/tm1651/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/tm1651/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/tmp102/common.yaml b/tests/components/tmp102/common.yaml index afc4a27fad..049a8067da 100644 --- a/tests/components/tmp102/common.yaml +++ b/tests/components/tmp102/common.yaml @@ -1,8 +1,4 @@ -i2c: - - id: i2c_tmp102 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: tmp102 + i2c_id: i2c_bus name: TMP102 Temperature diff --git a/tests/components/tmp102/test.esp32-ard.yaml b/tests/components/tmp102/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/tmp102/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/tmp102/test.esp32-c3-ard.yaml b/tests/components/tmp102/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/tmp102/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/tmp102/test.esp32-c3-idf.yaml b/tests/components/tmp102/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/tmp102/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/tmp102/test.esp32-idf.yaml b/tests/components/tmp102/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/tmp102/test.esp32-idf.yaml +++ b/tests/components/tmp102/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/tmp102/test.esp8266-ard.yaml b/tests/components/tmp102/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/tmp102/test.esp8266-ard.yaml +++ b/tests/components/tmp102/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/tmp102/test.rp2040-ard.yaml b/tests/components/tmp102/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/tmp102/test.rp2040-ard.yaml +++ b/tests/components/tmp102/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/tmp1075/common.yaml b/tests/components/tmp1075/common.yaml index 4c4c6c6f35..90025f231e 100644 --- a/tests/components/tmp1075/common.yaml +++ b/tests/components/tmp1075/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_tmp1075 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: tmp1075 + i2c_id: i2c_bus name: Temperature TMP1075 conversion_rate: 27.5ms alert: diff --git a/tests/components/tmp1075/test.esp32-ard.yaml b/tests/components/tmp1075/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/tmp1075/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/tmp1075/test.esp32-c3-ard.yaml b/tests/components/tmp1075/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/tmp1075/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/tmp1075/test.esp32-c3-idf.yaml b/tests/components/tmp1075/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/tmp1075/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/tmp1075/test.esp32-idf.yaml b/tests/components/tmp1075/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/tmp1075/test.esp32-idf.yaml +++ b/tests/components/tmp1075/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/tmp1075/test.esp8266-ard.yaml b/tests/components/tmp1075/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/tmp1075/test.esp8266-ard.yaml +++ b/tests/components/tmp1075/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/tmp1075/test.rp2040-ard.yaml b/tests/components/tmp1075/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/tmp1075/test.rp2040-ard.yaml +++ b/tests/components/tmp1075/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/tmp117/common.yaml b/tests/components/tmp117/common.yaml index f4a5688933..58419c2134 100644 --- a/tests/components/tmp117/common.yaml +++ b/tests/components/tmp117/common.yaml @@ -1,9 +1,5 @@ -i2c: - - id: i2c_tmp117 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: tmp117 + i2c_id: i2c_bus name: TMP117 Temperature update_interval: 5s diff --git a/tests/components/tmp117/test.esp32-ard.yaml b/tests/components/tmp117/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/tmp117/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/tmp117/test.esp32-c3-ard.yaml b/tests/components/tmp117/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/tmp117/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/tmp117/test.esp32-c3-idf.yaml b/tests/components/tmp117/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/tmp117/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/tmp117/test.esp32-idf.yaml b/tests/components/tmp117/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/tmp117/test.esp32-idf.yaml +++ b/tests/components/tmp117/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/tmp117/test.esp8266-ard.yaml b/tests/components/tmp117/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/tmp117/test.esp8266-ard.yaml +++ b/tests/components/tmp117/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/tmp117/test.rp2040-ard.yaml b/tests/components/tmp117/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/tmp117/test.rp2040-ard.yaml +++ b/tests/components/tmp117/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/tof10120/common.yaml b/tests/components/tof10120/common.yaml index 67643323d9..b360a27248 100644 --- a/tests/components/tof10120/common.yaml +++ b/tests/components/tof10120/common.yaml @@ -1,9 +1,5 @@ -i2c: - - id: i2c_tof10120 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: tof10120 + i2c_id: i2c_bus name: Distance sensor update_interval: 5s diff --git a/tests/components/tof10120/test.esp32-ard.yaml b/tests/components/tof10120/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/tof10120/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/tof10120/test.esp32-c3-ard.yaml b/tests/components/tof10120/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/tof10120/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/tof10120/test.esp32-c3-idf.yaml b/tests/components/tof10120/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/tof10120/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/tof10120/test.esp32-idf.yaml b/tests/components/tof10120/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/tof10120/test.esp32-idf.yaml +++ b/tests/components/tof10120/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/tof10120/test.esp8266-ard.yaml b/tests/components/tof10120/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/tof10120/test.esp8266-ard.yaml +++ b/tests/components/tof10120/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/tof10120/test.rp2040-ard.yaml b/tests/components/tof10120/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/tof10120/test.rp2040-ard.yaml +++ b/tests/components/tof10120/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/tormatic/common.yaml b/tests/components/tormatic/common.yaml index 0f1b33ac12..712bf7569b 100644 --- a/tests/components/tormatic/common.yaml +++ b/tests/components/tormatic/common.yaml @@ -1,12 +1,5 @@ -uart: - - id: uart_tormatic - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 9600 - cover: - platform: tormatic - uart_id: uart_tormatic id: tormatic_garage_door name: Tormatic Garage Door open_duration: 15s diff --git a/tests/components/tormatic/test.esp32-ard.yaml b/tests/components/tormatic/test.esp32-ard.yaml deleted file mode 100644 index f486544afa..0000000000 --- a/tests/components/tormatic/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 - -<<: !include common.yaml diff --git a/tests/components/tormatic/test.esp32-c3-ard.yaml b/tests/components/tormatic/test.esp32-c3-ard.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/tormatic/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/tormatic/test.esp32-c3-idf.yaml b/tests/components/tormatic/test.esp32-c3-idf.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/tormatic/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/tormatic/test.esp32-idf.yaml b/tests/components/tormatic/test.esp32-idf.yaml index f486544afa..b415125e84 100644 --- a/tests/components/tormatic/test.esp32-idf.yaml +++ b/tests/components/tormatic/test.esp32-idf.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 + tx_pin: GPIO4 + rx_pin: GPIO5 + +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/tormatic/test.esp8266-ard.yaml b/tests/components/tormatic/test.esp8266-ard.yaml index b516342f3b..96ab4ef6ac 100644 --- a/tests/components/tormatic/test.esp8266-ard.yaml +++ b/tests/components/tormatic/test.esp8266-ard.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: GPIO0 + rx_pin: GPIO2 + +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/tormatic/test.rp2040-ard.yaml b/tests/components/tormatic/test.rp2040-ard.yaml index b516342f3b..b28f2b5e05 100644 --- a/tests/components/tormatic/test.rp2040-ard.yaml +++ b/tests/components/tormatic/test.rp2040-ard.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO4 rx_pin: GPIO5 +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/toshiba/common.yaml b/tests/components/toshiba/common.yaml index 79a833980e..ba96c0628d 100644 --- a/tests/components/toshiba/common.yaml +++ b/tests/components/toshiba/common.yaml @@ -1,7 +1,4 @@ -remote_transmitter: - pin: ${pin} - carrier_duty_percent: 50% - climate: - platform: toshiba name: Toshiba Climate + transmitter_id: xmitr diff --git a/tests/components/toshiba/common_ras2819t.yaml b/tests/components/toshiba/common_ras2819t.yaml new file mode 100644 index 0000000000..157456ba81 --- /dev/null +++ b/tests/components/toshiba/common_ras2819t.yaml @@ -0,0 +1,5 @@ +climate: + - platform: toshiba + name: "RAS-2819T Climate" + model: RAS-2819T + receiver_id: rcvr diff --git a/tests/components/toshiba/test.esp32-ard.yaml b/tests/components/toshiba/test.esp32-ard.yaml deleted file mode 100644 index 7b012aa64c..0000000000 --- a/tests/components/toshiba/test.esp32-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/toshiba/test.esp32-c3-ard.yaml b/tests/components/toshiba/test.esp32-c3-ard.yaml deleted file mode 100644 index 7b012aa64c..0000000000 --- a/tests/components/toshiba/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/toshiba/test.esp32-c3-idf.yaml b/tests/components/toshiba/test.esp32-c3-idf.yaml deleted file mode 100644 index 7b012aa64c..0000000000 --- a/tests/components/toshiba/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/toshiba/test.esp32-idf.yaml b/tests/components/toshiba/test.esp32-idf.yaml index 7b012aa64c..e891f9dc85 100644 --- a/tests/components/toshiba/test.esp32-idf.yaml +++ b/tests/components/toshiba/test.esp32-idf.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO2 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/toshiba/test.esp8266-ard.yaml b/tests/components/toshiba/test.esp8266-ard.yaml index f5097fcf5f..4bed2f03e5 100644 --- a/tests/components/toshiba/test.esp8266-ard.yaml +++ b/tests/components/toshiba/test.esp8266-ard.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO5 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/toshiba/test_ras2819t.esp32-ard.yaml b/tests/components/toshiba/test_ras2819t.esp32-ard.yaml new file mode 100644 index 0000000000..d82ba54897 --- /dev/null +++ b/tests/components/toshiba/test_ras2819t.esp32-ard.yaml @@ -0,0 +1,5 @@ +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-ard.yaml + remote_receiver: !include ../../test_build_components/common/remote_receiver/esp32-ard.yaml + +<<: !include common_ras2819t.yaml diff --git a/tests/components/toshiba/test_ras2819t.esp32-c3-ard.yaml b/tests/components/toshiba/test_ras2819t.esp32-c3-ard.yaml new file mode 100644 index 0000000000..6858dd587f --- /dev/null +++ b/tests/components/toshiba/test_ras2819t.esp32-c3-ard.yaml @@ -0,0 +1,5 @@ +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-ard.yaml + remote_receiver: !include ../../test_build_components/common/remote_receiver/esp32-c3-ard.yaml + +<<: !include common_ras2819t.yaml diff --git a/tests/components/toshiba/test_ras2819t.esp32-idf.yaml b/tests/components/toshiba/test_ras2819t.esp32-idf.yaml new file mode 100644 index 0000000000..3facc5bbb3 --- /dev/null +++ b/tests/components/toshiba/test_ras2819t.esp32-idf.yaml @@ -0,0 +1,5 @@ +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml + remote_receiver: !include ../../test_build_components/common/remote_receiver/esp32-idf.yaml + +<<: !include common_ras2819t.yaml diff --git a/tests/components/toshiba/test_ras2819t.esp8266-ard.yaml b/tests/components/toshiba/test_ras2819t.esp8266-ard.yaml new file mode 100644 index 0000000000..3976dcc739 --- /dev/null +++ b/tests/components/toshiba/test_ras2819t.esp8266-ard.yaml @@ -0,0 +1,5 @@ +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml + remote_receiver: !include ../../test_build_components/common/remote_receiver/esp8266-ard.yaml + +<<: !include common_ras2819t.yaml diff --git a/tests/components/total_daily_energy/common.yaml b/tests/components/total_daily_energy/common.yaml index ae4d30408b..dd7e648da6 100644 --- a/tests/components/total_daily_energy/common.yaml +++ b/tests/components/total_daily_energy/common.yaml @@ -17,10 +17,10 @@ sensor: name: HLW8012 Voltage power: name: HLW8012 Power - id: hlw8012_power + id: total_daily_energy_hlw8012_power energy: name: HLW8012 Energy - id: hlw8012_energy + id: total_daily_energy_hlw8012_energy update_interval: 15s current_resistor: 0.001 ohm voltage_divider: 2351 @@ -29,4 +29,4 @@ sensor: model: hlw8012 - platform: total_daily_energy name: HLW8012 Total Daily Energy - power_id: hlw8012_power + power_id: total_daily_energy_hlw8012_power diff --git a/tests/components/total_daily_energy/test.esp32-ard.yaml b/tests/components/total_daily_energy/test.esp32-ard.yaml deleted file mode 100644 index 8b42b21b54..0000000000 --- a/tests/components/total_daily_energy/test.esp32-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - sel_pin: GPIO12 - cf_pin: GPIO13 - cf1_pin: GPIO14 - -<<: !include common.yaml diff --git a/tests/components/total_daily_energy/test.esp32-c3-ard.yaml b/tests/components/total_daily_energy/test.esp32-c3-ard.yaml deleted file mode 100644 index 8b0d069ce2..0000000000 --- a/tests/components/total_daily_energy/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - sel_pin: GPIO2 - cf_pin: GPIO3 - cf1_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/total_daily_energy/test.esp32-c3-idf.yaml b/tests/components/total_daily_energy/test.esp32-c3-idf.yaml deleted file mode 100644 index 8b0d069ce2..0000000000 --- a/tests/components/total_daily_energy/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - sel_pin: GPIO2 - cf_pin: GPIO3 - cf1_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/total_daily_energy/test.esp8266-ard.yaml b/tests/components/total_daily_energy/test.esp8266-ard.yaml index 8b42b21b54..ec9c0e43dc 100644 --- a/tests/components/total_daily_energy/test.esp8266-ard.yaml +++ b/tests/components/total_daily_energy/test.esp8266-ard.yaml @@ -1,6 +1,6 @@ substitutions: - sel_pin: GPIO12 - cf_pin: GPIO13 - cf1_pin: GPIO14 + sel_pin: GPIO0 + cf_pin: GPIO2 + cf1_pin: GPIO15 <<: !include common.yaml diff --git a/tests/components/tsl2561/common.yaml b/tests/components/tsl2561/common.yaml index d2b4f75df3..132bdb890e 100644 --- a/tests/components/tsl2561/common.yaml +++ b/tests/components/tsl2561/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_tsl2561 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: tsl2561 + i2c_id: i2c_bus name: TSL2561 Ambient Light address: 0x39 is_cs_package: true diff --git a/tests/components/tsl2561/test.esp32-ard.yaml b/tests/components/tsl2561/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/tsl2561/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/tsl2561/test.esp32-c3-ard.yaml b/tests/components/tsl2561/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/tsl2561/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/tsl2561/test.esp32-c3-idf.yaml b/tests/components/tsl2561/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/tsl2561/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/tsl2561/test.esp32-idf.yaml b/tests/components/tsl2561/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/tsl2561/test.esp32-idf.yaml +++ b/tests/components/tsl2561/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/tsl2561/test.esp8266-ard.yaml b/tests/components/tsl2561/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/tsl2561/test.esp8266-ard.yaml +++ b/tests/components/tsl2561/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/tsl2561/test.rp2040-ard.yaml b/tests/components/tsl2561/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/tsl2561/test.rp2040-ard.yaml +++ b/tests/components/tsl2561/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/tsl2591/common.yaml b/tests/components/tsl2591/common.yaml index d58c46fb48..93433cf968 100644 --- a/tests/components/tsl2591/common.yaml +++ b/tests/components/tsl2591/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_tsl2591 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: tsl2591 + i2c_id: i2c_bus id: test_tsl2591 address: 0x29 integration_time: 600ms diff --git a/tests/components/tsl2591/test.esp32-ard.yaml b/tests/components/tsl2591/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/tsl2591/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/tsl2591/test.esp32-c3-ard.yaml b/tests/components/tsl2591/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/tsl2591/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/tsl2591/test.esp32-c3-idf.yaml b/tests/components/tsl2591/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/tsl2591/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/tsl2591/test.esp32-idf.yaml b/tests/components/tsl2591/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/tsl2591/test.esp32-idf.yaml +++ b/tests/components/tsl2591/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/tsl2591/test.esp8266-ard.yaml b/tests/components/tsl2591/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/tsl2591/test.esp8266-ard.yaml +++ b/tests/components/tsl2591/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/tsl2591/test.rp2040-ard.yaml b/tests/components/tsl2591/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/tsl2591/test.rp2040-ard.yaml +++ b/tests/components/tsl2591/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/tt21100/common.yaml b/tests/components/tt21100/common.yaml index a5d7970429..56089aed1e 100644 --- a/tests/components/tt21100/common.yaml +++ b/tests/components/tt21100/common.yaml @@ -1,25 +1,24 @@ -i2c: - - id: i2c_tt21100 - scl: ${scl_pin} - sda: ${sda_pin} - display: - platform: ssd1306_i2c - id: ssd1306_display + i2c_id: i2c_bus + id: ssd1306_i2c_display model: SSD1306_128X64 reset_pin: ${disp_reset_pin} pages: - - id: page1 + - id: tt21100_page1 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); touchscreen: - platform: tt21100 - display: ssd1306_display + i2c_id: i2c_bus + id: tt21100_touchscreen + display: ssd1306_i2c_display interrupt_pin: ${interrupt_pin} reset_pin: ${reset_pin} binary_sensor: - platform: tt21100 + id: tt21100_button name: Home Button index: 1 diff --git a/tests/components/tt21100/test.esp32-ard.yaml b/tests/components/tt21100/test.esp32-ard.yaml deleted file mode 100644 index 05598719f9..0000000000 --- a/tests/components/tt21100/test.esp32-ard.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - disp_reset_pin: GPIO12 - scl_pin: GPIO13 - sda_pin: GPIO14 - interrupt_pin: GPIO15 - reset_pin: GPIO16 - -<<: !include common.yaml diff --git a/tests/components/tt21100/test.esp32-c3-ard.yaml b/tests/components/tt21100/test.esp32-c3-ard.yaml deleted file mode 100644 index 36a8ce2778..0000000000 --- a/tests/components/tt21100/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - disp_reset_pin: GPIO10 - scl_pin: GPIO0 - sda_pin: GPIO1 - interrupt_pin: GPIO2 - reset_pin: GPIO3 - -<<: !include common.yaml diff --git a/tests/components/tt21100/test.esp32-c3-idf.yaml b/tests/components/tt21100/test.esp32-c3-idf.yaml deleted file mode 100644 index 36a8ce2778..0000000000 --- a/tests/components/tt21100/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - disp_reset_pin: GPIO10 - scl_pin: GPIO0 - sda_pin: GPIO1 - interrupt_pin: GPIO2 - reset_pin: GPIO3 - -<<: !include common.yaml diff --git a/tests/components/tt21100/test.esp32-idf.yaml b/tests/components/tt21100/test.esp32-idf.yaml index 05598719f9..033aafb73c 100644 --- a/tests/components/tt21100/test.esp32-idf.yaml +++ b/tests/components/tt21100/test.esp32-idf.yaml @@ -1,8 +1,9 @@ substitutions: disp_reset_pin: GPIO12 - scl_pin: GPIO13 - sda_pin: GPIO14 interrupt_pin: GPIO15 - reset_pin: GPIO16 + reset_pin: GPIO4 + +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/tt21100/test.esp8266-ard.yaml b/tests/components/tt21100/test.esp8266-ard.yaml index 05598719f9..25d1ff82e3 100644 --- a/tests/components/tt21100/test.esp8266-ard.yaml +++ b/tests/components/tt21100/test.esp8266-ard.yaml @@ -1,8 +1,9 @@ substitutions: - disp_reset_pin: GPIO12 - scl_pin: GPIO13 - sda_pin: GPIO14 + disp_reset_pin: GPIO0 interrupt_pin: GPIO15 reset_pin: GPIO16 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/tt21100/test.rp2040-ard.yaml b/tests/components/tt21100/test.rp2040-ard.yaml index 36a8ce2778..0d13628294 100644 --- a/tests/components/tt21100/test.rp2040-ard.yaml +++ b/tests/components/tt21100/test.rp2040-ard.yaml @@ -1,8 +1,9 @@ substitutions: disp_reset_pin: GPIO10 - scl_pin: GPIO0 - sda_pin: GPIO1 interrupt_pin: GPIO2 reset_pin: GPIO3 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/ttp229_bsf/common.yaml b/tests/components/ttp229_bsf/common.yaml index 42c26a5d51..91134f5098 100644 --- a/tests/components/ttp229_bsf/common.yaml +++ b/tests/components/ttp229_bsf/common.yaml @@ -1,6 +1,6 @@ ttp229_bsf: - scl_pin: ${scl_pin} - sdo_pin: ${sdo_pin} + scl_pin: ${ttp229_scl_pin} + sdo_pin: ${ttp229_sdo_pin} binary_sensor: - platform: ttp229_bsf diff --git a/tests/components/ttp229_bsf/test.esp32-ard.yaml b/tests/components/ttp229_bsf/test.esp32-ard.yaml deleted file mode 100644 index 80ed75293f..0000000000 --- a/tests/components/ttp229_bsf/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sdo_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/ttp229_bsf/test.esp32-c3-ard.yaml b/tests/components/ttp229_bsf/test.esp32-c3-ard.yaml deleted file mode 100644 index 135b213edc..0000000000 --- a/tests/components/ttp229_bsf/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sdo_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ttp229_bsf/test.esp32-c3-idf.yaml b/tests/components/ttp229_bsf/test.esp32-c3-idf.yaml deleted file mode 100644 index 135b213edc..0000000000 --- a/tests/components/ttp229_bsf/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sdo_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ttp229_bsf/test.esp32-idf.yaml b/tests/components/ttp229_bsf/test.esp32-idf.yaml index 80ed75293f..8b7f6fe6ea 100644 --- a/tests/components/ttp229_bsf/test.esp32-idf.yaml +++ b/tests/components/ttp229_bsf/test.esp32-idf.yaml @@ -1,5 +1,8 @@ substitutions: - scl_pin: GPIO16 - sdo_pin: GPIO17 + ttp229_scl_pin: GPIO14 + ttp229_sdo_pin: GPIO5 + +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/ttp229_bsf/test.esp8266-ard.yaml b/tests/components/ttp229_bsf/test.esp8266-ard.yaml index 135b213edc..1832fde07f 100644 --- a/tests/components/ttp229_bsf/test.esp8266-ard.yaml +++ b/tests/components/ttp229_bsf/test.esp8266-ard.yaml @@ -1,5 +1,8 @@ substitutions: - scl_pin: GPIO5 - sdo_pin: GPIO4 + ttp229_scl_pin: GPIO0 + ttp229_sdo_pin: GPIO2 + +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/ttp229_bsf/test.rp2040-ard.yaml b/tests/components/ttp229_bsf/test.rp2040-ard.yaml index 135b213edc..1448c82347 100644 --- a/tests/components/ttp229_bsf/test.rp2040-ard.yaml +++ b/tests/components/ttp229_bsf/test.rp2040-ard.yaml @@ -1,5 +1,8 @@ substitutions: - scl_pin: GPIO5 - sdo_pin: GPIO4 + ttp229_scl_pin: GPIO2 + ttp229_sdo_pin: GPIO6 + +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/ttp229_lsf/common.yaml b/tests/components/ttp229_lsf/common.yaml index 5c0dbf9517..41402f5aac 100644 --- a/tests/components/ttp229_lsf/common.yaml +++ b/tests/components/ttp229_lsf/common.yaml @@ -1,9 +1,5 @@ -i2c: - - id: i2c_ttp229_lsf - scl: ${scl_pin} - sda: ${sda_pin} - ttp229_lsf: + i2c_id: i2c_bus binary_sensor: - platform: ttp229_lsf diff --git a/tests/components/ttp229_lsf/test.esp32-ard.yaml b/tests/components/ttp229_lsf/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/ttp229_lsf/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/ttp229_lsf/test.esp32-c3-ard.yaml b/tests/components/ttp229_lsf/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/ttp229_lsf/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ttp229_lsf/test.esp32-c3-idf.yaml b/tests/components/ttp229_lsf/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/ttp229_lsf/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ttp229_lsf/test.esp32-idf.yaml b/tests/components/ttp229_lsf/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/ttp229_lsf/test.esp32-idf.yaml +++ b/tests/components/ttp229_lsf/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/ttp229_lsf/test.esp8266-ard.yaml b/tests/components/ttp229_lsf/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/ttp229_lsf/test.esp8266-ard.yaml +++ b/tests/components/ttp229_lsf/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/ttp229_lsf/test.rp2040-ard.yaml b/tests/components/ttp229_lsf/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/ttp229_lsf/test.rp2040-ard.yaml +++ b/tests/components/ttp229_lsf/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/tuya/common.yaml b/tests/components/tuya/common.yaml index 2c40628139..e177b7d056 100644 --- a/tests/components/tuya/common.yaml +++ b/tests/components/tuya/common.yaml @@ -2,12 +2,6 @@ wifi: ssid: MySSID password: password1 -uart: - - id: uart_tuya - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 9600 - tuya: status_pin: number: ${status_pin} diff --git a/tests/components/tuya/test.esp32-ard.yaml b/tests/components/tuya/test.esp32-ard.yaml deleted file mode 100644 index 926a46cf73..0000000000 --- a/tests/components/tuya/test.esp32-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 - status_pin: GPIO12 - -<<: !include common.yaml diff --git a/tests/components/tuya/test.esp32-c3-ard.yaml b/tests/components/tuya/test.esp32-c3-ard.yaml deleted file mode 100644 index c62a0b10f6..0000000000 --- a/tests/components/tuya/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - status_pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/tuya/test.esp32-c3-idf.yaml b/tests/components/tuya/test.esp32-c3-idf.yaml deleted file mode 100644 index c62a0b10f6..0000000000 --- a/tests/components/tuya/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - status_pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/tuya/test.esp32-idf.yaml b/tests/components/tuya/test.esp32-idf.yaml index 926a46cf73..0baa48a6c5 100644 --- a/tests/components/tuya/test.esp32-idf.yaml +++ b/tests/components/tuya/test.esp32-idf.yaml @@ -1,6 +1,9 @@ substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 + tx_pin: GPIO4 + rx_pin: GPIO5 status_pin: GPIO12 +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/tuya/test.esp8266-ard.yaml b/tests/components/tuya/test.esp8266-ard.yaml index 11d46ed50e..1565495cf8 100644 --- a/tests/components/tuya/test.esp8266-ard.yaml +++ b/tests/components/tuya/test.esp8266-ard.yaml @@ -1,6 +1,9 @@ substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - status_pin: GPIO12 + tx_pin: GPIO0 + rx_pin: GPIO2 + status_pin: GPIO15 + +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/tuya/test.rp2040-ard.yaml b/tests/components/tuya/test.rp2040-ard.yaml index 11d46ed50e..0c3db54644 100644 --- a/tests/components/tuya/test.rp2040-ard.yaml +++ b/tests/components/tuya/test.rp2040-ard.yaml @@ -3,4 +3,7 @@ substitutions: rx_pin: GPIO5 status_pin: GPIO12 +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/tx20/test.esp32-ard.yaml b/tests/components/tx20/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/tx20/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/tx20/test.esp32-c3-ard.yaml b/tests/components/tx20/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/tx20/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/tx20/test.esp32-c3-idf.yaml b/tests/components/tx20/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/tx20/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/uart/common.h b/tests/components/uart/common.h new file mode 100644 index 0000000000..5597b86410 --- /dev/null +++ b/tests/components/uart/common.h @@ -0,0 +1,37 @@ +#pragma once +#include +#include +#include +#include +#include +#include "esphome/components/uart/uart_component.h" + +namespace esphome::uart::testing { + +using ::testing::_; +using ::testing::Return; +using ::testing::SaveArg; +using ::testing::DoAll; +using ::testing::Invoke; +using ::testing::SetArgPointee; + +// Derive a mock from UARTComponent to test the wrapper implementations. +class MockUARTComponent : public UARTComponent { + public: + using UARTComponent::write_array; + using UARTComponent::write_byte; + + // NOTE: std::vector is used here for test convenience. For production code, + // consider using StaticVector or FixedVector from esphome/core/helpers.h instead. + std::vector written_data; + + void write_array(const uint8_t *data, size_t len) override { written_data.assign(data, data + len); } + + MOCK_METHOD(bool, read_array, (uint8_t * data, size_t len), (override)); + MOCK_METHOD(bool, peek_byte, (uint8_t * data), (override)); + MOCK_METHOD(int, available, (), (override)); + MOCK_METHOD(void, flush, (), (override)); + MOCK_METHOD(void, check_logger_conflict, (), (override)); +}; + +} // namespace esphome::uart::testing diff --git a/tests/components/uart/test-uart_max_with_usb_cdc.esp32-c3-ard.yaml b/tests/components/uart/test-uart_max_with_usb_cdc.esp32-c3-ard.yaml deleted file mode 100644 index 2a73826c51..0000000000 --- a/tests/components/uart/test-uart_max_with_usb_cdc.esp32-c3-ard.yaml +++ /dev/null @@ -1,30 +0,0 @@ -<<: !include ../logger/common-usb_cdc.yaml - -esphome: - on_boot: - then: - - uart.write: - id: uart_1 - data: 'Hello World' - - uart.write: - id: uart_1 - data: [0x00, 0x20, 0x42] - -uart: - - id: uart_1 - tx_pin: 4 - rx_pin: 5 - baud_rate: 9600 - data_bits: 8 - rx_buffer_size: 512 - parity: EVEN - stop_bits: 2 - - - id: uart_2 - tx_pin: 6 - rx_pin: 7 - baud_rate: 9600 - data_bits: 8 - rx_buffer_size: 512 - parity: EVEN - stop_bits: 2 diff --git a/tests/components/uart/test-uart_max_with_usb_cdc.esp32-s2-ard.yaml b/tests/components/uart/test-uart_max_with_usb_cdc.esp32-s2-ard.yaml deleted file mode 100644 index 2a73826c51..0000000000 --- a/tests/components/uart/test-uart_max_with_usb_cdc.esp32-s2-ard.yaml +++ /dev/null @@ -1,30 +0,0 @@ -<<: !include ../logger/common-usb_cdc.yaml - -esphome: - on_boot: - then: - - uart.write: - id: uart_1 - data: 'Hello World' - - uart.write: - id: uart_1 - data: [0x00, 0x20, 0x42] - -uart: - - id: uart_1 - tx_pin: 4 - rx_pin: 5 - baud_rate: 9600 - data_bits: 8 - rx_buffer_size: 512 - parity: EVEN - stop_bits: 2 - - - id: uart_2 - tx_pin: 6 - rx_pin: 7 - baud_rate: 9600 - data_bits: 8 - rx_buffer_size: 512 - parity: EVEN - stop_bits: 2 diff --git a/tests/components/uart/test-uart_max_with_usb_cdc.esp32-s2-idf.yaml b/tests/components/uart/test-uart_max_with_usb_cdc.esp32-s2-idf.yaml index 2a73826c51..602766869c 100644 --- a/tests/components/uart/test-uart_max_with_usb_cdc.esp32-s2-idf.yaml +++ b/tests/components/uart/test-uart_max_with_usb_cdc.esp32-s2-idf.yaml @@ -14,17 +14,23 @@ uart: - id: uart_1 tx_pin: 4 rx_pin: 5 + flow_control_pin: 6 baud_rate: 9600 data_bits: 8 rx_buffer_size: 512 + rx_full_threshold: 10 + rx_timeout: 1 parity: EVEN stop_bits: 2 - id: uart_2 - tx_pin: 6 - rx_pin: 7 + tx_pin: 7 + rx_pin: 8 + flow_control_pin: 9 baud_rate: 9600 data_bits: 8 rx_buffer_size: 512 + rx_full_threshold: 10 + rx_timeout: 1 parity: EVEN stop_bits: 2 diff --git a/tests/components/uart/test-uart_max_with_usb_cdc.esp32-s3-ard.yaml b/tests/components/uart/test-uart_max_with_usb_cdc.esp32-s3-ard.yaml deleted file mode 100644 index 2a73826c51..0000000000 --- a/tests/components/uart/test-uart_max_with_usb_cdc.esp32-s3-ard.yaml +++ /dev/null @@ -1,30 +0,0 @@ -<<: !include ../logger/common-usb_cdc.yaml - -esphome: - on_boot: - then: - - uart.write: - id: uart_1 - data: 'Hello World' - - uart.write: - id: uart_1 - data: [0x00, 0x20, 0x42] - -uart: - - id: uart_1 - tx_pin: 4 - rx_pin: 5 - baud_rate: 9600 - data_bits: 8 - rx_buffer_size: 512 - parity: EVEN - stop_bits: 2 - - - id: uart_2 - tx_pin: 6 - rx_pin: 7 - baud_rate: 9600 - data_bits: 8 - rx_buffer_size: 512 - parity: EVEN - stop_bits: 2 diff --git a/tests/components/uart/test-uart_max_with_usb_serial_jtag.esp32-c3-idf.yaml b/tests/components/uart/test-uart_max_with_usb_serial_jtag.esp32-c3-idf.yaml index e0a07dde91..3151403896 100644 --- a/tests/components/uart/test-uart_max_with_usb_serial_jtag.esp32-c3-idf.yaml +++ b/tests/components/uart/test-uart_max_with_usb_serial_jtag.esp32-c3-idf.yaml @@ -14,17 +14,23 @@ uart: - id: uart_1 tx_pin: 4 rx_pin: 5 + flow_control_pin: 6 baud_rate: 9600 data_bits: 8 rx_buffer_size: 512 + rx_full_threshold: 10 + rx_timeout: 1 parity: EVEN stop_bits: 2 - id: uart_2 - tx_pin: 6 - rx_pin: 7 + tx_pin: 7 + rx_pin: 8 + flow_control_pin: 9 baud_rate: 9600 data_bits: 8 rx_buffer_size: 512 + rx_full_threshold: 10 + rx_timeout: 1 parity: EVEN stop_bits: 2 diff --git a/tests/components/uart/test-uart_max_with_usb_serial_jtag.esp32-s3-idf.yaml b/tests/components/uart/test-uart_max_with_usb_serial_jtag.esp32-s3-idf.yaml deleted file mode 100644 index e0a07dde91..0000000000 --- a/tests/components/uart/test-uart_max_with_usb_serial_jtag.esp32-s3-idf.yaml +++ /dev/null @@ -1,30 +0,0 @@ -<<: !include ../logger/common-usb_serial_jtag.yaml - -esphome: - on_boot: - then: - - uart.write: - id: uart_1 - data: 'Hello World' - - uart.write: - id: uart_1 - data: [0x00, 0x20, 0x42] - -uart: - - id: uart_1 - tx_pin: 4 - rx_pin: 5 - baud_rate: 9600 - data_bits: 8 - rx_buffer_size: 512 - parity: EVEN - stop_bits: 2 - - - id: uart_2 - tx_pin: 6 - rx_pin: 7 - baud_rate: 9600 - data_bits: 8 - rx_buffer_size: 512 - parity: EVEN - stop_bits: 2 diff --git a/tests/components/uart/test.esp32-ard.yaml b/tests/components/uart/test.esp32-ard.yaml deleted file mode 100644 index bef5b460ab..0000000000 --- a/tests/components/uart/test.esp32-ard.yaml +++ /dev/null @@ -1,15 +0,0 @@ -esphome: - on_boot: - then: - - uart.write: 'Hello World' - - uart.write: [0x00, 0x20, 0x42] - -uart: - - id: uart_uart - tx_pin: 17 - rx_pin: 16 - baud_rate: 9600 - data_bits: 8 - rx_buffer_size: 512 - parity: EVEN - stop_bits: 2 diff --git a/tests/components/uart/test.esp32-c3-ard.yaml b/tests/components/uart/test.esp32-c3-ard.yaml deleted file mode 100644 index 09178f1663..0000000000 --- a/tests/components/uart/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,15 +0,0 @@ -esphome: - on_boot: - then: - - uart.write: 'Hello World' - - uart.write: [0x00, 0x20, 0x42] - -uart: - - id: uart_uart - tx_pin: 4 - rx_pin: 5 - baud_rate: 9600 - data_bits: 8 - rx_buffer_size: 512 - parity: EVEN - stop_bits: 2 diff --git a/tests/components/uart/test.esp32-c3-idf.yaml b/tests/components/uart/test.esp32-c3-idf.yaml index 09178f1663..b053290a8b 100644 --- a/tests/components/uart/test.esp32-c3-idf.yaml +++ b/tests/components/uart/test.esp32-c3-idf.yaml @@ -8,8 +8,11 @@ uart: - id: uart_uart tx_pin: 4 rx_pin: 5 + flow_control_pin: 6 baud_rate: 9600 data_bits: 8 rx_buffer_size: 512 + rx_full_threshold: 10 + rx_timeout: 1 parity: EVEN stop_bits: 2 diff --git a/tests/components/uart/test.esp32-idf.yaml b/tests/components/uart/test.esp32-idf.yaml index 5a0ed7eba7..6ffd0d7282 100644 --- a/tests/components/uart/test.esp32-idf.yaml +++ b/tests/components/uart/test.esp32-idf.yaml @@ -3,16 +3,75 @@ esphome: then: - uart.write: 'Hello World' - uart.write: [0x00, 0x20, 0x42] + - uart.write: !lambda |- + return {0xAA, 0xBB, 0xCC}; uart: - id: uart_uart tx_pin: 17 rx_pin: 16 + flow_control_pin: 4 baud_rate: 9600 data_bits: 8 rx_buffer_size: 512 + rx_full_threshold: 10 + rx_timeout: 1 parity: EVEN stop_bits: 2 packet_transport: - platform: uart + +switch: + # Test uart switch with single state (array) + - platform: uart + name: "UART Switch Single Array" + uart_id: uart_uart + data: [0x01, 0x02, 0x03] + # Test uart switch with single state (string) + - platform: uart + name: "UART Switch Single String" + uart_id: uart_uart + data: "ON" + # Test uart switch with turn_on/turn_off (arrays) + - platform: uart + name: "UART Switch Dual Array" + uart_id: uart_uart + data: + turn_on: [0xA0, 0xA1, 0xA2] + turn_off: [0xB0, 0xB1, 0xB2] + # Test uart switch with turn_on/turn_off (strings) + - platform: uart + name: "UART Switch Dual String" + uart_id: uart_uart + data: + turn_on: "TURN_ON" + turn_off: "TURN_OFF" + +number: + - platform: template + name: "Test Number" + id: test_number + optimistic: true + min_value: 0 + max_value: 100 + step: 1 + +button: + # Test uart button with array data + - platform: uart + name: "UART Button Array" + uart_id: uart_uart + data: [0xFF, 0xEE, 0xDD] + # Test uart button with string data + - platform: uart + name: "UART Button String" + uart_id: uart_uart + data: "BUTTON_PRESS" + # Test uart button with lambda (function pointer) + - platform: template + name: "UART Lambda Test" + on_press: + - uart.write: !lambda |- + std::string cmd = "VALUE=" + str_sprintf("%.0f", id(test_number).state) + "\r\n"; + return std::vector(cmd.begin(), cmd.end()); diff --git a/tests/components/uart/test.esp8266-ard.yaml b/tests/components/uart/test.esp8266-ard.yaml index 09178f1663..566038ee3e 100644 --- a/tests/components/uart/test.esp8266-ard.yaml +++ b/tests/components/uart/test.esp8266-ard.yaml @@ -13,3 +13,21 @@ uart: rx_buffer_size: 512 parity: EVEN stop_bits: 2 + +switch: + - platform: uart + name: "UART Switch Array" + uart_id: uart_uart + data: [0x01, 0x02, 0x03] + - platform: uart + name: "UART Switch Dual" + uart_id: uart_uart + data: + turn_on: [0xA0, 0xA1] + turn_off: [0xB0, 0xB1] + +button: + - platform: uart + name: "UART Button" + uart_id: uart_uart + data: [0xFF, 0xEE] diff --git a/tests/components/uart/uart_component.cpp b/tests/components/uart/uart_component.cpp new file mode 100644 index 0000000000..2cab1f62ad --- /dev/null +++ b/tests/components/uart/uart_component.cpp @@ -0,0 +1,73 @@ +#include "common.h" + +namespace esphome::uart::testing { + +TEST(UARTComponentTest, SetGetBaudRate) { + MockUARTComponent mock; + mock.set_baud_rate(38400); + EXPECT_EQ(mock.get_baud_rate(), 38400); +} + +TEST(UARTComponentTest, SetGetStopBits) { + MockUARTComponent mock; + mock.set_stop_bits(2); + EXPECT_EQ(mock.get_stop_bits(), 2); +} + +TEST(UARTComponentTest, SetGetDataBits) { + MockUARTComponent mock; + mock.set_data_bits(7); + EXPECT_EQ(mock.get_data_bits(), 7); +} + +TEST(UARTComponentTest, SetGetParity) { + MockUARTComponent mock; + mock.set_parity(UARTParityOptions::UART_CONFIG_PARITY_EVEN); + EXPECT_EQ(mock.get_parity(), UARTParityOptions::UART_CONFIG_PARITY_EVEN); +} + +TEST(UARTComponentTest, SetGetRxBufferSize) { + MockUARTComponent mock; + mock.set_rx_buffer_size(128); + EXPECT_EQ(mock.get_rx_buffer_size(), 128); +} + +TEST(UARTComponentTest, WriteArrayVector) { + MockUARTComponent mock; + std::vector data = {10, 20, 30}; + mock.write_array(data); + EXPECT_EQ(mock.written_data, data); +} +TEST(UARTComponentTest, WriteByte) { + MockUARTComponent mock; + uint8_t byte = 0x79; + mock.write_byte(byte); + EXPECT_EQ(mock.written_data.size(), 1); + EXPECT_EQ(mock.written_data[0], byte); +} + +TEST(UARTComponentTest, WriteStr) { + MockUARTComponent mock; + const char *str = "Hello"; + std::vector captured; + mock.write_str(str); + EXPECT_EQ(mock.written_data.size(), strlen(str)); + EXPECT_EQ(0, strncmp(str, (const char *) mock.written_data.data(), mock.written_data.size())); +} + +// Tests for wrapper methods forwarding to pure virtual read_array +TEST(UARTComponentTest, ReadByteSuccess) { + MockUARTComponent mock; + uint8_t value = 0; + EXPECT_CALL(mock, read_array(&value, 1)).WillOnce(Return(true)); + EXPECT_TRUE(mock.read_byte(&value)); +} + +TEST(UARTComponentTest, ReadByteFailure) { + MockUARTComponent mock; + uint8_t value = 0xFF; + EXPECT_CALL(mock, read_array(&value, 1)).WillOnce(Return(false)); + EXPECT_FALSE(mock.read_byte(&value)); +} + +} // namespace esphome::uart::testing diff --git a/tests/components/uart/uart_device.cpp b/tests/components/uart/uart_device.cpp new file mode 100644 index 0000000000..c3f1d9078b --- /dev/null +++ b/tests/components/uart/uart_device.cpp @@ -0,0 +1,108 @@ +#include "common.h" +#include "esphome/components/uart/uart.h" + +namespace esphome::uart::testing { + +TEST(UARTDeviceTest, ReadByteSuccess) { + MockUARTComponent mock; + UARTDevice dev(&mock); + uint8_t value = 0; + EXPECT_CALL(mock, read_array(_, 1)).WillOnce(DoAll(SetArgPointee<0>(0x5A), Return(true))); + bool result = dev.read_byte(&value); + EXPECT_TRUE(result); + EXPECT_EQ(value, 0x5A); +} + +TEST(UARTDeviceTest, ReadByteFailure) { + MockUARTComponent mock; + UARTDevice dev(&mock); + uint8_t value = 0xFF; + EXPECT_CALL(mock, read_array(_, 1)).WillOnce(Return(false)); + bool result = dev.read_byte(&value); + EXPECT_FALSE(result); +} + +TEST(UARTDeviceTest, PeekByteSuccess) { + MockUARTComponent mock; + UARTDevice dev(&mock); + uint8_t value = 0; + EXPECT_CALL(mock, peek_byte(_)).WillOnce(DoAll(SetArgPointee<0>(0xA5), Return(true))); + bool result = dev.peek_byte(&value); + EXPECT_TRUE(result); + EXPECT_EQ(value, 0xA5); +} + +TEST(UARTDeviceTest, PeekByteFailure) { + MockUARTComponent mock; + UARTDevice dev(&mock); + uint8_t value = 0; + EXPECT_CALL(mock, peek_byte(_)).WillOnce(Return(false)); + bool result = dev.peek_byte(&value); + EXPECT_FALSE(result); +} + +TEST(UARTDeviceTest, Available) { + MockUARTComponent mock; + UARTDevice dev(&mock); + EXPECT_CALL(mock, available()).WillOnce(Return(5)); + EXPECT_EQ(dev.available(), 5); +} + +TEST(UARTDeviceTest, FlushCallsParent) { + MockUARTComponent mock; + UARTDevice dev(&mock); + EXPECT_CALL(mock, flush()).Times(1); + dev.flush(); +} + +TEST(UARTDeviceTest, WriteByteForwardsToWriteArray) { + MockUARTComponent mock; + UARTDevice dev(&mock); + dev.write_byte(0xAB); + EXPECT_EQ(mock.written_data.size(), 1); + EXPECT_EQ(mock.written_data[0], 0xAB); +} +TEST(UARTDeviceTest, WriteArrayPointer) { + MockUARTComponent mock; + UARTDevice dev(&mock); + uint8_t data[3] = {1, 2, 3}; + dev.write_array(data, 3); + EXPECT_EQ(mock.written_data.size(), 3); + EXPECT_EQ(mock.written_data, std::vector(data, data + 3)); +} + +TEST(UARTDeviceTest, WriteArrayVector) { + MockUARTComponent mock; + UARTDevice dev(&mock); + std::vector data = {4, 5, 6}; + dev.write_array(data); + EXPECT_EQ(mock.written_data, data); +} + +TEST(UARTDeviceTest, WriteArrayStdArray) { + MockUARTComponent mock; + UARTDevice dev(&mock); + std::array data = {7, 8, 9, 10}; + dev.write_array(data); + EXPECT_EQ(mock.written_data.size(), data.size()); + EXPECT_EQ(mock.written_data, std::vector(data.begin(), data.end())); +} + +TEST(UARTDeviceTest, WriteStrForwardsToWriteArray) { + MockUARTComponent mock; + UARTDevice dev(&mock); + const char *str = "ESPHome"; + dev.write_str(str); + EXPECT_EQ(mock.written_data.size(), strlen(str)); + EXPECT_EQ(0, strncmp(str, (const char *) mock.written_data.data(), mock.written_data.size())); +} + +TEST(UARTDeviceTest, WriteStrEmptyString) { + MockUARTComponent mock; + UARTDevice dev(&mock); + const char *str = ""; + dev.write_str(str); + EXPECT_EQ(mock.written_data.size(), 0); +} + +} // namespace esphome::uart::testing diff --git a/tests/components/udp/common.yaml b/tests/components/udp/common.yaml index 96224d0d1f..98546d49ef 100644 --- a/tests/components/udp/common.yaml +++ b/tests/components/udp/common.yaml @@ -17,3 +17,22 @@ udp: id: my_udp data: !lambda |- return std::vector{1,3,4,5,6}; + +number: + - platform: template + name: "UDP Number" + id: my_number + optimistic: true + min_value: 0 + max_value: 100 + step: 1 + +button: + - platform: template + name: "UDP Button" + on_press: + then: + - udp.write: + data: [0x01, 0x02, 0x03] + - udp.write: !lambda |- + return {0x10, 0x20, (uint8_t)id(my_number).state}; diff --git a/tests/components/udp/test.esp32-ard.yaml b/tests/components/udp/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/udp/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/udp/test.esp32-c3-ard.yaml b/tests/components/udp/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/udp/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/udp/test.esp32-c3-idf.yaml b/tests/components/udp/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/udp/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/udp/test.esp32-idf.yaml b/tests/components/udp/test.esp32-idf.yaml index dade44d145..b47e39c389 100644 --- a/tests/components/udp/test.esp32-idf.yaml +++ b/tests/components/udp/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/udp/test.esp8266-ard.yaml b/tests/components/udp/test.esp8266-ard.yaml index dade44d145..4a98b9388a 100644 --- a/tests/components/udp/test.esp8266-ard.yaml +++ b/tests/components/udp/test.esp8266-ard.yaml @@ -1 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/udp/test.host.yaml b/tests/components/udp/test.host.yaml index e735c37e4d..84e78894e5 100644 --- a/tests/components/udp/test.host.yaml +++ b/tests/components/udp/test.host.yaml @@ -1,4 +1,15 @@ -packages: - common: !include common.yaml - -wifi: !remove +udp: + id: my_udp + listen_address: 239.0.60.53 + addresses: ["239.0.60.53"] + on_receive: + - logger.log: + format: "Received %d bytes" + args: [data.size()] + - udp.write: + id: my_udp + data: "hello world" + - udp.write: + id: my_udp + data: !lambda |- + return std::vector{1,3,4,5,6}; diff --git a/tests/components/udp/test.rp2040-ard.yaml b/tests/components/udp/test.rp2040-ard.yaml index dade44d145..319a7c71a6 100644 --- a/tests/components/udp/test.rp2040-ard.yaml +++ b/tests/components/udp/test.rp2040-ard.yaml @@ -1 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/ufire_ec/common.yaml b/tests/components/ufire_ec/common.yaml index dcc957aaee..4260f0ab4c 100644 --- a/tests/components/ufire_ec/common.yaml +++ b/tests/components/ufire_ec/common.yaml @@ -7,16 +7,12 @@ esphome: temperature: !lambda "return id(test_sensor).state;" - ufire_ec.reset: -i2c: - - id: i2c_ufire_ec - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: template id: test_sensor lambda: "return 21;" - platform: ufire_ec + i2c_id: i2c_bus id: ufire_ec_board ec: name: Ufire EC diff --git a/tests/components/ufire_ec/test.esp32-ard.yaml b/tests/components/ufire_ec/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/ufire_ec/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/ufire_ec/test.esp32-c3-ard.yaml b/tests/components/ufire_ec/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/ufire_ec/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ufire_ec/test.esp32-c3-idf.yaml b/tests/components/ufire_ec/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/ufire_ec/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ufire_ec/test.esp32-idf.yaml b/tests/components/ufire_ec/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/ufire_ec/test.esp32-idf.yaml +++ b/tests/components/ufire_ec/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/ufire_ec/test.esp8266-ard.yaml b/tests/components/ufire_ec/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/ufire_ec/test.esp8266-ard.yaml +++ b/tests/components/ufire_ec/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/ufire_ec/test.rp2040-ard.yaml b/tests/components/ufire_ec/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/ufire_ec/test.rp2040-ard.yaml +++ b/tests/components/ufire_ec/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/ufire_ise/common.yaml b/tests/components/ufire_ise/common.yaml index d6ead8c479..f7865ea87b 100644 --- a/tests/components/ufire_ise/common.yaml +++ b/tests/components/ufire_ise/common.yaml @@ -9,16 +9,12 @@ esphome: solution: 4.0 - ufire_ise.reset: -i2c: - - id: i2c_ufire_ise - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: template id: test_sensor lambda: "return 21;" - platform: ufire_ise + i2c_id: i2c_bus id: ufire_ise_sensor temperature_sensor: test_sensor ph: diff --git a/tests/components/ufire_ise/test.esp32-ard.yaml b/tests/components/ufire_ise/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/ufire_ise/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/ufire_ise/test.esp32-c3-ard.yaml b/tests/components/ufire_ise/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/ufire_ise/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ufire_ise/test.esp32-c3-idf.yaml b/tests/components/ufire_ise/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/ufire_ise/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ufire_ise/test.esp32-idf.yaml b/tests/components/ufire_ise/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/ufire_ise/test.esp32-idf.yaml +++ b/tests/components/ufire_ise/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/ufire_ise/test.esp8266-ard.yaml b/tests/components/ufire_ise/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/ufire_ise/test.esp8266-ard.yaml +++ b/tests/components/ufire_ise/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/ufire_ise/test.rp2040-ard.yaml b/tests/components/ufire_ise/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/ufire_ise/test.rp2040-ard.yaml +++ b/tests/components/ufire_ise/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/uln2003/test.esp32-ard.yaml b/tests/components/uln2003/test.esp32-ard.yaml deleted file mode 100644 index ee4cff0923..0000000000 --- a/tests/components/uln2003/test.esp32-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - pin_a: GPIO12 - pin_b: GPIO13 - pin_c: GPIO14 - pin_d: GPIO15 - -<<: !include common.yaml diff --git a/tests/components/uln2003/test.esp32-c3-ard.yaml b/tests/components/uln2003/test.esp32-c3-ard.yaml deleted file mode 100644 index 11d16a4d5d..0000000000 --- a/tests/components/uln2003/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - pin_a: GPIO0 - pin_b: GPIO1 - pin_c: GPIO2 - pin_d: GPIO3 - -<<: !include common.yaml diff --git a/tests/components/uln2003/test.esp32-c3-idf.yaml b/tests/components/uln2003/test.esp32-c3-idf.yaml deleted file mode 100644 index 11d16a4d5d..0000000000 --- a/tests/components/uln2003/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - pin_a: GPIO0 - pin_b: GPIO1 - pin_c: GPIO2 - pin_d: GPIO3 - -<<: !include common.yaml diff --git a/tests/components/ultrasonic/test.esp32-ard.yaml b/tests/components/ultrasonic/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/ultrasonic/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/ultrasonic/test.esp32-c3-ard.yaml b/tests/components/ultrasonic/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/ultrasonic/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/ultrasonic/test.esp32-c3-idf.yaml b/tests/components/ultrasonic/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/ultrasonic/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/update/common.yaml b/tests/components/update/common.yaml index 45ed110352..521a0a6a5c 100644 --- a/tests/components/update/common.yaml +++ b/tests/components/update/common.yaml @@ -21,10 +21,12 @@ http_request: ota: - platform: http_request + id: update_http_request_ota update: - platform: http_request name: Firmware Update + ota_id: update_http_request_ota source: http://example.com/manifest.json on_update_available: - logger.log: "A new update is available" diff --git a/tests/components/update/test.esp32-ard.yaml b/tests/components/update/test.esp32-ard.yaml deleted file mode 100644 index c1937b5a10..0000000000 --- a/tests/components/update/test.esp32-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - verify_ssl: "false" - -<<: !include common.yaml diff --git a/tests/components/uponor_smatrix/common.yaml b/tests/components/uponor_smatrix/common.yaml index 8ee92bdfc5..7bb5e952ad 100644 --- a/tests/components/uponor_smatrix/common.yaml +++ b/tests/components/uponor_smatrix/common.yaml @@ -2,12 +2,6 @@ wifi: ssid: MySSID password: password1 -uart: - - id: uponor_uart - baud_rate: 19200 - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - time: - platform: sntp id: sntp_time @@ -17,19 +11,17 @@ time: - 192.168.178.1 uponor_smatrix: - uart_id: uponor_uart - address: 0x110B time_id: sntp_time - time_device_address: 0xDE13 + time_device_address: 0x110BDE13 climate: - platform: uponor_smatrix - address: 0xDE13 + address: 0x110BDE13 name: Thermostat Living Room sensor: - platform: uponor_smatrix - address: 0xDE13 + address: 0x110BDE13 humidity: name: Thermostat Humidity Living Room temperature: diff --git a/tests/components/uponor_smatrix/test.esp32-ard.yaml b/tests/components/uponor_smatrix/test.esp32-ard.yaml deleted file mode 100644 index f486544afa..0000000000 --- a/tests/components/uponor_smatrix/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 - -<<: !include common.yaml diff --git a/tests/components/uponor_smatrix/test.esp32-c3-ard.yaml b/tests/components/uponor_smatrix/test.esp32-c3-ard.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/uponor_smatrix/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/uponor_smatrix/test.esp32-c3-idf.yaml b/tests/components/uponor_smatrix/test.esp32-c3-idf.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/uponor_smatrix/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/uponor_smatrix/test.esp32-idf.yaml b/tests/components/uponor_smatrix/test.esp32-idf.yaml index f486544afa..76222997a8 100644 --- a/tests/components/uponor_smatrix/test.esp32-idf.yaml +++ b/tests/components/uponor_smatrix/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 +packages: + uart_19200: !include ../../test_build_components/common/uart_19200/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/uponor_smatrix/test.esp8266-ard.yaml b/tests/components/uponor_smatrix/test.esp8266-ard.yaml index b516342f3b..1f4483954b 100644 --- a/tests/components/uponor_smatrix/test.esp8266-ard.yaml +++ b/tests/components/uponor_smatrix/test.esp8266-ard.yaml @@ -1,5 +1,5 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 +packages: + uart_19200: !include ../../test_build_components/common/uart_19200/esp8266-ard.yaml + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/uponor_smatrix/test.rp2040-ard.yaml b/tests/components/uponor_smatrix/test.rp2040-ard.yaml index b516342f3b..65ba185aef 100644 --- a/tests/components/uponor_smatrix/test.rp2040-ard.yaml +++ b/tests/components/uponor_smatrix/test.rp2040-ard.yaml @@ -1,5 +1,5 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 +packages: + uart_19200: !include ../../test_build_components/common/uart_19200/rp2040-ard.yaml + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/uptime/common.yaml b/tests/components/uptime/common.yaml index 86b764e7ff..279258c670 100644 --- a/tests/components/uptime/common.yaml +++ b/tests/components/uptime/common.yaml @@ -3,6 +3,7 @@ wifi: time: - platform: sntp + id: sntp_time sensor: - platform: uptime diff --git a/tests/components/uptime/test.esp32-ard.yaml b/tests/components/uptime/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/uptime/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/uptime/test.esp32-c3-ard.yaml b/tests/components/uptime/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/uptime/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/uptime/test.esp32-c3-idf.yaml b/tests/components/uptime/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/uptime/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/usb_host/test.esp32-s3-idf.yaml b/tests/components/usb_host/test.esp32-s3-idf.yaml index a2892872e5..5360d1f6ff 100644 --- a/tests/components/usb_host/test.esp32-s3-idf.yaml +++ b/tests/components/usb_host/test.esp32-s3-idf.yaml @@ -1,4 +1,5 @@ usb_host: + max_transfer_requests: 32 # Test uint32_t bitmask path (17-32 requests) devices: - id: device_1 vid: 0x1234 diff --git a/tests/components/usb_uart/common.yaml b/tests/components/usb_uart/common.yaml index 46ad6291f9..474c3f5c8d 100644 --- a/tests/components/usb_uart/common.yaml +++ b/tests/components/usb_uart/common.yaml @@ -1,3 +1,6 @@ +usb_host: + max_transfer_requests: 32 + usb_uart: - id: uart_0 type: cdc_acm diff --git a/tests/components/vbus/common.yaml b/tests/components/vbus/common.yaml index a1f94cd839..33d9e2935d 100644 --- a/tests/components/vbus/common.yaml +++ b/tests/components/vbus/common.yaml @@ -1,9 +1,3 @@ -uart: - - id: uart_vbus - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 9600 - vbus: binary_sensor: diff --git a/tests/components/vbus/test.esp32-ard.yaml b/tests/components/vbus/test.esp32-ard.yaml deleted file mode 100644 index f486544afa..0000000000 --- a/tests/components/vbus/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 - -<<: !include common.yaml diff --git a/tests/components/vbus/test.esp32-c3-ard.yaml b/tests/components/vbus/test.esp32-c3-ard.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/vbus/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/vbus/test.esp32-c3-idf.yaml b/tests/components/vbus/test.esp32-c3-idf.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/vbus/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/vbus/test.esp32-idf.yaml b/tests/components/vbus/test.esp32-idf.yaml index f486544afa..2d29656c94 100644 --- a/tests/components/vbus/test.esp32-idf.yaml +++ b/tests/components/vbus/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/vbus/test.esp8266-ard.yaml b/tests/components/vbus/test.esp8266-ard.yaml index b516342f3b..5a05efa259 100644 --- a/tests/components/vbus/test.esp8266-ard.yaml +++ b/tests/components/vbus/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/vbus/test.rp2040-ard.yaml b/tests/components/vbus/test.rp2040-ard.yaml index b516342f3b..f1df2daf83 100644 --- a/tests/components/vbus/test.rp2040-ard.yaml +++ b/tests/components/vbus/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/veml3235/common.yaml b/tests/components/veml3235/common.yaml index b89a9e12c7..98ffb0729c 100644 --- a/tests/components/veml3235/common.yaml +++ b/tests/components/veml3235/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_veml3235 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: veml3235 + i2c_id: i2c_bus id: veml3235_sensor name: VEML3235 Light Sensor auto_gain: true diff --git a/tests/components/veml3235/test.esp32-ard.yaml b/tests/components/veml3235/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/veml3235/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/veml3235/test.esp32-c3-ard.yaml b/tests/components/veml3235/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/veml3235/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/veml3235/test.esp32-c3-idf.yaml b/tests/components/veml3235/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/veml3235/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/veml3235/test.esp32-idf.yaml b/tests/components/veml3235/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/veml3235/test.esp32-idf.yaml +++ b/tests/components/veml3235/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/veml3235/test.esp8266-ard.yaml b/tests/components/veml3235/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/veml3235/test.esp8266-ard.yaml +++ b/tests/components/veml3235/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/veml3235/test.rp2040-ard.yaml b/tests/components/veml3235/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/veml3235/test.rp2040-ard.yaml +++ b/tests/components/veml3235/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/veml7700/common.yaml b/tests/components/veml7700/common.yaml index af4ebee6e7..06c1d304c3 100644 --- a/tests/components/veml7700/common.yaml +++ b/tests/components/veml7700/common.yaml @@ -1,12 +1,7 @@ -i2c: - - id: i2c_veml7700 - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: veml7700 + i2c_id: i2c_bus address: 0x10 - i2c_id: i2c_veml7700 ambient_light: Ambient light ambient_light_counts: Ambient light counts full_spectrum: Full spectrum diff --git a/tests/components/veml7700/test.esp32-ard.yaml b/tests/components/veml7700/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/veml7700/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/veml7700/test.esp32-c3-ard.yaml b/tests/components/veml7700/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/veml7700/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/veml7700/test.esp32-c3-idf.yaml b/tests/components/veml7700/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/veml7700/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/veml7700/test.esp32-idf.yaml b/tests/components/veml7700/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/veml7700/test.esp32-idf.yaml +++ b/tests/components/veml7700/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/veml7700/test.esp8266-ard.yaml b/tests/components/veml7700/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/veml7700/test.esp8266-ard.yaml +++ b/tests/components/veml7700/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/veml7700/test.rp2040-ard.yaml b/tests/components/veml7700/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/veml7700/test.rp2040-ard.yaml +++ b/tests/components/veml7700/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/version/test.esp32-ard.yaml b/tests/components/version/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/version/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/version/test.esp32-c3-ard.yaml b/tests/components/version/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/version/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/version/test.esp32-c3-idf.yaml b/tests/components/version/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/version/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/canbus/test.esp32-c3-ard.yaml b/tests/components/version/test.nrf52-adafruit.yaml similarity index 100% rename from tests/components/canbus/test.esp32-c3-ard.yaml rename to tests/components/version/test.nrf52-adafruit.yaml diff --git a/tests/components/captive_portal/test.esp32-c3-ard.yaml b/tests/components/version/test.nrf52-mcumgr.yaml similarity index 100% rename from tests/components/captive_portal/test.esp32-c3-ard.yaml rename to tests/components/version/test.nrf52-mcumgr.yaml diff --git a/tests/components/vl53l0x/common.yaml b/tests/components/vl53l0x/common.yaml index 8346eae854..98277639cf 100644 --- a/tests/components/vl53l0x/common.yaml +++ b/tests/components/vl53l0x/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_vl53l0x - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: vl53l0x + i2c_id: i2c_bus name: VL53L0x Distance address: 0x29 enable_pin: 3 diff --git a/tests/components/vl53l0x/test.esp32-ard.yaml b/tests/components/vl53l0x/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/vl53l0x/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/vl53l0x/test.esp32-c3-ard.yaml b/tests/components/vl53l0x/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/vl53l0x/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/vl53l0x/test.esp32-c3-idf.yaml b/tests/components/vl53l0x/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/vl53l0x/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/vl53l0x/test.esp32-idf.yaml b/tests/components/vl53l0x/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/vl53l0x/test.esp32-idf.yaml +++ b/tests/components/vl53l0x/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/vl53l0x/test.esp8266-ard.yaml b/tests/components/vl53l0x/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/vl53l0x/test.esp8266-ard.yaml +++ b/tests/components/vl53l0x/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/vl53l0x/test.rp2040-ard.yaml b/tests/components/vl53l0x/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/vl53l0x/test.rp2040-ard.yaml +++ b/tests/components/vl53l0x/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/voice_assistant/common-idf.yaml b/tests/components/voice_assistant/common-idf.yaml index b1d249d5b4..ab8cbf2434 100644 --- a/tests/components/voice_assistant/common-idf.yaml +++ b/tests/components/voice_assistant/common-idf.yaml @@ -18,6 +18,7 @@ i2s_audio: micro_wake_word: id: mww_id + microphone: mic_id_external on_wake_word_detected: - voice_assistant.start: wake_word: !lambda return wake_word; diff --git a/tests/components/voice_assistant/test.esp32-ard.yaml b/tests/components/voice_assistant/test.esp32-ard.yaml deleted file mode 100644 index f6e553f9dc..0000000000 --- a/tests/components/voice_assistant/test.esp32-ard.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - i2s_lrclk_pin: GPIO16 - i2s_bclk_pin: GPIO17 - i2s_mclk_pin: GPIO15 - i2s_din_pin: GPIO13 - i2s_dout_pin: GPIO12 - -<<: !include common.yaml diff --git a/tests/components/voice_assistant/test.esp32-c3-ard.yaml b/tests/components/voice_assistant/test.esp32-c3-ard.yaml deleted file mode 100644 index f596d927cb..0000000000 --- a/tests/components/voice_assistant/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - i2s_lrclk_pin: GPIO6 - i2s_bclk_pin: GPIO7 - i2s_mclk_pin: GPIO5 - i2s_din_pin: GPIO3 - i2s_dout_pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/voice_assistant/test.esp32-c3-idf.yaml b/tests/components/voice_assistant/test.esp32-c3-idf.yaml deleted file mode 100644 index 46745e4308..0000000000 --- a/tests/components/voice_assistant/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - i2s_lrclk_pin: GPIO6 - i2s_bclk_pin: GPIO7 - i2s_mclk_pin: GPIO5 - i2s_din_pin: GPIO3 - i2s_dout_pin: GPIO2 - -<<: !include common-idf.yaml diff --git a/tests/components/voice_assistant/test.esp32-idf.yaml b/tests/components/voice_assistant/test.esp32-idf.yaml index 0fe5d347be..1c5c9ddf99 100644 --- a/tests/components/voice_assistant/test.esp32-idf.yaml +++ b/tests/components/voice_assistant/test.esp32-idf.yaml @@ -1,6 +1,6 @@ substitutions: - i2s_lrclk_pin: GPIO16 - i2s_bclk_pin: GPIO17 + i2s_lrclk_pin: GPIO4 + i2s_bclk_pin: GPIO5 i2s_mclk_pin: GPIO15 i2s_din_pin: GPIO13 i2s_dout_pin: GPIO12 diff --git a/tests/components/wake_on_lan/test.esp32-ard.yaml b/tests/components/wake_on_lan/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/wake_on_lan/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/wake_on_lan/test.esp32-c3-ard.yaml b/tests/components/wake_on_lan/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/wake_on_lan/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/wake_on_lan/test.esp32-c3-idf.yaml b/tests/components/wake_on_lan/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/wake_on_lan/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/wake_on_lan/test.esp32-idf.yaml b/tests/components/wake_on_lan/test.esp32-idf.yaml index dade44d145..b47e39c389 100644 --- a/tests/components/wake_on_lan/test.esp32-idf.yaml +++ b/tests/components/wake_on_lan/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/wake_on_lan/test.esp8266-ard.yaml b/tests/components/wake_on_lan/test.esp8266-ard.yaml index dade44d145..4a98b9388a 100644 --- a/tests/components/wake_on_lan/test.esp8266-ard.yaml +++ b/tests/components/wake_on_lan/test.esp8266-ard.yaml @@ -1 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/wake_on_lan/test.rp2040-ard.yaml b/tests/components/wake_on_lan/test.rp2040-ard.yaml index dade44d145..319a7c71a6 100644 --- a/tests/components/wake_on_lan/test.rp2040-ard.yaml +++ b/tests/components/wake_on_lan/test.rp2040-ard.yaml @@ -1 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/waveshare_epaper/common.yaml b/tests/components/waveshare_epaper/common.yaml index a2aa3134b5..b80a352c1f 100644 --- a/tests/components/waveshare_epaper/common.yaml +++ b/tests/components/waveshare_epaper/common.yaml @@ -1,14 +1,8 @@ -spi: - - id: spi_waveshare_epaper - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - display: # 1.54 inch displays - platform: waveshare_epaper id: epd_1_54 model: 1.54in - spi_id: spi_waveshare_epaper cs_pin: allow_other_uses: true number: ${cs_pin} @@ -28,7 +22,6 @@ display: - platform: waveshare_epaper id: epd_1_54v2 model: 1.54inv2 - spi_id: spi_waveshare_epaper cs_pin: allow_other_uses: true number: ${cs_pin} @@ -48,7 +41,6 @@ display: - platform: waveshare_epaper id: epd_1_54v2b model: 1.54inv2-b - spi_id: spi_waveshare_epaper cs_pin: allow_other_uses: true number: ${cs_pin} @@ -67,7 +59,6 @@ display: - platform: waveshare_epaper id: epd_1_54m09 model: 1.54in-m5coreink-m09 - spi_id: spi_waveshare_epaper cs_pin: allow_other_uses: true number: ${cs_pin} @@ -87,7 +78,6 @@ display: - platform: waveshare_epaper id: epd_2_13 model: 2.13in - spi_id: spi_waveshare_epaper cs_pin: allow_other_uses: true number: ${cs_pin} @@ -107,7 +97,6 @@ display: - platform: waveshare_epaper id: epd_2_13v2 model: 2.13inv2 - spi_id: spi_waveshare_epaper cs_pin: allow_other_uses: true number: ${cs_pin} @@ -127,7 +116,6 @@ display: - platform: waveshare_epaper id: epd_2_13ttgo model: 2.13in-ttgo - spi_id: spi_waveshare_epaper cs_pin: allow_other_uses: true number: ${cs_pin} @@ -147,7 +135,6 @@ display: - platform: waveshare_epaper id: epd_2_13ttgo_b1 model: 2.13in-ttgo-b1 - spi_id: spi_waveshare_epaper cs_pin: allow_other_uses: true number: ${cs_pin} @@ -167,7 +154,6 @@ display: - platform: waveshare_epaper id: epd_2_13ttgo_b73 model: 2.13in-ttgo-b73 - spi_id: spi_waveshare_epaper cs_pin: allow_other_uses: true number: ${cs_pin} @@ -187,7 +173,6 @@ display: - platform: waveshare_epaper id: epd_2_13ttgo_b74 model: 2.13in-ttgo-b74 - spi_id: spi_waveshare_epaper cs_pin: allow_other_uses: true number: ${cs_pin} @@ -207,7 +192,6 @@ display: - platform: waveshare_epaper id: epd_2_13dke model: 2.13in-ttgo-dke - spi_id: spi_waveshare_epaper cs_pin: allow_other_uses: true number: ${cs_pin} @@ -227,7 +211,6 @@ display: - platform: waveshare_epaper id: epd_2_13v3 model: 2.13inv3 - spi_id: spi_waveshare_epaper cs_pin: allow_other_uses: true number: ${cs_pin} @@ -248,7 +231,6 @@ display: - platform: waveshare_epaper id: epd_2_70 model: 2.70in - spi_id: spi_waveshare_epaper cs_pin: allow_other_uses: true number: ${cs_pin} @@ -267,7 +249,6 @@ display: - platform: waveshare_epaper id: epd_2_70b model: 2.70in-b - spi_id: spi_waveshare_epaper cs_pin: allow_other_uses: true number: ${cs_pin} @@ -286,7 +267,6 @@ display: - platform: waveshare_epaper id: epd_2_70bv2 model: 2.70in-bv2 - spi_id: spi_waveshare_epaper cs_pin: allow_other_uses: true number: ${cs_pin} @@ -305,7 +285,6 @@ display: - platform: waveshare_epaper id: epd_2_70v2 model: 2.70inv2 - spi_id: spi_waveshare_epaper cs_pin: allow_other_uses: true number: ${cs_pin} @@ -325,7 +304,6 @@ display: - platform: waveshare_epaper id: epd_2_90 model: 2.90in - spi_id: spi_waveshare_epaper cs_pin: allow_other_uses: true number: ${cs_pin} @@ -346,7 +324,6 @@ display: - platform: waveshare_epaper id: epd_2_90v2 model: 2.90inv2 - spi_id: spi_waveshare_epaper cs_pin: allow_other_uses: true number: ${cs_pin} @@ -367,7 +344,6 @@ display: - platform: waveshare_epaper id: epd_2_90b model: 2.90in-b - spi_id: spi_waveshare_epaper cs_pin: allow_other_uses: true number: ${cs_pin} @@ -386,7 +362,6 @@ display: - platform: waveshare_epaper id: epd_2_90bv3 model: 2.90in-bv3 - spi_id: spi_waveshare_epaper cs_pin: allow_other_uses: true number: ${cs_pin} @@ -405,7 +380,6 @@ display: - platform: waveshare_epaper id: epd_2_90v2r2 model: 2.90inv2-r2 - spi_id: spi_waveshare_epaper cs_pin: allow_other_uses: true number: ${cs_pin} @@ -425,7 +399,6 @@ display: - platform: waveshare_epaper id: epd_2_90dke model: 2.90in-dke - spi_id: spi_waveshare_epaper cs_pin: allow_other_uses: true number: ${cs_pin} @@ -446,7 +419,6 @@ display: - platform: waveshare_epaper id: epd_gdew029t5 model: gdew029t5 - spi_id: spi_waveshare_epaper cs_pin: allow_other_uses: true number: ${cs_pin} @@ -466,7 +438,6 @@ display: - platform: waveshare_epaper id: epd_gdew042t81 model: gdey042t81 - spi_id: spi_waveshare_epaper cs_pin: allow_other_uses: true number: ${cs_pin} @@ -487,7 +458,6 @@ display: - platform: waveshare_epaper id: epd_4_20 model: 4.20in - spi_id: spi_waveshare_epaper cs_pin: allow_other_uses: true number: ${cs_pin} @@ -506,7 +476,6 @@ display: - platform: waveshare_epaper id: epd_4_20bv2 model: 4.20in-bv2 - spi_id: spi_waveshare_epaper cs_pin: allow_other_uses: true number: ${cs_pin} @@ -525,7 +494,6 @@ display: - platform: waveshare_epaper id: epd_4_20in_bv2_bwr model: 4.20in-bv2-bwr - spi_id: spi_waveshare_epaper cs_pin: allow_other_uses: true number: ${cs_pin} @@ -545,7 +513,6 @@ display: - platform: waveshare_epaper id: epd_5_65 model: 5.65in-f - spi_id: spi_waveshare_epaper cs_pin: allow_other_uses: true number: ${cs_pin} @@ -565,7 +532,6 @@ display: - platform: waveshare_epaper id: epd_5_83 model: 5.83in - spi_id: spi_waveshare_epaper cs_pin: allow_other_uses: true number: ${cs_pin} @@ -584,7 +550,6 @@ display: - platform: waveshare_epaper id: epd_5_83v2 model: 5.83inv2 - spi_id: spi_waveshare_epaper cs_pin: allow_other_uses: true number: ${cs_pin} @@ -604,7 +569,6 @@ display: - platform: waveshare_epaper id: epd_7_50 model: 7.50in - spi_id: spi_waveshare_epaper cs_pin: allow_other_uses: true number: ${cs_pin} @@ -623,7 +587,6 @@ display: - platform: waveshare_epaper id: epd_7_50bv2 model: 7.50in-bv2 - spi_id: spi_waveshare_epaper cs_pin: allow_other_uses: true number: ${cs_pin} @@ -642,7 +605,6 @@ display: - platform: waveshare_epaper id: epd_7_50bv3 model: 7.50in-bv3 - spi_id: spi_waveshare_epaper cs_pin: allow_other_uses: true number: ${cs_pin} @@ -661,7 +623,6 @@ display: - platform: waveshare_epaper id: epd_7_50bv3_bwr model: 7.50in-bv3-bwr - spi_id: spi_waveshare_epaper cs_pin: allow_other_uses: true number: ${cs_pin} @@ -680,7 +641,6 @@ display: - platform: waveshare_epaper id: epd_7_50bc model: 7.50in-bc - spi_id: spi_waveshare_epaper cs_pin: allow_other_uses: true number: ${cs_pin} @@ -699,7 +659,6 @@ display: - platform: waveshare_epaper id: epd_7_50v2 model: 7.50inv2 - spi_id: spi_waveshare_epaper cs_pin: allow_other_uses: true number: ${cs_pin} @@ -718,7 +677,6 @@ display: - platform: waveshare_epaper id: epd_7_50v2alt model: 7.50inv2alt - spi_id: spi_waveshare_epaper cs_pin: allow_other_uses: true number: ${cs_pin} @@ -737,7 +695,6 @@ display: - platform: waveshare_epaper id: epd_7_50inv2p model: 7.50inv2p - spi_id: spi_waveshare_epaper cs_pin: allow_other_uses: true number: ${cs_pin} @@ -757,7 +714,6 @@ display: - platform: waveshare_epaper id: epd_7_50hdb model: 7.50in-hd-b - spi_id: spi_waveshare_epaper cs_pin: allow_other_uses: true number: ${cs_pin} @@ -777,7 +733,6 @@ display: - platform: waveshare_epaper id: epd_13_3k model: 13.3in-k - spi_id: spi_waveshare_epaper cs_pin: allow_other_uses: true number: ${cs_pin} @@ -795,7 +750,6 @@ display: - platform: waveshare_epaper model: 2.90in-d - spi_id: spi_waveshare_epaper cs_pin: allow_other_uses: true number: ${cs_pin} @@ -814,7 +768,6 @@ display: - platform: waveshare_epaper model: 2.90in - spi_id: spi_waveshare_epaper cs_pin: allow_other_uses: true number: ${cs_pin} @@ -834,7 +787,6 @@ display: - platform: waveshare_epaper model: 2.90inv2 - spi_id: spi_waveshare_epaper cs_pin: allow_other_uses: true number: ${cs_pin} @@ -853,7 +805,6 @@ display: - platform: waveshare_epaper model: 2.70in-b - spi_id: spi_waveshare_epaper cs_pin: allow_other_uses: true number: ${cs_pin} @@ -871,7 +822,6 @@ display: - platform: waveshare_epaper model: 2.70in-bv2 - spi_id: spi_waveshare_epaper cs_pin: allow_other_uses: true number: ${cs_pin} diff --git a/tests/components/waveshare_epaper/test.esp32-ard.yaml b/tests/components/waveshare_epaper/test.esp32-ard.yaml deleted file mode 100644 index c36345b984..0000000000 --- a/tests/components/waveshare_epaper/test.esp32-ard.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - clk_pin: GPIO18 - mosi_pin: GPIO23 - cs_pin: GPIO25 - dc_pin: GPIO26 - busy_pin: GPIO27 - reset_pin: GPIO32 - -<<: !include common.yaml diff --git a/tests/components/waveshare_epaper/test.esp32-c3-ard.yaml b/tests/components/waveshare_epaper/test.esp32-c3-ard.yaml deleted file mode 100644 index 4cb230f6f2..0000000000 --- a/tests/components/waveshare_epaper/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - cs_pin: GPIO4 - dc_pin: GPIO1 - busy_pin: GPIO2 - reset_pin: GPIO3 - -<<: !include common.yaml diff --git a/tests/components/waveshare_epaper/test.esp32-c3-idf.yaml b/tests/components/waveshare_epaper/test.esp32-c3-idf.yaml deleted file mode 100644 index 4cb230f6f2..0000000000 --- a/tests/components/waveshare_epaper/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - clk_pin: GPIO6 - mosi_pin: GPIO7 - cs_pin: GPIO4 - dc_pin: GPIO1 - busy_pin: GPIO2 - reset_pin: GPIO3 - -<<: !include common.yaml diff --git a/tests/components/waveshare_epaper/test.esp32-idf.yaml b/tests/components/waveshare_epaper/test.esp32-idf.yaml index 9e8b2fdec8..b7b12c8c66 100644 --- a/tests/components/waveshare_epaper/test.esp32-idf.yaml +++ b/tests/components/waveshare_epaper/test.esp32-idf.yaml @@ -1,9 +1,10 @@ substitutions: - clk_pin: GPIO16 - mosi_pin: GPIO17 cs_pin: GPIO4 dc_pin: GPIO5 - busy_pin: GPIO18 - reset_pin: GPIO19 + busy_pin: GPIO14 + reset_pin: GPIO15 + +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/waveshare_epaper/test.esp8266-ard.yaml b/tests/components/waveshare_epaper/test.esp8266-ard.yaml index ee8199bcc0..92452c840a 100644 --- a/tests/components/waveshare_epaper/test.esp8266-ard.yaml +++ b/tests/components/waveshare_epaper/test.esp8266-ard.yaml @@ -1,9 +1,12 @@ substitutions: - clk_pin: GPIO14 - mosi_pin: GPIO13 + clk_pin: GPIO0 + mosi_pin: GPIO2 cs_pin: GPIO4 dc_pin: GPIO5 busy_pin: GPIO15 reset_pin: GPIO16 +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/waveshare_epaper/test.rp2040-ard.yaml b/tests/components/waveshare_epaper/test.rp2040-ard.yaml index e92f6d421d..e9e001cdd4 100644 --- a/tests/components/waveshare_epaper/test.rp2040-ard.yaml +++ b/tests/components/waveshare_epaper/test.rp2040-ard.yaml @@ -6,4 +6,7 @@ substitutions: busy_pin: GPIO7 reset_pin: GPIO8 +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/web_server/test.esp32-c3-ard.yaml b/tests/components/web_server/test.esp32-c3-ard.yaml deleted file mode 100644 index 7e6658e20e..0000000000 --- a/tests/components/web_server/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common_v2.yaml diff --git a/tests/components/web_server/test.esp32-c3-idf.yaml b/tests/components/web_server/test.esp32-c3-idf.yaml deleted file mode 100644 index 7e6658e20e..0000000000 --- a/tests/components/web_server/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common_v2.yaml diff --git a/tests/components/web_server/test_ota.esp32-idf.yaml b/tests/components/web_server/test_ota.esp32-idf.yaml index 99873aa27b..98b080386b 100644 --- a/tests/components/web_server/test_ota.esp32-idf.yaml +++ b/tests/components/web_server/test_ota.esp32-idf.yaml @@ -12,8 +12,10 @@ packages: # Enable OTA for multipart upload testing ota: - platform: esphome + id: web_server_esphome_ota password: "test_ota_password" - platform: web_server + id: web_server_web_server_ota # Web server configuration web_server: diff --git a/tests/components/web_server_idf/common.yaml b/tests/components/web_server_idf/common.yaml new file mode 100644 index 0000000000..b1885af266 --- /dev/null +++ b/tests/components/web_server_idf/common.yaml @@ -0,0 +1,29 @@ +esphome: + name: test-web-server-idf + +esp32: + board: esp32dev + framework: + type: esp-idf + +network: + +# Add some entities to test SSE event formatting +sensor: + - platform: template + name: "Test Sensor" + id: test_sensor + update_interval: 60s + lambda: "return 42.5;" + +binary_sensor: + - platform: template + name: "Test Binary Sensor" + id: test_binary_sensor + lambda: "return true;" + +switch: + - platform: template + name: "Test Switch" + id: test_switch + optimistic: true diff --git a/tests/components/xiaomi_miscale copy/test.esp32-idf.yaml b/tests/components/web_server_idf/test.esp32-idf.yaml similarity index 65% rename from tests/components/xiaomi_miscale copy/test.esp32-idf.yaml rename to tests/components/web_server_idf/test.esp32-idf.yaml index dade44d145..c3b85178ef 100644 --- a/tests/components/xiaomi_miscale copy/test.esp32-idf.yaml +++ b/tests/components/web_server_idf/test.esp32-idf.yaml @@ -1 +1,3 @@ <<: !include common.yaml + +web_server: diff --git a/tests/components/whirlpool/common.yaml b/tests/components/whirlpool/common.yaml index 804c1aac26..6d55db1f08 100644 --- a/tests/components/whirlpool/common.yaml +++ b/tests/components/whirlpool/common.yaml @@ -1,7 +1,4 @@ -remote_transmitter: - pin: ${pin} - carrier_duty_percent: 50% - climate: - platform: whirlpool name: Whirlpool Climate + transmitter_id: xmitr diff --git a/tests/components/whirlpool/test.esp32-ard.yaml b/tests/components/whirlpool/test.esp32-ard.yaml deleted file mode 100644 index 7b012aa64c..0000000000 --- a/tests/components/whirlpool/test.esp32-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/whirlpool/test.esp32-c3-ard.yaml b/tests/components/whirlpool/test.esp32-c3-ard.yaml deleted file mode 100644 index 7b012aa64c..0000000000 --- a/tests/components/whirlpool/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/whirlpool/test.esp32-c3-idf.yaml b/tests/components/whirlpool/test.esp32-c3-idf.yaml deleted file mode 100644 index 7b012aa64c..0000000000 --- a/tests/components/whirlpool/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/whirlpool/test.esp32-idf.yaml b/tests/components/whirlpool/test.esp32-idf.yaml index 7b012aa64c..e891f9dc85 100644 --- a/tests/components/whirlpool/test.esp32-idf.yaml +++ b/tests/components/whirlpool/test.esp32-idf.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO2 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/whirlpool/test.esp8266-ard.yaml b/tests/components/whirlpool/test.esp8266-ard.yaml index f5097fcf5f..4bed2f03e5 100644 --- a/tests/components/whirlpool/test.esp8266-ard.yaml +++ b/tests/components/whirlpool/test.esp8266-ard.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO5 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/whynter/common.yaml b/tests/components/whynter/common.yaml index 04ad6bed54..63df11dd91 100644 --- a/tests/components/whynter/common.yaml +++ b/tests/components/whynter/common.yaml @@ -1,7 +1,4 @@ -remote_transmitter: - pin: ${pin} - carrier_duty_percent: 50% - climate: - platform: whynter name: Whynter Climate + transmitter_id: xmitr diff --git a/tests/components/whynter/test.esp32-ard.yaml b/tests/components/whynter/test.esp32-ard.yaml deleted file mode 100644 index 7b012aa64c..0000000000 --- a/tests/components/whynter/test.esp32-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/whynter/test.esp32-c3-ard.yaml b/tests/components/whynter/test.esp32-c3-ard.yaml deleted file mode 100644 index 7b012aa64c..0000000000 --- a/tests/components/whynter/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/whynter/test.esp32-c3-idf.yaml b/tests/components/whynter/test.esp32-c3-idf.yaml deleted file mode 100644 index 7b012aa64c..0000000000 --- a/tests/components/whynter/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/whynter/test.esp32-idf.yaml b/tests/components/whynter/test.esp32-idf.yaml index 7b012aa64c..e891f9dc85 100644 --- a/tests/components/whynter/test.esp32-idf.yaml +++ b/tests/components/whynter/test.esp32-idf.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO2 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/whynter/test.esp8266-ard.yaml b/tests/components/whynter/test.esp8266-ard.yaml index f5097fcf5f..4bed2f03e5 100644 --- a/tests/components/whynter/test.esp8266-ard.yaml +++ b/tests/components/whynter/test.esp8266-ard.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO5 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/wiegand/test.esp32-ard.yaml b/tests/components/wiegand/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/wiegand/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/wiegand/test.esp32-c3-ard.yaml b/tests/components/wiegand/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/wiegand/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/wiegand/test.esp32-c3-idf.yaml b/tests/components/wiegand/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/wiegand/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/wifi/common-eap.yaml b/tests/components/wifi/common-eap.yaml index 779cd6b49a..52319fa5a1 100644 --- a/tests/components/wifi/common-eap.yaml +++ b/tests/components/wifi/common-eap.yaml @@ -1,4 +1,5 @@ wifi: + fast_connect: true networks: - ssid: MySSID eap: diff --git a/tests/components/wifi/common.yaml b/tests/components/wifi/common.yaml index 343d44b177..7ce74ab00d 100644 --- a/tests/components/wifi/common.yaml +++ b/tests/components/wifi/common.yaml @@ -10,7 +10,19 @@ esphome: - logger.log: "Connected to WiFi!" on_error: - logger.log: "Failed to connect to WiFi!" + - if: + condition: wifi.ap_active + then: + - logger.log: "WiFi AP is active!" wifi: - ssid: MySSID - password: password1 + networks: + - ssid: MySSID + password: password1 + priority: 10 + - ssid: MySSID2 + password: password2 + priority: 5 + - ssid: MySSID3 + password: password3 + priority: 0 diff --git a/tests/components/wifi/test-eap.esp32-ard.yaml b/tests/components/wifi/test-eap.esp32-ard.yaml deleted file mode 100644 index 9177e5de10..0000000000 --- a/tests/components/wifi/test-eap.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common-eap.yaml diff --git a/tests/components/wifi/test.esp32-ard.yaml b/tests/components/wifi/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/wifi/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/wifi/test.esp32-c3-ard.yaml b/tests/components/wifi/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/wifi/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/wifi/test.esp32-c3-idf.yaml b/tests/components/wifi/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/wifi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/wifi/test.esp32-idf.yaml b/tests/components/wifi/test.esp32-idf.yaml index 91e235b9ce..3e01d7f990 100644 --- a/tests/components/wifi/test.esp32-idf.yaml +++ b/tests/components/wifi/test.esp32-idf.yaml @@ -1,7 +1,34 @@ psram: +# Tests the high performance request and release; requires the USE_WIFI_RUNTIME_POWER_SAVE define +esphome: + platformio_options: + build_flags: + - "-DUSE_WIFI_RUNTIME_POWER_SAVE" + on_boot: + - then: + - lambda: |- + esphome::wifi::global_wifi_component->request_high_performance(); + esphome::wifi::global_wifi_component->release_high_performance(); + wifi: use_psram: true + min_auth_mode: WPA + manual_ip: + static_ip: 192.168.1.100 + gateway: 192.168.1.1 + subnet: 255.255.255.0 + dns1: 1.1.1.1 + dns2: 8.8.8.8 + ap: + ssid: Fallback AP + password: fallback_password + manual_ip: + static_ip: 192.168.4.1 + gateway: 192.168.4.1 + subnet: 255.255.255.0 + +captive_portal: packages: - !include common.yaml diff --git a/tests/components/wifi/test.esp8266-ard.yaml b/tests/components/wifi/test.esp8266-ard.yaml index dade44d145..9cb0e3cf48 100644 --- a/tests/components/wifi/test.esp8266-ard.yaml +++ b/tests/components/wifi/test.esp8266-ard.yaml @@ -1 +1,5 @@ -<<: !include common.yaml +wifi: + min_auth_mode: WPA2 + +packages: + - !include common.yaml diff --git a/tests/components/wifi_info/test.esp32-ard.yaml b/tests/components/wifi_info/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/wifi_info/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/wifi_info/test.esp32-c3-ard.yaml b/tests/components/wifi_info/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/wifi_info/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/wifi_info/test.esp32-c3-idf.yaml b/tests/components/wifi_info/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/wifi_info/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/wifi_info/test.esp32-idf.yaml b/tests/components/wifi_info/test.esp32-idf.yaml index dade44d145..b47e39c389 100644 --- a/tests/components/wifi_info/test.esp32-idf.yaml +++ b/tests/components/wifi_info/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/wifi_info/test.esp8266-ard.yaml b/tests/components/wifi_info/test.esp8266-ard.yaml index dade44d145..4a98b9388a 100644 --- a/tests/components/wifi_info/test.esp8266-ard.yaml +++ b/tests/components/wifi_info/test.esp8266-ard.yaml @@ -1 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/wifi_info/test.rp2040-ard.yaml b/tests/components/wifi_info/test.rp2040-ard.yaml index dade44d145..319a7c71a6 100644 --- a/tests/components/wifi_info/test.rp2040-ard.yaml +++ b/tests/components/wifi_info/test.rp2040-ard.yaml @@ -1 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/wifi_signal/test.esp32-ard.yaml b/tests/components/wifi_signal/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/wifi_signal/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/wifi_signal/test.esp32-c3-ard.yaml b/tests/components/wifi_signal/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/wifi_signal/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/wifi_signal/test.esp32-c3-idf.yaml b/tests/components/wifi_signal/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/wifi_signal/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/wireguard/common.yaml b/tests/components/wireguard/common.yaml index cd7ab1075e..342ffa32f6 100644 --- a/tests/components/wireguard/common.yaml +++ b/tests/components/wireguard/common.yaml @@ -4,8 +4,10 @@ wifi: time: - platform: sntp + id: sntp_time wireguard: + time_id: sntp_time address: 172.16.34.100 netmask: 255.255.255.0 # NEVER use the following keys for your VPN -- they are now public! diff --git a/tests/components/wireguard/test.esp32-c3-ard.yaml b/tests/components/wireguard/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/wireguard/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/wireguard/test.esp32-c3-idf.yaml b/tests/components/wireguard/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/wireguard/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/wireguard/test.esp32-idf.yaml b/tests/components/wireguard/test.esp32-idf.yaml index 2798f8e566..90dbc1cf7d 100644 --- a/tests/components/wireguard/test.esp32-idf.yaml +++ b/tests/components/wireguard/test.esp32-idf.yaml @@ -1,3 +1,6 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + <<: !include common.yaml network: diff --git a/tests/components/wireguard/test.esp8266-ard.yaml b/tests/components/wireguard/test.esp8266-ard.yaml index 2798f8e566..ae6a3d6ce0 100644 --- a/tests/components/wireguard/test.esp8266-ard.yaml +++ b/tests/components/wireguard/test.esp8266-ard.yaml @@ -1,3 +1,6 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + <<: !include common.yaml network: diff --git a/tests/components/wk2132_i2c/common.yaml b/tests/components/wk2132_i2c/common.yaml index 942e01aafc..39013baeb2 100644 --- a/tests/components/wk2132_i2c/common.yaml +++ b/tests/components/wk2132_i2c/common.yaml @@ -1,14 +1,7 @@ -i2c: - id: i2c_bus - scl: ${scl_pin} - sda: ${sda_pin} - scan: true - frequency: 600kHz - wk2132_i2c: - id: wk2132_i2c_id - address: 0x70 i2c_id: i2c_bus + address: 0x70 uart: - id: wk2132_id_0 channel: 0 diff --git a/tests/components/wk2132_i2c/test.esp32-ard.yaml b/tests/components/wk2132_i2c/test.esp32-ard.yaml deleted file mode 100644 index 3b761d3fc1..0000000000 --- a/tests/components/wk2132_i2c/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO22 - sda_pin: GPIO21 - -<<: !include common.yaml diff --git a/tests/components/wk2132_i2c/test.esp32-idf.yaml b/tests/components/wk2132_i2c/test.esp32-idf.yaml index 3b761d3fc1..6b748a8f20 100644 --- a/tests/components/wk2132_i2c/test.esp32-idf.yaml +++ b/tests/components/wk2132_i2c/test.esp32-idf.yaml @@ -1,5 +1,5 @@ -substitutions: - scl_pin: GPIO22 - sda_pin: GPIO21 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + uart_bridge_2: !include ../../test_build_components/common/uart_bridge_2/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/wk2132_i2c/test.esp32-s3-ard.yaml b/tests/components/wk2132_i2c/test.esp32-s3-ard.yaml deleted file mode 100644 index 4942e3c2b3..0000000000 --- a/tests/components/wk2132_i2c/test.esp32-s3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO40 - sda_pin: GPIO41 - -<<: !include common.yaml diff --git a/tests/components/wk2132_i2c/test.esp32-s3-idf.yaml b/tests/components/wk2132_i2c/test.esp32-s3-idf.yaml deleted file mode 100644 index 4942e3c2b3..0000000000 --- a/tests/components/wk2132_i2c/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO40 - sda_pin: GPIO41 - -<<: !include common.yaml diff --git a/tests/components/wk2132_spi/common.yaml b/tests/components/wk2132_spi/common.yaml index a077b36998..18294974b9 100644 --- a/tests/components/wk2132_spi/common.yaml +++ b/tests/components/wk2132_spi/common.yaml @@ -1,27 +1,20 @@ -spi: - id: spi_bus - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - miso_pin: ${miso_pin} - wk2132_spi: - - id: wk2132_spi_id + - id: wk2132_spi_bridge cs_pin: ${cs_pin} - spi_id: spi_bus crystal: 11059200 data_rate: 1MHz uart: - - id: wk2132_spi_id0 + - id: wk2132_spi_uart0 channel: 0 baud_rate: 115200 stop_bits: 1 parity: none - - id: wk2132_spi_id1 + - id: wk2132_spi_uart1 channel: 1 baud_rate: 9600 # Ensures a sensor doesn't break validation sensor: - platform: a02yyuw - uart_id: wk2132_spi_id1 + uart_id: wk2132_spi_uart1 id: distance_sensor diff --git a/tests/components/wk2132_spi/test.esp32-ard.yaml b/tests/components/wk2132_spi/test.esp32-ard.yaml deleted file mode 100644 index 76e7138ab0..0000000000 --- a/tests/components/wk2132_spi/test.esp32-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO18 - miso_pin: GPIO19 - mosi_pin: GPIO23 - cs_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/wk2132_spi/test.esp32-idf.yaml b/tests/components/wk2132_spi/test.esp32-idf.yaml index 76e7138ab0..9202a691ba 100644 --- a/tests/components/wk2132_spi/test.esp32-idf.yaml +++ b/tests/components/wk2132_spi/test.esp32-idf.yaml @@ -1,7 +1,8 @@ substitutions: - clk_pin: GPIO18 - miso_pin: GPIO19 - mosi_pin: GPIO23 cs_pin: GPIO5 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + uart_bridge_2: !include ../../test_build_components/common/uart_bridge_2/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/wk2132_spi/test.esp32-s3-ard.yaml b/tests/components/wk2132_spi/test.esp32-s3-ard.yaml deleted file mode 100644 index b0aadf620a..0000000000 --- a/tests/components/wk2132_spi/test.esp32-s3-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO40 - miso_pin: GPIO41 - mosi_pin: GPIO6 - cs_pin: GPIO19 - -<<: !include common.yaml diff --git a/tests/components/wk2132_spi/test.esp32-s3-idf.yaml b/tests/components/wk2132_spi/test.esp32-s3-idf.yaml deleted file mode 100644 index b0aadf620a..0000000000 --- a/tests/components/wk2132_spi/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO40 - miso_pin: GPIO41 - mosi_pin: GPIO6 - cs_pin: GPIO19 - -<<: !include common.yaml diff --git a/tests/components/wk2168_i2c/common.yaml b/tests/components/wk2168_i2c/common.yaml index 10463e8abf..49f0d1ec6b 100644 --- a/tests/components/wk2168_i2c/common.yaml +++ b/tests/components/wk2168_i2c/common.yaml @@ -1,35 +1,28 @@ -i2c: - id: i2c_bus - scl: ${scl_pin} - sda: ${sda_pin} - scan: true - frequency: 600kHz - # component declaration wk2168_i2c: - - id: bridge_i2c + - id: wk2168_i2c_bridge i2c_id: i2c_bus address: 0x70 uart: - - id: id0 + - id: wk2168_i2c_uart0 channel: 0 baud_rate: 115200 stop_bits: 1 parity: none - - id: id1 + - id: wk2168_i2c_uart1 channel: 1 baud_rate: 115200 - - id: id2 + - id: wk2168_i2c_uart2 channel: 2 baud_rate: 115200 - - id: id3 + - id: wk2168_i2c_uart3 channel: 3 baud_rate: 9600 # Ensures a sensor doesn't break validation sensor: - platform: a02yyuw - uart_id: id3 + uart_id: wk2168_i2c_uart3 id: distance_sensor # individual binary_sensor inputs @@ -37,14 +30,14 @@ binary_sensor: - platform: gpio name: "pin_0" pin: - wk2168_i2c: bridge_i2c + wk2168_i2c: wk2168_i2c_bridge number: 0 mode: input: true - platform: gpio name: "pin_1" pin: - wk2168_i2c: bridge_i2c + wk2168_i2c: wk2168_i2c_bridge number: 1 mode: input: true @@ -55,14 +48,14 @@ switch: - platform: gpio name: "pin_2" pin: - wk2168_i2c: bridge_i2c + wk2168_i2c: wk2168_i2c_bridge number: 2 mode: output: true - platform: gpio name: "pin_3" pin: - wk2168_i2c: bridge_i2c + wk2168_i2c: wk2168_i2c_bridge number: 3 mode: output: true diff --git a/tests/components/wk2168_i2c/test.esp32-ard.yaml b/tests/components/wk2168_i2c/test.esp32-ard.yaml deleted file mode 100644 index 3b761d3fc1..0000000000 --- a/tests/components/wk2168_i2c/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO22 - sda_pin: GPIO21 - -<<: !include common.yaml diff --git a/tests/components/wk2168_i2c/test.esp32-idf.yaml b/tests/components/wk2168_i2c/test.esp32-idf.yaml index 3b761d3fc1..9d9f0d4931 100644 --- a/tests/components/wk2168_i2c/test.esp32-idf.yaml +++ b/tests/components/wk2168_i2c/test.esp32-idf.yaml @@ -1,5 +1,5 @@ -substitutions: - scl_pin: GPIO22 - sda_pin: GPIO21 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/wk2168_i2c/test.esp32-s3-ard.yaml b/tests/components/wk2168_i2c/test.esp32-s3-ard.yaml deleted file mode 100644 index 4942e3c2b3..0000000000 --- a/tests/components/wk2168_i2c/test.esp32-s3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO40 - sda_pin: GPIO41 - -<<: !include common.yaml diff --git a/tests/components/wk2168_i2c/test.esp32-s3-idf.yaml b/tests/components/wk2168_i2c/test.esp32-s3-idf.yaml deleted file mode 100644 index 4942e3c2b3..0000000000 --- a/tests/components/wk2168_i2c/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO40 - sda_pin: GPIO41 - -<<: !include common.yaml diff --git a/tests/components/wk2168_spi/common.yaml b/tests/components/wk2168_spi/common.yaml index fb126193fc..b402077aa3 100644 --- a/tests/components/wk2168_spi/common.yaml +++ b/tests/components/wk2168_spi/common.yaml @@ -1,35 +1,28 @@ -spi: - id: spi_bus - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - miso_pin: ${miso_pin} - wk2168_spi: - - id: bridge_spi + - id: wk2168_spi_bridge cs_pin: ${cs_pin} - spi_id: spi_bus crystal: 11059200 data_rate: 1MHz uart: - - id: id0 + - id: wk2168_spi_uart0 channel: 0 baud_rate: 115200 stop_bits: 1 parity: none - - id: id1 + - id: wk2168_spi_uart1 channel: 1 baud_rate: 115200 - - id: id2 + - id: wk2168_spi_uart2 channel: 2 baud_rate: 115200 - - id: id3 + - id: wk2168_spi_uart3 channel: 3 baud_rate: 9600 # Ensures a sensor doesn't break validation sensor: - platform: a02yyuw - uart_id: id3 + uart_id: wk2168_spi_uart3 id: distance_sensor # individual binary_sensor inputs @@ -37,14 +30,14 @@ binary_sensor: - platform: gpio name: "pin_0" pin: - wk2168_spi: bridge_spi + wk2168_spi: wk2168_spi_bridge number: 0 mode: input: true - platform: gpio name: "pin_1" pin: - wk2168_spi: bridge_spi + wk2168_spi: wk2168_spi_bridge number: 1 mode: input: true @@ -55,14 +48,14 @@ switch: - platform: gpio name: "pin_2" pin: - wk2168_spi: bridge_spi + wk2168_spi: wk2168_spi_bridge number: 2 mode: output: true - platform: gpio name: "pin_3" pin: - wk2168_spi: bridge_spi + wk2168_spi: wk2168_spi_bridge number: 3 mode: output: true diff --git a/tests/components/wk2168_spi/test.esp32-ard.yaml b/tests/components/wk2168_spi/test.esp32-ard.yaml deleted file mode 100644 index 76e7138ab0..0000000000 --- a/tests/components/wk2168_spi/test.esp32-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO18 - miso_pin: GPIO19 - mosi_pin: GPIO23 - cs_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/wk2168_spi/test.esp32-idf.yaml b/tests/components/wk2168_spi/test.esp32-idf.yaml index 76e7138ab0..2b56a46b70 100644 --- a/tests/components/wk2168_spi/test.esp32-idf.yaml +++ b/tests/components/wk2168_spi/test.esp32-idf.yaml @@ -1,7 +1,8 @@ substitutions: - clk_pin: GPIO18 - miso_pin: GPIO19 - mosi_pin: GPIO23 cs_pin: GPIO5 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/wk2168_spi/test.esp32-s3-ard.yaml b/tests/components/wk2168_spi/test.esp32-s3-ard.yaml deleted file mode 100644 index b0aadf620a..0000000000 --- a/tests/components/wk2168_spi/test.esp32-s3-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO40 - miso_pin: GPIO41 - mosi_pin: GPIO6 - cs_pin: GPIO19 - -<<: !include common.yaml diff --git a/tests/components/wk2168_spi/test.esp32-s3-idf.yaml b/tests/components/wk2168_spi/test.esp32-s3-idf.yaml deleted file mode 100644 index b0aadf620a..0000000000 --- a/tests/components/wk2168_spi/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO40 - miso_pin: GPIO41 - mosi_pin: GPIO6 - cs_pin: GPIO19 - -<<: !include common.yaml diff --git a/tests/components/wk2204_i2c/common.yaml b/tests/components/wk2204_i2c/common.yaml index 70c0f4babf..863633937b 100644 --- a/tests/components/wk2204_i2c/common.yaml +++ b/tests/components/wk2204_i2c/common.yaml @@ -1,10 +1,3 @@ -i2c: - id: i2c_bus - scl: ${scl_pin} - sda: ${sda_pin} - scan: true - frequency: 600kHz - wk2204_i2c: - id: wk2204_i2c_id i2c_id: i2c_bus diff --git a/tests/components/wk2204_i2c/test.esp32-ard.yaml b/tests/components/wk2204_i2c/test.esp32-ard.yaml deleted file mode 100644 index 3b761d3fc1..0000000000 --- a/tests/components/wk2204_i2c/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO22 - sda_pin: GPIO21 - -<<: !include common.yaml diff --git a/tests/components/wk2204_i2c/test.esp32-idf.yaml b/tests/components/wk2204_i2c/test.esp32-idf.yaml index 3b761d3fc1..9d9f0d4931 100644 --- a/tests/components/wk2204_i2c/test.esp32-idf.yaml +++ b/tests/components/wk2204_i2c/test.esp32-idf.yaml @@ -1,5 +1,5 @@ -substitutions: - scl_pin: GPIO22 - sda_pin: GPIO21 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/wk2204_i2c/test.esp32-s3-ard.yaml b/tests/components/wk2204_i2c/test.esp32-s3-ard.yaml deleted file mode 100644 index 4942e3c2b3..0000000000 --- a/tests/components/wk2204_i2c/test.esp32-s3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO40 - sda_pin: GPIO41 - -<<: !include common.yaml diff --git a/tests/components/wk2204_i2c/test.esp32-s3-idf.yaml b/tests/components/wk2204_i2c/test.esp32-s3-idf.yaml deleted file mode 100644 index 4942e3c2b3..0000000000 --- a/tests/components/wk2204_i2c/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO40 - sda_pin: GPIO41 - -<<: !include common.yaml diff --git a/tests/components/wk2204_spi/common.yaml b/tests/components/wk2204_spi/common.yaml index a08cdb906f..0b62a7a009 100644 --- a/tests/components/wk2204_spi/common.yaml +++ b/tests/components/wk2204_spi/common.yaml @@ -1,35 +1,28 @@ -spi: - id: spi_bus - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - miso_pin: ${miso_pin} - wk2204_spi: - - id: wk2204_spi_id + - id: wk2204_spi_bridge cs_pin: ${cs_pin} - spi_id: spi_bus crystal: 11059200 data_rate: 1MHz uart: - - id: wk2204_spi_id0 + - id: wk2204_spi_uart0 channel: 0 baud_rate: 115200 stop_bits: 1 parity: none - - id: wk2204_spi_id1 + - id: wk2204_spi_uart1 channel: 1 baud_rate: 921600 - - id: wk2204_spi_id2 + - id: wk2204_spi_uart2 channel: 2 baud_rate: 115200 stop_bits: 1 parity: none - - id: wk2204_spi_id3 + - id: wk2204_spi_uart3 channel: 3 baud_rate: 9600 # Ensures a sensor doesn't break validation sensor: - platform: a02yyuw - uart_id: wk2204_spi_id3 + uart_id: wk2204_spi_uart3 id: distance_sensor diff --git a/tests/components/wk2204_spi/test.esp32-ard.yaml b/tests/components/wk2204_spi/test.esp32-ard.yaml deleted file mode 100644 index 76e7138ab0..0000000000 --- a/tests/components/wk2204_spi/test.esp32-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO18 - miso_pin: GPIO19 - mosi_pin: GPIO23 - cs_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/wk2204_spi/test.esp32-idf.yaml b/tests/components/wk2204_spi/test.esp32-idf.yaml index 76e7138ab0..2b56a46b70 100644 --- a/tests/components/wk2204_spi/test.esp32-idf.yaml +++ b/tests/components/wk2204_spi/test.esp32-idf.yaml @@ -1,7 +1,8 @@ substitutions: - clk_pin: GPIO18 - miso_pin: GPIO19 - mosi_pin: GPIO23 cs_pin: GPIO5 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/wk2204_spi/test.esp32-s3-ard.yaml b/tests/components/wk2204_spi/test.esp32-s3-ard.yaml deleted file mode 100644 index b0aadf620a..0000000000 --- a/tests/components/wk2204_spi/test.esp32-s3-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO40 - miso_pin: GPIO41 - mosi_pin: GPIO6 - cs_pin: GPIO19 - -<<: !include common.yaml diff --git a/tests/components/wk2204_spi/test.esp32-s3-idf.yaml b/tests/components/wk2204_spi/test.esp32-s3-idf.yaml deleted file mode 100644 index b0aadf620a..0000000000 --- a/tests/components/wk2204_spi/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO40 - miso_pin: GPIO41 - mosi_pin: GPIO6 - cs_pin: GPIO19 - -<<: !include common.yaml diff --git a/tests/components/wk2212_i2c/common.yaml b/tests/components/wk2212_i2c/common.yaml index 0759ef8688..a754bec5c7 100644 --- a/tests/components/wk2212_i2c/common.yaml +++ b/tests/components/wk2212_i2c/common.yaml @@ -1,13 +1,6 @@ -i2c: - id: i2c_bus - scl: ${scl_pin} - sda: ${sda_pin} - scan: true - frequency: 600kHz - # component declaration wk2212_i2c: - - id: bridge_i2c + - id: wk2212_i2c_bridge i2c_id: i2c_bus address: 0x70 uart: @@ -33,14 +26,14 @@ binary_sensor: - platform: gpio name: "pin_0" pin: - wk2212_i2c: bridge_i2c + wk2212_i2c: wk2212_i2c_bridge number: 0 mode: input: true - platform: gpio name: "pin_1" pin: - wk2212_i2c: bridge_i2c + wk2212_i2c: wk2212_i2c_bridge number: 1 mode: input: true @@ -51,14 +44,14 @@ switch: - platform: gpio name: "pin_2" pin: - wk2212_i2c: bridge_i2c + wk2212_i2c: wk2212_i2c_bridge number: 2 mode: output: true - platform: gpio name: "pin_3" pin: - wk2212_i2c: bridge_i2c + wk2212_i2c: wk2212_i2c_bridge number: 3 mode: output: true diff --git a/tests/components/wk2212_i2c/test.esp32-ard.yaml b/tests/components/wk2212_i2c/test.esp32-ard.yaml deleted file mode 100644 index 3b761d3fc1..0000000000 --- a/tests/components/wk2212_i2c/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO22 - sda_pin: GPIO21 - -<<: !include common.yaml diff --git a/tests/components/wk2212_i2c/test.esp32-idf.yaml b/tests/components/wk2212_i2c/test.esp32-idf.yaml index 3b761d3fc1..9d9f0d4931 100644 --- a/tests/components/wk2212_i2c/test.esp32-idf.yaml +++ b/tests/components/wk2212_i2c/test.esp32-idf.yaml @@ -1,5 +1,5 @@ -substitutions: - scl_pin: GPIO22 - sda_pin: GPIO21 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/wk2212_i2c/test.esp32-s3-ard.yaml b/tests/components/wk2212_i2c/test.esp32-s3-ard.yaml deleted file mode 100644 index 4942e3c2b3..0000000000 --- a/tests/components/wk2212_i2c/test.esp32-s3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO40 - sda_pin: GPIO41 - -<<: !include common.yaml diff --git a/tests/components/wk2212_i2c/test.esp32-s3-idf.yaml b/tests/components/wk2212_i2c/test.esp32-s3-idf.yaml deleted file mode 100644 index 4942e3c2b3..0000000000 --- a/tests/components/wk2212_i2c/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO40 - sda_pin: GPIO41 - -<<: !include common.yaml diff --git a/tests/components/wk2212_spi/common.yaml b/tests/components/wk2212_spi/common.yaml index 693d2a9ab2..969f16bb12 100644 --- a/tests/components/wk2212_spi/common.yaml +++ b/tests/components/wk2212_spi/common.yaml @@ -1,29 +1,22 @@ -spi: - id: spi_bus - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - miso_pin: ${miso_pin} - wk2212_spi: - - id: bridge_spi + - id: wk2212_spi_bridge cs_pin: ${cs_pin} - spi_id: spi_bus crystal: 11059200 data_rate: 1MHz uart: - - id: id0 + - id: wk2212_spi_uart0 channel: 0 baud_rate: 115200 stop_bits: 1 parity: none - - id: id1 + - id: wk2212_spi_uart1 channel: 1 baud_rate: 9600 # Ensures a sensor doesn't break validation sensor: - platform: a02yyuw - uart_id: id1 + uart_id: wk2212_spi_uart1 id: distance_sensor # individual binary_sensor inputs @@ -31,14 +24,14 @@ binary_sensor: - platform: gpio name: "pin_0" pin: - wk2212_spi: bridge_spi + wk2212_spi: wk2212_spi_bridge number: 0 mode: input: true - platform: gpio name: "pin_1" pin: - wk2212_spi: bridge_spi + wk2212_spi: wk2212_spi_bridge number: 1 mode: input: true @@ -49,14 +42,14 @@ switch: - platform: gpio name: "pin_2" pin: - wk2212_spi: bridge_spi + wk2212_spi: wk2212_spi_bridge number: 2 mode: output: true - platform: gpio name: "pin_3" pin: - wk2212_spi: bridge_spi + wk2212_spi: wk2212_spi_bridge number: 3 mode: output: true diff --git a/tests/components/wk2212_spi/test.esp32-ard.yaml b/tests/components/wk2212_spi/test.esp32-ard.yaml deleted file mode 100644 index 76e7138ab0..0000000000 --- a/tests/components/wk2212_spi/test.esp32-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO18 - miso_pin: GPIO19 - mosi_pin: GPIO23 - cs_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/wk2212_spi/test.esp32-idf.yaml b/tests/components/wk2212_spi/test.esp32-idf.yaml index 76e7138ab0..2b56a46b70 100644 --- a/tests/components/wk2212_spi/test.esp32-idf.yaml +++ b/tests/components/wk2212_spi/test.esp32-idf.yaml @@ -1,7 +1,8 @@ substitutions: - clk_pin: GPIO18 - miso_pin: GPIO19 - mosi_pin: GPIO23 cs_pin: GPIO5 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/wk2212_spi/test.esp32-s3-ard.yaml b/tests/components/wk2212_spi/test.esp32-s3-ard.yaml deleted file mode 100644 index b0aadf620a..0000000000 --- a/tests/components/wk2212_spi/test.esp32-s3-ard.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO40 - miso_pin: GPIO41 - mosi_pin: GPIO6 - cs_pin: GPIO19 - -<<: !include common.yaml diff --git a/tests/components/wk2212_spi/test.esp32-s3-idf.yaml b/tests/components/wk2212_spi/test.esp32-s3-idf.yaml deleted file mode 100644 index b0aadf620a..0000000000 --- a/tests/components/wk2212_spi/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clk_pin: GPIO40 - miso_pin: GPIO41 - mosi_pin: GPIO6 - cs_pin: GPIO19 - -<<: !include common.yaml diff --git a/tests/components/wl_134/common.yaml b/tests/components/wl_134/common.yaml index 71c50be79b..e79331cb52 100644 --- a/tests/components/wl_134/common.yaml +++ b/tests/components/wl_134/common.yaml @@ -1,9 +1,3 @@ -uart: - - id: uart_wl_134 - tx_pin: ${tx_pin} - rx_pin: ${rx_pin} - baud_rate: 9600 - text_sensor: - platform: wl_134 name: Transponder Code diff --git a/tests/components/wl_134/test.esp32-ard.yaml b/tests/components/wl_134/test.esp32-ard.yaml deleted file mode 100644 index f486544afa..0000000000 --- a/tests/components/wl_134/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 - -<<: !include common.yaml diff --git a/tests/components/wl_134/test.esp32-c3-ard.yaml b/tests/components/wl_134/test.esp32-c3-ard.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/wl_134/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/wl_134/test.esp32-c3-idf.yaml b/tests/components/wl_134/test.esp32-c3-idf.yaml deleted file mode 100644 index b516342f3b..0000000000 --- a/tests/components/wl_134/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/wl_134/test.esp32-idf.yaml b/tests/components/wl_134/test.esp32-idf.yaml index f486544afa..b415125e84 100644 --- a/tests/components/wl_134/test.esp32-idf.yaml +++ b/tests/components/wl_134/test.esp32-idf.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO17 - rx_pin: GPIO16 + tx_pin: GPIO4 + rx_pin: GPIO5 + +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/wl_134/test.esp8266-ard.yaml b/tests/components/wl_134/test.esp8266-ard.yaml index b516342f3b..96ab4ef6ac 100644 --- a/tests/components/wl_134/test.esp8266-ard.yaml +++ b/tests/components/wl_134/test.esp8266-ard.yaml @@ -1,5 +1,8 @@ substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: GPIO0 + rx_pin: GPIO2 + +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/wl_134/test.rp2040-ard.yaml b/tests/components/wl_134/test.rp2040-ard.yaml index b516342f3b..b28f2b5e05 100644 --- a/tests/components/wl_134/test.rp2040-ard.yaml +++ b/tests/components/wl_134/test.rp2040-ard.yaml @@ -2,4 +2,7 @@ substitutions: tx_pin: GPIO4 rx_pin: GPIO5 +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/wled/test.esp32-c3-ard.yaml b/tests/components/wled/test.esp32-c3-ard.yaml deleted file mode 100644 index 156b31181e..0000000000 --- a/tests/components/wled/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,16 +0,0 @@ -wifi: - ssid: MySSID - password: password1 - -wled: - -light: - - platform: esp32_rmt_led_strip - id: led_matrix_32x8 - default_transition_length: 500ms - chipset: ws2812 - rgb_order: GRB - num_leds: 256 - pin: 2 - effects: - - wled: diff --git a/tests/components/wts01/common.yaml b/tests/components/wts01/common.yaml new file mode 100644 index 0000000000..966588c82a --- /dev/null +++ b/tests/components/wts01/common.yaml @@ -0,0 +1,3 @@ +sensor: + - platform: wts01 + id: wts01_sensor diff --git a/tests/components/wts01/test.esp32-idf.yaml b/tests/components/wts01/test.esp32-idf.yaml new file mode 100644 index 0000000000..b415125e84 --- /dev/null +++ b/tests/components/wts01/test.esp32-idf.yaml @@ -0,0 +1,8 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/wts01/test.esp8266-ard.yaml b/tests/components/wts01/test.esp8266-ard.yaml new file mode 100644 index 0000000000..89ca3ab5ae --- /dev/null +++ b/tests/components/wts01/test.esp8266-ard.yaml @@ -0,0 +1,8 @@ +substitutions: + tx_pin: GPIO1 + rx_pin: GPIO3 + +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/wts01/test.rp2040-ard.yaml b/tests/components/wts01/test.rp2040-ard.yaml new file mode 100644 index 0000000000..9246c39f08 --- /dev/null +++ b/tests/components/wts01/test.rp2040-ard.yaml @@ -0,0 +1,8 @@ +substitutions: + tx_pin: GPIO0 + rx_pin: GPIO1 + +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/x9c/test.esp32-ard.yaml b/tests/components/x9c/test.esp32-ard.yaml deleted file mode 100644 index 6dfe3a67eb..0000000000 --- a/tests/components/x9c/test.esp32-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - cs_pin: GPIO13 - inc_pin: GPIO14 - ud_pin: GPIO15 - -<<: !include common.yaml diff --git a/tests/components/x9c/test.esp32-c3-ard.yaml b/tests/components/x9c/test.esp32-c3-ard.yaml deleted file mode 100644 index b06e15a98c..0000000000 --- a/tests/components/x9c/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - cs_pin: GPIO3 - inc_pin: GPIO4 - ud_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/x9c/test.esp32-c3-idf.yaml b/tests/components/x9c/test.esp32-c3-idf.yaml deleted file mode 100644 index b06e15a98c..0000000000 --- a/tests/components/x9c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - cs_pin: GPIO3 - inc_pin: GPIO4 - ud_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/x9c/test.esp32-idf.yaml b/tests/components/x9c/test.esp32-idf.yaml index 6dfe3a67eb..7beb6e67cb 100644 --- a/tests/components/x9c/test.esp32-idf.yaml +++ b/tests/components/x9c/test.esp32-idf.yaml @@ -3,4 +3,7 @@ substitutions: inc_pin: GPIO14 ud_pin: GPIO15 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/x9c/test.esp8266-ard.yaml b/tests/components/x9c/test.esp8266-ard.yaml index 6dfe3a67eb..ae6b775ff7 100644 --- a/tests/components/x9c/test.esp8266-ard.yaml +++ b/tests/components/x9c/test.esp8266-ard.yaml @@ -1,6 +1,9 @@ substitutions: - cs_pin: GPIO13 - inc_pin: GPIO14 + cs_pin: GPIO0 + inc_pin: GPIO2 ud_pin: GPIO15 +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/x9c/test.rp2040-ard.yaml b/tests/components/x9c/test.rp2040-ard.yaml index b06e15a98c..f17627baf3 100644 --- a/tests/components/x9c/test.rp2040-ard.yaml +++ b/tests/components/x9c/test.rp2040-ard.yaml @@ -1,6 +1,9 @@ substitutions: - cs_pin: GPIO3 - inc_pin: GPIO4 - ud_pin: GPIO5 + cs_pin: GPIO6 + inc_pin: GPIO7 + ud_pin: GPIO8 + +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/xgzp68xx/common.yaml b/tests/components/xgzp68xx/common.yaml index 224dd9ed14..00e51e764c 100644 --- a/tests/components/xgzp68xx/common.yaml +++ b/tests/components/xgzp68xx/common.yaml @@ -1,12 +1,9 @@ -i2c: - - id: i2c_xgzp68xx - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: xgzp68xx + i2c_id: i2c_bus k_value: 4096 temperature: name: Pressure Temperature pressure: name: Differential pressure + oversampling: 1024x diff --git a/tests/components/xgzp68xx/test.esp32-ard.yaml b/tests/components/xgzp68xx/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/xgzp68xx/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/xgzp68xx/test.esp32-c3-ard.yaml b/tests/components/xgzp68xx/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/xgzp68xx/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/xgzp68xx/test.esp32-c3-idf.yaml b/tests/components/xgzp68xx/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/xgzp68xx/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/xgzp68xx/test.esp32-idf.yaml b/tests/components/xgzp68xx/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/xgzp68xx/test.esp32-idf.yaml +++ b/tests/components/xgzp68xx/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/xgzp68xx/test.esp8266-ard.yaml b/tests/components/xgzp68xx/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/xgzp68xx/test.esp8266-ard.yaml +++ b/tests/components/xgzp68xx/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/xgzp68xx/test.rp2040-ard.yaml b/tests/components/xgzp68xx/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/xgzp68xx/test.rp2040-ard.yaml +++ b/tests/components/xgzp68xx/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/xiaomi_ble/test.esp32-ard.yaml b/tests/components/xiaomi_ble/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_ble/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_ble/test.esp32-c3-ard.yaml b/tests/components/xiaomi_ble/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_ble/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_ble/test.esp32-c3-idf.yaml b/tests/components/xiaomi_ble/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_ble/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_ble/test.esp32-idf.yaml b/tests/components/xiaomi_ble/test.esp32-idf.yaml index dade44d145..7a6541ae76 100644 --- a/tests/components/xiaomi_ble/test.esp32-idf.yaml +++ b/tests/components/xiaomi_ble/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/xiaomi_cgd1/test.esp32-ard.yaml b/tests/components/xiaomi_cgd1/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_cgd1/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_cgd1/test.esp32-c3-ard.yaml b/tests/components/xiaomi_cgd1/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_cgd1/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_cgd1/test.esp32-c3-idf.yaml b/tests/components/xiaomi_cgd1/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_cgd1/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_cgd1/test.esp32-idf.yaml b/tests/components/xiaomi_cgd1/test.esp32-idf.yaml index dade44d145..7a6541ae76 100644 --- a/tests/components/xiaomi_cgd1/test.esp32-idf.yaml +++ b/tests/components/xiaomi_cgd1/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/xiaomi_cgdk2/test.esp32-ard.yaml b/tests/components/xiaomi_cgdk2/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_cgdk2/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_cgdk2/test.esp32-c3-ard.yaml b/tests/components/xiaomi_cgdk2/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_cgdk2/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_cgdk2/test.esp32-c3-idf.yaml b/tests/components/xiaomi_cgdk2/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_cgdk2/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_cgdk2/test.esp32-idf.yaml b/tests/components/xiaomi_cgdk2/test.esp32-idf.yaml index dade44d145..7a6541ae76 100644 --- a/tests/components/xiaomi_cgdk2/test.esp32-idf.yaml +++ b/tests/components/xiaomi_cgdk2/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/xiaomi_cgg1/test.esp32-ard.yaml b/tests/components/xiaomi_cgg1/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_cgg1/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_cgg1/test.esp32-c3-ard.yaml b/tests/components/xiaomi_cgg1/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_cgg1/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_cgg1/test.esp32-c3-idf.yaml b/tests/components/xiaomi_cgg1/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_cgg1/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_cgg1/test.esp32-idf.yaml b/tests/components/xiaomi_cgg1/test.esp32-idf.yaml index dade44d145..7a6541ae76 100644 --- a/tests/components/xiaomi_cgg1/test.esp32-idf.yaml +++ b/tests/components/xiaomi_cgg1/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/xiaomi_cgpr1/test.esp32-ard.yaml b/tests/components/xiaomi_cgpr1/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_cgpr1/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_cgpr1/test.esp32-c3-ard.yaml b/tests/components/xiaomi_cgpr1/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_cgpr1/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_cgpr1/test.esp32-c3-idf.yaml b/tests/components/xiaomi_cgpr1/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_cgpr1/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_cgpr1/test.esp32-idf.yaml b/tests/components/xiaomi_cgpr1/test.esp32-idf.yaml index dade44d145..7a6541ae76 100644 --- a/tests/components/xiaomi_cgpr1/test.esp32-idf.yaml +++ b/tests/components/xiaomi_cgpr1/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/xiaomi_gcls002/test.esp32-ard.yaml b/tests/components/xiaomi_gcls002/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_gcls002/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_gcls002/test.esp32-c3-ard.yaml b/tests/components/xiaomi_gcls002/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_gcls002/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_gcls002/test.esp32-c3-idf.yaml b/tests/components/xiaomi_gcls002/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_gcls002/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_gcls002/test.esp32-idf.yaml b/tests/components/xiaomi_gcls002/test.esp32-idf.yaml index dade44d145..7a6541ae76 100644 --- a/tests/components/xiaomi_gcls002/test.esp32-idf.yaml +++ b/tests/components/xiaomi_gcls002/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/xiaomi_hhccjcy01/test.esp32-ard.yaml b/tests/components/xiaomi_hhccjcy01/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_hhccjcy01/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_hhccjcy01/test.esp32-c3-ard.yaml b/tests/components/xiaomi_hhccjcy01/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_hhccjcy01/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_hhccjcy01/test.esp32-c3-idf.yaml b/tests/components/xiaomi_hhccjcy01/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_hhccjcy01/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_hhccjcy01/test.esp32-idf.yaml b/tests/components/xiaomi_hhccjcy01/test.esp32-idf.yaml index dade44d145..7a6541ae76 100644 --- a/tests/components/xiaomi_hhccjcy01/test.esp32-idf.yaml +++ b/tests/components/xiaomi_hhccjcy01/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/xiaomi_hhccpot002/test.esp32-ard.yaml b/tests/components/xiaomi_hhccpot002/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_hhccpot002/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_hhccpot002/test.esp32-c3-ard.yaml b/tests/components/xiaomi_hhccpot002/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_hhccpot002/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_hhccpot002/test.esp32-c3-idf.yaml b/tests/components/xiaomi_hhccpot002/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_hhccpot002/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_hhccpot002/test.esp32-idf.yaml b/tests/components/xiaomi_hhccpot002/test.esp32-idf.yaml index dade44d145..7a6541ae76 100644 --- a/tests/components/xiaomi_hhccpot002/test.esp32-idf.yaml +++ b/tests/components/xiaomi_hhccpot002/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/xiaomi_jqjcy01ym/test.esp32-ard.yaml b/tests/components/xiaomi_jqjcy01ym/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_jqjcy01ym/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_jqjcy01ym/test.esp32-c3-ard.yaml b/tests/components/xiaomi_jqjcy01ym/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_jqjcy01ym/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_jqjcy01ym/test.esp32-c3-idf.yaml b/tests/components/xiaomi_jqjcy01ym/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_jqjcy01ym/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_jqjcy01ym/test.esp32-idf.yaml b/tests/components/xiaomi_jqjcy01ym/test.esp32-idf.yaml index dade44d145..7a6541ae76 100644 --- a/tests/components/xiaomi_jqjcy01ym/test.esp32-idf.yaml +++ b/tests/components/xiaomi_jqjcy01ym/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/xiaomi_lywsd02/test.esp32-ard.yaml b/tests/components/xiaomi_lywsd02/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_lywsd02/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_lywsd02/test.esp32-c3-ard.yaml b/tests/components/xiaomi_lywsd02/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_lywsd02/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_lywsd02/test.esp32-c3-idf.yaml b/tests/components/xiaomi_lywsd02/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_lywsd02/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_lywsd02/test.esp32-idf.yaml b/tests/components/xiaomi_lywsd02/test.esp32-idf.yaml index dade44d145..7a6541ae76 100644 --- a/tests/components/xiaomi_lywsd02/test.esp32-idf.yaml +++ b/tests/components/xiaomi_lywsd02/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/xiaomi_lywsd02mmc/test.esp32-ard.yaml b/tests/components/xiaomi_lywsd02mmc/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_lywsd02mmc/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_lywsd02mmc/test.esp32-c3-ard.yaml b/tests/components/xiaomi_lywsd02mmc/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_lywsd02mmc/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_lywsd02mmc/test.esp32-c3-idf.yaml b/tests/components/xiaomi_lywsd02mmc/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_lywsd02mmc/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_lywsd02mmc/test.esp32-idf.yaml b/tests/components/xiaomi_lywsd02mmc/test.esp32-idf.yaml index dade44d145..7a6541ae76 100644 --- a/tests/components/xiaomi_lywsd02mmc/test.esp32-idf.yaml +++ b/tests/components/xiaomi_lywsd02mmc/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/xiaomi_lywsd03mmc/test.esp32-ard.yaml b/tests/components/xiaomi_lywsd03mmc/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_lywsd03mmc/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_lywsd03mmc/test.esp32-c3-ard.yaml b/tests/components/xiaomi_lywsd03mmc/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_lywsd03mmc/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_lywsd03mmc/test.esp32-c3-idf.yaml b/tests/components/xiaomi_lywsd03mmc/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_lywsd03mmc/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_lywsd03mmc/test.esp32-idf.yaml b/tests/components/xiaomi_lywsd03mmc/test.esp32-idf.yaml index dade44d145..7a6541ae76 100644 --- a/tests/components/xiaomi_lywsd03mmc/test.esp32-idf.yaml +++ b/tests/components/xiaomi_lywsd03mmc/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/xiaomi_lywsdcgq/test.esp32-ard.yaml b/tests/components/xiaomi_lywsdcgq/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_lywsdcgq/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_lywsdcgq/test.esp32-c3-ard.yaml b/tests/components/xiaomi_lywsdcgq/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_lywsdcgq/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_lywsdcgq/test.esp32-c3-idf.yaml b/tests/components/xiaomi_lywsdcgq/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_lywsdcgq/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_lywsdcgq/test.esp32-idf.yaml b/tests/components/xiaomi_lywsdcgq/test.esp32-idf.yaml index dade44d145..7a6541ae76 100644 --- a/tests/components/xiaomi_lywsdcgq/test.esp32-idf.yaml +++ b/tests/components/xiaomi_lywsdcgq/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/xiaomi_mhoc303/test.esp32-ard.yaml b/tests/components/xiaomi_mhoc303/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_mhoc303/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_mhoc303/test.esp32-c3-ard.yaml b/tests/components/xiaomi_mhoc303/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_mhoc303/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_mhoc303/test.esp32-c3-idf.yaml b/tests/components/xiaomi_mhoc303/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_mhoc303/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_mhoc303/test.esp32-idf.yaml b/tests/components/xiaomi_mhoc303/test.esp32-idf.yaml index dade44d145..7a6541ae76 100644 --- a/tests/components/xiaomi_mhoc303/test.esp32-idf.yaml +++ b/tests/components/xiaomi_mhoc303/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/xiaomi_mhoc401/test.esp32-ard.yaml b/tests/components/xiaomi_mhoc401/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_mhoc401/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_mhoc401/test.esp32-c3-ard.yaml b/tests/components/xiaomi_mhoc401/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_mhoc401/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_mhoc401/test.esp32-c3-idf.yaml b/tests/components/xiaomi_mhoc401/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_mhoc401/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_mhoc401/test.esp32-idf.yaml b/tests/components/xiaomi_mhoc401/test.esp32-idf.yaml index dade44d145..7a6541ae76 100644 --- a/tests/components/xiaomi_mhoc401/test.esp32-idf.yaml +++ b/tests/components/xiaomi_mhoc401/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/xiaomi_miscale copy/common.yaml b/tests/components/xiaomi_miscale copy/common.yaml deleted file mode 100644 index 89f32ad199..0000000000 --- a/tests/components/xiaomi_miscale copy/common.yaml +++ /dev/null @@ -1,9 +0,0 @@ -esp32_ble_tracker: - -sensor: - - platform: xiaomi_miscale - mac_address: '5C:CA:D3:70:D4:A2' - weight: - name: "Xiaomi Mi Scale Weight" - impedance: - name: "Xiaomi Mi Scale Impedance" diff --git a/tests/components/xiaomi_miscale copy/test.esp32-ard.yaml b/tests/components/xiaomi_miscale copy/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_miscale copy/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_miscale copy/test.esp32-c3-ard.yaml b/tests/components/xiaomi_miscale copy/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_miscale copy/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_miscale copy/test.esp32-c3-idf.yaml b/tests/components/xiaomi_miscale copy/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_miscale copy/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_miscale/test.esp32-ard.yaml b/tests/components/xiaomi_miscale/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_miscale/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_miscale/test.esp32-c3-ard.yaml b/tests/components/xiaomi_miscale/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_miscale/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_miscale/test.esp32-c3-idf.yaml b/tests/components/xiaomi_miscale/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_miscale/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_miscale/test.esp32-idf.yaml b/tests/components/xiaomi_miscale/test.esp32-idf.yaml index dade44d145..7a6541ae76 100644 --- a/tests/components/xiaomi_miscale/test.esp32-idf.yaml +++ b/tests/components/xiaomi_miscale/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/xiaomi_mjyd02yla/test.esp32-ard.yaml b/tests/components/xiaomi_mjyd02yla/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_mjyd02yla/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_mjyd02yla/test.esp32-c3-ard.yaml b/tests/components/xiaomi_mjyd02yla/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_mjyd02yla/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_mjyd02yla/test.esp32-c3-idf.yaml b/tests/components/xiaomi_mjyd02yla/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_mjyd02yla/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_mjyd02yla/test.esp32-idf.yaml b/tests/components/xiaomi_mjyd02yla/test.esp32-idf.yaml index dade44d145..7a6541ae76 100644 --- a/tests/components/xiaomi_mjyd02yla/test.esp32-idf.yaml +++ b/tests/components/xiaomi_mjyd02yla/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/xiaomi_mue4094rt/test.esp32-ard.yaml b/tests/components/xiaomi_mue4094rt/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_mue4094rt/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_mue4094rt/test.esp32-c3-ard.yaml b/tests/components/xiaomi_mue4094rt/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_mue4094rt/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_mue4094rt/test.esp32-c3-idf.yaml b/tests/components/xiaomi_mue4094rt/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_mue4094rt/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_mue4094rt/test.esp32-idf.yaml b/tests/components/xiaomi_mue4094rt/test.esp32-idf.yaml index dade44d145..7a6541ae76 100644 --- a/tests/components/xiaomi_mue4094rt/test.esp32-idf.yaml +++ b/tests/components/xiaomi_mue4094rt/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/xiaomi_rtcgq02lm/test.esp32-ard.yaml b/tests/components/xiaomi_rtcgq02lm/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_rtcgq02lm/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_rtcgq02lm/test.esp32-c3-ard.yaml b/tests/components/xiaomi_rtcgq02lm/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_rtcgq02lm/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_rtcgq02lm/test.esp32-c3-idf.yaml b/tests/components/xiaomi_rtcgq02lm/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_rtcgq02lm/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_rtcgq02lm/test.esp32-idf.yaml b/tests/components/xiaomi_rtcgq02lm/test.esp32-idf.yaml index dade44d145..7a6541ae76 100644 --- a/tests/components/xiaomi_rtcgq02lm/test.esp32-idf.yaml +++ b/tests/components/xiaomi_rtcgq02lm/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/xiaomi_wx08zm/test.esp32-ard.yaml b/tests/components/xiaomi_wx08zm/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_wx08zm/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_wx08zm/test.esp32-c3-ard.yaml b/tests/components/xiaomi_wx08zm/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_wx08zm/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_wx08zm/test.esp32-c3-idf.yaml b/tests/components/xiaomi_wx08zm/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_wx08zm/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_wx08zm/test.esp32-idf.yaml b/tests/components/xiaomi_wx08zm/test.esp32-idf.yaml index dade44d145..7a6541ae76 100644 --- a/tests/components/xiaomi_wx08zm/test.esp32-idf.yaml +++ b/tests/components/xiaomi_wx08zm/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/xiaomi_xmwsdj04mmc/test.esp32-ard.yaml b/tests/components/xiaomi_xmwsdj04mmc/test.esp32-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_xmwsdj04mmc/test.esp32-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_xmwsdj04mmc/test.esp32-c3-ard.yaml b/tests/components/xiaomi_xmwsdj04mmc/test.esp32-c3-ard.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_xmwsdj04mmc/test.esp32-c3-ard.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_xmwsdj04mmc/test.esp32-c3-idf.yaml b/tests/components/xiaomi_xmwsdj04mmc/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145..0000000000 --- a/tests/components/xiaomi_xmwsdj04mmc/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/xiaomi_xmwsdj04mmc/test.esp32-idf.yaml b/tests/components/xiaomi_xmwsdj04mmc/test.esp32-idf.yaml index dade44d145..7a6541ae76 100644 --- a/tests/components/xiaomi_xmwsdj04mmc/test.esp32-idf.yaml +++ b/tests/components/xiaomi_xmwsdj04mmc/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/xl9535/common.yaml b/tests/components/xl9535/common.yaml index e01163cf12..81e96131ab 100644 --- a/tests/components/xl9535/common.yaml +++ b/tests/components/xl9535/common.yaml @@ -1,10 +1,6 @@ -i2c: - - id: i2c_xl9535 - scl: ${scl_pin} - sda: ${sda_pin} - xl9535: - id: xl9535_hub + i2c_id: i2c_bus address: 0x20 binary_sensor: diff --git a/tests/components/xl9535/test.esp32-ard.yaml b/tests/components/xl9535/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/xl9535/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/xl9535/test.esp32-c3-ard.yaml b/tests/components/xl9535/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/xl9535/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/xl9535/test.esp32-c3-idf.yaml b/tests/components/xl9535/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/xl9535/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/xl9535/test.esp32-idf.yaml b/tests/components/xl9535/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/xl9535/test.esp32-idf.yaml +++ b/tests/components/xl9535/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/xl9535/test.esp8266-ard.yaml b/tests/components/xl9535/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/xl9535/test.esp8266-ard.yaml +++ b/tests/components/xl9535/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/xl9535/test.rp2040-ard.yaml b/tests/components/xl9535/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/xl9535/test.rp2040-ard.yaml +++ b/tests/components/xl9535/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/xpt2046/common.yaml b/tests/components/xpt2046/common.yaml index 9ef680cff4..3a8e3286a6 100644 --- a/tests/components/xpt2046/common.yaml +++ b/tests/components/xpt2046/common.yaml @@ -1,9 +1,3 @@ -spi: - - id: spi_xpt2046 - clk_pin: ${clk_pin} - mosi_pin: ${mosi_pin} - miso_pin: ${miso_pin} - display: - platform: ili9xxx id: xpt_display diff --git a/tests/components/xpt2046/test.esp32-ard.yaml b/tests/components/xpt2046/test.esp32-ard.yaml deleted file mode 100644 index b39174947b..0000000000 --- a/tests/components/xpt2046/test.esp32-ard.yaml +++ /dev/null @@ -1,11 +0,0 @@ -substitutions: - clk_pin: GPIO17 - mosi_pin: GPIO18 - miso_pin: GPIO19 - dc_pin: GPIO13 - cs_pin: GPIO14 - disp_cs_pin: GPIO4 - interrupt_pin: GPIO21 - reset_pin: GPIO22 - -<<: !include common.yaml diff --git a/tests/components/xpt2046/test.esp32-c3-ard.yaml b/tests/components/xpt2046/test.esp32-c3-ard.yaml deleted file mode 100644 index 79b84902ac..0000000000 --- a/tests/components/xpt2046/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,11 +0,0 @@ -substitutions: - clk_pin: GPIO4 - mosi_pin: GPIO5 - miso_pin: GPIO6 - dc_pin: GPIO7 - cs_pin: GPIO0 - disp_cs_pin: GPIO1 - interrupt_pin: GPIO3 - reset_pin: GPIO10 - -<<: !include common.yaml diff --git a/tests/components/xpt2046/test.esp32-c3-idf.yaml b/tests/components/xpt2046/test.esp32-c3-idf.yaml deleted file mode 100644 index 79b84902ac..0000000000 --- a/tests/components/xpt2046/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,11 +0,0 @@ -substitutions: - clk_pin: GPIO4 - mosi_pin: GPIO5 - miso_pin: GPIO6 - dc_pin: GPIO7 - cs_pin: GPIO0 - disp_cs_pin: GPIO1 - interrupt_pin: GPIO3 - reset_pin: GPIO10 - -<<: !include common.yaml diff --git a/tests/components/xpt2046/test.esp32-idf.yaml b/tests/components/xpt2046/test.esp32-idf.yaml index b39174947b..608e135d74 100644 --- a/tests/components/xpt2046/test.esp32-idf.yaml +++ b/tests/components/xpt2046/test.esp32-idf.yaml @@ -1,11 +1,11 @@ substitutions: - clk_pin: GPIO17 - mosi_pin: GPIO18 - miso_pin: GPIO19 dc_pin: GPIO13 cs_pin: GPIO14 disp_cs_pin: GPIO4 interrupt_pin: GPIO21 reset_pin: GPIO22 +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/xpt2046/test.esp8266-ard.yaml b/tests/components/xpt2046/test.esp8266-ard.yaml index 246c5c8953..e2779a03b9 100644 --- a/tests/components/xpt2046/test.esp8266-ard.yaml +++ b/tests/components/xpt2046/test.esp8266-ard.yaml @@ -1,5 +1,5 @@ substitutions: - clk_pin: GPIO14 + clk_pin: GPIO0 mosi_pin: GPIO13 miso_pin: GPIO12 dc_pin: GPIO15 @@ -8,4 +8,7 @@ substitutions: interrupt_pin: GPIO5 reset_pin: GPIO2 +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/xpt2046/test.rp2040-ard.yaml b/tests/components/xpt2046/test.rp2040-ard.yaml index e693b363d9..c547bdc0c9 100644 --- a/tests/components/xpt2046/test.rp2040-ard.yaml +++ b/tests/components/xpt2046/test.rp2040-ard.yaml @@ -8,4 +8,7 @@ substitutions: interrupt_pin: GPIO2 reset_pin: GPIO3 +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml + <<: !include common.yaml diff --git a/tests/components/yashima/common.yaml b/tests/components/yashima/common.yaml index bfe181f1a6..431c27ebb3 100644 --- a/tests/components/yashima/common.yaml +++ b/tests/components/yashima/common.yaml @@ -1,7 +1,4 @@ -remote_transmitter: - pin: ${pin} - carrier_duty_percent: 50% - climate: - platform: yashima name: Yashima Climate + transmitter_id: xmitr diff --git a/tests/components/yashima/test.esp32-ard.yaml b/tests/components/yashima/test.esp32-ard.yaml deleted file mode 100644 index 7b012aa64c..0000000000 --- a/tests/components/yashima/test.esp32-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/yashima/test.esp32-c3-ard.yaml b/tests/components/yashima/test.esp32-c3-ard.yaml deleted file mode 100644 index 7b012aa64c..0000000000 --- a/tests/components/yashima/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/yashima/test.esp32-c3-idf.yaml b/tests/components/yashima/test.esp32-c3-idf.yaml deleted file mode 100644 index 7b012aa64c..0000000000 --- a/tests/components/yashima/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/yashima/test.esp32-idf.yaml b/tests/components/yashima/test.esp32-idf.yaml index 7b012aa64c..e891f9dc85 100644 --- a/tests/components/yashima/test.esp32-idf.yaml +++ b/tests/components/yashima/test.esp32-idf.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO2 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/yashima/test.esp8266-ard.yaml b/tests/components/yashima/test.esp8266-ard.yaml index f5097fcf5f..4bed2f03e5 100644 --- a/tests/components/yashima/test.esp8266-ard.yaml +++ b/tests/components/yashima/test.esp8266-ard.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO5 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/zhlt01/common.yaml b/tests/components/zhlt01/common.yaml index 0adbe77325..d0fd531c87 100644 --- a/tests/components/zhlt01/common.yaml +++ b/tests/components/zhlt01/common.yaml @@ -1,7 +1,4 @@ -remote_transmitter: - pin: ${pin} - carrier_duty_percent: 50% - climate: - platform: zhlt01 name: ZH/LT-01 Climate + transmitter_id: xmitr diff --git a/tests/components/zhlt01/test.esp32-ard.yaml b/tests/components/zhlt01/test.esp32-ard.yaml deleted file mode 100644 index 7b012aa64c..0000000000 --- a/tests/components/zhlt01/test.esp32-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/zhlt01/test.esp32-c3-ard.yaml b/tests/components/zhlt01/test.esp32-c3-ard.yaml deleted file mode 100644 index 7b012aa64c..0000000000 --- a/tests/components/zhlt01/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/zhlt01/test.esp32-c3-idf.yaml b/tests/components/zhlt01/test.esp32-c3-idf.yaml deleted file mode 100644 index 7b012aa64c..0000000000 --- a/tests/components/zhlt01/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/zhlt01/test.esp32-idf.yaml b/tests/components/zhlt01/test.esp32-idf.yaml index 7b012aa64c..e891f9dc85 100644 --- a/tests/components/zhlt01/test.esp32-idf.yaml +++ b/tests/components/zhlt01/test.esp32-idf.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO2 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/zhlt01/test.esp8266-ard.yaml b/tests/components/zhlt01/test.esp8266-ard.yaml index f5097fcf5f..4bed2f03e5 100644 --- a/tests/components/zhlt01/test.esp8266-ard.yaml +++ b/tests/components/zhlt01/test.esp8266-ard.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO5 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/zio_ultrasonic/common.yaml b/tests/components/zio_ultrasonic/common.yaml index e13853d8f1..34a3144db9 100644 --- a/tests/components/zio_ultrasonic/common.yaml +++ b/tests/components/zio_ultrasonic/common.yaml @@ -1,9 +1,5 @@ -i2c: - - id: i2c_zio_ultrasonic - scl: ${scl_pin} - sda: ${sda_pin} - sensor: - platform: zio_ultrasonic + i2c_id: i2c_bus name: "Distance" update_interval: 60s diff --git a/tests/components/zio_ultrasonic/test.esp32-ard.yaml b/tests/components/zio_ultrasonic/test.esp32-ard.yaml deleted file mode 100644 index 63c3bd6afd..0000000000 --- a/tests/components/zio_ultrasonic/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/zio_ultrasonic/test.esp32-c3-ard.yaml b/tests/components/zio_ultrasonic/test.esp32-c3-ard.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/zio_ultrasonic/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/zio_ultrasonic/test.esp32-c3-idf.yaml b/tests/components/zio_ultrasonic/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2c29ca4e..0000000000 --- a/tests/components/zio_ultrasonic/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/zio_ultrasonic/test.esp32-idf.yaml b/tests/components/zio_ultrasonic/test.esp32-idf.yaml index 63c3bd6afd..b47e39c389 100644 --- a/tests/components/zio_ultrasonic/test.esp32-idf.yaml +++ b/tests/components/zio_ultrasonic/test.esp32-idf.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO16 - sda_pin: GPIO17 +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/zio_ultrasonic/test.esp8266-ard.yaml b/tests/components/zio_ultrasonic/test.esp8266-ard.yaml index ee2c29ca4e..4a98b9388a 100644 --- a/tests/components/zio_ultrasonic/test.esp8266-ard.yaml +++ b/tests/components/zio_ultrasonic/test.esp8266-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/zio_ultrasonic/test.rp2040-ard.yaml b/tests/components/zio_ultrasonic/test.rp2040-ard.yaml index ee2c29ca4e..319a7c71a6 100644 --- a/tests/components/zio_ultrasonic/test.rp2040-ard.yaml +++ b/tests/components/zio_ultrasonic/test.rp2040-ard.yaml @@ -1,5 +1,4 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/zwave_proxy/common.yaml b/tests/components/zwave_proxy/common.yaml new file mode 100644 index 0000000000..097e4b43a5 --- /dev/null +++ b/tests/components/zwave_proxy/common.yaml @@ -0,0 +1,9 @@ +wifi: + ssid: MySSID + password: password1 + power_save_mode: none + +api: + +zwave_proxy: + id: zw_proxy diff --git a/tests/components/zwave_proxy/test.esp32-idf.yaml b/tests/components/zwave_proxy/test.esp32-idf.yaml new file mode 100644 index 0000000000..b415125e84 --- /dev/null +++ b/tests/components/zwave_proxy/test.esp32-idf.yaml @@ -0,0 +1,8 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/zwave_proxy/test.esp8266-ard.yaml b/tests/components/zwave_proxy/test.esp8266-ard.yaml new file mode 100644 index 0000000000..96ab4ef6ac --- /dev/null +++ b/tests/components/zwave_proxy/test.esp8266-ard.yaml @@ -0,0 +1,8 @@ +substitutions: + tx_pin: GPIO0 + rx_pin: GPIO2 + +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/zwave_proxy/test.rp2040-ard.yaml b/tests/components/zwave_proxy/test.rp2040-ard.yaml new file mode 100644 index 0000000000..b28f2b5e05 --- /dev/null +++ b/tests/components/zwave_proxy/test.rp2040-ard.yaml @@ -0,0 +1,8 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/zyaura/test.esp32-ard.yaml b/tests/components/zyaura/test.esp32-ard.yaml deleted file mode 100644 index d295973e3f..0000000000 --- a/tests/components/zyaura/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - clock_pin: GPIO16 - data_pin: GPIO17 - -<<: !include common.yaml diff --git a/tests/components/zyaura/test.esp32-c3-ard.yaml b/tests/components/zyaura/test.esp32-c3-ard.yaml deleted file mode 100644 index 7808481215..0000000000 --- a/tests/components/zyaura/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - clock_pin: GPIO5 - data_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/zyaura/test.esp32-c3-idf.yaml b/tests/components/zyaura/test.esp32-c3-idf.yaml deleted file mode 100644 index 7808481215..0000000000 --- a/tests/components/zyaura/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - clock_pin: GPIO5 - data_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/zyaura/test.esp32-idf.yaml b/tests/components/zyaura/test.esp32-idf.yaml index d295973e3f..a4ecdb6c49 100644 --- a/tests/components/zyaura/test.esp32-idf.yaml +++ b/tests/components/zyaura/test.esp32-idf.yaml @@ -1,5 +1,5 @@ substitutions: - clock_pin: GPIO16 - data_pin: GPIO17 + clock_pin: GPIO4 + data_pin: GPIO5 <<: !include common.yaml diff --git a/tests/components/zyaura/test.esp8266-ard.yaml b/tests/components/zyaura/test.esp8266-ard.yaml index 7808481215..7c7f1e1a11 100644 --- a/tests/components/zyaura/test.esp8266-ard.yaml +++ b/tests/components/zyaura/test.esp8266-ard.yaml @@ -1,5 +1,5 @@ substitutions: - clock_pin: GPIO5 - data_pin: GPIO4 + clock_pin: GPIO0 + data_pin: GPIO2 <<: !include common.yaml diff --git a/tests/dashboard/conftest.py b/tests/dashboard/conftest.py new file mode 100644 index 0000000000..f95adef749 --- /dev/null +++ b/tests/dashboard/conftest.py @@ -0,0 +1,43 @@ +"""Common fixtures for dashboard tests.""" + +from __future__ import annotations + +from pathlib import Path +from unittest.mock import MagicMock, Mock + +import pytest +import pytest_asyncio + +from esphome.dashboard.core import ESPHomeDashboard +from esphome.dashboard.entries import DashboardEntries + + +@pytest.fixture +def mock_settings(tmp_path: Path) -> MagicMock: + """Create mock dashboard settings.""" + settings = MagicMock() + settings.config_dir = str(tmp_path) + settings.absolute_config_dir = tmp_path + return settings + + +@pytest.fixture +def mock_dashboard(mock_settings: MagicMock) -> Mock: + """Create a mock dashboard.""" + dashboard = Mock(spec=ESPHomeDashboard) + dashboard.settings = mock_settings + dashboard.entries = Mock() + dashboard.entries.async_all.return_value = [] + dashboard.stop_event = Mock() + dashboard.stop_event.is_set.return_value = True + dashboard.ping_request = Mock() + dashboard.ignored_devices = set() + dashboard.bus = Mock() + dashboard.bus.async_fire = Mock() + return dashboard + + +@pytest_asyncio.fixture +async def dashboard_entries(mock_dashboard: Mock) -> DashboardEntries: + """Create a DashboardEntries instance for testing.""" + return DashboardEntries(mock_dashboard) diff --git a/tests/dashboard/status/__init__.py b/tests/dashboard/status/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/dashboard/status/test_dns.py b/tests/dashboard/status/test_dns.py new file mode 100644 index 0000000000..9ca48ba2d8 --- /dev/null +++ b/tests/dashboard/status/test_dns.py @@ -0,0 +1,121 @@ +"""Unit tests for esphome.dashboard.dns module.""" + +from __future__ import annotations + +import time +from unittest.mock import patch + +import pytest + +from esphome.dashboard.dns import DNSCache + + +@pytest.fixture +def dns_cache_fixture() -> DNSCache: + """Create a DNSCache instance.""" + return DNSCache() + + +def test_get_cached_addresses_not_in_cache(dns_cache_fixture: DNSCache) -> None: + """Test get_cached_addresses when hostname is not in cache.""" + now = time.monotonic() + result = dns_cache_fixture.get_cached_addresses("unknown.example.com", now) + assert result is None + + +def test_get_cached_addresses_expired(dns_cache_fixture: DNSCache) -> None: + """Test get_cached_addresses when cache entry is expired.""" + now = time.monotonic() + # Add entry that's already expired + dns_cache_fixture._cache["example.com"] = (now - 1, ["192.168.1.10"]) + + result = dns_cache_fixture.get_cached_addresses("example.com", now) + assert result is None + # Expired entry should still be in cache (not removed by get_cached_addresses) + assert "example.com" in dns_cache_fixture._cache + + +def test_get_cached_addresses_valid(dns_cache_fixture: DNSCache) -> None: + """Test get_cached_addresses with valid cache entry.""" + now = time.monotonic() + # Add entry that expires in 60 seconds + dns_cache_fixture._cache["example.com"] = ( + now + 60, + ["192.168.1.10", "192.168.1.11"], + ) + + result = dns_cache_fixture.get_cached_addresses("example.com", now) + assert result == ["192.168.1.10", "192.168.1.11"] + # Entry should still be in cache + assert "example.com" in dns_cache_fixture._cache + + +def test_get_cached_addresses_hostname_normalization( + dns_cache_fixture: DNSCache, +) -> None: + """Test get_cached_addresses normalizes hostname.""" + now = time.monotonic() + # Add entry with lowercase hostname + dns_cache_fixture._cache["example.com"] = (now + 60, ["192.168.1.10"]) + + # Test with various forms + assert dns_cache_fixture.get_cached_addresses("EXAMPLE.COM", now) == [ + "192.168.1.10" + ] + assert dns_cache_fixture.get_cached_addresses("example.com.", now) == [ + "192.168.1.10" + ] + assert dns_cache_fixture.get_cached_addresses("EXAMPLE.COM.", now) == [ + "192.168.1.10" + ] + + +def test_get_cached_addresses_ipv6(dns_cache_fixture: DNSCache) -> None: + """Test get_cached_addresses with IPv6 addresses.""" + now = time.monotonic() + dns_cache_fixture._cache["example.com"] = (now + 60, ["2001:db8::1", "fe80::1"]) + + result = dns_cache_fixture.get_cached_addresses("example.com", now) + assert result == ["2001:db8::1", "fe80::1"] + + +def test_get_cached_addresses_empty_list(dns_cache_fixture: DNSCache) -> None: + """Test get_cached_addresses with empty address list.""" + now = time.monotonic() + dns_cache_fixture._cache["example.com"] = (now + 60, []) + + result = dns_cache_fixture.get_cached_addresses("example.com", now) + assert result == [] + + +def test_get_cached_addresses_exception_in_cache(dns_cache_fixture: DNSCache) -> None: + """Test get_cached_addresses when cache contains an exception.""" + now = time.monotonic() + # Store an exception (from failed resolution) + dns_cache_fixture._cache["example.com"] = (now + 60, OSError("Resolution failed")) + + result = dns_cache_fixture.get_cached_addresses("example.com", now) + assert result is None # Should return None for exceptions + + +def test_async_resolve_not_called(dns_cache_fixture: DNSCache) -> None: + """Test that get_cached_addresses never calls async_resolve.""" + now = time.monotonic() + + with patch.object(dns_cache_fixture, "async_resolve") as mock_resolve: + # Test non-cached + result = dns_cache_fixture.get_cached_addresses("uncached.com", now) + assert result is None + mock_resolve.assert_not_called() + + # Test expired + dns_cache_fixture._cache["expired.com"] = (now - 1, ["192.168.1.10"]) + result = dns_cache_fixture.get_cached_addresses("expired.com", now) + assert result is None + mock_resolve.assert_not_called() + + # Test valid + dns_cache_fixture._cache["valid.com"] = (now + 60, ["192.168.1.10"]) + result = dns_cache_fixture.get_cached_addresses("valid.com", now) + assert result == ["192.168.1.10"] + mock_resolve.assert_not_called() diff --git a/tests/dashboard/status/test_mdns.py b/tests/dashboard/status/test_mdns.py new file mode 100644 index 0000000000..56c6d254cf --- /dev/null +++ b/tests/dashboard/status/test_mdns.py @@ -0,0 +1,240 @@ +"""Unit tests for esphome.dashboard.status.mdns module.""" + +from __future__ import annotations + +from unittest.mock import Mock, patch + +import pytest +import pytest_asyncio +from zeroconf import AddressResolver, IPVersion + +from esphome.dashboard.const import DashboardEvent +from esphome.dashboard.status.mdns import MDNSStatus +from esphome.zeroconf import DiscoveredImport + + +@pytest_asyncio.fixture +async def mdns_status(mock_dashboard: Mock) -> MDNSStatus: + """Create an MDNSStatus instance in async context.""" + # We're in an async context so get_running_loop will work + return MDNSStatus(mock_dashboard) + + +@pytest.mark.asyncio +async def test_get_cached_addresses_no_zeroconf(mdns_status: MDNSStatus) -> None: + """Test get_cached_addresses when no zeroconf instance is available.""" + mdns_status.aiozc = None + result = mdns_status.get_cached_addresses("device.local") + assert result is None + + +@pytest.mark.asyncio +async def test_get_cached_addresses_not_in_cache(mdns_status: MDNSStatus) -> None: + """Test get_cached_addresses when address is not in cache.""" + mdns_status.aiozc = Mock() + mdns_status.aiozc.zeroconf = Mock() + + with patch("esphome.dashboard.status.mdns.AddressResolver") as mock_resolver: + mock_info = Mock(spec=AddressResolver) + mock_info.load_from_cache.return_value = False + mock_resolver.return_value = mock_info + + result = mdns_status.get_cached_addresses("device.local") + assert result is None + mock_info.load_from_cache.assert_called_once_with(mdns_status.aiozc.zeroconf) + + +@pytest.mark.asyncio +async def test_get_cached_addresses_found_in_cache(mdns_status: MDNSStatus) -> None: + """Test get_cached_addresses when address is found in cache.""" + mdns_status.aiozc = Mock() + mdns_status.aiozc.zeroconf = Mock() + + with patch("esphome.dashboard.status.mdns.AddressResolver") as mock_resolver: + mock_info = Mock(spec=AddressResolver) + mock_info.load_from_cache.return_value = True + mock_info.parsed_scoped_addresses.return_value = ["192.168.1.10", "fe80::1"] + mock_resolver.return_value = mock_info + + result = mdns_status.get_cached_addresses("device.local") + assert result == ["192.168.1.10", "fe80::1"] + mock_info.load_from_cache.assert_called_once_with(mdns_status.aiozc.zeroconf) + mock_info.parsed_scoped_addresses.assert_called_once_with(IPVersion.All) + + +@pytest.mark.asyncio +async def test_get_cached_addresses_with_trailing_dot(mdns_status: MDNSStatus) -> None: + """Test get_cached_addresses with hostname having trailing dot.""" + mdns_status.aiozc = Mock() + mdns_status.aiozc.zeroconf = Mock() + + with patch("esphome.dashboard.status.mdns.AddressResolver") as mock_resolver: + mock_info = Mock(spec=AddressResolver) + mock_info.load_from_cache.return_value = True + mock_info.parsed_scoped_addresses.return_value = ["192.168.1.10"] + mock_resolver.return_value = mock_info + + result = mdns_status.get_cached_addresses("device.local.") + assert result == ["192.168.1.10"] + # Should normalize to device.local. for zeroconf + mock_resolver.assert_called_once_with("device.local.") + + +@pytest.mark.asyncio +async def test_get_cached_addresses_uppercase_hostname(mdns_status: MDNSStatus) -> None: + """Test get_cached_addresses with uppercase hostname.""" + mdns_status.aiozc = Mock() + mdns_status.aiozc.zeroconf = Mock() + + with patch("esphome.dashboard.status.mdns.AddressResolver") as mock_resolver: + mock_info = Mock(spec=AddressResolver) + mock_info.load_from_cache.return_value = True + mock_info.parsed_scoped_addresses.return_value = ["192.168.1.10"] + mock_resolver.return_value = mock_info + + result = mdns_status.get_cached_addresses("DEVICE.LOCAL") + assert result == ["192.168.1.10"] + # Should normalize to device.local. for zeroconf + mock_resolver.assert_called_once_with("device.local.") + + +@pytest.mark.asyncio +async def test_get_cached_addresses_simple_hostname(mdns_status: MDNSStatus) -> None: + """Test get_cached_addresses with simple hostname (no domain).""" + mdns_status.aiozc = Mock() + mdns_status.aiozc.zeroconf = Mock() + + with patch("esphome.dashboard.status.mdns.AddressResolver") as mock_resolver: + mock_info = Mock(spec=AddressResolver) + mock_info.load_from_cache.return_value = True + mock_info.parsed_scoped_addresses.return_value = ["192.168.1.10"] + mock_resolver.return_value = mock_info + + result = mdns_status.get_cached_addresses("device") + assert result == ["192.168.1.10"] + # Should append .local. for zeroconf + mock_resolver.assert_called_once_with("device.local.") + + +@pytest.mark.asyncio +async def test_get_cached_addresses_ipv6_only(mdns_status: MDNSStatus) -> None: + """Test get_cached_addresses returning only IPv6 addresses.""" + mdns_status.aiozc = Mock() + mdns_status.aiozc.zeroconf = Mock() + + with patch("esphome.dashboard.status.mdns.AddressResolver") as mock_resolver: + mock_info = Mock(spec=AddressResolver) + mock_info.load_from_cache.return_value = True + mock_info.parsed_scoped_addresses.return_value = ["fe80::1", "2001:db8::1"] + mock_resolver.return_value = mock_info + + result = mdns_status.get_cached_addresses("device.local") + assert result == ["fe80::1", "2001:db8::1"] + + +@pytest.mark.asyncio +async def test_get_cached_addresses_empty_list(mdns_status: MDNSStatus) -> None: + """Test get_cached_addresses returning empty list from cache.""" + mdns_status.aiozc = Mock() + mdns_status.aiozc.zeroconf = Mock() + + with patch("esphome.dashboard.status.mdns.AddressResolver") as mock_resolver: + mock_info = Mock(spec=AddressResolver) + mock_info.load_from_cache.return_value = True + mock_info.parsed_scoped_addresses.return_value = [] + mock_resolver.return_value = mock_info + + result = mdns_status.get_cached_addresses("device.local") + assert result == [] + + +@pytest.mark.asyncio +async def test_async_setup_success(mock_dashboard: Mock) -> None: + """Test successful async_setup.""" + mdns_status = MDNSStatus(mock_dashboard) + with patch("esphome.dashboard.status.mdns.AsyncEsphomeZeroconf") as mock_zc: + mock_zc.return_value = Mock() + result = mdns_status.async_setup() + assert result is True + assert mdns_status.aiozc is not None + + +@pytest.mark.asyncio +async def test_async_setup_failure(mock_dashboard: Mock) -> None: + """Test async_setup with OSError.""" + mdns_status = MDNSStatus(mock_dashboard) + with patch("esphome.dashboard.status.mdns.AsyncEsphomeZeroconf") as mock_zc: + mock_zc.side_effect = OSError("Network error") + result = mdns_status.async_setup() + assert result is False + assert mdns_status.aiozc is None + + +@pytest.mark.asyncio +async def test_on_import_update_device_added(mdns_status: MDNSStatus) -> None: + """Test _on_import_update when a device is added.""" + # Create a DiscoveredImport object + discovered = DiscoveredImport( + device_name="test_device", + friendly_name="Test Device", + package_import_url="https://example.com/package", + project_name="test_project", + project_version="1.0.0", + network="wifi", + ) + + # Call _on_import_update with a device + mdns_status._on_import_update("test_device", discovered) + + # Should fire IMPORTABLE_DEVICE_ADDED event + mock_dashboard = mdns_status.dashboard + mock_dashboard.bus.async_fire.assert_called_once() + call_args = mock_dashboard.bus.async_fire.call_args + assert call_args[0][0] == DashboardEvent.IMPORTABLE_DEVICE_ADDED + assert "device" in call_args[0][1] + device_data = call_args[0][1]["device"] + assert device_data["name"] == "test_device" + assert device_data["friendly_name"] == "Test Device" + assert device_data["project_name"] == "test_project" + assert device_data["ignored"] is False + + +@pytest.mark.asyncio +async def test_on_import_update_device_ignored(mdns_status: MDNSStatus) -> None: + """Test _on_import_update when a device is ignored.""" + # Add device to ignored list + mdns_status.dashboard.ignored_devices.add("ignored_device") + + # Create a DiscoveredImport object for ignored device + discovered = DiscoveredImport( + device_name="ignored_device", + friendly_name="Ignored Device", + package_import_url="https://example.com/package", + project_name="test_project", + project_version="1.0.0", + network="ethernet", + ) + + # Call _on_import_update with an ignored device + mdns_status._on_import_update("ignored_device", discovered) + + # Should fire IMPORTABLE_DEVICE_ADDED event with ignored=True + mock_dashboard = mdns_status.dashboard + mock_dashboard.bus.async_fire.assert_called_once() + call_args = mock_dashboard.bus.async_fire.call_args + assert call_args[0][0] == DashboardEvent.IMPORTABLE_DEVICE_ADDED + device_data = call_args[0][1]["device"] + assert device_data["name"] == "ignored_device" + assert device_data["ignored"] is True + + +@pytest.mark.asyncio +async def test_on_import_update_device_removed(mdns_status: MDNSStatus) -> None: + """Test _on_import_update when a device is removed.""" + # Call _on_import_update with None (device removed) + mdns_status._on_import_update("removed_device", None) + + # Should fire IMPORTABLE_DEVICE_REMOVED event + mdns_status.dashboard.bus.async_fire.assert_called_once_with( + DashboardEvent.IMPORTABLE_DEVICE_REMOVED, {"name": "removed_device"} + ) diff --git a/tests/dashboard/test_entries.py b/tests/dashboard/test_entries.py new file mode 100644 index 0000000000..9a3a776b28 --- /dev/null +++ b/tests/dashboard/test_entries.py @@ -0,0 +1,288 @@ +"""Tests for dashboard entries Path-related functionality.""" + +from __future__ import annotations + +import os +from pathlib import Path +import tempfile +from unittest.mock import Mock + +import pytest + +from esphome.core import CORE +from esphome.dashboard.const import DashboardEvent +from esphome.dashboard.entries import DashboardEntries, DashboardEntry + + +def create_cache_key() -> tuple[int, int, float, int]: + """Helper to create a valid DashboardCacheKeyType.""" + return (0, 0, 0.0, 0) + + +@pytest.fixture(autouse=True) +def setup_core(): + """Set up CORE for testing.""" + with tempfile.TemporaryDirectory() as tmpdir: + CORE.config_path = Path(tmpdir) / "test.yaml" + yield + CORE.reset() + + +def test_dashboard_entry_path_initialization() -> None: + """Test DashboardEntry initializes with path correctly.""" + test_path = Path("/test/config/device.yaml") + cache_key = create_cache_key() + + entry = DashboardEntry(test_path, cache_key) + + assert entry.path == test_path + assert entry.cache_key == cache_key + + +def test_dashboard_entry_path_with_absolute_path() -> None: + """Test DashboardEntry handles absolute paths.""" + # Use a truly absolute path for the platform + test_path = Path.cwd() / "absolute" / "path" / "to" / "config.yaml" + cache_key = create_cache_key() + + entry = DashboardEntry(test_path, cache_key) + + assert entry.path == test_path + assert entry.path.is_absolute() + + +def test_dashboard_entry_path_with_relative_path() -> None: + """Test DashboardEntry handles relative paths.""" + test_path = Path("configs/device.yaml") + cache_key = create_cache_key() + + entry = DashboardEntry(test_path, cache_key) + + assert entry.path == test_path + assert not entry.path.is_absolute() + + +@pytest.mark.asyncio +async def test_dashboard_entries_get_by_path( + dashboard_entries: DashboardEntries, tmp_path: Path +) -> None: + """Test getting entry by path.""" + # Create a test file + test_file = tmp_path / "device.yaml" + test_file.write_text("test config") + + # Update entries to load the file + await dashboard_entries.async_update_entries() + + # Verify the entry was loaded + all_entries = dashboard_entries.async_all() + assert len(all_entries) == 1 + entry = all_entries[0] + assert entry.path == test_file + + # Also verify get() works with Path + result = dashboard_entries.get(test_file) + assert result == entry + + +@pytest.mark.asyncio +async def test_dashboard_entries_get_nonexistent_path( + dashboard_entries: DashboardEntries, +) -> None: + """Test getting non-existent entry returns None.""" + result = dashboard_entries.get("/nonexistent/path.yaml") + assert result is None + + +@pytest.mark.asyncio +async def test_dashboard_entries_path_normalization( + dashboard_entries: DashboardEntries, tmp_path: Path +) -> None: + """Test that paths are handled consistently.""" + # Create a test file + test_file = tmp_path / "device.yaml" + test_file.write_text("test config") + + # Update entries to load the file + await dashboard_entries.async_update_entries() + + # Get the entry by path + result = dashboard_entries.get(test_file) + assert result is not None + + +@pytest.mark.asyncio +async def test_dashboard_entries_path_with_spaces( + dashboard_entries: DashboardEntries, tmp_path: Path +) -> None: + """Test handling paths with spaces.""" + # Create a test file with spaces in name + test_file = tmp_path / "my device.yaml" + test_file.write_text("test config") + + # Update entries to load the file + await dashboard_entries.async_update_entries() + + # Get the entry by path + result = dashboard_entries.get(test_file) + assert result is not None + assert result.path == test_file + + +@pytest.mark.asyncio +async def test_dashboard_entries_path_with_special_chars( + dashboard_entries: DashboardEntries, tmp_path: Path +) -> None: + """Test handling paths with special characters.""" + # Create a test file with special characters + test_file = tmp_path / "device-01_test.yaml" + test_file.write_text("test config") + + # Update entries to load the file + await dashboard_entries.async_update_entries() + + # Get the entry by path + result = dashboard_entries.get(test_file) + assert result is not None + + +def test_dashboard_entries_windows_path() -> None: + """Test handling Windows-style paths.""" + test_path = Path(r"C:\Users\test\esphome\device.yaml") + cache_key = create_cache_key() + + entry = DashboardEntry(test_path, cache_key) + + assert entry.path == test_path + + +@pytest.mark.asyncio +async def test_dashboard_entries_path_to_cache_key_mapping( + dashboard_entries: DashboardEntries, tmp_path: Path +) -> None: + """Test internal entries storage with paths and cache keys.""" + # Create test files + file1 = tmp_path / "device1.yaml" + file2 = tmp_path / "device2.yaml" + file1.write_text("test config 1") + file2.write_text("test config 2") + + # Update entries to load the files + await dashboard_entries.async_update_entries() + + # Get entries and verify they have different cache keys + entry1 = dashboard_entries.get(file1) + entry2 = dashboard_entries.get(file2) + + assert entry1 is not None + assert entry2 is not None + assert entry1.cache_key != entry2.cache_key + + +def test_dashboard_entry_path_property() -> None: + """Test that path property returns expected value.""" + test_path = Path("/test/config/device.yaml") + entry = DashboardEntry(test_path, create_cache_key()) + + assert entry.path == test_path + assert isinstance(entry.path, Path) + + +@pytest.mark.asyncio +async def test_dashboard_entries_all_returns_entries_with_paths( + dashboard_entries: DashboardEntries, tmp_path: Path +) -> None: + """Test that all() returns entries with their paths intact.""" + # Create test files + files = [ + tmp_path / "device1.yaml", + tmp_path / "device2.yaml", + tmp_path / "device3.yaml", + ] + + for file in files: + file.write_text("test config") + + # Update entries to load the files + await dashboard_entries.async_update_entries() + + all_entries = dashboard_entries.async_all() + + assert len(all_entries) == len(files) + retrieved_paths = [entry.path for entry in all_entries] + assert set(retrieved_paths) == set(files) + + +@pytest.mark.asyncio +async def test_async_update_entries_removed_path( + dashboard_entries: DashboardEntries, mock_dashboard: Mock, tmp_path: Path +) -> None: + """Test that removed files trigger ENTRY_REMOVED event.""" + + # Create a test file + test_file = tmp_path / "device.yaml" + test_file.write_text("test config") + + # First update to add the entry + await dashboard_entries.async_update_entries() + + # Verify entry was added + all_entries = dashboard_entries.async_all() + assert len(all_entries) == 1 + entry = all_entries[0] + + # Delete the file + test_file.unlink() + + # Second update to detect removal + await dashboard_entries.async_update_entries() + + # Verify entry was removed + all_entries = dashboard_entries.async_all() + assert len(all_entries) == 0 + + # Verify ENTRY_REMOVED event was fired + mock_dashboard.bus.async_fire.assert_any_call( + DashboardEvent.ENTRY_REMOVED, {"entry": entry} + ) + + +@pytest.mark.asyncio +async def test_async_update_entries_updated_path( + dashboard_entries: DashboardEntries, mock_dashboard: Mock, tmp_path: Path +) -> None: + """Test that modified files trigger ENTRY_UPDATED event.""" + + # Create a test file + test_file = tmp_path / "device.yaml" + test_file.write_text("test config") + + # First update to add the entry + await dashboard_entries.async_update_entries() + + # Verify entry was added + all_entries = dashboard_entries.async_all() + assert len(all_entries) == 1 + entry = all_entries[0] + original_cache_key = entry.cache_key + + # Modify the file to change its mtime + test_file.write_text("updated config") + # Explicitly change the mtime to ensure it's different + stat = test_file.stat() + os.utime(test_file, (stat.st_atime, stat.st_mtime + 1)) + + # Second update to detect modification + await dashboard_entries.async_update_entries() + + # Verify entry is still there with updated cache key + all_entries = dashboard_entries.async_all() + assert len(all_entries) == 1 + updated_entry = all_entries[0] + assert updated_entry == entry # Same entry object + assert updated_entry.cache_key != original_cache_key # But cache key updated + + # Verify ENTRY_UPDATED event was fired + mock_dashboard.bus.async_fire.assert_any_call( + DashboardEvent.ENTRY_UPDATED, {"entry": entry} + ) diff --git a/tests/dashboard/test_settings.py b/tests/dashboard/test_settings.py new file mode 100644 index 0000000000..91a8ec70c3 --- /dev/null +++ b/tests/dashboard/test_settings.py @@ -0,0 +1,223 @@ +"""Tests for dashboard settings Path-related functionality.""" + +from __future__ import annotations + +from argparse import Namespace +from pathlib import Path +import tempfile + +import pytest + +from esphome.core import CORE +from esphome.dashboard.settings import DashboardSettings + + +@pytest.fixture +def dashboard_settings(tmp_path: Path) -> DashboardSettings: + """Create DashboardSettings instance with temp directory.""" + settings = DashboardSettings() + # Resolve symlinks to ensure paths match + resolved_dir = tmp_path.resolve() + settings.config_dir = resolved_dir + settings.absolute_config_dir = resolved_dir + return settings + + +def test_rel_path_simple(dashboard_settings: DashboardSettings) -> None: + """Test rel_path with simple relative path.""" + result = dashboard_settings.rel_path("config.yaml") + + expected = dashboard_settings.config_dir / "config.yaml" + assert result == expected + + +def test_rel_path_multiple_components(dashboard_settings: DashboardSettings) -> None: + """Test rel_path with multiple path components.""" + result = dashboard_settings.rel_path("subfolder", "device", "config.yaml") + + expected = dashboard_settings.config_dir / "subfolder" / "device" / "config.yaml" + assert result == expected + + +def test_rel_path_with_dots(dashboard_settings: DashboardSettings) -> None: + """Test rel_path prevents directory traversal.""" + # This should raise ValueError as it tries to go outside config_dir + with pytest.raises(ValueError): + dashboard_settings.rel_path("..", "outside.yaml") + + +def test_rel_path_absolute_path_within_config( + dashboard_settings: DashboardSettings, +) -> None: + """Test rel_path with absolute path that's within config dir.""" + internal_path = dashboard_settings.absolute_config_dir / "internal.yaml" + + internal_path.touch() + result = dashboard_settings.rel_path("internal.yaml") + expected = dashboard_settings.config_dir / "internal.yaml" + assert result == expected + + +def test_rel_path_absolute_path_outside_config( + dashboard_settings: DashboardSettings, +) -> None: + """Test rel_path with absolute path outside config dir raises error.""" + outside_path = "/tmp/outside/config.yaml" + + with pytest.raises(ValueError): + dashboard_settings.rel_path(outside_path) + + +def test_rel_path_empty_args(dashboard_settings: DashboardSettings) -> None: + """Test rel_path with no arguments returns config_dir.""" + result = dashboard_settings.rel_path() + assert result == dashboard_settings.config_dir + + +def test_rel_path_with_pathlib_path(dashboard_settings: DashboardSettings) -> None: + """Test rel_path works with Path objects as arguments.""" + path_obj = Path("subfolder") / "config.yaml" + result = dashboard_settings.rel_path(path_obj) + + expected = dashboard_settings.config_dir / "subfolder" / "config.yaml" + assert result == expected + + +def test_rel_path_normalizes_slashes(dashboard_settings: DashboardSettings) -> None: + """Test rel_path normalizes path separators.""" + # os.path.join normalizes slashes on Windows but preserves them on Unix + # Test that providing components separately gives same result + result1 = dashboard_settings.rel_path("folder", "subfolder", "file.yaml") + result2 = dashboard_settings.rel_path("folder", "subfolder", "file.yaml") + assert result1 == result2 + + # Also test that the result is as expected + expected = dashboard_settings.config_dir / "folder" / "subfolder" / "file.yaml" + assert result1 == expected + + +def test_rel_path_handles_spaces(dashboard_settings: DashboardSettings) -> None: + """Test rel_path handles paths with spaces.""" + result = dashboard_settings.rel_path("my folder", "my config.yaml") + + expected = dashboard_settings.config_dir / "my folder" / "my config.yaml" + assert result == expected + + +def test_rel_path_handles_special_chars(dashboard_settings: DashboardSettings) -> None: + """Test rel_path handles paths with special characters.""" + result = dashboard_settings.rel_path("device-01_test", "config.yaml") + + expected = dashboard_settings.config_dir / "device-01_test" / "config.yaml" + assert result == expected + + +def test_config_dir_as_path_property(dashboard_settings: DashboardSettings) -> None: + """Test that config_dir can be accessed and used with Path operations.""" + config_path = dashboard_settings.config_dir + + assert config_path.exists() + assert config_path.is_dir() + assert config_path.is_absolute() + + +def test_absolute_config_dir_property(dashboard_settings: DashboardSettings) -> None: + """Test absolute_config_dir is a Path object.""" + assert isinstance(dashboard_settings.absolute_config_dir, Path) + assert dashboard_settings.absolute_config_dir.exists() + assert dashboard_settings.absolute_config_dir.is_dir() + assert dashboard_settings.absolute_config_dir.is_absolute() + + +def test_rel_path_symlink_inside_config(dashboard_settings: DashboardSettings) -> None: + """Test rel_path with symlink that points inside config dir.""" + target = dashboard_settings.absolute_config_dir / "target.yaml" + target.touch() + symlink = dashboard_settings.absolute_config_dir / "link.yaml" + symlink.symlink_to(target) + result = dashboard_settings.rel_path("link.yaml") + expected = dashboard_settings.config_dir / "link.yaml" + assert result == expected + + +def test_rel_path_symlink_outside_config(dashboard_settings: DashboardSettings) -> None: + """Test rel_path with symlink that points outside config dir.""" + with tempfile.NamedTemporaryFile(suffix=".yaml") as tmp: + symlink = dashboard_settings.absolute_config_dir / "external_link.yaml" + symlink.symlink_to(tmp.name) + with pytest.raises(ValueError): + dashboard_settings.rel_path("external_link.yaml") + + +def test_rel_path_with_none_arg(dashboard_settings: DashboardSettings) -> None: + """Test rel_path handles None arguments gracefully.""" + result = dashboard_settings.rel_path("None") + expected = dashboard_settings.config_dir / "None" + assert result == expected + + +def test_rel_path_with_numeric_args(dashboard_settings: DashboardSettings) -> None: + """Test rel_path handles numeric arguments.""" + result = dashboard_settings.rel_path("123", "456.789") + expected = dashboard_settings.config_dir / "123" / "456.789" + assert result == expected + + +def test_config_path_parent_resolves_to_config_dir(tmp_path: Path) -> None: + """Test that CORE.config_path.parent resolves to config_dir after parse_args. + + This is a regression test for issue #11280 where binary download failed + when using packages with secrets after the Path migration in 2025.10.0. + + The issue was that after switching from os.path to Path: + - Before: os.path.dirname("/config/.") → "/config" + - After: Path("/config/.").parent → Path("/") (normalized first!) + + The fix uses a sentinel file so .parent returns the correct directory: + - Fixed: Path("/config/___DASHBOARD_SENTINEL___.yaml").parent → Path("/config") + """ + # Create test directory structure with secrets and packages + config_dir = tmp_path / "config" + config_dir.mkdir() + + # Create secrets.yaml with obviously fake test values + secrets_file = config_dir / "secrets.yaml" + secrets_file.write_text( + "wifi_ssid: TEST-DUMMY-SSID\n" + "wifi_password: not-a-real-password-just-for-testing\n" + ) + + # Create package file that uses secrets + package_file = config_dir / "common.yaml" + package_file.write_text( + "wifi:\n ssid: !secret wifi_ssid\n password: !secret wifi_password\n" + ) + + # Create main device config that includes the package + device_config = config_dir / "test-device.yaml" + device_config.write_text( + "esphome:\n name: test-device\n\npackages:\n common: !include common.yaml\n" + ) + + # Set up dashboard settings with our test config directory + settings = DashboardSettings() + args = Namespace( + configuration=str(config_dir), + password=None, + username=None, + ha_addon=False, + verbose=False, + ) + settings.parse_args(args) + + # Verify that CORE.config_path.parent correctly points to the config directory + # This is critical for secret resolution in yaml_util.py which does: + # main_config_dir = CORE.config_path.parent + # main_secret_yml = main_config_dir / "secrets.yaml" + assert CORE.config_path.parent == config_dir.resolve() + assert (CORE.config_path.parent / "secrets.yaml").exists() + assert (CORE.config_path.parent / "common.yaml").exists() + + # Verify that CORE.config_path itself uses the sentinel file + assert CORE.config_path.name == "___DASHBOARD_SENTINEL___.yaml" + assert not CORE.config_path.exists() # Sentinel file doesn't actually exist diff --git a/tests/dashboard/test_web_server.py b/tests/dashboard/test_web_server.py index b77ab7a7a3..385841b1c8 100644 --- a/tests/dashboard/test_web_server.py +++ b/tests/dashboard/test_web_server.py @@ -1,23 +1,60 @@ from __future__ import annotations +from argparse import Namespace import asyncio +from collections.abc import Generator +from contextlib import asynccontextmanager +import gzip import json import os -from unittest.mock import Mock +from pathlib import Path +from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest import pytest_asyncio -from tornado.httpclient import AsyncHTTPClient, HTTPResponse +from tornado.httpclient import AsyncHTTPClient, HTTPClientError, HTTPResponse from tornado.httpserver import HTTPServer from tornado.ioloop import IOLoop from tornado.testing import bind_unused_port +from tornado.websocket import WebSocketClientConnection, websocket_connect +from esphome import yaml_util +from esphome.core import CORE from esphome.dashboard import web_server +from esphome.dashboard.const import DashboardEvent from esphome.dashboard.core import DASHBOARD +from esphome.dashboard.entries import ( + DashboardEntry, + EntryStateSource, + bool_to_entry_state, +) +from esphome.dashboard.models import build_importable_device_dict +from esphome.dashboard.web_server import DashboardSubscriber +from esphome.zeroconf import DiscoveredImport from .common import get_fixture_path +def get_build_path(base_path: Path, device_name: str) -> Path: + """Get the build directory path for a device. + + This is a test helper that constructs the standard ESPHome build directory + structure. Note: This helper does NOT perform path traversal sanitization + because it's only used in tests where we control the inputs. The actual + web_server.py code handles sanitization in DownloadBinaryRequestHandler.get() + via file_name.replace("..", "").lstrip("/"). + + Args: + base_path: The base temporary path (typically tmp_path from pytest) + device_name: The name of the device (should not contain path separators + in production use, but tests may use it for specific scenarios) + + Returns: + Path to the build directory (.esphome/build/device_name) + """ + return base_path / ".esphome" / "build" / device_name + + class DashboardTestHelper: def __init__(self, io_loop: IOLoop, client: AsyncHTTPClient, port: int) -> None: self.io_loop = io_loop @@ -34,6 +71,66 @@ class DashboardTestHelper: return await future +@pytest.fixture +def mock_async_run_system_command() -> Generator[MagicMock]: + """Fixture to mock async_run_system_command.""" + with patch("esphome.dashboard.web_server.async_run_system_command") as mock: + yield mock + + +@pytest.fixture +def mock_trash_storage_path(tmp_path: Path) -> Generator[MagicMock]: + """Fixture to mock trash_storage_path.""" + trash_dir = tmp_path / "trash" + with patch( + "esphome.dashboard.web_server.trash_storage_path", return_value=trash_dir + ) as mock: + yield mock + + +@pytest.fixture +def mock_archive_storage_path(tmp_path: Path) -> Generator[MagicMock]: + """Fixture to mock archive_storage_path.""" + archive_dir = tmp_path / "archive" + with patch( + "esphome.dashboard.web_server.archive_storage_path", + return_value=archive_dir, + ) as mock: + yield mock + + +@pytest.fixture +def mock_dashboard_settings() -> Generator[MagicMock]: + """Fixture to mock dashboard settings.""" + with patch("esphome.dashboard.web_server.settings") as mock_settings: + # Set default auth settings to avoid authentication issues + mock_settings.using_auth = False + mock_settings.on_ha_addon = False + yield mock_settings + + +@pytest.fixture +def mock_ext_storage_path(tmp_path: Path) -> Generator[MagicMock]: + """Fixture to mock ext_storage_path.""" + with patch("esphome.dashboard.web_server.ext_storage_path") as mock: + mock.return_value = str(tmp_path / "storage.json") + yield mock + + +@pytest.fixture +def mock_storage_json() -> Generator[MagicMock]: + """Fixture to mock StorageJSON.""" + with patch("esphome.dashboard.web_server.StorageJSON") as mock: + yield mock + + +@pytest.fixture +def mock_idedata() -> Generator[MagicMock]: + """Fixture to mock platformio_api.IDEData.""" + with patch("esphome.dashboard.web_server.platformio_api.IDEData") as mock: + yield mock + + @pytest_asyncio.fixture() async def dashboard() -> DashboardTestHelper: sock, port = bind_unused_port() @@ -63,6 +160,33 @@ async def dashboard() -> DashboardTestHelper: io_loop.close() +@asynccontextmanager +async def websocket_connection(dashboard: DashboardTestHelper): + """Async context manager for WebSocket connections.""" + url = f"ws://127.0.0.1:{dashboard.port}/events" + ws = await websocket_connect(url) + try: + yield ws + finally: + if ws: + ws.close() + + +@pytest_asyncio.fixture +async def websocket_client(dashboard: DashboardTestHelper) -> WebSocketClientConnection: + """Create a WebSocket connection for testing.""" + url = f"ws://127.0.0.1:{dashboard.port}/events" + ws = await websocket_connect(url) + + # Read and discard initial state message + await ws.read_message() + + yield ws + + if ws: + ws.close() + + @pytest.mark.asyncio async def test_main_page(dashboard: DashboardTestHelper) -> None: response = await dashboard.fetch("/") @@ -80,3 +204,1366 @@ async def test_devices_page(dashboard: DashboardTestHelper) -> None: first_device = configured_devices[0] assert first_device["name"] == "pico" assert first_device["configuration"] == "pico.yaml" + + +@pytest.mark.asyncio +async def test_wizard_handler_invalid_input(dashboard: DashboardTestHelper) -> None: + """Test the WizardRequestHandler.post method with invalid inputs.""" + # Test with missing name (should fail with 422) + body_no_name = json.dumps( + { + "name": "", # Empty name + "platform": "ESP32", + "board": "esp32dev", + } + ) + with pytest.raises(HTTPClientError) as exc_info: + await dashboard.fetch( + "/wizard", + method="POST", + body=body_no_name, + headers={"Content-Type": "application/json"}, + ) + assert exc_info.value.code == 422 + + # Test with invalid wizard type (should fail with 422) + body_invalid_type = json.dumps( + { + "name": "test_device", + "type": "invalid_type", + "platform": "ESP32", + "board": "esp32dev", + } + ) + with pytest.raises(HTTPClientError) as exc_info: + await dashboard.fetch( + "/wizard", + method="POST", + body=body_invalid_type, + headers={"Content-Type": "application/json"}, + ) + assert exc_info.value.code == 422 + + +@pytest.mark.asyncio +async def test_wizard_handler_conflict(dashboard: DashboardTestHelper) -> None: + """Test the WizardRequestHandler.post when config already exists.""" + # Try to create a wizard for existing pico.yaml (should conflict) + body = json.dumps( + { + "name": "pico", # This already exists in fixtures + "platform": "ESP32", + "board": "esp32dev", + } + ) + with pytest.raises(HTTPClientError) as exc_info: + await dashboard.fetch( + "/wizard", + method="POST", + body=body, + headers={"Content-Type": "application/json"}, + ) + assert exc_info.value.code == 409 + + +@pytest.mark.asyncio +async def test_download_binary_handler_not_found( + dashboard: DashboardTestHelper, +) -> None: + """Test the DownloadBinaryRequestHandler.get with non-existent config.""" + with pytest.raises(HTTPClientError) as exc_info: + await dashboard.fetch( + "/download.bin?configuration=nonexistent.yaml", + method="GET", + ) + assert exc_info.value.code == 404 + + +@pytest.mark.asyncio +@pytest.mark.usefixtures("mock_ext_storage_path") +async def test_download_binary_handler_no_file_param( + dashboard: DashboardTestHelper, + tmp_path: Path, + mock_storage_json: MagicMock, +) -> None: + """Test the DownloadBinaryRequestHandler.get without file parameter.""" + # Mock storage to exist, but still should fail without file param + mock_storage = Mock() + mock_storage.name = "test_device" + mock_storage.firmware_bin_path = str(tmp_path / "firmware.bin") + mock_storage_json.load.return_value = mock_storage + + with pytest.raises(HTTPClientError) as exc_info: + await dashboard.fetch( + "/download.bin?configuration=pico.yaml", + method="GET", + ) + assert exc_info.value.code == 400 + + +@pytest.mark.asyncio +@pytest.mark.usefixtures("mock_ext_storage_path") +async def test_download_binary_handler_with_file( + dashboard: DashboardTestHelper, + tmp_path: Path, + mock_storage_json: MagicMock, +) -> None: + """Test the DownloadBinaryRequestHandler.get with existing binary file.""" + # Create a fake binary file + build_dir = tmp_path / ".esphome" / "build" / "test" + build_dir.mkdir(parents=True) + firmware_file = build_dir / "firmware.bin" + firmware_file.write_bytes(b"fake firmware content") + + # Mock storage JSON + mock_storage = Mock() + mock_storage.name = "test_device" + mock_storage.firmware_bin_path = firmware_file + mock_storage_json.load.return_value = mock_storage + + response = await dashboard.fetch( + "/download.bin?configuration=test.yaml&file=firmware.bin", + method="GET", + ) + assert response.code == 200 + assert response.body == b"fake firmware content" + assert response.headers["Content-Type"] == "application/octet-stream" + assert "attachment" in response.headers["Content-Disposition"] + assert "test_device-firmware.bin" in response.headers["Content-Disposition"] + + +@pytest.mark.asyncio +@pytest.mark.usefixtures("mock_ext_storage_path") +async def test_download_binary_handler_compressed( + dashboard: DashboardTestHelper, + tmp_path: Path, + mock_storage_json: MagicMock, +) -> None: + """Test the DownloadBinaryRequestHandler.get with compression.""" + # Create a fake binary file + build_dir = tmp_path / ".esphome" / "build" / "test" + build_dir.mkdir(parents=True) + firmware_file = build_dir / "firmware.bin" + original_content = b"fake firmware content for compression test" + firmware_file.write_bytes(original_content) + + # Mock storage JSON + mock_storage = Mock() + mock_storage.name = "test_device" + mock_storage.firmware_bin_path = firmware_file + mock_storage_json.load.return_value = mock_storage + + response = await dashboard.fetch( + "/download.bin?configuration=test.yaml&file=firmware.bin&compressed=1", + method="GET", + ) + assert response.code == 200 + # Decompress and verify content + decompressed = gzip.decompress(response.body) + assert decompressed == original_content + assert response.headers["Content-Type"] == "application/octet-stream" + assert "firmware.bin.gz" in response.headers["Content-Disposition"] + + +@pytest.mark.asyncio +@pytest.mark.usefixtures("mock_ext_storage_path") +async def test_download_binary_handler_custom_download_name( + dashboard: DashboardTestHelper, + tmp_path: Path, + mock_storage_json: MagicMock, +) -> None: + """Test the DownloadBinaryRequestHandler.get with custom download name.""" + # Create a fake binary file + build_dir = tmp_path / ".esphome" / "build" / "test" + build_dir.mkdir(parents=True) + firmware_file = build_dir / "firmware.bin" + firmware_file.write_bytes(b"content") + + # Mock storage JSON + mock_storage = Mock() + mock_storage.name = "test_device" + mock_storage.firmware_bin_path = firmware_file + mock_storage_json.load.return_value = mock_storage + + response = await dashboard.fetch( + "/download.bin?configuration=test.yaml&file=firmware.bin&download=custom_name.bin", + method="GET", + ) + assert response.code == 200 + assert "custom_name.bin" in response.headers["Content-Disposition"] + + +@pytest.mark.asyncio +@pytest.mark.usefixtures("mock_ext_storage_path") +async def test_download_binary_handler_idedata_fallback( + dashboard: DashboardTestHelper, + tmp_path: Path, + mock_async_run_system_command: MagicMock, + mock_storage_json: MagicMock, + mock_idedata: MagicMock, +) -> None: + """Test the DownloadBinaryRequestHandler.get falling back to idedata for extra images.""" + # Create build directory but no bootloader file initially + build_dir = tmp_path / ".esphome" / "build" / "test" + build_dir.mkdir(parents=True) + firmware_file = build_dir / "firmware.bin" + firmware_file.write_bytes(b"firmware") + + # Create bootloader file that idedata will find + bootloader_file = tmp_path / "bootloader.bin" + bootloader_file.write_bytes(b"bootloader content") + + # Mock storage JSON + mock_storage = Mock() + mock_storage.name = "test_device" + mock_storage.firmware_bin_path = firmware_file + mock_storage_json.load.return_value = mock_storage + + # Mock idedata response + mock_image = Mock() + mock_image.path = str(bootloader_file) + mock_idedata_instance = Mock() + mock_idedata_instance.extra_flash_images = [mock_image] + mock_idedata.return_value = mock_idedata_instance + + # Mock async_run_system_command to return idedata JSON + mock_async_run_system_command.return_value = (0, '{"extra_flash_images": []}', "") + + response = await dashboard.fetch( + "/download.bin?configuration=test.yaml&file=bootloader.bin", + method="GET", + ) + assert response.code == 200 + assert response.body == b"bootloader content" + + +@pytest.mark.asyncio +@pytest.mark.usefixtures("mock_ext_storage_path") +async def test_download_binary_handler_subdirectory_file( + dashboard: DashboardTestHelper, + tmp_path: Path, + mock_storage_json: MagicMock, +) -> None: + """Test the DownloadBinaryRequestHandler.get with file in subdirectory (nRF52 case). + + This is a regression test for issue #11343 where the Path migration broke + downloads for nRF52 firmware files in subdirectories like 'zephyr/zephyr.uf2'. + + The issue was that with_name() doesn't accept path separators: + - Before: path = storage_json.firmware_bin_path.with_name(file_name) + ValueError: Invalid name 'zephyr/zephyr.uf2' + - After: path = storage_json.firmware_bin_path.parent.joinpath(file_name) + Works correctly with subdirectory paths + """ + # Create a fake nRF52 build structure with firmware in subdirectory + build_dir = get_build_path(tmp_path, "nrf52-device") + zephyr_dir = build_dir / "zephyr" + zephyr_dir.mkdir(parents=True) + + # Create the main firmware binary (would be in build root) + firmware_file = build_dir / "firmware.bin" + firmware_file.write_bytes(b"main firmware") + + # Create the UF2 file in zephyr subdirectory (nRF52 specific) + uf2_file = zephyr_dir / "zephyr.uf2" + uf2_file.write_bytes(b"nRF52 UF2 firmware content") + + # Mock storage JSON + mock_storage = Mock() + mock_storage.name = "nrf52-device" + mock_storage.firmware_bin_path = firmware_file + mock_storage_json.load.return_value = mock_storage + + # Request the UF2 file with subdirectory path + response = await dashboard.fetch( + "/download.bin?configuration=nrf52-device.yaml&file=zephyr/zephyr.uf2", + method="GET", + ) + assert response.code == 200 + assert response.body == b"nRF52 UF2 firmware content" + assert response.headers["Content-Type"] == "application/octet-stream" + assert "attachment" in response.headers["Content-Disposition"] + # Download name should be device-name + full file path + assert "nrf52-device-zephyr/zephyr.uf2" in response.headers["Content-Disposition"] + + +@pytest.mark.asyncio +@pytest.mark.usefixtures("mock_ext_storage_path") +async def test_download_binary_handler_subdirectory_file_url_encoded( + dashboard: DashboardTestHelper, + tmp_path: Path, + mock_storage_json: MagicMock, +) -> None: + """Test the DownloadBinaryRequestHandler.get with URL-encoded subdirectory path. + + Verifies that URL-encoded paths (e.g., zephyr%2Fzephyr.uf2) are correctly + decoded and handled, and that custom download names work with subdirectories. + """ + # Create a fake build structure with firmware in subdirectory + build_dir = get_build_path(tmp_path, "test") + zephyr_dir = build_dir / "zephyr" + zephyr_dir.mkdir(parents=True) + + firmware_file = build_dir / "firmware.bin" + firmware_file.write_bytes(b"content") + + uf2_file = zephyr_dir / "zephyr.uf2" + uf2_file.write_bytes(b"content") + + # Mock storage JSON + mock_storage = Mock() + mock_storage.name = "test_device" + mock_storage.firmware_bin_path = firmware_file + mock_storage_json.load.return_value = mock_storage + + # Request with URL-encoded path and custom download name + response = await dashboard.fetch( + "/download.bin?configuration=test.yaml&file=zephyr%2Fzephyr.uf2&download=custom_name.bin", + method="GET", + ) + assert response.code == 200 + assert "custom_name.bin" in response.headers["Content-Disposition"] + + +@pytest.mark.asyncio +@pytest.mark.usefixtures("mock_ext_storage_path") +@pytest.mark.parametrize( + "attack_path", + [ + pytest.param("../../../secrets.yaml", id="basic_traversal"), + pytest.param("..%2F..%2F..%2Fsecrets.yaml", id="url_encoded"), + pytest.param("zephyr/../../../secrets.yaml", id="traversal_with_prefix"), + pytest.param("/etc/passwd", id="absolute_path"), + pytest.param("//etc/passwd", id="double_slash_absolute"), + pytest.param("....//secrets.yaml", id="multiple_dots"), + ], +) +async def test_download_binary_handler_path_traversal_protection( + dashboard: DashboardTestHelper, + tmp_path: Path, + mock_storage_json: MagicMock, + attack_path: str, +) -> None: + """Test that DownloadBinaryRequestHandler prevents path traversal attacks. + + Verifies that attempts to use '..' in file paths are sanitized to prevent + accessing files outside the build directory. Tests multiple attack vectors. + """ + # Create build structure + build_dir = get_build_path(tmp_path, "test") + build_dir.mkdir(parents=True) + firmware_file = build_dir / "firmware.bin" + firmware_file.write_bytes(b"firmware content") + + # Create a sensitive file outside the build directory that should NOT be accessible + sensitive_file = tmp_path / "secrets.yaml" + sensitive_file.write_bytes(b"secret: my_secret_password") + + # Mock storage JSON + mock_storage = Mock() + mock_storage.name = "test_device" + mock_storage.firmware_bin_path = firmware_file + mock_storage_json.load.return_value = mock_storage + + # Attempt path traversal attack - should be blocked + with pytest.raises(HTTPClientError) as exc_info: + await dashboard.fetch( + f"/download.bin?configuration=test.yaml&file={attack_path}", + method="GET", + ) + # Should get 404 (file not found after sanitization) or 500 (idedata fails) + assert exc_info.value.code in (404, 500) + + +@pytest.mark.asyncio +@pytest.mark.usefixtures("mock_ext_storage_path") +async def test_download_binary_handler_multiple_subdirectory_levels( + dashboard: DashboardTestHelper, + tmp_path: Path, + mock_storage_json: MagicMock, +) -> None: + """Test downloading files from multiple subdirectory levels. + + Verifies that joinpath correctly handles multi-level paths like 'build/output/firmware.bin'. + """ + # Create nested directory structure + build_dir = get_build_path(tmp_path, "test") + nested_dir = build_dir / "build" / "output" + nested_dir.mkdir(parents=True) + + firmware_file = build_dir / "firmware.bin" + firmware_file.write_bytes(b"main") + + nested_file = nested_dir / "firmware.bin" + nested_file.write_bytes(b"nested firmware content") + + # Mock storage JSON + mock_storage = Mock() + mock_storage.name = "test_device" + mock_storage.firmware_bin_path = firmware_file + mock_storage_json.load.return_value = mock_storage + + response = await dashboard.fetch( + "/download.bin?configuration=test.yaml&file=build/output/firmware.bin", + method="GET", + ) + assert response.code == 200 + assert response.body == b"nested firmware content" + + +@pytest.mark.asyncio +async def test_edit_request_handler_post_invalid_file( + dashboard: DashboardTestHelper, +) -> None: + """Test the EditRequestHandler.post with non-yaml file.""" + with pytest.raises(HTTPClientError) as exc_info: + await dashboard.fetch( + "/edit?configuration=test.txt", + method="POST", + body=b"content", + ) + assert exc_info.value.code == 404 + + +@pytest.mark.asyncio +async def test_edit_request_handler_post_existing( + dashboard: DashboardTestHelper, + tmp_path: Path, + mock_dashboard_settings: MagicMock, +) -> None: + """Test the EditRequestHandler.post with existing yaml file.""" + # Create a temporary yaml file to edit (don't modify fixtures) + test_file = tmp_path / "test_edit.yaml" + test_file.write_text("esphome:\n name: original\n") + + # Configure the mock settings + mock_dashboard_settings.rel_path.return_value = test_file + mock_dashboard_settings.absolute_config_dir = test_file.parent + + new_content = "esphome:\n name: modified\n" + response = await dashboard.fetch( + "/edit?configuration=test_edit.yaml", + method="POST", + body=new_content.encode(), + ) + assert response.code == 200 + + # Verify the file was actually modified + assert test_file.read_text() == new_content + + +@pytest.mark.asyncio +async def test_unarchive_request_handler( + dashboard: DashboardTestHelper, + mock_archive_storage_path: MagicMock, + mock_dashboard_settings: MagicMock, + tmp_path: Path, +) -> None: + """Test the UnArchiveRequestHandler.post method.""" + # Set up an archived file + archive_dir = mock_archive_storage_path.return_value + archive_dir.mkdir(parents=True, exist_ok=True) + archived_file = archive_dir / "archived.yaml" + archived_file.write_text("test content") + + # Set up the destination path where the file should be moved + config_dir = tmp_path / "config" + config_dir.mkdir(parents=True, exist_ok=True) + destination_file = config_dir / "archived.yaml" + mock_dashboard_settings.rel_path.return_value = destination_file + + response = await dashboard.fetch( + "/unarchive?configuration=archived.yaml", + method="POST", + body=b"", + ) + assert response.code == 200 + + # Verify the file was actually moved from archive to config + assert not archived_file.exists() # File should be gone from archive + assert destination_file.exists() # File should now be in config + assert destination_file.read_text() == "test content" # Content preserved + + +@pytest.mark.asyncio +async def test_secret_keys_handler_no_file(dashboard: DashboardTestHelper) -> None: + """Test the SecretKeysRequestHandler.get when no secrets file exists.""" + # By default, there's no secrets file in the test fixtures + with pytest.raises(HTTPClientError) as exc_info: + await dashboard.fetch("/secret_keys", method="GET") + assert exc_info.value.code == 404 + + +@pytest.mark.asyncio +async def test_secret_keys_handler_with_file( + dashboard: DashboardTestHelper, + tmp_path: Path, + mock_dashboard_settings: MagicMock, +) -> None: + """Test the SecretKeysRequestHandler.get when secrets file exists.""" + # Create a secrets file in temp directory + secrets_file = tmp_path / "secrets.yaml" + secrets_file.write_text( + "wifi_ssid: TestNetwork\nwifi_password: TestPass123\napi_key: test_key\n" + ) + + # Configure mock to return our temp secrets file + # Since the file actually exists, os.path.isfile will return True naturally + mock_dashboard_settings.rel_path.return_value = secrets_file + + response = await dashboard.fetch("/secret_keys", method="GET") + assert response.code == 200 + data = json.loads(response.body.decode()) + assert "wifi_ssid" in data + assert "wifi_password" in data + assert "api_key" in data + + +@pytest.mark.asyncio +async def test_json_config_handler( + dashboard: DashboardTestHelper, + mock_async_run_system_command: MagicMock, +) -> None: + """Test the JsonConfigRequestHandler.get method.""" + # This will actually run the esphome config command on pico.yaml + mock_output = json.dumps( + { + "esphome": {"name": "pico"}, + "esp32": {"board": "esp32dev"}, + } + ) + mock_async_run_system_command.return_value = (0, mock_output, "") + + response = await dashboard.fetch( + "/json-config?configuration=pico.yaml", method="GET" + ) + assert response.code == 200 + data = json.loads(response.body.decode()) + assert data["esphome"]["name"] == "pico" + + +@pytest.mark.asyncio +async def test_json_config_handler_invalid_config( + dashboard: DashboardTestHelper, + mock_async_run_system_command: MagicMock, +) -> None: + """Test the JsonConfigRequestHandler.get with invalid config.""" + # Simulate esphome config command failure + mock_async_run_system_command.return_value = (1, "", "Error: Invalid configuration") + + with pytest.raises(HTTPClientError) as exc_info: + await dashboard.fetch("/json-config?configuration=pico.yaml", method="GET") + assert exc_info.value.code == 422 + + +@pytest.mark.asyncio +async def test_json_config_handler_not_found(dashboard: DashboardTestHelper) -> None: + """Test the JsonConfigRequestHandler.get with non-existent file.""" + with pytest.raises(HTTPClientError) as exc_info: + await dashboard.fetch( + "/json-config?configuration=nonexistent.yaml", method="GET" + ) + assert exc_info.value.code == 404 + + +def test_start_web_server_with_address_port( + tmp_path: Path, + mock_trash_storage_path: MagicMock, + mock_archive_storage_path: MagicMock, +) -> None: + """Test the start_web_server function with address and port.""" + app = Mock() + trash_dir = mock_trash_storage_path.return_value + archive_dir = mock_archive_storage_path.return_value + + # Create trash dir to test migration + trash_dir.mkdir() + (trash_dir / "old.yaml").write_text("old") + + web_server.start_web_server(app, None, "127.0.0.1", 6052, str(tmp_path / "config")) + + # The function calls app.listen directly for non-socket mode + app.listen.assert_called_once_with(6052, "127.0.0.1") + + # Verify trash was moved to archive + assert not trash_dir.exists() + assert archive_dir.exists() + assert (archive_dir / "old.yaml").exists() + + +@pytest.mark.asyncio +async def test_edit_request_handler_get(dashboard: DashboardTestHelper) -> None: + """Test EditRequestHandler.get method.""" + # Test getting a valid yaml file + response = await dashboard.fetch("/edit?configuration=pico.yaml") + assert response.code == 200 + assert response.headers["content-type"] == "application/yaml" + content = response.body.decode() + assert "esphome:" in content # Verify it's a valid ESPHome config + + # Test getting a non-existent file + with pytest.raises(HTTPClientError) as exc_info: + await dashboard.fetch("/edit?configuration=nonexistent.yaml") + assert exc_info.value.code == 404 + + # Test getting a non-yaml file + with pytest.raises(HTTPClientError) as exc_info: + await dashboard.fetch("/edit?configuration=test.txt") + assert exc_info.value.code == 404 + + # Test path traversal attempt + with pytest.raises(HTTPClientError) as exc_info: + await dashboard.fetch("/edit?configuration=../../../etc/passwd") + assert exc_info.value.code == 404 + + +@pytest.mark.asyncio +async def test_archive_request_handler_post( + dashboard: DashboardTestHelper, + mock_archive_storage_path: MagicMock, + mock_ext_storage_path: MagicMock, + tmp_path: Path, +) -> None: + """Test ArchiveRequestHandler.post method without storage_json.""" + + # Set up temp directories + config_dir = Path(get_fixture_path("conf")) + archive_dir = tmp_path / "archive" + + # Create a test configuration file + test_config = config_dir / "test_archive.yaml" + test_config.write_text("esphome:\n name: test_archive\n") + + # Archive the configuration + response = await dashboard.fetch( + "/archive", + method="POST", + body="configuration=test_archive.yaml", + headers={"Content-Type": "application/x-www-form-urlencoded"}, + ) + assert response.code == 200 + + # Verify file was moved to archive + assert not test_config.exists() + assert (archive_dir / "test_archive.yaml").exists() + assert ( + archive_dir / "test_archive.yaml" + ).read_text() == "esphome:\n name: test_archive\n" + + +@pytest.mark.asyncio +async def test_archive_handler_with_build_folder( + dashboard: DashboardTestHelper, + mock_archive_storage_path: MagicMock, + mock_ext_storage_path: MagicMock, + mock_dashboard_settings: MagicMock, + mock_storage_json: MagicMock, + tmp_path: Path, +) -> None: + """Test ArchiveRequestHandler.post with storage_json and build folder.""" + config_dir = tmp_path / "config" + config_dir.mkdir() + archive_dir = tmp_path / "archive" + archive_dir.mkdir() + build_dir = tmp_path / "build" + build_dir.mkdir() + + configuration = "test_device.yaml" + test_config = config_dir / configuration + test_config.write_text("esphome:\n name: test_device\n") + + build_folder = build_dir / "test_device" + build_folder.mkdir() + (build_folder / "firmware.bin").write_text("binary content") + (build_folder / ".pioenvs").mkdir() + + mock_dashboard_settings.config_dir = str(config_dir) + mock_dashboard_settings.rel_path.return_value = test_config + mock_archive_storage_path.return_value = archive_dir + + mock_storage = MagicMock() + mock_storage.name = "test_device" + mock_storage.build_path = build_folder + mock_storage_json.load.return_value = mock_storage + + response = await dashboard.fetch( + "/archive", + method="POST", + body=f"configuration={configuration}", + headers={"Content-Type": "application/x-www-form-urlencoded"}, + ) + assert response.code == 200 + + assert not test_config.exists() + assert (archive_dir / configuration).exists() + + assert not build_folder.exists() + assert not (archive_dir / "test_device").exists() + + +@pytest.mark.asyncio +async def test_archive_handler_no_build_folder( + dashboard: DashboardTestHelper, + mock_archive_storage_path: MagicMock, + mock_ext_storage_path: MagicMock, + mock_dashboard_settings: MagicMock, + mock_storage_json: MagicMock, + tmp_path: Path, +) -> None: + """Test ArchiveRequestHandler.post with storage_json but no build folder.""" + config_dir = tmp_path / "config" + config_dir.mkdir() + archive_dir = tmp_path / "archive" + archive_dir.mkdir() + + configuration = "test_device.yaml" + test_config = config_dir / configuration + test_config.write_text("esphome:\n name: test_device\n") + + mock_dashboard_settings.config_dir = str(config_dir) + mock_dashboard_settings.rel_path.return_value = test_config + mock_archive_storage_path.return_value = archive_dir + + mock_storage = MagicMock() + mock_storage.name = "test_device" + mock_storage.build_path = None + mock_storage_json.load.return_value = mock_storage + + response = await dashboard.fetch( + "/archive", + method="POST", + body=f"configuration={configuration}", + headers={"Content-Type": "application/x-www-form-urlencoded"}, + ) + assert response.code == 200 + + assert not test_config.exists() + assert (archive_dir / configuration).exists() + assert not (archive_dir / "test_device").exists() + + +@pytest.mark.skipif(os.name == "nt", reason="Unix sockets are not supported on Windows") +@pytest.mark.usefixtures("mock_trash_storage_path", "mock_archive_storage_path") +def test_start_web_server_with_unix_socket(tmp_path: Path) -> None: + """Test the start_web_server function with unix socket.""" + app = Mock() + socket_path = tmp_path / "test.sock" + + # Don't create trash_dir - it doesn't exist, so no migration needed + with ( + patch("tornado.httpserver.HTTPServer") as mock_server_class, + patch("tornado.netutil.bind_unix_socket") as mock_bind, + ): + server = Mock() + mock_server_class.return_value = server + mock_bind.return_value = Mock() + + web_server.start_web_server( + app, str(socket_path), None, None, str(tmp_path / "config") + ) + + mock_server_class.assert_called_once_with(app) + mock_bind.assert_called_once_with(str(socket_path), mode=0o666) + server.add_socket.assert_called_once() + + +def test_build_cache_arguments_no_entry(mock_dashboard: Mock) -> None: + """Test with no entry returns empty list.""" + result = web_server.build_cache_arguments(None, mock_dashboard, 0.0) + assert result == [] + + +def test_build_cache_arguments_no_address_no_name(mock_dashboard: Mock) -> None: + """Test with entry but no address or name.""" + entry = Mock(spec=web_server.DashboardEntry) + entry.address = None + entry.name = None + result = web_server.build_cache_arguments(entry, mock_dashboard, 0.0) + assert result == [] + + +def test_build_cache_arguments_mdns_address_cached(mock_dashboard: Mock) -> None: + """Test with .local address that has cached mDNS results.""" + entry = Mock(spec=web_server.DashboardEntry) + entry.address = "device.local" + entry.name = None + mock_dashboard.mdns_status = Mock() + mock_dashboard.mdns_status.get_cached_addresses.return_value = [ + "192.168.1.10", + "fe80::1", + ] + + result = web_server.build_cache_arguments(entry, mock_dashboard, 0.0) + + assert result == [ + "--mdns-address-cache", + "device.local=192.168.1.10,fe80::1", + ] + mock_dashboard.mdns_status.get_cached_addresses.assert_called_once_with( + "device.local" + ) + + +def test_build_cache_arguments_dns_address_cached(mock_dashboard: Mock) -> None: + """Test with non-.local address that has cached DNS results.""" + entry = Mock(spec=web_server.DashboardEntry) + entry.address = "example.com" + entry.name = None + mock_dashboard.dns_cache = Mock() + mock_dashboard.dns_cache.get_cached_addresses.return_value = [ + "93.184.216.34", + "2606:2800:220:1:248:1893:25c8:1946", + ] + + now = 100.0 + result = web_server.build_cache_arguments(entry, mock_dashboard, now) + + # IPv6 addresses are sorted before IPv4 + assert result == [ + "--dns-address-cache", + "example.com=2606:2800:220:1:248:1893:25c8:1946,93.184.216.34", + ] + mock_dashboard.dns_cache.get_cached_addresses.assert_called_once_with( + "example.com", now + ) + + +def test_build_cache_arguments_name_without_address(mock_dashboard: Mock) -> None: + """Test with name but no address - should check mDNS with .local suffix.""" + entry = Mock(spec=web_server.DashboardEntry) + entry.name = "my-device" + entry.address = None + mock_dashboard.mdns_status = Mock() + mock_dashboard.mdns_status.get_cached_addresses.return_value = ["192.168.1.20"] + + result = web_server.build_cache_arguments(entry, mock_dashboard, 0.0) + + assert result == [ + "--mdns-address-cache", + "my-device.local=192.168.1.20", + ] + mock_dashboard.mdns_status.get_cached_addresses.assert_called_once_with( + "my-device.local" + ) + + +@pytest.mark.asyncio +async def test_websocket_connection_initial_state( + dashboard: DashboardTestHelper, +) -> None: + """Test WebSocket connection and initial state.""" + async with websocket_connection(dashboard) as ws: + # Should receive initial state with configured and importable devices + msg = await ws.read_message() + assert msg is not None + data = json.loads(msg) + assert data["event"] == "initial_state" + assert "devices" in data["data"] + assert "configured" in data["data"]["devices"] + assert "importable" in data["data"]["devices"] + + # Check configured devices + configured = data["data"]["devices"]["configured"] + assert len(configured) > 0 + assert configured[0]["name"] == "pico" # From test fixtures + + +@pytest.mark.asyncio +async def test_websocket_ping_pong( + dashboard: DashboardTestHelper, websocket_client: WebSocketClientConnection +) -> None: + """Test WebSocket ping/pong mechanism.""" + # Send ping + await websocket_client.write_message(json.dumps({"event": "ping"})) + + # Should receive pong + msg = await websocket_client.read_message() + assert msg is not None + data = json.loads(msg) + assert data["event"] == "pong" + + +@pytest.mark.asyncio +async def test_websocket_invalid_json( + dashboard: DashboardTestHelper, websocket_client: WebSocketClientConnection +) -> None: + """Test WebSocket handling of invalid JSON.""" + # Send invalid JSON + await websocket_client.write_message("not valid json {]") + + # Send a valid ping to verify connection is still alive + await websocket_client.write_message(json.dumps({"event": "ping"})) + + # Should receive pong, confirming the connection wasn't closed by invalid JSON + msg = await websocket_client.read_message() + assert msg is not None + data = json.loads(msg) + assert data["event"] == "pong" + + +@pytest.mark.asyncio +async def test_websocket_authentication_required( + dashboard: DashboardTestHelper, +) -> None: + """Test WebSocket authentication when auth is required.""" + with patch( + "esphome.dashboard.web_server.is_authenticated" + ) as mock_is_authenticated: + mock_is_authenticated.return_value = False + + # Try to connect - should be rejected with 401 + url = f"ws://127.0.0.1:{dashboard.port}/events" + with pytest.raises(HTTPClientError) as exc_info: + await websocket_connect(url) + # Should get HTTP 401 Unauthorized + assert exc_info.value.code == 401 + + +@pytest.mark.asyncio +async def test_websocket_authentication_not_required( + dashboard: DashboardTestHelper, +) -> None: + """Test WebSocket connection when no auth is required.""" + with patch( + "esphome.dashboard.web_server.is_authenticated" + ) as mock_is_authenticated: + mock_is_authenticated.return_value = True + + # Should be able to connect successfully + async with websocket_connection(dashboard) as ws: + msg = await ws.read_message() + assert msg is not None + data = json.loads(msg) + assert data["event"] == "initial_state" + + +@pytest.mark.asyncio +async def test_websocket_entry_state_changed( + dashboard: DashboardTestHelper, websocket_client: WebSocketClientConnection +) -> None: + """Test WebSocket entry state changed event.""" + # Simulate entry state change + entry = DASHBOARD.entries.async_all()[0] + state = bool_to_entry_state(True, EntryStateSource.MDNS) + DASHBOARD.bus.async_fire( + DashboardEvent.ENTRY_STATE_CHANGED, {"entry": entry, "state": state} + ) + + # Should receive state change event + msg = await websocket_client.read_message() + assert msg is not None + data = json.loads(msg) + assert data["event"] == "entry_state_changed" + assert data["data"]["filename"] == entry.filename + assert data["data"]["name"] == entry.name + assert data["data"]["state"] is True + + +@pytest.mark.asyncio +async def test_websocket_entry_added( + dashboard: DashboardTestHelper, websocket_client: WebSocketClientConnection +) -> None: + """Test WebSocket entry added event.""" + # Create a mock entry + mock_entry = Mock(spec=DashboardEntry) + mock_entry.filename = "test.yaml" + mock_entry.name = "test_device" + mock_entry.to_dict.return_value = { + "name": "test_device", + "filename": "test.yaml", + "configuration": "test.yaml", + } + + # Simulate entry added + DASHBOARD.bus.async_fire(DashboardEvent.ENTRY_ADDED, {"entry": mock_entry}) + + # Should receive entry added event + msg = await websocket_client.read_message() + assert msg is not None + data = json.loads(msg) + assert data["event"] == "entry_added" + assert data["data"]["device"]["name"] == "test_device" + assert data["data"]["device"]["filename"] == "test.yaml" + + +@pytest.mark.asyncio +async def test_websocket_entry_removed( + dashboard: DashboardTestHelper, websocket_client: WebSocketClientConnection +) -> None: + """Test WebSocket entry removed event.""" + # Create a mock entry + mock_entry = Mock(spec=DashboardEntry) + mock_entry.filename = "removed.yaml" + mock_entry.name = "removed_device" + mock_entry.to_dict.return_value = { + "name": "removed_device", + "filename": "removed.yaml", + "configuration": "removed.yaml", + } + + # Simulate entry removed + DASHBOARD.bus.async_fire(DashboardEvent.ENTRY_REMOVED, {"entry": mock_entry}) + + # Should receive entry removed event + msg = await websocket_client.read_message() + assert msg is not None + data = json.loads(msg) + assert data["event"] == "entry_removed" + assert data["data"]["device"]["name"] == "removed_device" + assert data["data"]["device"]["filename"] == "removed.yaml" + + +@pytest.mark.asyncio +async def test_websocket_importable_device_added( + dashboard: DashboardTestHelper, websocket_client: WebSocketClientConnection +) -> None: + """Test WebSocket importable device added event with real DiscoveredImport.""" + # Create a real DiscoveredImport object + discovered = DiscoveredImport( + device_name="new_import_device", + friendly_name="New Import Device", + package_import_url="https://example.com/package", + project_name="test_project", + project_version="1.0.0", + network="wifi", + ) + + # Directly fire the event as the mDNS system would + device_dict = build_importable_device_dict(DASHBOARD, discovered) + DASHBOARD.bus.async_fire( + DashboardEvent.IMPORTABLE_DEVICE_ADDED, {"device": device_dict} + ) + + # Should receive importable device added event + msg = await websocket_client.read_message() + assert msg is not None + data = json.loads(msg) + assert data["event"] == "importable_device_added" + assert data["data"]["device"]["name"] == "new_import_device" + assert data["data"]["device"]["friendly_name"] == "New Import Device" + assert data["data"]["device"]["project_name"] == "test_project" + assert data["data"]["device"]["network"] == "wifi" + assert data["data"]["device"]["ignored"] is False + + +@pytest.mark.asyncio +async def test_websocket_importable_device_added_ignored( + dashboard: DashboardTestHelper, websocket_client: WebSocketClientConnection +) -> None: + """Test WebSocket importable device added event for ignored device.""" + # Add device to ignored list + DASHBOARD.ignored_devices.add("ignored_device") + + # Create a real DiscoveredImport object + discovered = DiscoveredImport( + device_name="ignored_device", + friendly_name="Ignored Device", + package_import_url="https://example.com/package", + project_name="test_project", + project_version="1.0.0", + network="ethernet", + ) + + # Directly fire the event as the mDNS system would + device_dict = build_importable_device_dict(DASHBOARD, discovered) + DASHBOARD.bus.async_fire( + DashboardEvent.IMPORTABLE_DEVICE_ADDED, {"device": device_dict} + ) + + # Should receive importable device added event with ignored=True + msg = await websocket_client.read_message() + assert msg is not None + data = json.loads(msg) + assert data["event"] == "importable_device_added" + assert data["data"]["device"]["name"] == "ignored_device" + assert data["data"]["device"]["friendly_name"] == "Ignored Device" + assert data["data"]["device"]["network"] == "ethernet" + assert data["data"]["device"]["ignored"] is True + + +@pytest.mark.asyncio +async def test_websocket_importable_device_removed( + dashboard: DashboardTestHelper, websocket_client: WebSocketClientConnection +) -> None: + """Test WebSocket importable device removed event.""" + # Simulate importable device removed + DASHBOARD.bus.async_fire( + DashboardEvent.IMPORTABLE_DEVICE_REMOVED, + {"name": "removed_import_device"}, + ) + + # Should receive importable device removed event + msg = await websocket_client.read_message() + assert msg is not None + data = json.loads(msg) + assert data["event"] == "importable_device_removed" + assert data["data"]["name"] == "removed_import_device" + + +@pytest.mark.asyncio +async def test_websocket_importable_device_already_configured( + dashboard: DashboardTestHelper, websocket_client: WebSocketClientConnection +) -> None: + """Test that importable device event is not sent if device is already configured.""" + # Get an existing configured device name + existing_entry = DASHBOARD.entries.async_all()[0] + + # Simulate importable device added with same name as configured device + DASHBOARD.bus.async_fire( + DashboardEvent.IMPORTABLE_DEVICE_ADDED, + { + "device": { + "name": existing_entry.name, + "friendly_name": "Should Not Be Sent", + "package_import_url": "https://example.com/package", + "project_name": "test_project", + "project_version": "1.0.0", + "network": "wifi", + } + }, + ) + + # Send a ping to ensure connection is still alive + await websocket_client.write_message(json.dumps({"event": "ping"})) + + # Should only receive pong, not the importable device event + msg = await websocket_client.read_message() + assert msg is not None + data = json.loads(msg) + assert data["event"] == "pong" + + +@pytest.mark.asyncio +async def test_websocket_multiple_connections(dashboard: DashboardTestHelper) -> None: + """Test multiple WebSocket connections.""" + async with ( + websocket_connection(dashboard) as ws1, + websocket_connection(dashboard) as ws2, + ): + # Both should receive initial state + msg1 = await ws1.read_message() + assert msg1 is not None + data1 = json.loads(msg1) + assert data1["event"] == "initial_state" + + msg2 = await ws2.read_message() + assert msg2 is not None + data2 = json.loads(msg2) + assert data2["event"] == "initial_state" + + # Fire an event - both should receive it + entry = DASHBOARD.entries.async_all()[0] + state = bool_to_entry_state(False, EntryStateSource.MDNS) + DASHBOARD.bus.async_fire( + DashboardEvent.ENTRY_STATE_CHANGED, {"entry": entry, "state": state} + ) + + msg1 = await ws1.read_message() + assert msg1 is not None + data1 = json.loads(msg1) + assert data1["event"] == "entry_state_changed" + + msg2 = await ws2.read_message() + assert msg2 is not None + data2 = json.loads(msg2) + assert data2["event"] == "entry_state_changed" + + +@pytest.mark.asyncio +async def test_dashboard_subscriber_lifecycle(dashboard: DashboardTestHelper) -> None: + """Test DashboardSubscriber lifecycle.""" + subscriber = DashboardSubscriber() + + # Initially no subscribers + assert len(subscriber._subscribers) == 0 + assert subscriber._event_loop_task is None + + # Add a subscriber + mock_websocket = Mock() + unsubscribe = subscriber.subscribe(mock_websocket) + + # Should have started the event loop task + assert len(subscriber._subscribers) == 1 + assert subscriber._event_loop_task is not None + + # Unsubscribe + unsubscribe() + + # Should have stopped the task + assert len(subscriber._subscribers) == 0 + + +@pytest.mark.asyncio +async def test_dashboard_subscriber_entries_update_interval( + dashboard: DashboardTestHelper, +) -> None: + """Test DashboardSubscriber entries update interval.""" + # Patch the constants to make the test run faster + with ( + patch("esphome.dashboard.web_server.DASHBOARD_POLL_INTERVAL", 0.01), + patch("esphome.dashboard.web_server.DASHBOARD_ENTRIES_UPDATE_ITERATIONS", 2), + patch("esphome.dashboard.web_server.settings") as mock_settings, + patch("esphome.dashboard.web_server.DASHBOARD") as mock_dashboard, + ): + mock_settings.status_use_mqtt = False + + # Mock dashboard dependencies + mock_dashboard.ping_request = Mock() + mock_dashboard.ping_request.set = Mock() + mock_dashboard.entries = Mock() + mock_dashboard.entries.async_request_update_entries = Mock() + + subscriber = DashboardSubscriber() + mock_websocket = Mock() + + # Subscribe to start the event loop + unsubscribe = subscriber.subscribe(mock_websocket) + + # Wait for a few iterations to ensure entries update is called + await asyncio.sleep(0.05) # Should be enough for 2+ iterations + + # Unsubscribe to stop the task + unsubscribe() + + # Verify entries update was called + assert mock_dashboard.entries.async_request_update_entries.call_count >= 1 + # Verify ping request was set multiple times + assert mock_dashboard.ping_request.set.call_count >= 2 + + +@pytest.mark.asyncio +async def test_websocket_refresh_command( + dashboard: DashboardTestHelper, websocket_client: WebSocketClientConnection +) -> None: + """Test WebSocket refresh command triggers dashboard update.""" + with patch("esphome.dashboard.web_server.DASHBOARD_SUBSCRIBER") as mock_subscriber: + mock_subscriber.request_refresh = Mock() + + # Send refresh command + await websocket_client.write_message(json.dumps({"event": "refresh"})) + + # Give it a moment to process + await asyncio.sleep(0.01) + + # Verify request_refresh was called + mock_subscriber.request_refresh.assert_called_once() + + +@pytest.mark.asyncio +async def test_dashboard_subscriber_refresh_event( + dashboard: DashboardTestHelper, +) -> None: + """Test DashboardSubscriber refresh event triggers immediate update.""" + # Patch the constants to make the test run faster + with ( + patch( + "esphome.dashboard.web_server.DASHBOARD_POLL_INTERVAL", 1.0 + ), # Long timeout + patch( + "esphome.dashboard.web_server.DASHBOARD_ENTRIES_UPDATE_ITERATIONS", 100 + ), # Won't reach naturally + patch("esphome.dashboard.web_server.settings") as mock_settings, + patch("esphome.dashboard.web_server.DASHBOARD") as mock_dashboard, + ): + mock_settings.status_use_mqtt = False + + # Mock dashboard dependencies + mock_dashboard.ping_request = Mock() + mock_dashboard.ping_request.set = Mock() + mock_dashboard.entries = Mock() + mock_dashboard.entries.async_request_update_entries = AsyncMock() + + subscriber = DashboardSubscriber() + mock_websocket = Mock() + + # Subscribe to start the event loop + unsubscribe = subscriber.subscribe(mock_websocket) + + # Wait a bit to ensure loop is running + await asyncio.sleep(0.01) + + # Verify entries update hasn't been called yet (iterations not reached) + assert mock_dashboard.entries.async_request_update_entries.call_count == 0 + + # Request refresh + subscriber.request_refresh() + + # Wait for the refresh to be processed + await asyncio.sleep(0.01) + + # Now entries update should have been called + assert mock_dashboard.entries.async_request_update_entries.call_count == 1 + + # Unsubscribe to stop the task + unsubscribe() + + # Give it a moment to clean up + await asyncio.sleep(0.01) + + +@pytest.mark.asyncio +async def test_dashboard_yaml_loading_with_packages_and_secrets( + tmp_path: Path, +) -> None: + """Test dashboard YAML loading with packages referencing secrets. + + This is a regression test for issue #11280 where binary download failed + when using packages with secrets after the Path migration in 2025.10.0. + + This test verifies that CORE.config_path initialization in the dashboard + allows yaml_util.load_yaml() to correctly resolve secrets from packages. + """ + # Create test directory structure with secrets and packages + config_dir = tmp_path / "config" + config_dir.mkdir() + + # Create secrets.yaml with obviously fake test values + secrets_file = config_dir / "secrets.yaml" + secrets_file.write_text( + "wifi_ssid: TEST-DUMMY-SSID\n" + "wifi_password: not-a-real-password-just-for-testing\n" + ) + + # Create package file that uses secrets + package_file = config_dir / "common.yaml" + package_file.write_text( + "wifi:\n ssid: !secret wifi_ssid\n password: !secret wifi_password\n" + ) + + # Create main device config that includes the package + device_config = config_dir / "test-download-secrets.yaml" + device_config.write_text( + "esphome:\n name: test-download-secrets\n platform: ESP32\n board: esp32dev\n\n" + "packages:\n common: !include common.yaml\n" + ) + + # Initialize DASHBOARD settings with our test config directory + # This is what sets CORE.config_path - the critical code path for the bug + args = Namespace( + configuration=str(config_dir), + password=None, + username=None, + ha_addon=False, + verbose=False, + ) + DASHBOARD.settings.parse_args(args) + + # With the fix: CORE.config_path should be config_dir / "___DASHBOARD_SENTINEL___.yaml" + # so CORE.config_path.parent would be config_dir + # Without the fix: CORE.config_path is config_dir / "." which normalizes to config_dir + # so CORE.config_path.parent would be tmp_path (the parent of config_dir) + + # The fix ensures CORE.config_path.parent points to config_dir + assert CORE.config_path.parent == config_dir.resolve(), ( + f"CORE.config_path.parent should point to config_dir. " + f"Got {CORE.config_path.parent}, expected {config_dir.resolve()}. " + f"CORE.config_path is {CORE.config_path}" + ) + + # Now load the YAML with packages that reference secrets + # This is where the bug would manifest - yaml_util.load_yaml would fail + # to find secrets.yaml because CORE.config_path.parent pointed to the wrong place + config = yaml_util.load_yaml(device_config) + # If we get here, secret resolution worked! + assert "esphome" in config + assert config["esphome"]["name"] == "test-download-secrets" diff --git a/tests/dashboard/test_web_server_paths.py b/tests/dashboard/test_web_server_paths.py new file mode 100644 index 0000000000..b596ebb581 --- /dev/null +++ b/tests/dashboard/test_web_server_paths.py @@ -0,0 +1,223 @@ +"""Tests for dashboard web_server Path-related functionality.""" + +from __future__ import annotations + +import gzip +import os +from pathlib import Path +from unittest.mock import MagicMock, patch + +from esphome.dashboard import web_server + + +def test_get_base_frontend_path_production() -> None: + """Test get_base_frontend_path in production mode.""" + mock_module = MagicMock() + mock_module.where.return_value = Path("/usr/local/lib/esphome_dashboard") + + with ( + patch.dict(os.environ, {}, clear=True), + patch.dict("sys.modules", {"esphome_dashboard": mock_module}), + ): + result = web_server.get_base_frontend_path() + assert result == Path("/usr/local/lib/esphome_dashboard") + mock_module.where.assert_called_once() + + +def test_get_base_frontend_path_dev_mode() -> None: + """Test get_base_frontend_path in development mode.""" + test_path = "/home/user/esphome/dashboard" + + with patch.dict(os.environ, {"ESPHOME_DASHBOARD_DEV": test_path}): + result = web_server.get_base_frontend_path() + + # The function uses Path.resolve() which resolves symlinks + # The actual function adds "/" to the path, so we simulate that + test_path_with_slash = test_path if test_path.endswith("/") else test_path + "/" + expected = ( + Path(os.getcwd()) / test_path_with_slash / "esphome_dashboard" + ).resolve() + assert result == expected + + +def test_get_base_frontend_path_dev_mode_with_trailing_slash() -> None: + """Test get_base_frontend_path in dev mode with trailing slash.""" + test_path = "/home/user/esphome/dashboard/" + + with patch.dict(os.environ, {"ESPHOME_DASHBOARD_DEV": test_path}): + result = web_server.get_base_frontend_path() + + # The function uses Path.resolve() which resolves symlinks + expected = (Path.cwd() / test_path / "esphome_dashboard").resolve() + assert result == expected + + +def test_get_base_frontend_path_dev_mode_relative_path() -> None: + """Test get_base_frontend_path with relative dev path.""" + test_path = "./dashboard" + + with patch.dict(os.environ, {"ESPHOME_DASHBOARD_DEV": test_path}): + result = web_server.get_base_frontend_path() + + # The function uses Path.resolve() which resolves symlinks + # The actual function adds "/" to the path, so we simulate that + test_path_with_slash = test_path if test_path.endswith("/") else test_path + "/" + expected = ( + Path(os.getcwd()) / test_path_with_slash / "esphome_dashboard" + ).resolve() + assert result == expected + assert result.is_absolute() + + +def test_get_static_path_single_component() -> None: + """Test get_static_path with single path component.""" + with patch("esphome.dashboard.web_server.get_base_frontend_path") as mock_base: + mock_base.return_value = Path("/base/frontend") + + result = web_server.get_static_path("file.js") + + assert result == Path("/base/frontend") / "static" / "file.js" + + +def test_get_static_path_multiple_components() -> None: + """Test get_static_path with multiple path components.""" + with patch("esphome.dashboard.web_server.get_base_frontend_path") as mock_base: + mock_base.return_value = Path("/base/frontend") + + result = web_server.get_static_path("js", "esphome", "index.js") + + assert ( + result == Path("/base/frontend") / "static" / "js" / "esphome" / "index.js" + ) + + +def test_get_static_path_empty_args() -> None: + """Test get_static_path with no arguments.""" + with patch("esphome.dashboard.web_server.get_base_frontend_path") as mock_base: + mock_base.return_value = Path("/base/frontend") + + result = web_server.get_static_path() + + assert result == Path("/base/frontend") / "static" + + +def test_get_static_path_with_pathlib_path() -> None: + """Test get_static_path with Path objects.""" + with patch("esphome.dashboard.web_server.get_base_frontend_path") as mock_base: + mock_base.return_value = Path("/base/frontend") + + path_obj = Path("js") / "app.js" + result = web_server.get_static_path(str(path_obj)) + + assert result == Path("/base/frontend") / "static" / "js" / "app.js" + + +def test_get_static_file_url_production() -> None: + """Test get_static_file_url in production mode.""" + web_server.get_static_file_url.cache_clear() + mock_module = MagicMock() + mock_path = MagicMock(spec=Path) + mock_path.read_bytes.return_value = b"test content" + + with ( + patch.dict(os.environ, {}, clear=True), + patch.dict("sys.modules", {"esphome_dashboard": mock_module}), + patch("esphome.dashboard.web_server.get_static_path") as mock_get_path, + ): + mock_get_path.return_value = mock_path + result = web_server.get_static_file_url("js/app.js") + assert result.startswith("./static/js/app.js?hash=") + + +def test_get_static_file_url_dev_mode() -> None: + """Test get_static_file_url in development mode.""" + with patch.dict(os.environ, {"ESPHOME_DASHBOARD_DEV": "/dev/path"}): + web_server.get_static_file_url.cache_clear() + result = web_server.get_static_file_url("js/app.js") + + assert result == "./static/js/app.js" + + +def test_get_static_file_url_index_js_special_case() -> None: + """Test get_static_file_url replaces index.js with entrypoint.""" + web_server.get_static_file_url.cache_clear() + mock_module = MagicMock() + mock_module.entrypoint.return_value = "main.js" + + with ( + patch.dict(os.environ, {}, clear=True), + patch.dict("sys.modules", {"esphome_dashboard": mock_module}), + ): + result = web_server.get_static_file_url("js/esphome/index.js") + assert result == "./static/js/esphome/main.js" + + +def test_load_file_path(tmp_path: Path) -> None: + """Test loading a file.""" + test_file = tmp_path / "test.txt" + test_file.write_bytes(b"test content") + + with open(test_file, "rb") as f: + content = f.read() + assert content == b"test content" + + +def test_load_file_compressed_path(tmp_path: Path) -> None: + """Test loading a compressed file.""" + test_file = tmp_path / "test.txt.gz" + + with gzip.open(test_file, "wb") as gz: + gz.write(b"compressed content") + + with gzip.open(test_file, "rb") as gz: + content = gz.read() + assert content == b"compressed content" + + +def test_path_normalization_in_static_path() -> None: + """Test that paths are normalized correctly.""" + with patch("esphome.dashboard.web_server.get_base_frontend_path") as mock_base: + mock_base.return_value = Path("/base/frontend") + + # Test with separate components + result1 = web_server.get_static_path("js", "app.js") + result2 = web_server.get_static_path("js", "app.js") + + assert result1 == result2 + assert result1 == Path("/base/frontend") / "static" / "js" / "app.js" + + +def test_windows_path_handling() -> None: + """Test handling of Windows-style paths.""" + with patch("esphome.dashboard.web_server.get_base_frontend_path") as mock_base: + mock_base.return_value = Path(r"C:\Program Files\esphome\frontend") + + result = web_server.get_static_path("js", "app.js") + + # Path should handle this correctly on the platform + expected = ( + Path(r"C:\Program Files\esphome\frontend") / "static" / "js" / "app.js" + ) + assert result == expected + + +def test_path_with_special_characters() -> None: + """Test paths with special characters.""" + with patch("esphome.dashboard.web_server.get_base_frontend_path") as mock_base: + mock_base.return_value = Path("/base/frontend") + + result = web_server.get_static_path("js-modules", "app_v1.0.js") + + assert ( + result == Path("/base/frontend") / "static" / "js-modules" / "app_v1.0.js" + ) + + +def test_path_with_spaces() -> None: + """Test paths with spaces.""" + with patch("esphome.dashboard.web_server.get_base_frontend_path") as mock_base: + mock_base.return_value = Path("/base/my frontend") + + result = web_server.get_static_path("my js", "my app.js") + + assert result == Path("/base/my frontend") / "static" / "my js" / "my app.js" diff --git a/tests/dashboard/util/test_file.py b/tests/dashboard/util/test_file.py deleted file mode 100644 index 51ba10b328..0000000000 --- a/tests/dashboard/util/test_file.py +++ /dev/null @@ -1,56 +0,0 @@ -import os -from pathlib import Path -from unittest.mock import patch - -import py -import pytest - -from esphome.dashboard.util.file import write_file, write_utf8_file - - -def test_write_utf8_file(tmp_path: Path) -> None: - write_utf8_file(tmp_path.joinpath("foo.txt"), "foo") - assert tmp_path.joinpath("foo.txt").read_text() == "foo" - - with pytest.raises(OSError): - write_utf8_file(Path("/dev/not-writable"), "bar") - - -def test_write_file(tmp_path: Path) -> None: - write_file(tmp_path.joinpath("foo.txt"), b"foo") - assert tmp_path.joinpath("foo.txt").read_text() == "foo" - - -def test_write_utf8_file_fails_at_rename( - tmpdir: py.path.local, caplog: pytest.LogCaptureFixture -) -> None: - """Test that if rename fails not not remove, we do not log the failed cleanup.""" - test_dir = tmpdir.mkdir("files") - test_file = Path(test_dir / "test.json") - - with ( - pytest.raises(OSError), - patch("esphome.dashboard.util.file.os.replace", side_effect=OSError), - ): - write_utf8_file(test_file, '{"some":"data"}', False) - - assert not os.path.exists(test_file) - - assert "File replacement cleanup failed" not in caplog.text - - -def test_write_utf8_file_fails_at_rename_and_remove( - tmpdir: py.path.local, caplog: pytest.LogCaptureFixture -) -> None: - """Test that if rename and remove both fail, we log the failed cleanup.""" - test_dir = tmpdir.mkdir("files") - test_file = Path(test_dir / "test.json") - - with ( - pytest.raises(OSError), - patch("esphome.dashboard.util.file.os.remove", side_effect=OSError), - patch("esphome.dashboard.util.file.os.replace", side_effect=OSError), - ): - write_utf8_file(test_file, '{"some":"data"}', False) - - assert "File replacement cleanup failed" in caplog.text diff --git a/tests/integration/README.md b/tests/integration/README.md index 8fce81bb80..2a6b6fe564 100644 --- a/tests/integration/README.md +++ b/tests/integration/README.md @@ -7,6 +7,7 @@ This directory contains end-to-end integration tests for ESPHome, focusing on te - `conftest.py` - Common fixtures and utilities - `const.py` - Constants used throughout the integration tests - `types.py` - Type definitions for fixtures and functions +- `state_utils.py` - State handling utilities (e.g., `InitialStateHelper`, `build_key_to_entity_mapping`) - `fixtures/` - YAML configuration files for tests - `test_*.py` - Individual test files @@ -26,6 +27,32 @@ The `yaml_config` fixture automatically loads YAML configurations based on the t - `reserved_tcp_port` - Reserves a TCP port by holding the socket open until ESPHome needs it - `unused_tcp_port` - Provides the reserved port number for each test +### Helper Utilities + +#### InitialStateHelper (`state_utils.py`) + +The `InitialStateHelper` class solves a common problem in integration tests: when an API client connects, ESPHome automatically broadcasts the current state of all entities. This can interfere with tests that want to track only new state changes triggered by test actions. + +**What it does:** +- Tracks all entities (except stateless ones like buttons) +- Swallows the first state broadcast for each entity +- Forwards all subsequent state changes to your test callback +- Provides `wait_for_initial_states()` to synchronize before test actions + +**When to use it:** +- Any test that triggers entity state changes and needs to verify them +- Tests that would otherwise see duplicate or unexpected states +- Tests that need clean separation between initial state and test-triggered changes + +**Implementation details:** +- Uses `(device_id, key)` tuples to uniquely identify entities across devices +- Automatically excludes `ButtonInfo` entities (stateless) +- Provides debug logging to track state reception (use `--log-cli-level=DEBUG`) +- Safe for concurrent use with multiple entity types + +**Future work:** +Consider converting existing integration tests to use `InitialStateHelper` for more reliable state tracking and to eliminate race conditions related to initial state broadcasts. + ### Writing Tests The simplest way to write a test is to use the `run_compiled` and `api_client_connected` fixtures: @@ -125,6 +152,54 @@ async def test_my_sensor( ``` ##### State Subscription Pattern + +**Recommended: Using InitialStateHelper** + +When an API client connects, ESPHome automatically sends the current state of all entities. The `InitialStateHelper` (from `state_utils.py`) handles this by swallowing these initial states and only forwarding subsequent state changes to your test callback: + +```python +from .state_utils import InitialStateHelper + +# Track state changes with futures +loop = asyncio.get_running_loop() +states: dict[int, EntityState] = {} +state_future: asyncio.Future[EntityState] = loop.create_future() + +def on_state(state: EntityState) -> None: + """This callback only receives NEW state changes, not initial states.""" + states[state.key] = state + # Check for specific condition using isinstance + if isinstance(state, SensorState) and state.state == expected_value: + if not state_future.done(): + state_future.set_result(state) + +# Get entities and set up state synchronization +entities, services = await client.list_entities_services() +initial_state_helper = InitialStateHelper(entities) + +# Subscribe with the wrapper that filters initial states +client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + +# Wait for all initial states to be broadcast +try: + await initial_state_helper.wait_for_initial_states() +except TimeoutError: + pytest.fail("Timeout waiting for initial states") + +# Now perform your test actions - on_state will only receive new changes +# ... trigger state changes ... + +# Wait for expected state +try: + result = await asyncio.wait_for(state_future, timeout=5.0) +except asyncio.TimeoutError: + pytest.fail(f"Expected state not received. Got: {list(states.values())}") +``` + +**Legacy: Manual State Tracking** + +If you need to handle initial states manually (not recommended for new tests): + ```python # Track state changes with futures loop = asyncio.get_running_loop() diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 55bf0b97a7..965363972f 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -58,6 +58,8 @@ def _get_platformio_env(cache_dir: Path) -> dict[str, str]: env["PLATFORMIO_CORE_DIR"] = str(cache_dir) env["PLATFORMIO_CACHE_DIR"] = str(cache_dir / ".cache") env["PLATFORMIO_LIBDEPS_DIR"] = str(cache_dir / "libdeps") + # Prevent cache cleaning during integration tests + env["ESPHOME_SKIP_CLEAN_BUILD"] = "1" return env @@ -68,6 +70,11 @@ def shared_platformio_cache() -> Generator[Path]: test_cache_dir = Path.home() / ".esphome-integration-tests" cache_dir = test_cache_dir / "platformio" + # Create the temp directory that PlatformIO uses to avoid race conditions + # This ensures it exists and won't be deleted by parallel processes + platformio_tmp_dir = cache_dir / ".cache" / "tmp" + platformio_tmp_dir.mkdir(parents=True, exist_ok=True) + # Use a lock file in the home directory to ensure only one process initializes the cache # This is needed when running with pytest-xdist # The lock file must be in a directory that already exists to avoid race conditions @@ -83,17 +90,11 @@ def shared_platformio_cache() -> Generator[Path]: test_cache_dir.mkdir(exist_ok=True) with tempfile.TemporaryDirectory() as tmpdir: - # Create a basic host config + # Use the cache_init fixture for initialization init_dir = Path(tmpdir) + fixture_path = Path(__file__).parent / "fixtures" / "cache_init.yaml" config_path = init_dir / "cache_init.yaml" - config_path.write_text("""esphome: - name: cache-init -host: -api: - encryption: - key: "IIevImVI42I0FGos5nLqFK91jrJehrgidI0ArwMLr8w=" -logger: -""") + config_path.write_text(fixture_path.read_text()) # Run compilation to populate the cache # We must succeed here to avoid race conditions where multiple @@ -105,6 +106,7 @@ logger: check=True, cwd=init_dir, env=env, + close_fds=False, ) # Lock is held until here, ensuring cache is fully populated before any test proceeds @@ -245,6 +247,7 @@ async def compile_esphome( # Start in a new process group to isolate signal handling start_new_session=True, env=env, + close_fds=False, ) await proc.wait() @@ -269,7 +272,7 @@ async def compile_esphome( def _read_config_and_get_binary(): CORE.reset() # Reset CORE state between test runs - CORE.config_path = str(config_path) + CORE.config_path = config_path config = esphome.config.read_config( {"command": "compile", "config": str(config_path)} ) @@ -344,7 +347,8 @@ async def wait_and_connect_api_client( noise_psk: str | None = None, client_info: str = "integration-test", timeout: float = API_CONNECTION_TIMEOUT, -) -> AsyncGenerator[APIClient]: + return_disconnect_event: bool = False, +) -> AsyncGenerator[APIClient | tuple[APIClient, asyncio.Event]]: """Wait for API to be available and connect.""" client = APIClient( address=address, @@ -357,14 +361,17 @@ async def wait_and_connect_api_client( # Create a future to signal when connected loop = asyncio.get_running_loop() connected_future: asyncio.Future[None] = loop.create_future() + disconnect_event = asyncio.Event() async def on_connect() -> None: """Called when successfully connected.""" + disconnect_event.clear() # Clear the disconnect event on new connection if not connected_future.done(): connected_future.set_result(None) async def on_disconnect(expected_disconnect: bool) -> None: """Called when disconnected.""" + disconnect_event.set() if not connected_future.done() and not expected_disconnect: connected_future.set_exception( APIConnectionError("Disconnected before fully connected") @@ -395,7 +402,10 @@ async def wait_and_connect_api_client( except TimeoutError: raise TimeoutError(f"Failed to connect to API after {timeout} seconds") - yield client + if return_disconnect_event: + yield client, disconnect_event + else: + yield client finally: # Stop reconnect logic and disconnect await reconnect_logic.stop() @@ -428,6 +438,33 @@ async def api_client_connected( yield _connect_client +@pytest_asyncio.fixture +async def api_client_connected_with_disconnect( + unused_tcp_port: int, +) -> AsyncGenerator: + """Factory for creating connected API client context managers with disconnect event.""" + + def _connect_client_with_disconnect( + address: str = LOCALHOST, + port: int | None = None, + password: str = "", + noise_psk: str | None = None, + client_info: str = "integration-test", + timeout: float = API_CONNECTION_TIMEOUT, + ): + return wait_and_connect_api_client( + address=address, + port=port if port is not None else unused_tcp_port, + password=password, + noise_psk=noise_psk, + client_info=client_info, + timeout=timeout, + return_disconnect_event=True, + ) + + yield _connect_client_with_disconnect + + async def _read_stream_lines( stream: asyncio.StreamReader, lines: list[str], @@ -477,6 +514,7 @@ async def run_binary_and_wait_for_port( # Start in a new process group to isolate signal handling start_new_session=True, pass_fds=(device_fd,), + close_fds=False, ) # Close the device end in the parent process diff --git a/tests/integration/fixtures/action_concurrent_reentry.yaml b/tests/integration/fixtures/action_concurrent_reentry.yaml new file mode 100644 index 0000000000..68d36d1510 --- /dev/null +++ b/tests/integration/fixtures/action_concurrent_reentry.yaml @@ -0,0 +1,105 @@ +esphome: + name: action-concurrent-reentry + on_boot: + - priority: -100 + then: + - repeat: + count: 5 + then: + - lambda: id(handler_wait_until)->execute(id(global_counter)); + - lambda: id(handler_repeat)->execute(id(global_counter)); + - lambda: id(handler_while)->execute(id(global_counter)); + - lambda: id(handler_script_wait)->execute(id(global_counter)); + - delay: 50ms + - lambda: id(global_counter)++; + - delay: 50ms + +host: + +api: + +globals: + - id: global_counter + type: int + +script: + - id: handler_wait_until + + mode: parallel + + parameters: + arg: int + + then: + - wait_until: + condition: + lambda: return id(global_counter) == 5; + + - logger.log: + format: "AFTER wait_until ARG %d" + args: + - arg + + - id: handler_script_wait + + mode: parallel + + parameters: + arg: int + + then: + - script.wait: handler_wait_until + + - logger.log: + format: "AFTER script.wait ARG %d" + args: + - arg + + - id: handler_repeat + + mode: parallel + + parameters: + arg: int + + then: + - repeat: + count: 3 + then: + - logger.log: + format: "IN repeat %d ARG %d" + args: + - iteration + - arg + - delay: 100ms + + - logger.log: + format: "AFTER repeat ARG %d" + args: + - arg + + - id: handler_while + + mode: parallel + + parameters: + arg: int + + then: + - while: + condition: + lambda: return id(global_counter) != 5; + then: + - logger.log: + format: "IN while ARG %d" + args: + - arg + - delay: 100ms + + - logger.log: + format: "AFTER while ARG %d" + args: + - arg + +logger: + level: DEBUG diff --git a/tests/integration/fixtures/api_custom_services.yaml b/tests/integration/fixtures/api_custom_services.yaml index 41efc95b85..a597c74126 100644 --- a/tests/integration/fixtures/api_custom_services.yaml +++ b/tests/integration/fixtures/api_custom_services.yaml @@ -11,6 +11,28 @@ api: then: - logger.log: "YAML service called" + # Test YAML service with arguments (tests UserServiceBase with const char* array) + - action: test_yaml_service_with_args + variables: + my_int: int + my_string: string + then: + - logger.log: + format: "YAML service with args: %d, %s" + args: [my_int, my_string.c_str()] + + # Test YAML service with multiple arguments + - action: test_yaml_service_many_args + variables: + arg1: int + arg2: float + arg3: bool + arg4: string + then: + - logger.log: + format: "YAML service many args: %d, %.2f, %d, %s" + args: [arg1, arg2, arg3, arg4.c_str()] + logger: level: DEBUG diff --git a/tests/integration/fixtures/areas_and_devices.yaml b/tests/integration/fixtures/areas_and_devices.yaml index 12ab070e55..08b02e6e1e 100644 --- a/tests/integration/fixtures/areas_and_devices.yaml +++ b/tests/integration/fixtures/areas_and_devices.yaml @@ -55,6 +55,12 @@ sensor: lambda: return 4.0; update_interval: 0.1s + - platform: template + name: Living Room Sensor + device_id: "" + lambda: return 5.0; + update_interval: 0.1s + # Switches with the same name on different devices to test device_id lookup switch: # Switch with no device_id (defaults to 0) @@ -96,3 +102,23 @@ switch: - logger.log: "Turning on Test Switch on Motion Detector" turn_off_action: - logger.log: "Turning off Test Switch on Motion Detector" + + - platform: template + name: Living Room Blank Switch + device_id: "" + id: test_switch_blank_living_room + optimistic: true + turn_on_action: + - logger.log: "Turning on Living Room Blank Switch" + turn_off_action: + - logger.log: "Turning off Living Room Blank Switch" + + - platform: template + name: Living Room None Switch + device_id: + id: test_switch_none_living_room + optimistic: true + turn_on_action: + - logger.log: "Turning on Living Room None Switch" + turn_off_action: + - logger.log: "Turning off Living Room None Switch" diff --git a/tests/integration/fixtures/automation_wait_actions.yaml b/tests/integration/fixtures/automation_wait_actions.yaml new file mode 100644 index 0000000000..65a61be14f --- /dev/null +++ b/tests/integration/fixtures/automation_wait_actions.yaml @@ -0,0 +1,130 @@ +esphome: + name: test-automation-wait-actions + +host: + +api: + actions: + # Test 1: Trigger wait_until automation 5 times rapidly + - action: test_wait_until + then: + - logger.log: "=== TEST 1: Triggering wait_until automation 5 times ===" + # Publish 5 different values to trigger the on_value automation 5 times + - sensor.template.publish: + id: wait_until_sensor + state: 1 + - sensor.template.publish: + id: wait_until_sensor + state: 2 + - sensor.template.publish: + id: wait_until_sensor + state: 3 + - sensor.template.publish: + id: wait_until_sensor + state: 4 + - sensor.template.publish: + id: wait_until_sensor + state: 5 + # Wait then satisfy the condition so all 5 waiting actions complete + - delay: 100ms + - globals.set: + id: test_flag + value: 'true' + + # Test 2: Trigger script.wait automation 5 times rapidly + - action: test_script_wait + then: + - logger.log: "=== TEST 2: Triggering script.wait automation 5 times ===" + # Start a long-running script + - script.execute: blocking_script + # Publish 5 different values to trigger the on_value automation 5 times + - sensor.template.publish: + id: script_wait_sensor + state: 1 + - sensor.template.publish: + id: script_wait_sensor + state: 2 + - sensor.template.publish: + id: script_wait_sensor + state: 3 + - sensor.template.publish: + id: script_wait_sensor + state: 4 + - sensor.template.publish: + id: script_wait_sensor + state: 5 + + # Test 3: Trigger wait_until timeout automation 5 times rapidly + - action: test_wait_timeout + then: + - logger.log: "=== TEST 3: Triggering timeout automation 5 times ===" + # Publish 5 different values (condition will never be true, all will timeout) + - sensor.template.publish: + id: timeout_sensor + state: 1 + - sensor.template.publish: + id: timeout_sensor + state: 2 + - sensor.template.publish: + id: timeout_sensor + state: 3 + - sensor.template.publish: + id: timeout_sensor + state: 4 + - sensor.template.publish: + id: timeout_sensor + state: 5 + +logger: + level: DEBUG + +globals: + - id: test_flag + type: bool + restore_value: false + initial_value: 'false' + + - id: timeout_flag + type: bool + restore_value: false + initial_value: 'false' + +# Sensors with wait_until/script.wait in their on_value automations +sensor: + # Test 1: on_value automation with wait_until + - platform: template + id: wait_until_sensor + on_value: + # This wait_until will be hit 5 times before any complete + - wait_until: + condition: + lambda: return id(test_flag); + - logger.log: "wait_until automation completed" + + # Test 2: on_value automation with script.wait + - platform: template + id: script_wait_sensor + on_value: + # This script.wait will be hit 5 times before any complete + - script.wait: blocking_script + - logger.log: "script.wait automation completed" + + # Test 3: on_value automation with wait_until timeout + - platform: template + id: timeout_sensor + on_value: + # This wait_until will be hit 5 times, all will timeout + - wait_until: + condition: + lambda: return id(timeout_flag); + timeout: 200ms + - logger.log: "timeout automation completed" + +script: + # Blocking script for script.wait test + - id: blocking_script + mode: single + then: + - logger.log: "Blocking script: START" + - delay: 200ms + - logger.log: "Blocking script: END" diff --git a/tests/integration/fixtures/batch_delay_zero_rapid_transitions.yaml b/tests/integration/fixtures/batch_delay_zero_rapid_transitions.yaml index 32cacfaa79..f7b0fdcb63 100644 --- a/tests/integration/fixtures/batch_delay_zero_rapid_transitions.yaml +++ b/tests/integration/fixtures/batch_delay_zero_rapid_transitions.yaml @@ -34,10 +34,9 @@ binary_sensor: ESP_LOGD("test", "Button ON at %u", now); } return true; - } else { - // Only log state change - if (id(ir_remote_button).state) { - ESP_LOGD("test", "Button OFF at %u", now); - } - return false; } + // Only log state change + if (id(ir_remote_button).state) { + ESP_LOGD("test", "Button OFF at %u", now); + } + return false; diff --git a/tests/integration/fixtures/cache_init.yaml b/tests/integration/fixtures/cache_init.yaml new file mode 100644 index 0000000000..de208196cd --- /dev/null +++ b/tests/integration/fixtures/cache_init.yaml @@ -0,0 +1,10 @@ +esphome: + name: cache-init + +host: + +api: + encryption: + key: "IIevImVI42I0FGos5nLqFK91jrJehrgidI0ArwMLr8w=" + +logger: diff --git a/tests/integration/fixtures/climate_custom_fan_modes_and_presets.yaml b/tests/integration/fixtures/climate_custom_fan_modes_and_presets.yaml new file mode 100644 index 0000000000..3996d0f169 --- /dev/null +++ b/tests/integration/fixtures/climate_custom_fan_modes_and_presets.yaml @@ -0,0 +1,41 @@ +esphome: + name: climate-custom-modes-test +host: +api: +logger: + +sensor: + - platform: template + id: thermostat_sensor + lambda: "return 22.0;" + +climate: + - platform: thermostat + id: test_thermostat + name: Test Thermostat Custom Modes + sensor: thermostat_sensor + default_preset: "Eco Plus" + preset: + - name: Away + default_target_temperature_low: 16°C + default_target_temperature_high: 20°C + - name: Eco Plus + default_target_temperature_low: 18°C + default_target_temperature_high: 22°C + - name: Super Saver + default_target_temperature_low: 20°C + default_target_temperature_high: 24°C + - name: Vacation Mode + default_target_temperature_low: 15°C + default_target_temperature_high: 18°C + idle_action: + - logger.log: idle_action + cool_action: + - logger.log: cool_action + heat_action: + - logger.log: heat_action + min_cooling_off_time: 10s + min_cooling_run_time: 10s + min_heating_off_time: 10s + min_heating_run_time: 10s + min_idle_time: 10s diff --git a/tests/integration/fixtures/continuation_actions.yaml b/tests/integration/fixtures/continuation_actions.yaml new file mode 100644 index 0000000000..bdfe149cb7 --- /dev/null +++ b/tests/integration/fixtures/continuation_actions.yaml @@ -0,0 +1,174 @@ +esphome: + name: test-continuation-actions + +host: + +api: + actions: + # Test 1: IfAction with ContinuationAction (then/else branches) + - action: test_if_action + variables: + condition: bool + value: int + then: + - logger.log: + format: "Test if: condition=%s, value=%d" + args: ['YESNO(condition)', 'value'] + - if: + condition: + lambda: 'return condition;' + then: + - logger.log: + format: "if-then executed: value=%d" + args: ['value'] + else: + - logger.log: + format: "if-else executed: value=%d" + args: ['value'] + - logger.log: "if completed" + + # Test 2: Nested IfAction (multiple ContinuationAction instances) + - action: test_nested_if + variables: + outer: bool + inner: bool + then: + - logger.log: + format: "Test nested if: outer=%s, inner=%s" + args: ['YESNO(outer)', 'YESNO(inner)'] + - if: + condition: + lambda: 'return outer;' + then: + - if: + condition: + lambda: 'return inner;' + then: + - logger.log: "nested-both-true" + else: + - logger.log: "nested-outer-true-inner-false" + else: + - logger.log: "nested-outer-false" + - logger.log: "nested if completed" + + # Test 3: WhileAction with WhileLoopContinuation + - action: test_while_action + variables: + max_count: int + then: + - logger.log: + format: "Test while: max_count=%d" + args: ['max_count'] + - globals.set: + id: continuation_test_counter + value: !lambda 'return 0;' + - while: + condition: + lambda: 'return id(continuation_test_counter) < max_count;' + then: + - logger.log: + format: "while-iteration-%d" + args: ['id(continuation_test_counter)'] + - globals.set: + id: continuation_test_counter + value: !lambda 'return id(continuation_test_counter) + 1;' + - logger.log: "while completed" + + # Test 4: RepeatAction with RepeatLoopContinuation + - action: test_repeat_action + variables: + count: int + then: + - logger.log: + format: "Test repeat: count=%d" + args: ['count'] + - repeat: + count: !lambda 'return count;' + then: + - logger.log: + format: "repeat-iteration-%d" + args: ['iteration'] + - logger.log: "repeat completed" + + # Test 5: Combined continuations (if + while + repeat) + - action: test_combined + variables: + do_loop: bool + loop_count: int + then: + - logger.log: + format: "Test combined: do_loop=%s, loop_count=%d" + args: ['YESNO(do_loop)', 'loop_count'] + - if: + condition: + lambda: 'return do_loop;' + then: + - repeat: + count: !lambda 'return loop_count;' + then: + - globals.set: + id: continuation_test_counter + value: !lambda 'return iteration;' + - while: + condition: + lambda: 'return id(continuation_test_counter) > 0;' + then: + - logger.log: + format: "combined-repeat%d-while%d" + args: ['iteration', 'id(continuation_test_counter)'] + - globals.set: + id: continuation_test_counter + value: !lambda 'return id(continuation_test_counter) - 1;' + else: + - logger.log: "combined-skipped" + - logger.log: "combined completed" + + # Test 6: Rapid triggers to verify memory efficiency + - action: test_rapid_if + then: + - logger.log: "=== Rapid if test start ===" + - sensor.template.publish: + id: rapid_sensor + state: 1 + - sensor.template.publish: + id: rapid_sensor + state: 2 + - sensor.template.publish: + id: rapid_sensor + state: 3 + - sensor.template.publish: + id: rapid_sensor + state: 4 + - sensor.template.publish: + id: rapid_sensor + state: 5 + - logger.log: "=== Rapid if test published 5 values ===" + +logger: + level: DEBUG + +globals: + - id: continuation_test_counter + type: int + restore_value: false + initial_value: '0' + +# Sensor to test rapid automation triggers with if/else (ContinuationAction) +sensor: + - platform: template + id: rapid_sensor + on_value: + - if: + condition: + lambda: 'return x > 2;' + then: + - logger.log: + format: "rapid-if-then: value=%d" + args: ['(int)x'] + else: + - logger.log: + format: "rapid-if-else: value=%d" + args: ['(int)x'] + - logger.log: + format: "rapid-if-completed: value=%d" + args: ['(int)x'] diff --git a/tests/integration/fixtures/crc8_helper.yaml b/tests/integration/fixtures/crc8_helper.yaml new file mode 100644 index 0000000000..e97e23eab0 --- /dev/null +++ b/tests/integration/fixtures/crc8_helper.yaml @@ -0,0 +1,17 @@ +esphome: + name: crc8-helper-test + +host: + +api: + +logger: + level: INFO + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + components: [crc8_test_component] + +crc8_test_component: diff --git a/tests/integration/fixtures/external_components/crc8_test_component/__init__.py b/tests/integration/fixtures/external_components/crc8_test_component/__init__.py new file mode 100644 index 0000000000..6032b0861f --- /dev/null +++ b/tests/integration/fixtures/external_components/crc8_test_component/__init__.py @@ -0,0 +1,17 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID + +crc8_test_component_ns = cg.esphome_ns.namespace("crc8_test_component") +CRC8TestComponent = crc8_test_component_ns.class_("CRC8TestComponent", cg.Component) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(CRC8TestComponent), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) diff --git a/tests/integration/fixtures/external_components/crc8_test_component/crc8_test_component.cpp b/tests/integration/fixtures/external_components/crc8_test_component/crc8_test_component.cpp new file mode 100644 index 0000000000..6c46af19fd --- /dev/null +++ b/tests/integration/fixtures/external_components/crc8_test_component/crc8_test_component.cpp @@ -0,0 +1,170 @@ +#include "crc8_test_component.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace crc8_test_component { + +static const char *const TAG = "crc8_test"; + +void CRC8TestComponent::setup() { + ESP_LOGI(TAG, "CRC8 Helper Function Integration Test Starting"); + + // Run all test suites + test_crc8_dallas_maxim(); + test_crc8_sensirion_style(); + test_crc8_pec_style(); + test_crc8_parameter_equivalence(); + test_crc8_edge_cases(); + test_component_compatibility(); + + ESP_LOGI(TAG, "CRC8 Integration Test Complete"); +} + +void CRC8TestComponent::test_crc8_dallas_maxim() { + ESP_LOGI(TAG, "Testing Dallas/Maxim CRC8 (default parameters)"); + + // Test vectors for Dallas/Maxim CRC8 (polynomial 0x8C, LSB-first, init 0x00) + const uint8_t test1[] = {0x01}; + const uint8_t test2[] = {0xFF}; + const uint8_t test3[] = {0x12, 0x34}; + const uint8_t test4[] = {0xAA, 0xBB, 0xCC}; + const uint8_t test5[] = {0x01, 0x02, 0x03, 0x04, 0x05}; + + bool all_passed = true; + all_passed &= verify_crc8("Dallas [0x01]", test1, sizeof(test1), 0x5E); + all_passed &= verify_crc8("Dallas [0xFF]", test2, sizeof(test2), 0x35); + all_passed &= verify_crc8("Dallas [0x12, 0x34]", test3, sizeof(test3), 0xA2); + all_passed &= verify_crc8("Dallas [0xAA, 0xBB, 0xCC]", test4, sizeof(test4), 0xD4); + all_passed &= verify_crc8("Dallas [0x01...0x05]", test5, sizeof(test5), 0x2A); + + log_test_result("Dallas/Maxim CRC8", all_passed); +} + +void CRC8TestComponent::test_crc8_sensirion_style() { + ESP_LOGI(TAG, "Testing Sensirion CRC8 (0x31 poly, MSB-first, init 0xFF)"); + + const uint8_t test1[] = {0x00}; + const uint8_t test2[] = {0x01}; + const uint8_t test3[] = {0xFF}; + const uint8_t test4[] = {0x12, 0x34}; + const uint8_t test5[] = {0xBE, 0xEF}; + + bool all_passed = true; + all_passed &= verify_crc8("Sensirion [0x00]", test1, sizeof(test1), 0xAC, 0xFF, 0x31, true); + all_passed &= verify_crc8("Sensirion [0x01]", test2, sizeof(test2), 0x9D, 0xFF, 0x31, true); + all_passed &= verify_crc8("Sensirion [0xFF]", test3, sizeof(test3), 0x00, 0xFF, 0x31, true); + all_passed &= verify_crc8("Sensirion [0x12, 0x34]", test4, sizeof(test4), 0x37, 0xFF, 0x31, true); + all_passed &= verify_crc8("Sensirion [0xBE, 0xEF]", test5, sizeof(test5), 0x92, 0xFF, 0x31, true); + + log_test_result("Sensirion CRC8", all_passed); +} + +void CRC8TestComponent::test_crc8_pec_style() { + ESP_LOGI(TAG, "Testing PEC CRC8 (0x07 poly, MSB-first, init 0x00)"); + + const uint8_t test1[] = {0x00}; + const uint8_t test2[] = {0x01}; + const uint8_t test3[] = {0xFF}; + const uint8_t test4[] = {0x12, 0x34}; + const uint8_t test5[] = {0xAA, 0xBB}; + + bool all_passed = true; + all_passed &= verify_crc8("PEC [0x00]", test1, sizeof(test1), 0x00, 0x00, 0x07, true); + all_passed &= verify_crc8("PEC [0x01]", test2, sizeof(test2), 0x07, 0x00, 0x07, true); + all_passed &= verify_crc8("PEC [0xFF]", test3, sizeof(test3), 0xF3, 0x00, 0x07, true); + all_passed &= verify_crc8("PEC [0x12, 0x34]", test4, sizeof(test4), 0xF1, 0x00, 0x07, true); + all_passed &= verify_crc8("PEC [0xAA, 0xBB]", test5, sizeof(test5), 0xB2, 0x00, 0x07, true); + + log_test_result("PEC CRC8", all_passed); +} + +void CRC8TestComponent::test_crc8_parameter_equivalence() { + ESP_LOGI(TAG, "Testing parameter equivalence"); + + const uint8_t test_data[] = {0x12, 0x34, 0x56, 0x78}; + + // Test that default parameters work as expected + uint8_t default_result = crc8(test_data, sizeof(test_data)); + uint8_t explicit_result = crc8(test_data, sizeof(test_data), 0x00, 0x8C, false); + + bool passed = (default_result == explicit_result); + if (!passed) { + ESP_LOGE(TAG, "Parameter equivalence FAILED: default=0x%02X, explicit=0x%02X", default_result, explicit_result); + } + + log_test_result("Parameter equivalence", passed); +} + +void CRC8TestComponent::test_crc8_edge_cases() { + ESP_LOGI(TAG, "Testing edge cases"); + + bool all_passed = true; + + // Empty array test + const uint8_t empty[] = {}; + uint8_t empty_result = crc8(empty, 0); + bool empty_passed = (empty_result == 0x00); // Should return init value + if (!empty_passed) { + ESP_LOGE(TAG, "Empty array test FAILED: expected 0x00, got 0x%02X", empty_result); + } + all_passed &= empty_passed; + + // Single byte tests + const uint8_t single_zero[] = {0x00}; + const uint8_t single_ff[] = {0xFF}; + all_passed &= verify_crc8("Single [0x00]", single_zero, 1, 0x00); + all_passed &= verify_crc8("Single [0xFF]", single_ff, 1, 0x35); + + log_test_result("Edge cases", all_passed); +} + +void CRC8TestComponent::test_component_compatibility() { + ESP_LOGI(TAG, "Testing component compatibility"); + + // Test specific component use cases + bool all_passed = true; + + // AGS10-style data (Sensirion CRC8) + const uint8_t ags10_data[] = {0x12, 0x34, 0x56}; + uint8_t ags10_result = crc8(ags10_data, sizeof(ags10_data), 0xFF, 0x31, true); + ESP_LOGI(TAG, "AGS10-style CRC8: 0x%02X", ags10_result); + + // LC709203F-style data (PEC CRC8) + const uint8_t lc_data[] = {0xAA, 0xBB}; + uint8_t lc_result = crc8(lc_data, sizeof(lc_data), 0x00, 0x07, true); + ESP_LOGI(TAG, "LC709203F-style CRC8: 0x%02X", lc_result); + + // DallasTemperature-style data (Dallas CRC8) + const uint8_t dallas_data[] = {0x28, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC}; + uint8_t dallas_result = crc8(dallas_data, sizeof(dallas_data)); + ESP_LOGI(TAG, "Dallas-style CRC8: 0x%02X", dallas_result); + + all_passed = true; // These are just demonstration tests + log_test_result("Component compatibility", all_passed); +} + +bool CRC8TestComponent::verify_crc8(const char *test_name, const uint8_t *data, uint8_t len, uint8_t expected, + uint8_t crc, uint8_t poly, bool msb_first) { + uint8_t result = esphome::crc8(data, len, crc, poly, msb_first); + bool passed = (result == expected); + + if (passed) { + ESP_LOGI(TAG, "%s: PASS (0x%02X)", test_name, result); + } else { + ESP_LOGE(TAG, "%s: FAIL - expected 0x%02X, got 0x%02X", test_name, expected, result); + } + + return passed; +} + +void CRC8TestComponent::log_test_result(const char *test_name, bool passed) { + if (passed) { + ESP_LOGI(TAG, "%s: ALL TESTS PASSED", test_name); + } else { + ESP_LOGE(TAG, "%s: SOME TESTS FAILED", test_name); + } +} + +} // namespace crc8_test_component +} // namespace esphome diff --git a/tests/integration/fixtures/external_components/crc8_test_component/crc8_test_component.h b/tests/integration/fixtures/external_components/crc8_test_component/crc8_test_component.h new file mode 100644 index 0000000000..3b8847259c --- /dev/null +++ b/tests/integration/fixtures/external_components/crc8_test_component/crc8_test_component.h @@ -0,0 +1,29 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace crc8_test_component { + +class CRC8TestComponent : public Component { + public: + void setup() override; + + private: + void test_crc8_dallas_maxim(); + void test_crc8_sensirion_style(); + void test_crc8_pec_style(); + void test_crc8_parameter_equivalence(); + void test_crc8_edge_cases(); + void test_component_compatibility(); + void test_old_vs_new_implementations(); + + void log_test_result(const char *test_name, bool passed); + bool verify_crc8(const char *test_name, const uint8_t *data, uint8_t len, uint8_t expected, uint8_t crc = 0x00, + uint8_t poly = 0x8C, bool msb_first = false); +}; + +} // namespace crc8_test_component +} // namespace esphome diff --git a/tests/integration/fixtures/external_components/gpio_expander_test_component/__init__.py b/tests/integration/fixtures/external_components/gpio_expander_test_component/__init__.py new file mode 100644 index 0000000000..5672f80004 --- /dev/null +++ b/tests/integration/fixtures/external_components/gpio_expander_test_component/__init__.py @@ -0,0 +1,25 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID + +AUTO_LOAD = ["gpio_expander"] + +gpio_expander_test_component_ns = cg.esphome_ns.namespace( + "gpio_expander_test_component" +) + +GPIOExpanderTestComponent = gpio_expander_test_component_ns.class_( + "GPIOExpanderTestComponent", cg.Component +) + + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(GPIOExpanderTestComponent), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) diff --git a/tests/integration/fixtures/external_components/gpio_expander_test_component/gpio_expander_test_component.cpp b/tests/integration/fixtures/external_components/gpio_expander_test_component/gpio_expander_test_component.cpp new file mode 100644 index 0000000000..6e128687c4 --- /dev/null +++ b/tests/integration/fixtures/external_components/gpio_expander_test_component/gpio_expander_test_component.cpp @@ -0,0 +1,40 @@ +#include "gpio_expander_test_component.h" + +#include "esphome/core/application.h" +#include "esphome/core/log.h" + +namespace esphome::gpio_expander_test_component { + +static const char *const TAG = "gpio_expander_test"; + +void GPIOExpanderTestComponent::setup() { + for (uint8_t pin = 0; pin < 32; pin++) { + this->digital_read(pin); + } + + this->digital_read(3); + this->digital_read(3); + this->digital_read(4); + this->digital_read(3); + this->digital_read(10); + this->reset_pin_cache_(); // Reset cache to ensure next read is from hardware + this->digital_read(15); + this->digital_read(14); + this->digital_read(14); + + ESP_LOGD(TAG, "DONE"); +} + +bool GPIOExpanderTestComponent::digital_read_hw(uint8_t pin) { + ESP_LOGD(TAG, "digital_read_hw pin=%d", pin); + // Return true to indicate successful read operation + return true; +} + +bool GPIOExpanderTestComponent::digital_read_cache(uint8_t pin) { + ESP_LOGD(TAG, "digital_read_cache pin=%d", pin); + // Return the pin state (always HIGH for testing) + return true; +} + +} // namespace esphome::gpio_expander_test_component diff --git a/tests/integration/fixtures/external_components/gpio_expander_test_component/gpio_expander_test_component.h b/tests/integration/fixtures/external_components/gpio_expander_test_component/gpio_expander_test_component.h new file mode 100644 index 0000000000..ffaee2cd65 --- /dev/null +++ b/tests/integration/fixtures/external_components/gpio_expander_test_component/gpio_expander_test_component.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/gpio_expander/cached_gpio.h" +#include "esphome/core/component.h" + +namespace esphome::gpio_expander_test_component { + +class GPIOExpanderTestComponent : public Component, public esphome::gpio_expander::CachedGpioExpander { + public: + void setup() override; + + protected: + bool digital_read_hw(uint8_t pin) override; + bool digital_read_cache(uint8_t pin) override; + void digital_write_hw(uint8_t pin, bool value) override{}; +}; + +} // namespace esphome::gpio_expander_test_component diff --git a/tests/integration/fixtures/external_components/gpio_expander_test_component_uint16/__init__.py b/tests/integration/fixtures/external_components/gpio_expander_test_component_uint16/__init__.py new file mode 100644 index 0000000000..76f20b942c --- /dev/null +++ b/tests/integration/fixtures/external_components/gpio_expander_test_component_uint16/__init__.py @@ -0,0 +1,24 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID + +AUTO_LOAD = ["gpio_expander"] + +gpio_expander_test_component_uint16_ns = cg.esphome_ns.namespace( + "gpio_expander_test_component_uint16" +) + +GPIOExpanderTestUint16Component = gpio_expander_test_component_uint16_ns.class_( + "GPIOExpanderTestUint16Component", cg.Component +) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(GPIOExpanderTestUint16Component), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) diff --git a/tests/integration/fixtures/external_components/gpio_expander_test_component_uint16/gpio_expander_test_component_uint16.cpp b/tests/integration/fixtures/external_components/gpio_expander_test_component_uint16/gpio_expander_test_component_uint16.cpp new file mode 100644 index 0000000000..09537c81bb --- /dev/null +++ b/tests/integration/fixtures/external_components/gpio_expander_test_component_uint16/gpio_expander_test_component_uint16.cpp @@ -0,0 +1,43 @@ +#include "gpio_expander_test_component_uint16.h" +#include "esphome/core/log.h" + +namespace esphome::gpio_expander_test_component_uint16 { + +static const char *const TAG = "gpio_expander_test_uint16"; + +void GPIOExpanderTestUint16Component::setup() { + ESP_LOGD(TAG, "Testing uint16_t bank (single 16-pin bank)"); + + // Test reading all 16 pins - first should trigger hw read, rest use cache + for (uint8_t pin = 0; pin < 16; pin++) { + this->digital_read(pin); + } + + // Reset cache and test specific reads + ESP_LOGD(TAG, "Resetting cache for uint16_t test"); + this->reset_pin_cache_(); + + // First read triggers hw for entire bank + this->digital_read(5); + // These should all use cache since they're in the same bank + this->digital_read(10); + this->digital_read(15); + this->digital_read(0); + + ESP_LOGD(TAG, "DONE_UINT16"); +} + +bool GPIOExpanderTestUint16Component::digital_read_hw(uint8_t pin) { + ESP_LOGD(TAG, "uint16_digital_read_hw pin=%d", pin); + // In a real component, this would read from I2C/SPI into internal state + // For testing, we just return true to indicate successful read + return true; // Return true to indicate successful read +} + +bool GPIOExpanderTestUint16Component::digital_read_cache(uint8_t pin) { + ESP_LOGD(TAG, "uint16_digital_read_cache pin=%d", pin); + // Return the actual pin state from our test pattern + return (this->test_state_ >> pin) & 1; +} + +} // namespace esphome::gpio_expander_test_component_uint16 diff --git a/tests/integration/fixtures/external_components/gpio_expander_test_component_uint16/gpio_expander_test_component_uint16.h b/tests/integration/fixtures/external_components/gpio_expander_test_component_uint16/gpio_expander_test_component_uint16.h new file mode 100644 index 0000000000..be102f9b57 --- /dev/null +++ b/tests/integration/fixtures/external_components/gpio_expander_test_component_uint16/gpio_expander_test_component_uint16.h @@ -0,0 +1,23 @@ +#pragma once + +#include "esphome/components/gpio_expander/cached_gpio.h" +#include "esphome/core/component.h" + +namespace esphome::gpio_expander_test_component_uint16 { + +// Test component using uint16_t bank type (single 16-pin bank) +class GPIOExpanderTestUint16Component : public Component, + public esphome::gpio_expander::CachedGpioExpander { + public: + void setup() override; + + protected: + bool digital_read_hw(uint8_t pin) override; + bool digital_read_cache(uint8_t pin) override; + void digital_write_hw(uint8_t pin, bool value) override{}; + + private: + uint16_t test_state_{0xAAAA}; // Test pattern: alternating bits +}; + +} // namespace esphome::gpio_expander_test_component_uint16 diff --git a/tests/integration/fixtures/external_components/loop_test_component/loop_test_component.h b/tests/integration/fixtures/external_components/loop_test_component/loop_test_component.h index cdc04d491b..3dca2da2e9 100644 --- a/tests/integration/fixtures/external_components/loop_test_component/loop_test_component.h +++ b/tests/integration/fixtures/external_components/loop_test_component/loop_test_component.h @@ -39,7 +39,7 @@ template class EnableAction : public Action { public: EnableAction(LoopTestComponent *parent) : parent_(parent) {} - void play(Ts... x) override { this->parent_->service_enable(); } + void play(const Ts &...x) override { this->parent_->service_enable(); } protected: LoopTestComponent *parent_; @@ -49,7 +49,7 @@ template class DisableAction : public Action { public: DisableAction(LoopTestComponent *parent) : parent_(parent) {} - void play(Ts... x) override { this->parent_->service_disable(); } + void play(const Ts &...x) override { this->parent_->service_disable(); } protected: LoopTestComponent *parent_; diff --git a/tests/integration/fixtures/gpio_expander_cache.yaml b/tests/integration/fixtures/gpio_expander_cache.yaml new file mode 100644 index 0000000000..8b5375af4c --- /dev/null +++ b/tests/integration/fixtures/gpio_expander_cache.yaml @@ -0,0 +1,21 @@ +esphome: + name: gpio-expander-cache +host: + +logger: + level: DEBUG + +api: + +# External component that uses gpio_expander::CachedGpioExpander +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + components: [gpio_expander_test_component, gpio_expander_test_component_uint16] + +# Test with uint8_t (multiple banks) +gpio_expander_test_component: + +# Test with uint16_t (single bank) +gpio_expander_test_component_uint16: diff --git a/tests/integration/fixtures/host_mode_climate_basic_state.yaml b/tests/integration/fixtures/host_mode_climate_basic_state.yaml new file mode 100644 index 0000000000..f79d684fc6 --- /dev/null +++ b/tests/integration/fixtures/host_mode_climate_basic_state.yaml @@ -0,0 +1,112 @@ +esphome: + name: host-climate-test +host: +api: +logger: + +climate: + - platform: thermostat + id: dual_mode_thermostat + name: Dual-mode Thermostat + sensor: host_thermostat_temperature_sensor + humidity_sensor: host_thermostat_humidity_sensor + humidity_hysteresis: 1.0 + min_cooling_off_time: 20s + min_cooling_run_time: 20s + max_cooling_run_time: 30s + supplemental_cooling_delta: 3.0 + min_heating_off_time: 20s + min_heating_run_time: 20s + max_heating_run_time: 30s + supplemental_heating_delta: 3.0 + min_fanning_off_time: 20s + min_fanning_run_time: 20s + min_idle_time: 10s + visual: + min_humidity: 20% + max_humidity: 70% + min_temperature: 15.0 + max_temperature: 32.0 + temperature_step: 0.1 + default_preset: home + preset: + - name: "away" + default_target_temperature_low: 18.0 + default_target_temperature_high: 24.0 + - name: "home" + default_target_temperature_low: 18.0 + default_target_temperature_high: 24.0 + auto_mode: + - logger.log: "AUTO mode set" + heat_cool_mode: + - logger.log: "HEAT_COOL mode set" + cool_action: + - switch.turn_on: air_cond + supplemental_cooling_action: + - switch.turn_on: air_cond_2 + heat_action: + - switch.turn_on: heater + supplemental_heating_action: + - switch.turn_on: heater_2 + dry_action: + - switch.turn_on: air_cond + fan_only_action: + - switch.turn_on: fan_only + idle_action: + - switch.turn_off: air_cond + - switch.turn_off: air_cond_2 + - switch.turn_off: heater + - switch.turn_off: heater_2 + - switch.turn_off: fan_only + humidity_control_humidify_action: + - switch.turn_on: humidifier + humidity_control_off_action: + - switch.turn_off: humidifier + +sensor: + - platform: template + id: host_thermostat_humidity_sensor + unit_of_measurement: °C + accuracy_decimals: 2 + state_class: measurement + force_update: true + lambda: return 42.0; + update_interval: 0.1s + - platform: template + id: host_thermostat_temperature_sensor + unit_of_measurement: °C + accuracy_decimals: 2 + state_class: measurement + force_update: true + lambda: return 22.0; + update_interval: 0.1s + +switch: + - platform: template + id: air_cond + name: Air Conditioner + optimistic: true + - platform: template + id: air_cond_2 + name: Air Conditioner 2 + optimistic: true + - platform: template + id: fan_only + name: Fan + optimistic: true + - platform: template + id: heater + name: Heater + optimistic: true + - platform: template + id: heater_2 + name: Heater 2 + optimistic: true + - platform: template + id: dehumidifier + name: Dehumidifier + optimistic: true + - platform: template + id: humidifier + name: Humidifier + optimistic: true diff --git a/tests/integration/fixtures/host_mode_climate_control.yaml b/tests/integration/fixtures/host_mode_climate_control.yaml new file mode 100644 index 0000000000..c60e0597a2 --- /dev/null +++ b/tests/integration/fixtures/host_mode_climate_control.yaml @@ -0,0 +1,108 @@ +esphome: + name: host-climate-test +host: +api: +logger: + +climate: + - platform: thermostat + id: dual_mode_thermostat + name: Dual-mode Thermostat + sensor: host_thermostat_temperature_sensor + humidity_sensor: host_thermostat_humidity_sensor + humidity_hysteresis: 1.0 + min_cooling_off_time: 20s + min_cooling_run_time: 20s + max_cooling_run_time: 30s + supplemental_cooling_delta: 3.0 + min_heating_off_time: 20s + min_heating_run_time: 20s + max_heating_run_time: 30s + supplemental_heating_delta: 3.0 + min_fanning_off_time: 20s + min_fanning_run_time: 20s + min_idle_time: 10s + visual: + min_humidity: 20% + max_humidity: 70% + min_temperature: 15.0 + max_temperature: 32.0 + temperature_step: 0.1 + default_preset: home + preset: + - name: "away" + default_target_temperature_low: 18.0 + default_target_temperature_high: 24.0 + - name: "home" + default_target_temperature_low: 18.0 + default_target_temperature_high: 24.0 + auto_mode: + - logger.log: "AUTO mode set" + heat_cool_mode: + - logger.log: "HEAT_COOL mode set" + cool_action: + - switch.turn_on: air_cond + supplemental_cooling_action: + - switch.turn_on: air_cond_2 + heat_action: + - switch.turn_on: heater + supplemental_heating_action: + - switch.turn_on: heater_2 + dry_action: + - switch.turn_on: air_cond + fan_only_action: + - switch.turn_on: fan_only + idle_action: + - switch.turn_off: air_cond + - switch.turn_off: air_cond_2 + - switch.turn_off: heater + - switch.turn_off: heater_2 + - switch.turn_off: fan_only + humidity_control_humidify_action: + - switch.turn_on: humidifier + humidity_control_off_action: + - switch.turn_off: humidifier + +sensor: + - platform: template + id: host_thermostat_humidity_sensor + unit_of_measurement: °C + accuracy_decimals: 2 + state_class: measurement + force_update: true + lambda: return 42.0; + update_interval: 0.1s + - platform: template + id: host_thermostat_temperature_sensor + unit_of_measurement: °C + accuracy_decimals: 2 + state_class: measurement + force_update: true + lambda: return 22.0; + update_interval: 0.1s + +switch: + - platform: template + id: air_cond + name: Air Conditioner + optimistic: true + - platform: template + id: air_cond_2 + name: Air Conditioner 2 + optimistic: true + - platform: template + id: fan_only + name: Fan + optimistic: true + - platform: template + id: heater + name: Heater + optimistic: true + - platform: template + id: heater_2 + name: Heater 2 + optimistic: true + - platform: template + id: humidifier + name: Humidifier + optimistic: true diff --git a/tests/integration/fixtures/host_mode_empty_string_options.yaml b/tests/integration/fixtures/host_mode_empty_string_options.yaml index ab8e6cd005..a170511c46 100644 --- a/tests/integration/fixtures/host_mode_empty_string_options.yaml +++ b/tests/integration/fixtures/host_mode_empty_string_options.yaml @@ -41,6 +41,17 @@ select: - "" # Empty string at the end initial_option: "Choice X" + - platform: template + name: "Select Initial Option Test" + id: select_initial_option_test + optimistic: true + options: + - "First" + - "Second" + - "Third" + - "Fourth" + initial_option: "Third" # Test non-default initial option + # Add a sensor to ensure we have other entities in the list sensor: - platform: template diff --git a/tests/integration/fixtures/host_mode_many_entities.yaml b/tests/integration/fixtures/host_mode_many_entities.yaml index 5e085a15c9..acb03f235b 100644 --- a/tests/integration/fixtures/host_mode_many_entities.yaml +++ b/tests/integration/fixtures/host_mode_many_entities.yaml @@ -210,7 +210,15 @@ sensor: name: "Test Sensor 50" lambda: return 50.0; update_interval: 0.1s - # Temperature sensor for the thermostat + # Sensors for the thermostat + - platform: template + name: "Humidity Sensor" + id: humidity_sensor + lambda: return 35.0; + unit_of_measurement: "%" + device_class: humidity + state_class: measurement + update_interval: 5s - platform: template name: "Temperature Sensor" id: temp_sensor @@ -295,6 +303,11 @@ valve: - logger.log: "Valve stopping" output: + - platform: template + id: humidifier_output + type: binary + write_action: + - logger.log: "Humidifier output changed" - platform: template id: heater_output type: binary @@ -305,18 +318,31 @@ output: type: binary write_action: - logger.log: "Cooler output changed" + - platform: template + id: fan_output + type: binary + write_action: + - logger.log: "Fan output changed" climate: - platform: thermostat name: "Test Thermostat" sensor: temp_sensor + humidity_sensor: humidity_sensor default_preset: Home on_boot_restore_from: default_preset min_heating_off_time: 1s min_heating_run_time: 1s min_cooling_off_time: 1s min_cooling_run_time: 1s + min_fan_mode_switching_time: 1s min_idle_time: 1s + visual: + min_humidity: 20% + max_humidity: 70% + min_temperature: 15.0 + max_temperature: 32.0 + temperature_step: 0.1 heat_action: - output.turn_on: heater_output cool_action: @@ -324,6 +350,14 @@ climate: idle_action: - output.turn_off: heater_output - output.turn_off: cooler_output + humidity_control_humidify_action: + - output.turn_on: humidifier_output + humidity_control_off_action: + - output.turn_off: humidifier_output + fan_mode_auto_action: + - output.turn_off: fan_output + fan_mode_on_action: + - output.turn_on: fan_output preset: - name: Home default_target_temperature_low: 20 @@ -373,3 +407,20 @@ button: name: "Test Button" on_press: - logger.log: "Button pressed" + +# Date, Time, and DateTime entities +datetime: + - platform: template + type: date + name: "Test Date" + initial_value: "2023-05-13" + optimistic: true + - platform: template + type: time + name: "Test Time" + initial_value: "12:30:00" + optimistic: true + - platform: template + type: datetime + name: "Test DateTime" + optimistic: true diff --git a/tests/integration/fixtures/host_preferences_save_load.yaml b/tests/integration/fixtures/host_preferences_save_load.yaml new file mode 100644 index 0000000000..929c5f7ff0 --- /dev/null +++ b/tests/integration/fixtures/host_preferences_save_load.yaml @@ -0,0 +1,110 @@ +esphome: + name: test_device + on_boot: + - lambda: |- + ESP_LOGD("test", "Host preferences test starting"); + +host: + +logger: + level: DEBUG + +api: + +preferences: + flash_write_interval: 0s # Disable automatic saving for test control + +switch: + - platform: template + name: "Test Switch" + id: test_switch + optimistic: true + restore_mode: DISABLED # Don't auto-restore for test control + +number: + - platform: template + name: "Test Number" + id: test_number + min_value: 0 + max_value: 100 + step: 0.1 + optimistic: true + restore_value: false # Don't auto-restore for test control + +button: + - platform: template + name: "Save Preferences" + on_press: + - lambda: |- + // Save current values to preferences + ESPPreferenceObject switch_pref = global_preferences->make_preference(0x1234); + ESPPreferenceObject number_pref = global_preferences->make_preference(0x5678); + + bool switch_value = id(test_switch).state; + float number_value = id(test_number).state; + + if (switch_pref.save(&switch_value)) { + ESP_LOGI("test", "Preference saved: key=switch, value=%.1f", switch_value ? 1.0 : 0.0); + } + if (number_pref.save(&number_value)) { + ESP_LOGI("test", "Preference saved: key=number, value=%.1f", number_value); + } + + // Force sync to disk + global_preferences->sync(); + + - platform: template + name: "Load Preferences" + on_press: + - lambda: |- + // Load values from preferences + ESPPreferenceObject switch_pref = global_preferences->make_preference(0x1234); + ESPPreferenceObject number_pref = global_preferences->make_preference(0x5678); + + // Also try to load non-existent preferences (tests our fix) + ESPPreferenceObject fake_pref1 = global_preferences->make_preference(0x9999); + ESPPreferenceObject fake_pref2 = global_preferences->make_preference(0xAAAA); + + bool switch_value = false; + float number_value = 0.0; + uint32_t fake_value = 0; + int loaded_count = 0; + + // These should not exist and shouldn't create map entries + fake_pref1.load(&fake_value); + fake_pref2.load(&fake_value); + + if (switch_pref.load(&switch_value)) { + id(test_switch).publish_state(switch_value); + ESP_LOGI("test", "Preference loaded: key=switch, value=%.1f", switch_value ? 1.0 : 0.0); + loaded_count++; + } else { + ESP_LOGW("test", "Failed to load switch preference"); + } + + if (number_pref.load(&number_value)) { + id(test_number).publish_state(number_value); + ESP_LOGI("test", "Preference loaded: key=number, value=%.1f", number_value); + loaded_count++; + } else { + ESP_LOGW("test", "Failed to load number preference"); + } + + // Log completion message for the test to detect + ESP_LOGI("test", "Final load test: loaded %d preferences successfully", loaded_count); + + - platform: template + name: "Verify Preferences" + on_press: + - lambda: |- + // Verify current values match what we expect + bool switch_value = id(test_switch).state; + float number_value = id(test_number).state; + + // After loading, switch should be true (1.0) and number should be 42.5 + if (switch_value == true && number_value == 42.5) { + ESP_LOGI("test", "Preferences verified: values match!"); + } else { + ESP_LOGE("test", "Preferences mismatch: switch=%d, number=%.1f", + switch_value, number_value); + } diff --git a/tests/integration/fixtures/light_calls.yaml b/tests/integration/fixtures/light_calls.yaml index d692a11765..2b7650526f 100644 --- a/tests/integration/fixtures/light_calls.yaml +++ b/tests/integration/fixtures/light_calls.yaml @@ -56,10 +56,29 @@ light: warm_white_color_temperature: 2000 K constant_brightness: true effects: + # Use default parameters: - random: - name: "Random Effect" + # Customize parameters - use longer names to potentially trigger buffer issues + - random: + name: "My Very Slow Random Effect With Long Name" + transition_length: 30ms + update_interval: 30ms + - random: + name: "My Fast Random Effect That Changes Quickly" + transition_length: 4ms + update_interval: 5ms + - random: + name: "Random Effect With Medium Length Name Here" transition_length: 100ms update_interval: 200ms + - random: + name: "Another Random Effect With Different Parameters" + transition_length: 2ms + update_interval: 3ms + - random: + name: "Yet Another Random Effect To Test Memory" + transition_length: 15ms + update_interval: 20ms - strobe: name: "Strobe Effect" - pulse: @@ -73,6 +92,17 @@ light: red: test_red green: test_green blue: test_blue + effects: + # Same random effects to test for cross-contamination + - random: + - random: + name: "RGB Slow Random" + transition_length: 20ms + update_interval: 25ms + - random: + name: "RGB Fast Random" + transition_length: 2ms + update_interval: 3ms - platform: binary name: "Test Binary Light" diff --git a/tests/integration/fixtures/multi_device_preferences.yaml b/tests/integration/fixtures/multi_device_preferences.yaml new file mode 100644 index 0000000000..634d7157b2 --- /dev/null +++ b/tests/integration/fixtures/multi_device_preferences.yaml @@ -0,0 +1,165 @@ +esphome: + name: multi-device-preferences-test + # Define multiple devices for testing preference storage + devices: + - id: device_a + name: Device A + - id: device_b + name: Device B + +host: +api: # Port will be automatically injected +logger: + level: DEBUG + +# Test entities with restore modes to verify preference storage + +# Switches with same name on different devices - test restore mode +switch: + - platform: template + name: Light + id: light_device_a + device_id: device_a + restore_mode: RESTORE_DEFAULT_OFF + turn_on_action: + - lambda: |- + ESP_LOGI("test", "Device A Light turned ON"); + turn_off_action: + - lambda: |- + ESP_LOGI("test", "Device A Light turned OFF"); + + - platform: template + name: Light + id: light_device_b + device_id: device_b + restore_mode: RESTORE_DEFAULT_ON # Different default to test uniqueness + turn_on_action: + - lambda: |- + ESP_LOGI("test", "Device B Light turned ON"); + turn_off_action: + - lambda: |- + ESP_LOGI("test", "Device B Light turned OFF"); + + - platform: template + name: Light + id: light_main + restore_mode: RESTORE_DEFAULT_OFF + turn_on_action: + - lambda: |- + ESP_LOGI("test", "Main Light turned ON"); + turn_off_action: + - lambda: |- + ESP_LOGI("test", "Main Light turned OFF"); + +# Numbers with restore to test preference storage +number: + - platform: template + name: Setpoint + id: setpoint_device_a + device_id: device_a + min_value: 10.0 + max_value: 30.0 + step: 0.5 + restore_value: true + initial_value: 20.0 + set_action: + - lambda: |- + ESP_LOGI("test", "Device A Setpoint set to %.1f", x); + id(setpoint_device_a).state = x; + + - platform: template + name: Setpoint + id: setpoint_device_b + device_id: device_b + min_value: 10.0 + max_value: 30.0 + step: 0.5 + restore_value: true + initial_value: 25.0 # Different initial to test uniqueness + set_action: + - lambda: |- + ESP_LOGI("test", "Device B Setpoint set to %.1f", x); + id(setpoint_device_b).state = x; + + - platform: template + name: Setpoint + id: setpoint_main + min_value: 10.0 + max_value: 30.0 + step: 0.5 + restore_value: true + initial_value: 22.0 + set_action: + - lambda: |- + ESP_LOGI("test", "Main Setpoint set to %.1f", x); + id(setpoint_main).state = x; + +# Selects with restore to test preference storage +select: + - platform: template + name: Mode + id: mode_device_a + device_id: device_a + options: + - "Auto" + - "Manual" + - "Off" + restore_value: true + initial_option: "Auto" + set_action: + - lambda: |- + ESP_LOGI("test", "Device A Mode set to %s", x.c_str()); + id(mode_device_a).state = x; + + - platform: template + name: Mode + id: mode_device_b + device_id: device_b + options: + - "Auto" + - "Manual" + - "Off" + restore_value: true + initial_option: "Manual" # Different initial to test uniqueness + set_action: + - lambda: |- + ESP_LOGI("test", "Device B Mode set to %s", x.c_str()); + id(mode_device_b).state = x; + + - platform: template + name: Mode + id: mode_main + options: + - "Auto" + - "Manual" + - "Off" + restore_value: true + initial_option: "Off" + set_action: + - lambda: |- + ESP_LOGI("test", "Main Mode set to %s", x.c_str()); + id(mode_main).state = x; + +# Button to trigger preference logging test +button: + - platform: template + name: Test Preferences + on_press: + - lambda: |- + ESP_LOGI("test", "Testing preference storage uniqueness:"); + ESP_LOGI("test", "Device A Light state: %s", id(light_device_a).state ? "ON" : "OFF"); + ESP_LOGI("test", "Device B Light state: %s", id(light_device_b).state ? "ON" : "OFF"); + ESP_LOGI("test", "Main Light state: %s", id(light_main).state ? "ON" : "OFF"); + ESP_LOGI("test", "Device A Setpoint: %.1f", id(setpoint_device_a).state); + ESP_LOGI("test", "Device B Setpoint: %.1f", id(setpoint_device_b).state); + ESP_LOGI("test", "Main Setpoint: %.1f", id(setpoint_main).state); + ESP_LOGI("test", "Device A Mode: %s", id(mode_device_a).state.c_str()); + ESP_LOGI("test", "Device B Mode: %s", id(mode_device_b).state.c_str()); + ESP_LOGI("test", "Main Mode: %s", id(mode_main).state.c_str()); + // Log preference hashes for entities that actually store preferences + ESP_LOGI("test", "Device A Switch Pref Hash: %u", id(light_device_a).get_preference_hash()); + ESP_LOGI("test", "Device B Switch Pref Hash: %u", id(light_device_b).get_preference_hash()); + ESP_LOGI("test", "Main Switch Pref Hash: %u", id(light_main).get_preference_hash()); + ESP_LOGI("test", "Device A Number Pref Hash: %u", id(setpoint_device_a).get_preference_hash()); + ESP_LOGI("test", "Device B Number Pref Hash: %u", id(setpoint_device_b).get_preference_hash()); + ESP_LOGI("test", "Main Number Pref Hash: %u", id(setpoint_main).get_preference_hash()); diff --git a/tests/integration/fixtures/noise_corrupt_encrypted_frame.yaml b/tests/integration/fixtures/noise_corrupt_encrypted_frame.yaml new file mode 100644 index 0000000000..6f0266c6fd --- /dev/null +++ b/tests/integration/fixtures/noise_corrupt_encrypted_frame.yaml @@ -0,0 +1,11 @@ +esphome: + name: oversized-noise + +host: + +api: + encryption: + key: N4Yle5YirwZhPiHHsdZLdOA73ndj/84veVaLhTvxCuU= + +logger: + level: VERY_VERBOSE diff --git a/tests/integration/fixtures/noise_encryption_key_clear_protection.yaml b/tests/integration/fixtures/noise_encryption_key_clear_protection.yaml new file mode 100644 index 0000000000..3ce84cd373 --- /dev/null +++ b/tests/integration/fixtures/noise_encryption_key_clear_protection.yaml @@ -0,0 +1,10 @@ +esphome: + name: noise-key-test + +host: + +api: + encryption: + key: "zX9/JHxMKwpP0jUGsF0iESCm1wRvNgR6NkKVOhn7kSs=" + +logger: diff --git a/tests/integration/fixtures/noise_encryption_key_protection.yaml b/tests/integration/fixtures/noise_encryption_key_protection.yaml new file mode 100644 index 0000000000..3ce84cd373 --- /dev/null +++ b/tests/integration/fixtures/noise_encryption_key_protection.yaml @@ -0,0 +1,10 @@ +esphome: + name: noise-key-test + +host: + +api: + encryption: + key: "zX9/JHxMKwpP0jUGsF0iESCm1wRvNgR6NkKVOhn7kSs=" + +logger: diff --git a/tests/integration/fixtures/oversized_payload_noise.yaml b/tests/integration/fixtures/oversized_payload_noise.yaml new file mode 100644 index 0000000000..6f0266c6fd --- /dev/null +++ b/tests/integration/fixtures/oversized_payload_noise.yaml @@ -0,0 +1,11 @@ +esphome: + name: oversized-noise + +host: + +api: + encryption: + key: N4Yle5YirwZhPiHHsdZLdOA73ndj/84veVaLhTvxCuU= + +logger: + level: VERY_VERBOSE diff --git a/tests/integration/fixtures/oversized_payload_plaintext.yaml b/tests/integration/fixtures/oversized_payload_plaintext.yaml new file mode 100644 index 0000000000..44ece4f770 --- /dev/null +++ b/tests/integration/fixtures/oversized_payload_plaintext.yaml @@ -0,0 +1,9 @@ +esphome: + name: oversized-plaintext + +host: + +api: + +logger: + level: VERY_VERBOSE diff --git a/tests/integration/fixtures/oversized_protobuf_message_id_noise.yaml b/tests/integration/fixtures/oversized_protobuf_message_id_noise.yaml new file mode 100644 index 0000000000..6f0266c6fd --- /dev/null +++ b/tests/integration/fixtures/oversized_protobuf_message_id_noise.yaml @@ -0,0 +1,11 @@ +esphome: + name: oversized-noise + +host: + +api: + encryption: + key: N4Yle5YirwZhPiHHsdZLdOA73ndj/84veVaLhTvxCuU= + +logger: + level: VERY_VERBOSE diff --git a/tests/integration/fixtures/oversized_protobuf_message_id_plaintext.yaml b/tests/integration/fixtures/oversized_protobuf_message_id_plaintext.yaml new file mode 100644 index 0000000000..1e9eadfdc5 --- /dev/null +++ b/tests/integration/fixtures/oversized_protobuf_message_id_plaintext.yaml @@ -0,0 +1,9 @@ +esphome: + name: oversized-protobuf-plaintext + +host: + +api: + +logger: + level: VERY_VERBOSE diff --git a/tests/integration/fixtures/parallel_script_delays.yaml b/tests/integration/fixtures/parallel_script_delays.yaml new file mode 100644 index 0000000000..71d5b904e9 --- /dev/null +++ b/tests/integration/fixtures/parallel_script_delays.yaml @@ -0,0 +1,45 @@ +esphome: + name: test-parallel-delays + +host: + +logger: + level: VERY_VERBOSE + +api: + actions: + - action: test_parallel_delays + then: + # Start three parallel script instances with small delays between starts + - globals.set: + id: instance_counter + value: '1' + - script.execute: parallel_delay_script + - delay: 10ms + - globals.set: + id: instance_counter + value: '2' + - script.execute: parallel_delay_script + - delay: 10ms + - globals.set: + id: instance_counter + value: '3' + - script.execute: parallel_delay_script + +globals: + - id: instance_counter + type: int + initial_value: '0' + +script: + - id: parallel_delay_script + mode: parallel + then: + - lambda: !lambda |- + int instance = id(instance_counter); + ESP_LOGI("TEST", "Parallel script instance %d started", instance); + - delay: 1s + - lambda: !lambda |- + static int completed_counter = 0; + completed_counter++; + ESP_LOGI("TEST", "Parallel script instance %d completed after delay", completed_counter); diff --git a/tests/integration/fixtures/runtime_stats.yaml b/tests/integration/fixtures/runtime_stats.yaml index aad1c275fb..fd34cdb939 100644 --- a/tests/integration/fixtures/runtime_stats.yaml +++ b/tests/integration/fixtures/runtime_stats.yaml @@ -32,6 +32,7 @@ switch: name: "Test Switch" id: test_switch optimistic: true + lambda: return false; interval: - interval: 0.5s diff --git a/tests/integration/fixtures/scheduler_pool.yaml b/tests/integration/fixtures/scheduler_pool.yaml new file mode 100644 index 0000000000..5389125188 --- /dev/null +++ b/tests/integration/fixtures/scheduler_pool.yaml @@ -0,0 +1,282 @@ +esphome: + name: scheduler-pool-test + on_boot: + priority: -100 + then: + - logger.log: "Starting scheduler pool tests" + debug_scheduler: true # Enable scheduler debug logging + +host: +api: + services: + - service: run_phase_1 + then: + - script.execute: test_pool_recycling + - service: run_phase_2 + then: + - script.execute: test_sensor_polling + - service: run_phase_3 + then: + - script.execute: test_communication_patterns + - service: run_phase_4 + then: + - script.execute: test_defer_patterns + - service: run_phase_5 + then: + - script.execute: test_pool_reuse_verification + - service: run_phase_6 + then: + - script.execute: test_full_pool_reuse + - service: run_phase_7 + then: + - script.execute: test_same_defer_optimization + - service: run_complete + then: + - script.execute: complete_test +logger: + level: VERY_VERBOSE # Need VERY_VERBOSE to see pool debug messages + +globals: + - id: create_count + type: int + initial_value: '0' + - id: cancel_count + type: int + initial_value: '0' + - id: interval_counter + type: int + initial_value: '0' + - id: pool_test_done + type: bool + initial_value: 'false' + +script: + - id: test_pool_recycling + then: + - logger.log: "Testing scheduler pool recycling with realistic usage patterns" + - lambda: |- + auto *component = id(test_sensor); + + // Simulate realistic component behavior with timeouts that complete naturally + ESP_LOGI("test", "Phase 1: Simulating normal component lifecycle"); + + // Sensor update timeouts (common pattern) + App.scheduler.set_timeout(component, "sensor_init", 10, []() { + ESP_LOGD("test", "Sensor initialized"); + id(create_count)++; + }); + + // Retry timeout (gets cancelled if successful) + App.scheduler.set_timeout(component, "retry_timeout", 50, []() { + ESP_LOGD("test", "Retry timeout executed"); + id(create_count)++; + }); + + // Simulate successful operation - cancel retry + App.scheduler.set_timeout(component, "success_sim", 20, []() { + ESP_LOGD("test", "Operation succeeded, cancelling retry"); + App.scheduler.cancel_timeout(id(test_sensor), "retry_timeout"); + id(cancel_count)++; + }); + + id(create_count) += 3; + ESP_LOGI("test", "Phase 1 complete"); + + - id: test_sensor_polling + then: + - lambda: |- + // Simulate sensor polling pattern + ESP_LOGI("test", "Phase 2: Simulating sensor polling patterns"); + auto *component = id(test_sensor); + + // Multiple sensors with different update intervals + // These should only allocate once and reuse the same item for each interval execution + App.scheduler.set_interval(component, "temp_sensor", 10, []() { + ESP_LOGD("test", "Temperature sensor update"); + id(interval_counter)++; + if (id(interval_counter) >= 3) { + App.scheduler.cancel_interval(id(test_sensor), "temp_sensor"); + ESP_LOGD("test", "Temperature sensor stopped"); + } + }); + + App.scheduler.set_interval(component, "humidity_sensor", 15, []() { + ESP_LOGD("test", "Humidity sensor update"); + id(interval_counter)++; + if (id(interval_counter) >= 5) { + App.scheduler.cancel_interval(id(test_sensor), "humidity_sensor"); + ESP_LOGD("test", "Humidity sensor stopped"); + } + }); + + // Only 2 allocations for the intervals, no matter how many times they execute + id(create_count) += 2; + ESP_LOGD("test", "Created 2 intervals - they will reuse same items for each execution"); + ESP_LOGI("test", "Phase 2 complete"); + + - id: test_communication_patterns + then: + - lambda: |- + // Simulate communication patterns (WiFi/API reconnects, etc) + ESP_LOGI("test", "Phase 3: Simulating communication patterns"); + auto *component = id(test_sensor); + + // Connection timeout pattern + App.scheduler.set_timeout(component, "connect_timeout", 200, []() { + ESP_LOGD("test", "Connection timeout - would retry"); + id(create_count)++; + + // Schedule retry + App.scheduler.set_timeout(id(test_sensor), "connect_retry", 100, []() { + ESP_LOGD("test", "Retrying connection"); + id(create_count)++; + }); + }); + + // Heartbeat pattern + App.scheduler.set_interval(component, "heartbeat", 50, []() { + ESP_LOGD("test", "Heartbeat"); + id(interval_counter)++; + if (id(interval_counter) >= 10) { + App.scheduler.cancel_interval(id(test_sensor), "heartbeat"); + ESP_LOGD("test", "Heartbeat stopped"); + } + }); + + id(create_count) += 2; + ESP_LOGI("test", "Phase 3 complete"); + + - id: test_defer_patterns + then: + - lambda: |- + // Simulate defer patterns (state changes, async operations) + ESP_LOGI("test", "Phase 4: Simulating heavy defer patterns like ratgdo"); + + auto *component = id(test_sensor); + + // Simulate a burst of defer operations like ratgdo does with state updates + // These should execute immediately and recycle quickly to the pool + for (int i = 0; i < 10; i++) { + std::string defer_name = "defer_" + std::to_string(i); + App.scheduler.set_timeout(component, defer_name, 0, [i]() { + ESP_LOGD("test", "Defer %d executed", i); + // Force a small delay between defer executions to see recycling + if (i == 5) { + ESP_LOGI("test", "Half of defers executed, checking pool status"); + } + }); + } + + id(create_count) += 10; + ESP_LOGD("test", "Created 10 defer operations (0ms timeouts)"); + + // Also create some named defers that might get replaced + App.scheduler.set_timeout(component, "state_update", 0, []() { + ESP_LOGD("test", "State update 1"); + }); + + // Replace the same named defer (should cancel previous) + App.scheduler.set_timeout(component, "state_update", 0, []() { + ESP_LOGD("test", "State update 2 (replaced)"); + }); + + id(create_count) += 2; + id(cancel_count) += 1; // One cancelled due to replacement + + ESP_LOGI("test", "Phase 4 complete"); + + - id: test_pool_reuse_verification + then: + - lambda: |- + ESP_LOGI("test", "Phase 5: Verifying pool reuse after everything settles"); + + // Cancel any remaining intervals + auto *component = id(test_sensor); + App.scheduler.cancel_interval(component, "temp_sensor"); + App.scheduler.cancel_interval(component, "humidity_sensor"); + App.scheduler.cancel_interval(component, "heartbeat"); + + ESP_LOGD("test", "Cancelled any remaining intervals"); + + // The pool should have items from completed timeouts in earlier phases. + // Phase 1 had 3 timeouts that completed and were recycled. + // Phase 3 had 1 timeout that completed and was recycled. + // Phase 4 had 3 defers that completed and were recycled. + // So we should have a decent pool size already from naturally completed items. + + // Now create 8 new timeouts - they should reuse from pool when available + int reuse_test_count = 8; + + for (int i = 0; i < reuse_test_count; i++) { + std::string name = "reuse_test_" + std::to_string(i); + App.scheduler.set_timeout(component, name, 10 + i * 5, [i]() { + ESP_LOGD("test", "Reuse test %d completed", i); + }); + } + + ESP_LOGI("test", "Created %d items for reuse verification", reuse_test_count); + id(create_count) += reuse_test_count; + ESP_LOGI("test", "Phase 5 complete"); + + - id: test_full_pool_reuse + then: + - lambda: |- + ESP_LOGI("test", "Phase 6: Testing pool size limits after Phase 5 items complete"); + + // At this point, all Phase 5 timeouts should have completed and been recycled. + // The pool should be at its maximum size (5). + // Creating 10 new items tests that: + // - First 5 items reuse from the pool + // - Remaining 5 items allocate new (pool empty) + // - Pool doesn't grow beyond MAX_POOL_SIZE of 5 + + auto *component = id(test_sensor); + int full_reuse_count = 10; + + for (int i = 0; i < full_reuse_count; i++) { + std::string name = "full_reuse_" + std::to_string(i); + App.scheduler.set_timeout(component, name, 10 + i * 5, [i]() { + ESP_LOGD("test", "Full reuse test %d completed", i); + }); + } + + ESP_LOGI("test", "Created %d items for full pool reuse verification", full_reuse_count); + id(create_count) += full_reuse_count; + ESP_LOGI("test", "Phase 6 complete"); + + - id: test_same_defer_optimization + then: + - lambda: |- + ESP_LOGI("test", "Phase 7: Testing same-named defer optimization"); + + auto *component = id(test_sensor); + + // Create 10 defers with the same name - should optimize to update callback in-place + // This pattern is common in components like ratgdo that repeatedly defer state updates + for (int i = 0; i < 10; i++) { + App.scheduler.set_timeout(component, "repeated_defer", 0, [i]() { + ESP_LOGD("test", "Repeated defer executed with value: %d", i); + }); + } + + // Only the first should allocate, the rest should update in-place + // We expect only 1 allocation for all 10 operations + id(create_count) += 1; // Only count 1 since others should be optimized + + ESP_LOGD("test", "Created 10 same-named defers (should only allocate once)"); + ESP_LOGI("test", "Phase 7 complete"); + + - id: complete_test + then: + - lambda: |- + ESP_LOGI("test", "Pool recycling test complete - created %d items, cancelled %d, intervals %d", + id(create_count), id(cancel_count), id(interval_counter)); + +sensor: + - platform: template + name: Test Sensor + id: test_sensor + lambda: return 1.0; + update_interval: never + +# No interval - tests will be triggered from Python via API services diff --git a/tests/integration/fixtures/scheduler_removed_item_race.yaml b/tests/integration/fixtures/scheduler_removed_item_race.yaml new file mode 100644 index 0000000000..2f8a7fb987 --- /dev/null +++ b/tests/integration/fixtures/scheduler_removed_item_race.yaml @@ -0,0 +1,139 @@ +esphome: + name: scheduler-removed-item-race + +host: + +api: + services: + - service: run_test + then: + - script.execute: run_test_script + +logger: + level: DEBUG + +globals: + - id: test_passed + type: bool + initial_value: 'true' + - id: removed_item_executed + type: int + initial_value: '0' + - id: normal_item_executed + type: int + initial_value: '0' + +sensor: + - platform: template + id: test_sensor + name: "Test Sensor" + update_interval: never + lambda: return 0.0; + +script: + - id: run_test_script + then: + - logger.log: "=== Starting Removed Item Race Test ===" + + # This test creates a scenario where: + # 1. First item in heap is NOT cancelled (cleanup stops immediately) + # 2. Items behind it ARE cancelled (remain in heap after cleanup) + # 3. All items execute at the same time, including cancelled ones + + - lambda: |- + // The key to hitting the race: + // 1. Add items in a specific order to control heap structure + // 2. Cancel ONLY items that won't be at the front + // 3. Ensure the first item stays non-cancelled so cleanup_() stops immediately + + // Schedule all items to execute at the SAME time (1ms from now) + // Using 1ms instead of 0 to avoid defer queue on multi-core platforms + // This ensures they'll all be ready together and go through the heap + const uint32_t exec_time = 1; + + // CRITICAL: Add a non-cancellable item FIRST + // This will be at the front of the heap and block cleanup_() + App.scheduler.set_timeout(id(test_sensor), "blocker", exec_time, []() { + ESP_LOGD("test", "Blocker timeout executed (expected) - was at front of heap"); + id(normal_item_executed)++; + }); + + // Now add items that we WILL cancel + // These will be behind the blocker in the heap + App.scheduler.set_timeout(id(test_sensor), "cancel_1", exec_time, []() { + ESP_LOGE("test", "RACE: Cancelled timeout 1 executed after being cancelled!"); + id(removed_item_executed)++; + id(test_passed) = false; + }); + + App.scheduler.set_timeout(id(test_sensor), "cancel_2", exec_time, []() { + ESP_LOGE("test", "RACE: Cancelled timeout 2 executed after being cancelled!"); + id(removed_item_executed)++; + id(test_passed) = false; + }); + + App.scheduler.set_timeout(id(test_sensor), "cancel_3", exec_time, []() { + ESP_LOGE("test", "RACE: Cancelled timeout 3 executed after being cancelled!"); + id(removed_item_executed)++; + id(test_passed) = false; + }); + + // Add some more normal items + App.scheduler.set_timeout(id(test_sensor), "normal_1", exec_time, []() { + ESP_LOGD("test", "Normal timeout 1 executed (expected)"); + id(normal_item_executed)++; + }); + + App.scheduler.set_timeout(id(test_sensor), "normal_2", exec_time, []() { + ESP_LOGD("test", "Normal timeout 2 executed (expected)"); + id(normal_item_executed)++; + }); + + App.scheduler.set_timeout(id(test_sensor), "normal_3", exec_time, []() { + ESP_LOGD("test", "Normal timeout 3 executed (expected)"); + id(normal_item_executed)++; + }); + + // Force items into the heap before cancelling + App.scheduler.process_to_add(); + + // NOW cancel the items - they're behind "blocker" in the heap + // When cleanup_() runs, it will see "blocker" (not removed) at the front + // and stop immediately, leaving cancel_1, cancel_2, cancel_3 in the heap + bool c1 = App.scheduler.cancel_timeout(id(test_sensor), "cancel_1"); + bool c2 = App.scheduler.cancel_timeout(id(test_sensor), "cancel_2"); + bool c3 = App.scheduler.cancel_timeout(id(test_sensor), "cancel_3"); + + ESP_LOGD("test", "Cancelled items (behind blocker): %s, %s, %s", + c1 ? "true" : "false", + c2 ? "true" : "false", + c3 ? "true" : "false"); + + // The heap now has: + // - "blocker" at front (not cancelled) + // - cancelled items behind it (marked remove=true but still in heap) + // - When all execute at once, cleanup_() stops at "blocker" + // - The loop then executes ALL ready items including cancelled ones + + ESP_LOGD("test", "Setup complete. Blocker at front prevents cleanup of cancelled items behind it"); + + # Wait for all timeouts to execute (or not) + - delay: 20ms + + # Check results + - lambda: |- + ESP_LOGI("test", "=== Test Results ==="); + ESP_LOGI("test", "Normal items executed: %d (expected 4)", id(normal_item_executed)); + ESP_LOGI("test", "Removed items executed: %d (expected 0)", id(removed_item_executed)); + + if (id(removed_item_executed) > 0) { + ESP_LOGE("test", "TEST FAILED: %d cancelled items were executed!", id(removed_item_executed)); + id(test_passed) = false; + } else if (id(normal_item_executed) != 4) { + ESP_LOGE("test", "TEST FAILED: Expected 4 normal items, got %d", id(normal_item_executed)); + id(test_passed) = false; + } else { + ESP_LOGI("test", "TEST PASSED: No cancelled items were executed"); + } + + ESP_LOGI("test", "=== Test Complete ==="); diff --git a/tests/integration/fixtures/script_delay_with_params.yaml b/tests/integration/fixtures/script_delay_with_params.yaml new file mode 100644 index 0000000000..2a0f16d9fe --- /dev/null +++ b/tests/integration/fixtures/script_delay_with_params.yaml @@ -0,0 +1,131 @@ +esphome: + name: test-script-delay-params + +host: + +api: + actions: + # Test case from issue #12044: parent script with repeat calling child with delay + - action: test_repeat_with_delay + then: + - logger.log: "=== TEST: Repeat loop calling script with delay and parameters ===" + - script.execute: father_script + + # Test case from issue #12043: script.wait with delayed child script + - action: test_script_wait + then: + - logger.log: "=== TEST: script.wait with delayed child script ===" + - script.execute: show_start_page + - script.wait: show_start_page + - logger.log: "After wait: script completed successfully" + + # Test: Delay with different parameter types + - action: test_delay_param_types + then: + - logger.log: "=== TEST: Delay with various parameter types ===" + - script.execute: + id: delay_with_int + val: 42 + - delay: 50ms + - script.execute: + id: delay_with_string + msg: "test message" + - delay: 50ms + - script.execute: + id: delay_with_float + num: 3.14 + +logger: + level: DEBUG + +script: + # Reproduces issue #12044: child script with conditional delay + - id: son_script + mode: single + parameters: + iteration: int + then: + - logger.log: + format: "Son script started with iteration %d" + args: ['iteration'] + - if: + condition: + lambda: 'return iteration >= 5;' + then: + - logger.log: + format: "Son script delaying for iteration %d" + args: ['iteration'] + - delay: 100ms + - logger.log: + format: "Son script finished with iteration %d" + args: ['iteration'] + + # Reproduces issue #12044: parent script with repeat loop + - id: father_script + mode: single + then: + - repeat: + count: 10 + then: + - logger.log: + format: "Father iteration %d: calling son" + args: ['iteration'] + - script.execute: + id: son_script + iteration: !lambda 'return iteration;' + - script.wait: son_script + - logger.log: + format: "Father iteration %d: son finished, wait returned" + args: ['iteration'] + + # Reproduces issue #12043: script.wait hangs + - id: show_start_page + mode: single + then: + - logger.log: "Start page: beginning" + - delay: 100ms + - logger.log: "Start page: after delay" + - delay: 100ms + - logger.log: "Start page: completed" + + # Test delay with int parameter + - id: delay_with_int + mode: single + parameters: + val: int + then: + - logger.log: + format: "Int test: before delay, val=%d" + args: ['val'] + - delay: 50ms + - logger.log: + format: "Int test: after delay, val=%d" + args: ['val'] + + # Test delay with string parameter + - id: delay_with_string + mode: single + parameters: + msg: string + then: + - logger.log: + format: "String test: before delay, msg=%s" + args: ['msg.c_str()'] + - delay: 50ms + - logger.log: + format: "String test: after delay, msg=%s" + args: ['msg.c_str()'] + + # Test delay with float parameter + - id: delay_with_float + mode: single + parameters: + num: float + then: + - logger.log: + format: "Float test: before delay, num=%.2f" + args: ['num'] + - delay: 50ms + - logger.log: + format: "Float test: after delay, num=%.2f" + args: ['num'] diff --git a/tests/integration/fixtures/script_queued.yaml b/tests/integration/fixtures/script_queued.yaml new file mode 100644 index 0000000000..996dd6436f --- /dev/null +++ b/tests/integration/fixtures/script_queued.yaml @@ -0,0 +1,170 @@ +esphome: + name: test-script-queued + +host: +api: + actions: + # Test 1: Queue depth with default max_runs=5 + - action: test_queue_depth + then: + - logger.log: "=== TEST 1: Queue depth (max_runs=5 means 5 total, reject 6-7) ===" + - script.execute: + id: queue_depth_script + value: 1 + - script.execute: + id: queue_depth_script + value: 2 + - script.execute: + id: queue_depth_script + value: 3 + - script.execute: + id: queue_depth_script + value: 4 + - script.execute: + id: queue_depth_script + value: 5 + - script.execute: + id: queue_depth_script + value: 6 + - script.execute: + id: queue_depth_script + value: 7 + + # Test 2: Ring buffer wrap test + - action: test_ring_buffer + then: + - logger.log: "=== TEST 2: Ring buffer wrap (should process A, B, C in order) ===" + - script.execute: + id: wrap_script + msg: "A" + - script.execute: + id: wrap_script + msg: "B" + - script.execute: + id: wrap_script + msg: "C" + + # Test 3: Stop clears queue + - action: test_stop_clears + then: + - logger.log: "=== TEST 3: Stop clears queue (should only see 1, then 'STOPPED') ===" + - script.execute: + id: stop_script + num: 1 + - script.execute: + id: stop_script + num: 2 + - script.execute: + id: stop_script + num: 3 + - delay: 50ms + - logger.log: "STOPPING script now" + - script.stop: stop_script + + # Test 4: Verify rejection (max_runs=3) + - action: test_rejection + then: + - logger.log: "=== TEST 4: Verify rejection (max_runs=3 means 3 total, reject 4-8) ===" + - script.execute: + id: rejection_script + val: 1 + - script.execute: + id: rejection_script + val: 2 + - script.execute: + id: rejection_script + val: 3 + - script.execute: + id: rejection_script + val: 4 + - script.execute: + id: rejection_script + val: 5 + - script.execute: + id: rejection_script + val: 6 + - script.execute: + id: rejection_script + val: 7 + - script.execute: + id: rejection_script + val: 8 + + # Test 5: No parameters test + - action: test_no_params + then: + - logger.log: "=== TEST 5: No params (should process 3 times) ===" + - script.execute: no_params_script + - script.execute: no_params_script + - script.execute: no_params_script + +logger: + level: DEBUG + +script: + # Test script 1: Queue depth test (default max_runs=5) + - id: queue_depth_script + mode: queued + parameters: + value: int + then: + - logger.log: + format: "Queue test: START item %d" + args: ['value'] + - delay: 100ms + - logger.log: + format: "Queue test: END item %d" + args: ['value'] + + # Test script 2: Ring buffer wrap test (max_runs=3) + - id: wrap_script + mode: queued + max_runs: 3 + parameters: + msg: string + then: + - logger.log: + format: "Ring buffer: START '%s'" + args: ['msg.c_str()'] + - delay: 50ms + - logger.log: + format: "Ring buffer: END '%s'" + args: ['msg.c_str()'] + + # Test script 3: Stop test + - id: stop_script + mode: queued + max_runs: 5 + parameters: + num: int + then: + - logger.log: + format: "Stop test: START %d" + args: ['num'] + - delay: 100ms + - logger.log: + format: "Stop test: END %d" + args: ['num'] + + # Test script 4: Rejection test (max_runs=3) + - id: rejection_script + mode: queued + max_runs: 3 + parameters: + val: int + then: + - logger.log: + format: "Rejection test: START %d" + args: ['val'] + - delay: 200ms + - logger.log: + format: "Rejection test: END %d" + args: ['val'] + + # Test script 5: No parameters + - id: no_params_script + mode: queued + then: + - logger.log: "No params: START" + - delay: 50ms + - logger.log: "No params: END" diff --git a/tests/integration/fixtures/script_wait_on_boot.yaml b/tests/integration/fixtures/script_wait_on_boot.yaml new file mode 100644 index 0000000000..8736b02294 --- /dev/null +++ b/tests/integration/fixtures/script_wait_on_boot.yaml @@ -0,0 +1,54 @@ +esphome: + name: test-script-wait-on-boot + on_boot: + # Use default priority (600.0) which is same as ScriptWaitAction's setup priority + # This tests the race condition where on_boot runs before ScriptWaitAction::setup() + then: + - logger.log: "=== on_boot: Starting boot sequence ===" + - script.execute: show_start_page + - script.wait: show_start_page + - logger.log: "=== on_boot: First script completed, starting second ===" + - script.execute: flip_thru_pages + - script.wait: flip_thru_pages + - logger.log: "=== on_boot: All boot scripts completed successfully ===" + +host: + +api: + actions: + # Manual trigger for additional testing + - action: test_script_wait + then: + - logger.log: "=== Manual test: Starting ===" + - script.execute: show_start_page + - script.wait: show_start_page + - logger.log: "=== Manual test: First script completed ===" + - script.execute: flip_thru_pages + - script.wait: flip_thru_pages + - logger.log: "=== Manual test: All completed ===" + +logger: + level: DEBUG + +script: + # First script - simulates display initialization + - id: show_start_page + mode: single + then: + - logger.log: "show_start_page: Starting" + - delay: 100ms + - logger.log: "show_start_page: After delay 1" + - delay: 100ms + - logger.log: "show_start_page: Completed" + + # Second script - simulates page flip sequence + - id: flip_thru_pages + mode: single + then: + - logger.log: "flip_thru_pages: Starting" + - delay: 50ms + - logger.log: "flip_thru_pages: Page 1" + - delay: 50ms + - logger.log: "flip_thru_pages: Page 2" + - delay: 50ms + - logger.log: "flip_thru_pages: Completed" diff --git a/tests/integration/fixtures/sensor_filters_batch_window.yaml b/tests/integration/fixtures/sensor_filters_batch_window.yaml new file mode 100644 index 0000000000..58a254c215 --- /dev/null +++ b/tests/integration/fixtures/sensor_filters_batch_window.yaml @@ -0,0 +1,58 @@ +esphome: + name: test-batch-window-filters + +host: +api: + batch_delay: 0ms # Disable batching to receive all state updates +logger: + level: DEBUG + +# Template sensor that we'll use to publish values +sensor: + - platform: template + name: "Source Sensor" + id: source_sensor + accuracy_decimals: 2 + + # Batch window filters (window_size == send_every) - use streaming filters + - platform: copy + source_id: source_sensor + name: "Min Sensor" + id: min_sensor + filters: + - min: + window_size: 5 + send_every: 5 + send_first_at: 1 + + - platform: copy + source_id: source_sensor + name: "Max Sensor" + id: max_sensor + filters: + - max: + window_size: 5 + send_every: 5 + send_first_at: 1 + + - platform: copy + source_id: source_sensor + name: "Moving Avg Sensor" + id: moving_avg_sensor + filters: + - sliding_window_moving_average: + window_size: 5 + send_every: 5 + send_first_at: 1 + +# Button to trigger publishing test values +button: + - platform: template + name: "Publish Values Button" + id: publish_button + on_press: + - lambda: |- + // Publish 10 values: 1.0, 2.0, ..., 10.0 + for (int i = 1; i <= 10; i++) { + id(source_sensor).publish_state(float(i)); + } diff --git a/tests/integration/fixtures/sensor_filters_nan_handling.yaml b/tests/integration/fixtures/sensor_filters_nan_handling.yaml new file mode 100644 index 0000000000..fcb12cfde5 --- /dev/null +++ b/tests/integration/fixtures/sensor_filters_nan_handling.yaml @@ -0,0 +1,84 @@ +esphome: + name: test-nan-handling + +host: +api: + batch_delay: 0ms # Disable batching to receive all state updates +logger: + level: DEBUG + +sensor: + - platform: template + name: "Source NaN Sensor" + id: source_nan_sensor + accuracy_decimals: 2 + + - platform: copy + source_id: source_nan_sensor + name: "Min NaN Sensor" + id: min_nan_sensor + filters: + - min: + window_size: 5 + send_every: 5 + send_first_at: 1 + + - platform: copy + source_id: source_nan_sensor + name: "Max NaN Sensor" + id: max_nan_sensor + filters: + - max: + window_size: 5 + send_every: 5 + send_first_at: 1 + +script: + - id: publish_nan_values_script + then: + - sensor.template.publish: + id: source_nan_sensor + state: 10.0 + - delay: 20ms + - sensor.template.publish: + id: source_nan_sensor + state: !lambda 'return NAN;' + - delay: 20ms + - sensor.template.publish: + id: source_nan_sensor + state: 5.0 + - delay: 20ms + - sensor.template.publish: + id: source_nan_sensor + state: !lambda 'return NAN;' + - delay: 20ms + - sensor.template.publish: + id: source_nan_sensor + state: 15.0 + - delay: 20ms + - sensor.template.publish: + id: source_nan_sensor + state: 8.0 + - delay: 20ms + - sensor.template.publish: + id: source_nan_sensor + state: !lambda 'return NAN;' + - delay: 20ms + - sensor.template.publish: + id: source_nan_sensor + state: 12.0 + - delay: 20ms + - sensor.template.publish: + id: source_nan_sensor + state: 3.0 + - delay: 20ms + - sensor.template.publish: + id: source_nan_sensor + state: !lambda 'return NAN;' + +button: + - platform: template + name: "Publish NaN Values Button" + id: publish_nan_button + on_press: + - script.execute: publish_nan_values_script diff --git a/tests/integration/fixtures/sensor_filters_ring_buffer.yaml b/tests/integration/fixtures/sensor_filters_ring_buffer.yaml new file mode 100644 index 0000000000..ea7a326b8d --- /dev/null +++ b/tests/integration/fixtures/sensor_filters_ring_buffer.yaml @@ -0,0 +1,115 @@ +esphome: + name: test-sliding-window-filters + +host: +api: + batch_delay: 0ms # Disable batching to receive all state updates +logger: + level: DEBUG + +# Template sensor that we'll use to publish values +sensor: + - platform: template + name: "Source Sensor" + id: source_sensor + accuracy_decimals: 2 + + # ACTUAL sliding window filters (window_size != send_every) - use ring buffers + # Window of 5, send every 2 values + - platform: copy + source_id: source_sensor + name: "Sliding Min Sensor" + id: sliding_min_sensor + filters: + - min: + window_size: 5 + send_every: 2 + send_first_at: 1 + + - platform: copy + source_id: source_sensor + name: "Sliding Max Sensor" + id: sliding_max_sensor + filters: + - max: + window_size: 5 + send_every: 2 + send_first_at: 1 + + - platform: copy + source_id: source_sensor + name: "Sliding Median Sensor" + id: sliding_median_sensor + filters: + - median: + window_size: 5 + send_every: 2 + send_first_at: 1 + + - platform: copy + source_id: source_sensor + name: "Sliding Moving Avg Sensor" + id: sliding_moving_avg_sensor + filters: + - sliding_window_moving_average: + window_size: 5 + send_every: 2 + send_first_at: 1 + +# Button to trigger publishing test values +script: + - id: publish_values_script + then: + # Publish 10 values: 1.0, 2.0, ..., 10.0 + # With window_size=5, send_every=2, send_first_at=1: + # - Output at position 1: window=[1], min=1, max=1, median=1, avg=1 + # - Output at position 3: window=[1,2,3], min=1, max=3, median=2, avg=2 + # - Output at position 5: window=[1,2,3,4,5], min=1, max=5, median=3, avg=3 + # - Output at position 7: window=[3,4,5,6,7], min=3, max=7, median=5, avg=5 + # - Output at position 9: window=[5,6,7,8,9], min=5, max=9, median=7, avg=7 + - sensor.template.publish: + id: source_sensor + state: 1.0 + - delay: 20ms + - sensor.template.publish: + id: source_sensor + state: 2.0 + - delay: 20ms + - sensor.template.publish: + id: source_sensor + state: 3.0 + - delay: 20ms + - sensor.template.publish: + id: source_sensor + state: 4.0 + - delay: 20ms + - sensor.template.publish: + id: source_sensor + state: 5.0 + - delay: 20ms + - sensor.template.publish: + id: source_sensor + state: 6.0 + - delay: 20ms + - sensor.template.publish: + id: source_sensor + state: 7.0 + - delay: 20ms + - sensor.template.publish: + id: source_sensor + state: 8.0 + - delay: 20ms + - sensor.template.publish: + id: source_sensor + state: 9.0 + - delay: 20ms + - sensor.template.publish: + id: source_sensor + state: 10.0 + +button: + - platform: template + name: "Publish Values Button" + id: publish_button + on_press: + - script.execute: publish_values_script diff --git a/tests/integration/fixtures/sensor_filters_ring_buffer_wraparound.yaml b/tests/integration/fixtures/sensor_filters_ring_buffer_wraparound.yaml new file mode 100644 index 0000000000..bd5980160b --- /dev/null +++ b/tests/integration/fixtures/sensor_filters_ring_buffer_wraparound.yaml @@ -0,0 +1,72 @@ +esphome: + name: test-ring-buffer-wraparound + +host: +api: + batch_delay: 0ms # Disable batching to receive all state updates +logger: + level: DEBUG + +sensor: + - platform: template + name: "Source Wraparound Sensor" + id: source_wraparound + accuracy_decimals: 2 + + - platform: copy + source_id: source_wraparound + name: "Wraparound Min Sensor" + id: wraparound_min_sensor + filters: + - min: + window_size: 3 + send_every: 3 + send_first_at: 1 + +script: + - id: publish_wraparound_script + then: + # Publish 9 values to test ring buffer wraparound + # Values: 10, 20, 30, 5, 25, 15, 40, 35, 20 + - sensor.template.publish: + id: source_wraparound + state: 10.0 + - delay: 20ms + - sensor.template.publish: + id: source_wraparound + state: 20.0 + - delay: 20ms + - sensor.template.publish: + id: source_wraparound + state: 30.0 + - delay: 20ms + - sensor.template.publish: + id: source_wraparound + state: 5.0 + - delay: 20ms + - sensor.template.publish: + id: source_wraparound + state: 25.0 + - delay: 20ms + - sensor.template.publish: + id: source_wraparound + state: 15.0 + - delay: 20ms + - sensor.template.publish: + id: source_wraparound + state: 40.0 + - delay: 20ms + - sensor.template.publish: + id: source_wraparound + state: 35.0 + - delay: 20ms + - sensor.template.publish: + id: source_wraparound + state: 20.0 + +button: + - platform: template + name: "Publish Wraparound Button" + id: publish_wraparound_button + on_press: + - script.execute: publish_wraparound_script diff --git a/tests/integration/fixtures/sensor_filters_sliding_window.yaml b/tests/integration/fixtures/sensor_filters_sliding_window.yaml new file mode 100644 index 0000000000..2055118811 --- /dev/null +++ b/tests/integration/fixtures/sensor_filters_sliding_window.yaml @@ -0,0 +1,123 @@ +esphome: + name: test-sliding-window-filters + +host: +api: + batch_delay: 0ms # Disable batching to receive all state updates +logger: + level: DEBUG + +# Template sensor that we'll use to publish values +sensor: + - platform: template + name: "Source Sensor" + id: source_sensor + accuracy_decimals: 2 + + # Min filter sensor + - platform: copy + source_id: source_sensor + name: "Min Sensor" + id: min_sensor + filters: + - min: + window_size: 5 + send_every: 5 + send_first_at: 1 + + # Max filter sensor + - platform: copy + source_id: source_sensor + name: "Max Sensor" + id: max_sensor + filters: + - max: + window_size: 5 + send_every: 5 + send_first_at: 1 + + # Median filter sensor + - platform: copy + source_id: source_sensor + name: "Median Sensor" + id: median_sensor + filters: + - median: + window_size: 5 + send_every: 5 + send_first_at: 1 + + # Quantile filter sensor (90th percentile) + - platform: copy + source_id: source_sensor + name: "Quantile Sensor" + id: quantile_sensor + filters: + - quantile: + window_size: 5 + send_every: 5 + send_first_at: 1 + quantile: 0.9 + + # Moving average filter sensor + - platform: copy + source_id: source_sensor + name: "Moving Avg Sensor" + id: moving_avg_sensor + filters: + - sliding_window_moving_average: + window_size: 5 + send_every: 5 + send_first_at: 1 + +# Script to publish values with delays +script: + - id: publish_values_script + then: + - sensor.template.publish: + id: source_sensor + state: 1.0 + - delay: 20ms + - sensor.template.publish: + id: source_sensor + state: 2.0 + - delay: 20ms + - sensor.template.publish: + id: source_sensor + state: 3.0 + - delay: 20ms + - sensor.template.publish: + id: source_sensor + state: 4.0 + - delay: 20ms + - sensor.template.publish: + id: source_sensor + state: 5.0 + - delay: 20ms + - sensor.template.publish: + id: source_sensor + state: 6.0 + - delay: 20ms + - sensor.template.publish: + id: source_sensor + state: 7.0 + - delay: 20ms + - sensor.template.publish: + id: source_sensor + state: 8.0 + - delay: 20ms + - sensor.template.publish: + id: source_sensor + state: 9.0 + - delay: 20ms + - sensor.template.publish: + id: source_sensor + state: 10.0 + +# Button to trigger publishing test values +button: + - platform: template + name: "Publish Values Button" + id: publish_button + on_press: + - script.execute: publish_values_script diff --git a/tests/integration/fixtures/sensor_filters_value_list.yaml b/tests/integration/fixtures/sensor_filters_value_list.yaml new file mode 100644 index 0000000000..2b796a5be1 --- /dev/null +++ b/tests/integration/fixtures/sensor_filters_value_list.yaml @@ -0,0 +1,332 @@ +esphome: + name: test-value-list-filters + +host: +api: + batch_delay: 0ms # Disable batching to receive all state updates +logger: + level: DEBUG + +# Template sensors - one for each test to avoid cross-test interference +sensor: + - platform: template + name: "Source Sensor 1" + id: source_sensor_1 + accuracy_decimals: 1 + + - platform: template + name: "Source Sensor 2" + id: source_sensor_2 + accuracy_decimals: 1 + + - platform: template + name: "Source Sensor 3" + id: source_sensor_3 + accuracy_decimals: 1 + + - platform: template + name: "Source Sensor 4" + id: source_sensor_4 + accuracy_decimals: 1 + + - platform: template + name: "Source Sensor 5" + id: source_sensor_5 + accuracy_decimals: 1 + + - platform: template + name: "Source Sensor 6" + id: source_sensor_6 + accuracy_decimals: 2 + + - platform: template + name: "Source Sensor 7" + id: source_sensor_7 + accuracy_decimals: 1 + + # FilterOutValueFilter - single value + - platform: copy + source_id: source_sensor_1 + name: "Filter Out Single" + id: filter_out_single + filters: + - filter_out: 42.0 + + # FilterOutValueFilter - multiple values + - platform: copy + source_id: source_sensor_2 + name: "Filter Out Multiple" + id: filter_out_multiple + filters: + - filter_out: [0.0, 42.0, 100.0] + + # FilterOutValueFilter - with NaN + - platform: copy + source_id: source_sensor_1 + name: "Filter Out NaN" + id: filter_out_nan + filters: + - filter_out: nan + + # ThrottleWithPriorityFilter - single priority value + - platform: copy + source_id: source_sensor_3 + name: "Throttle Priority Single" + id: throttle_priority_single + filters: + - throttle_with_priority: + timeout: 200ms + value: 42.0 + + # ThrottleWithPriorityFilter - multiple priority values + - platform: copy + source_id: source_sensor_4 + name: "Throttle Priority Multiple" + id: throttle_priority_multiple + filters: + - throttle_with_priority: + timeout: 200ms + value: [0.0, 42.0, 100.0] + + # Edge case: Filter Out NaN explicitly + - platform: copy + source_id: source_sensor_5 + name: "Filter Out NaN Test" + id: filter_out_nan_test + filters: + - filter_out: nan + + # Edge case: Accuracy decimals - 2 decimals + - platform: copy + source_id: source_sensor_6 + name: "Filter Out Accuracy 2" + id: filter_out_accuracy_2 + filters: + - filter_out: 42.0 + + # Edge case: Throttle with NaN priority + - platform: copy + source_id: source_sensor_7 + name: "Throttle Priority NaN" + id: throttle_priority_nan + filters: + - throttle_with_priority: + timeout: 200ms + value: nan + +# Script to test FilterOutValueFilter +script: + - id: test_filter_out_single + then: + # Should pass through: 1.0, 2.0, 3.0 + # Should filter out: 42.0 + - sensor.template.publish: + id: source_sensor_1 + state: 1.0 + - delay: 20ms + - sensor.template.publish: + id: source_sensor_1 + state: 42.0 # Filtered out + - delay: 20ms + - sensor.template.publish: + id: source_sensor_1 + state: 2.0 + - delay: 20ms + - sensor.template.publish: + id: source_sensor_1 + state: 42.0 # Filtered out + - delay: 20ms + - sensor.template.publish: + id: source_sensor_1 + state: 3.0 + + - id: test_filter_out_multiple + then: + # Should filter out: 0.0, 42.0, 100.0 + # Should pass through: 1.0, 2.0, 50.0 + - sensor.template.publish: + id: source_sensor_2 + state: 0.0 # Filtered out + - delay: 20ms + - sensor.template.publish: + id: source_sensor_2 + state: 1.0 + - delay: 20ms + - sensor.template.publish: + id: source_sensor_2 + state: 42.0 # Filtered out + - delay: 20ms + - sensor.template.publish: + id: source_sensor_2 + state: 2.0 + - delay: 20ms + - sensor.template.publish: + id: source_sensor_2 + state: 100.0 # Filtered out + - delay: 20ms + - sensor.template.publish: + id: source_sensor_2 + state: 50.0 + + - id: test_throttle_priority_single + then: + # 42.0 bypasses throttle, other values are throttled + - sensor.template.publish: + id: source_sensor_3 + state: 1.0 # First value - passes + - delay: 50ms + - sensor.template.publish: + id: source_sensor_3 + state: 2.0 # Throttled + - delay: 50ms + - sensor.template.publish: + id: source_sensor_3 + state: 42.0 # Priority - passes immediately + - delay: 50ms + - sensor.template.publish: + id: source_sensor_3 + state: 3.0 # Throttled + - delay: 250ms # Wait for throttle to expire + - sensor.template.publish: + id: source_sensor_3 + state: 4.0 # Passes after timeout + + - id: test_throttle_priority_multiple + then: + # 0.0, 42.0, 100.0 bypass throttle + - sensor.template.publish: + id: source_sensor_4 + state: 1.0 # First value - passes + - delay: 50ms + - sensor.template.publish: + id: source_sensor_4 + state: 2.0 # Throttled + - delay: 50ms + - sensor.template.publish: + id: source_sensor_4 + state: 0.0 # Priority - passes + - delay: 50ms + - sensor.template.publish: + id: source_sensor_4 + state: 3.0 # Throttled + - delay: 50ms + - sensor.template.publish: + id: source_sensor_4 + state: 42.0 # Priority - passes + - delay: 50ms + - sensor.template.publish: + id: source_sensor_4 + state: 4.0 # Throttled + - delay: 50ms + - sensor.template.publish: + id: source_sensor_4 + state: 100.0 # Priority - passes + + - id: test_filter_out_nan + then: + # NaN should be filtered out, regular values pass + - sensor.template.publish: + id: source_sensor_5 + state: 1.0 # Pass + - delay: 20ms + - sensor.template.publish: + id: source_sensor_5 + state: !lambda 'return NAN;' # Filtered out + - delay: 20ms + - sensor.template.publish: + id: source_sensor_5 + state: 2.0 # Pass + - delay: 20ms + - sensor.template.publish: + id: source_sensor_5 + state: !lambda 'return NAN;' # Filtered out + - delay: 20ms + - sensor.template.publish: + id: source_sensor_5 + state: 3.0 # Pass + + - id: test_filter_out_accuracy_2 + then: + # With 2 decimal places, 42.00 filtered, 42.01 and 42.15 pass + - sensor.template.publish: + id: source_sensor_6 + state: 42.0 # Filtered (rounds to 42.00) + - delay: 20ms + - sensor.template.publish: + id: source_sensor_6 + state: 42.01 # Pass (rounds to 42.01) + - delay: 20ms + - sensor.template.publish: + id: source_sensor_6 + state: 42.15 # Pass (rounds to 42.15) + - delay: 20ms + - sensor.template.publish: + id: source_sensor_6 + state: 42.0 # Filtered (rounds to 42.00) + + - id: test_throttle_priority_nan + then: + # NaN bypasses throttle, regular values throttled + - sensor.template.publish: + id: source_sensor_7 + state: 1.0 # First value - passes + - delay: 50ms + - sensor.template.publish: + id: source_sensor_7 + state: 2.0 # Throttled + - delay: 50ms + - sensor.template.publish: + id: source_sensor_7 + state: !lambda 'return NAN;' # Priority NaN - passes + - delay: 50ms + - sensor.template.publish: + id: source_sensor_7 + state: 3.0 # Throttled + - delay: 50ms + - sensor.template.publish: + id: source_sensor_7 + state: !lambda 'return NAN;' # Priority NaN - passes + +# Buttons to trigger each test +button: + - platform: template + name: "Test Filter Out Single" + id: btn_filter_out_single + on_press: + - script.execute: test_filter_out_single + + - platform: template + name: "Test Filter Out Multiple" + id: btn_filter_out_multiple + on_press: + - script.execute: test_filter_out_multiple + + - platform: template + name: "Test Throttle Priority Single" + id: btn_throttle_priority_single + on_press: + - script.execute: test_throttle_priority_single + + - platform: template + name: "Test Throttle Priority Multiple" + id: btn_throttle_priority_multiple + on_press: + - script.execute: test_throttle_priority_multiple + + - platform: template + name: "Test Filter Out NaN" + id: btn_filter_out_nan + on_press: + - script.execute: test_filter_out_nan + + - platform: template + name: "Test Filter Out Accuracy 2" + id: btn_filter_out_accuracy_2 + on_press: + - script.execute: test_filter_out_accuracy_2 + + - platform: template + name: "Test Throttle Priority NaN" + id: btn_throttle_priority_nan + on_press: + - script.execute: test_throttle_priority_nan diff --git a/tests/integration/fixtures/sensor_timeout_filter.yaml b/tests/integration/fixtures/sensor_timeout_filter.yaml new file mode 100644 index 0000000000..dbd4db3242 --- /dev/null +++ b/tests/integration/fixtures/sensor_timeout_filter.yaml @@ -0,0 +1,150 @@ +esphome: + name: test-timeout-filters + +host: +api: + batch_delay: 0ms # Disable batching to receive all state updates +logger: + level: DEBUG + +# Template sensors that we'll use to publish values +sensor: + - platform: template + name: "Source Timeout Last" + id: source_timeout_last + accuracy_decimals: 1 + + - platform: template + name: "Source Timeout Reset" + id: source_timeout_reset + accuracy_decimals: 1 + + - platform: template + name: "Source Timeout Static" + id: source_timeout_static + accuracy_decimals: 1 + + - platform: template + name: "Source Timeout Lambda" + id: source_timeout_lambda + accuracy_decimals: 1 + + # Test 1: TimeoutFilter - "last" mode (outputs last received value) + - platform: copy + source_id: source_timeout_last + name: "Timeout Last Sensor" + id: timeout_last_sensor + filters: + - timeout: + timeout: 100ms + value: last # Explicitly specify "last" mode to use TimeoutFilter class + + # Test 2: TimeoutFilter - reset behavior (same filter, different source) + - platform: copy + source_id: source_timeout_reset + name: "Timeout Reset Sensor" + id: timeout_reset_sensor + filters: + - timeout: + timeout: 100ms + value: last # Explicitly specify "last" mode + + # Test 3: TimeoutFilterConfigured - static value mode + - platform: copy + source_id: source_timeout_static + name: "Timeout Static Sensor" + id: timeout_static_sensor + filters: + - timeout: + timeout: 100ms + value: 99.9 + + # Test 4: TimeoutFilterConfigured - lambda mode + - platform: copy + source_id: source_timeout_lambda + name: "Timeout Lambda Sensor" + id: timeout_lambda_sensor + filters: + - timeout: + timeout: 100ms + value: !lambda "return -1.0;" + +# Scripts to publish values with controlled timing +script: + # Test 1: Single value followed by timeout + - id: test_timeout_last_script + then: + # Publish initial value + - sensor.template.publish: + id: source_timeout_last + state: 42.0 + # Wait for timeout to fire (100ms + margin) + - delay: 150ms + + # Test 2: Multiple values before timeout (should reset timer) + - id: test_timeout_reset_script + then: + # Publish first value + - sensor.template.publish: + id: source_timeout_reset + state: 10.0 + # Wait 50ms (halfway to timeout) + - delay: 50ms + # Publish second value (resets timeout) + - sensor.template.publish: + id: source_timeout_reset + state: 20.0 + # Wait 50ms (halfway to timeout again) + - delay: 50ms + # Publish third value (resets timeout) + - sensor.template.publish: + id: source_timeout_reset + state: 30.0 + # Wait for timeout to fire (100ms + margin) + - delay: 150ms + + # Test 3: Static value timeout + - id: test_timeout_static_script + then: + # Publish initial value + - sensor.template.publish: + id: source_timeout_static + state: 55.5 + # Wait for timeout to fire + - delay: 150ms + + # Test 4: Lambda value timeout + - id: test_timeout_lambda_script + then: + # Publish initial value + - sensor.template.publish: + id: source_timeout_lambda + state: 77.7 + # Wait for timeout to fire + - delay: 150ms + +# Buttons to trigger each test scenario +button: + - platform: template + name: "Test Timeout Last Button" + id: test_timeout_last_button + on_press: + - script.execute: test_timeout_last_script + + - platform: template + name: "Test Timeout Reset Button" + id: test_timeout_reset_button + on_press: + - script.execute: test_timeout_reset_script + + - platform: template + name: "Test Timeout Static Button" + id: test_timeout_static_button + on_press: + - script.execute: test_timeout_static_script + + - platform: template + name: "Test Timeout Lambda Button" + id: test_timeout_lambda_button + on_press: + - script.execute: test_timeout_lambda_script diff --git a/tests/integration/fixtures/template_alarm_control_panel_many_sensors.yaml b/tests/integration/fixtures/template_alarm_control_panel_many_sensors.yaml new file mode 100644 index 0000000000..836d3f11d5 --- /dev/null +++ b/tests/integration/fixtures/template_alarm_control_panel_many_sensors.yaml @@ -0,0 +1,136 @@ +esphome: + name: template-alarm-many-sensors + friendly_name: "Template Alarm Control Panel with Many Sensors" + +logger: + +host: + +api: + +binary_sensor: + - platform: template + id: sensor1 + name: "Door 1" + - platform: template + id: sensor2 + name: "Door 2" + - platform: template + id: sensor3 + name: "Window 1" + - platform: template + id: sensor4 + name: "Window 2" + - platform: template + id: sensor5 + name: "Motion 1" + - platform: template + id: sensor6 + name: "Motion 2" + - platform: template + id: sensor7 + name: "Glass Break 1" + - platform: template + id: sensor8 + name: "Glass Break 2" + - platform: template + id: sensor9 + name: "Smoke Detector" + - platform: template + id: sensor10 + name: "CO Detector" + +alarm_control_panel: + - platform: template + id: test_alarm + name: "Test Alarm" + codes: + - "1234" + requires_code_to_arm: true + arming_away_time: 5s + arming_home_time: 3s + arming_night_time: 3s + pending_time: 10s + trigger_time: 300s + restore_mode: ALWAYS_DISARMED + binary_sensors: + - input: sensor1 + bypass_armed_home: false + bypass_armed_night: false + bypass_auto: true + chime: true + trigger_mode: DELAYED + - input: sensor2 + bypass_armed_home: false + bypass_armed_night: false + bypass_auto: true + chime: true + trigger_mode: DELAYED + - input: sensor3 + bypass_armed_home: true + bypass_armed_night: false + bypass_auto: false + chime: false + trigger_mode: DELAYED + - input: sensor4 + bypass_armed_home: true + bypass_armed_night: false + bypass_auto: false + chime: false + trigger_mode: DELAYED + - input: sensor5 + bypass_armed_home: false + bypass_armed_night: true + bypass_auto: false + chime: false + trigger_mode: INSTANT + - input: sensor6 + bypass_armed_home: false + bypass_armed_night: true + bypass_auto: false + chime: false + trigger_mode: INSTANT + - input: sensor7 + bypass_armed_home: false + bypass_armed_night: false + bypass_auto: false + chime: false + trigger_mode: INSTANT + - input: sensor8 + bypass_armed_home: false + bypass_armed_night: false + bypass_auto: false + chime: false + trigger_mode: INSTANT + - input: sensor9 + bypass_armed_home: false + bypass_armed_night: false + bypass_auto: false + chime: false + trigger_mode: INSTANT_ALWAYS + - input: sensor10 + bypass_armed_home: false + bypass_armed_night: false + bypass_auto: false + chime: false + trigger_mode: INSTANT_ALWAYS + on_disarmed: + - logger.log: "Alarm disarmed" + on_arming: + - logger.log: "Alarm arming" + on_armed_away: + - logger.log: "Alarm armed away" + on_armed_home: + - logger.log: "Alarm armed home" + on_armed_night: + - logger.log: "Alarm armed night" + on_pending: + - logger.log: "Alarm pending" + on_triggered: + - logger.log: "Alarm triggered" + on_cleared: + - logger.log: "Alarm cleared" + on_chime: + - logger.log: "Chime activated" + on_ready: + - logger.log: "Sensors ready state changed" diff --git a/tests/integration/fixtures/wait_until_fifo_ordering.yaml b/tests/integration/fixtures/wait_until_fifo_ordering.yaml new file mode 100644 index 0000000000..5dd60c8755 --- /dev/null +++ b/tests/integration/fixtures/wait_until_fifo_ordering.yaml @@ -0,0 +1,82 @@ +esphome: + name: test-wait-until-ordering + +host: + +api: + actions: + - action: test_wait_until_fifo + then: + - logger.log: "=== TEST: wait_until should execute in FIFO order ===" + - globals.set: + id: gate_open + value: 'false' + - delay: 100ms + # Start multiple parallel executions of coordinator script + # Each will call the shared waiter script, queueing in same wait_until + - script.execute: coordinator_0 + - script.execute: coordinator_1 + - script.execute: coordinator_2 + - script.execute: coordinator_3 + - script.execute: coordinator_4 + # Give scripts time to reach wait_until and queue + - delay: 200ms + - logger.log: "Opening gate - all wait_until should complete now" + - globals.set: + id: gate_open + value: 'true' + - delay: 500ms + - logger.log: "Test complete" + +globals: + - id: gate_open + type: bool + initial_value: 'false' + +script: + # Shared waiter with single wait_until action (all coordinators call this) + - id: waiter + mode: parallel + parameters: + iter: int + then: + - lambda: 'ESP_LOGD("main", "Queueing iteration %d", iter);' + - wait_until: + condition: + lambda: 'return id(gate_open);' + timeout: 5s + - lambda: 'ESP_LOGD("main", "Completed iteration %d", iter);' + + # Coordinator scripts - each calls shared waiter with different iteration number + - id: coordinator_0 + then: + - script.execute: + id: waiter + iter: 0 + + - id: coordinator_1 + then: + - script.execute: + id: waiter + iter: 1 + + - id: coordinator_2 + then: + - script.execute: + id: waiter + iter: 2 + + - id: coordinator_3 + then: + - script.execute: + id: waiter + iter: 3 + + - id: coordinator_4 + then: + - script.execute: + id: waiter + iter: 4 + +logger: + level: DEBUG diff --git a/tests/integration/fixtures/wait_until_mid_loop_timing.yaml b/tests/integration/fixtures/wait_until_mid_loop_timing.yaml new file mode 100644 index 0000000000..32f59e81a1 --- /dev/null +++ b/tests/integration/fixtures/wait_until_mid_loop_timing.yaml @@ -0,0 +1,109 @@ +# Test for PR #11676 bug: wait_until timeout when triggered mid-component-loop +# This demonstrates that App.get_loop_component_start_time() is stale when +# wait_until is triggered partway through a component's loop execution + +esphome: + name: wait-mid-loop + +host: + +api: + actions: + - action: test_mid_loop_timeout + then: + - logger.log: "=== Test: wait_until triggered mid-loop should timeout correctly ===" + + # Reset test state + - globals.set: + id: test_complete + value: 'false' + + # Trigger the slow script that will call wait_until mid-execution + - script.execute: slow_script + + # Wait for test to complete (should take ~300ms: 100ms delay + 200ms timeout) + - wait_until: + condition: + lambda: return id(test_complete); + timeout: 2s + + - if: + condition: + lambda: return id(test_complete); + then: + - logger.log: "✓ Test PASSED: wait_until timed out correctly" + else: + - logger.log: "✗ Test FAILED: wait_until did not complete properly" + +logger: + level: DEBUG + +globals: + - id: test_complete + type: bool + restore_value: false + initial_value: 'false' + + - id: test_condition + type: bool + restore_value: false + initial_value: 'false' + + - id: timeout_start_time + type: uint32_t + restore_value: false + initial_value: '0' + + - id: timeout_end_time + type: uint32_t + restore_value: false + initial_value: '0' + +script: + # This script simulates a component that takes time during its execution + # When wait_until is triggered mid-script, the loop_component_start_time + # will be stale (from when the script's component loop started) + - id: slow_script + then: + - logger.log: "Script: Starting, about to do some work..." + + # Simulate component doing work for 100ms + # This represents time spent in a component's loop() before triggering wait_until + - delay: 100ms + + - logger.log: "Script: 100ms elapsed, now starting wait_until with 200ms timeout" + - lambda: |- + // Record when timeout starts + id(timeout_start_time) = millis(); + id(test_condition) = false; + + # At this point: + # - Script component's loop started 100ms ago + # - App.loop_component_start_time_ = time from 100ms ago (stale!) + # - wait_until will capture millis() NOW (fresh) + # - BUG: loop() will use stale loop_component_start_time, causing immediate timeout + + - wait_until: + condition: + lambda: return id(test_condition); + timeout: 200ms + + - lambda: |- + // Record when timeout completes + id(timeout_end_time) = millis(); + uint32_t elapsed = id(timeout_end_time) - id(timeout_start_time); + + ESP_LOGD("TEST", "wait_until completed after %u ms (expected ~200ms)", elapsed); + + // Check if timeout took approximately correct time + // Should be ~200ms, not <50ms (immediate timeout) + if (elapsed >= 150 && elapsed <= 250) { + ESP_LOGD("TEST", "✓ Timeout duration correct: %u ms", elapsed); + id(test_complete) = true; + } else { + ESP_LOGE("TEST", "✗ Timeout duration WRONG: %u ms (expected 150-250ms)", elapsed); + if (elapsed < 50) { + ESP_LOGE("TEST", " → Likely BUG: Immediate timeout due to stale loop_component_start_time"); + } + id(test_complete) = false; + } diff --git a/tests/integration/fixtures/wait_until_on_boot.yaml b/tests/integration/fixtures/wait_until_on_boot.yaml new file mode 100644 index 0000000000..358bef971b --- /dev/null +++ b/tests/integration/fixtures/wait_until_on_boot.yaml @@ -0,0 +1,47 @@ +# Test for wait_until in on_boot automation +# Reproduces bug where wait_until in on_boot would hang forever +# because WaitUntilAction::setup() would disable_loop() after +# play_complex() had already enabled it. + +esphome: + name: wait-until-on-boot + on_boot: + then: + - logger.log: "on_boot: Starting wait_until test" + - globals.set: + id: on_boot_started + value: 'true' + - wait_until: + condition: + lambda: return id(test_flag); + timeout: 5s + - logger.log: "on_boot: wait_until completed successfully" + +host: + +logger: + level: DEBUG + +globals: + - id: on_boot_started + type: bool + initial_value: 'false' + - id: test_flag + type: bool + initial_value: 'false' + +api: + actions: + - action: set_test_flag + then: + - globals.set: + id: test_flag + value: 'true' + - action: check_on_boot_started + then: + - lambda: |- + if (id(on_boot_started)) { + ESP_LOGI("test", "on_boot has started"); + } else { + ESP_LOGI("test", "on_boot has NOT started"); + } diff --git a/tests/integration/state_utils.py b/tests/integration/state_utils.py new file mode 100644 index 0000000000..6434a41ddf --- /dev/null +++ b/tests/integration/state_utils.py @@ -0,0 +1,173 @@ +"""Shared utilities for ESPHome integration tests - state handling.""" + +from __future__ import annotations + +import asyncio +import logging + +from aioesphomeapi import ButtonInfo, EntityInfo, EntityState + +_LOGGER = logging.getLogger(__name__) + + +def build_key_to_entity_mapping( + entities: list[EntityInfo], entity_names: list[str] +) -> dict[int, str]: + """Build a mapping from entity keys to entity names. + + Args: + entities: List of entity info objects from the API + entity_names: List of entity names to search for in object_ids + + Returns: + Dictionary mapping entity keys to entity names + """ + key_to_entity: dict[int, str] = {} + for entity in entities: + obj_id = entity.object_id.lower() + for entity_name in entity_names: + if entity_name in obj_id: + key_to_entity[entity.key] = entity_name + break + return key_to_entity + + +class InitialStateHelper: + """Helper to wait for initial states before processing test states. + + When an API client connects, ESPHome sends the current state of all entities. + This helper wraps the user's state callback and swallows the first state for + each entity, then forwards all subsequent states to the user callback. + + Usage: + entities, services = await client.list_entities_services() + helper = InitialStateHelper(entities) + client.subscribe_states(helper.on_state_wrapper(user_callback)) + await helper.wait_for_initial_states() + # Access initial states via helper.initial_states[key] + """ + + def __init__(self, entities: list[EntityInfo]) -> None: + """Initialize the helper. + + Args: + entities: All entities from list_entities_services() + """ + # Set of (device_id, key) tuples waiting for initial state + # Buttons are stateless, so exclude them + self._wait_initial_states = { + (entity.device_id, entity.key) + for entity in entities + if not isinstance(entity, ButtonInfo) + } + # Keep entity info for debugging - use (device_id, key) tuple + self._entities_by_id = { + (entity.device_id, entity.key): entity for entity in entities + } + # Store initial states by key for test access + self.initial_states: dict[int, EntityState] = {} + + # Log all entities + _LOGGER.debug( + "InitialStateHelper: Found %d total entities: %s", + len(entities), + [(type(e).__name__, e.object_id) for e in entities], + ) + + # Log which ones we're waiting for + _LOGGER.debug( + "InitialStateHelper: Waiting for %d entities (excluding ButtonInfo): %s", + len(self._wait_initial_states), + [self._entities_by_id[k].object_id for k in self._wait_initial_states], + ) + + # Log which ones we're NOT waiting for + not_waiting = { + (e.device_id, e.key) for e in entities + } - self._wait_initial_states + if not_waiting: + not_waiting_info = [ + f"{type(self._entities_by_id[k]).__name__}:{self._entities_by_id[k].object_id}" + for k in not_waiting + ] + _LOGGER.debug( + "InitialStateHelper: NOT waiting for %d entities: %s", + len(not_waiting), + not_waiting_info, + ) + + # Create future in the running event loop + self._initial_states_received = asyncio.get_running_loop().create_future() + # If no entities to wait for, mark complete immediately + if not self._wait_initial_states: + self._initial_states_received.set_result(True) + + def on_state_wrapper(self, user_callback): + """Wrap a user callback to track initial states. + + Args: + user_callback: The user's state callback function + + Returns: + Wrapped callback that swallows first state per entity, forwards rest + """ + + def wrapper(state: EntityState) -> None: + """Swallow initial state per entity, forward subsequent states.""" + # Create entity identifier tuple + entity_id = (state.device_id, state.key) + + # Log which entity is sending state + if entity_id in self._entities_by_id: + entity = self._entities_by_id[entity_id] + _LOGGER.debug( + "Received state for %s (type: %s, device_id: %s, key: %d)", + entity.object_id, + type(entity).__name__, + state.device_id, + state.key, + ) + + # If this entity is waiting for initial state + if entity_id in self._wait_initial_states: + # Store the initial state for test access + self.initial_states[state.key] = state + + # Remove from waiting set + self._wait_initial_states.discard(entity_id) + + _LOGGER.debug( + "Swallowed initial state for %s, %d entities remaining", + self._entities_by_id[entity_id].object_id + if entity_id in self._entities_by_id + else entity_id, + len(self._wait_initial_states), + ) + + # Check if we've now seen all entities + if ( + not self._wait_initial_states + and not self._initial_states_received.done() + ): + _LOGGER.debug("All initial states received") + self._initial_states_received.set_result(True) + + # Don't forward initial state to user + return + + # Forward subsequent states to user callback + _LOGGER.debug("Forwarding state to user callback") + user_callback(state) + + return wrapper + + async def wait_for_initial_states(self, timeout: float = 5.0) -> None: + """Wait for all initial states to be received. + + Args: + timeout: Maximum time to wait in seconds + + Raises: + asyncio.TimeoutError: If initial states aren't received within timeout + """ + await asyncio.wait_for(self._initial_states_received, timeout=timeout) diff --git a/tests/integration/test_action_concurrent_reentry.py b/tests/integration/test_action_concurrent_reentry.py new file mode 100644 index 0000000000..aa5801ca2b --- /dev/null +++ b/tests/integration/test_action_concurrent_reentry.py @@ -0,0 +1,92 @@ +"""Integration test for API conditional memory optimization with triggers and services.""" + +from __future__ import annotations + +import asyncio +import collections +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_action_concurrent_reentry( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """ + This test runs a script in parallel with varying arguments and verifies if + each script keeps its original argument throughout its execution + """ + test_complete = asyncio.Event() + expected = {0, 1, 2, 3, 4} + + # Patterns to match in logs + after_wait_until_pattern = re.compile(r"AFTER wait_until ARG (\d+)") + after_script_wait_pattern = re.compile(r"AFTER script\.wait ARG (\d+)") + after_repeat_pattern = re.compile(r"AFTER repeat ARG (\d+)") + in_repeat_pattern = re.compile(r"IN repeat (\d+) ARG (\d+)") + after_while_pattern = re.compile(r"AFTER while ARG (\d+)") + in_while_pattern = re.compile(r"IN while ARG (\d+)") + + after_wait_until_args = [] + after_script_wait_args = [] + after_while_args = [] + in_while_args = [] + after_repeat_args = [] + in_repeat_args = collections.defaultdict(list) + + def check_output(line: str) -> None: + """Check log output for expected messages.""" + if test_complete.is_set(): + return + + if mo := after_wait_until_pattern.search(line): + after_wait_until_args.append(int(mo.group(1))) + elif mo := after_script_wait_pattern.search(line): + after_script_wait_args.append(int(mo.group(1))) + elif mo := in_while_pattern.search(line): + in_while_args.append(int(mo.group(1))) + elif mo := after_while_pattern.search(line): + after_while_args.append(int(mo.group(1))) + elif mo := in_repeat_pattern.search(line): + in_repeat_args[int(mo.group(1))].append(int(mo.group(2))) + elif mo := after_repeat_pattern.search(line): + after_repeat_args.append(int(mo.group(1))) + if len(after_repeat_args) == len(expected): + test_complete.set() + + # Run with log monitoring + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Verify device info + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "action-concurrent-reentry" + + # Wait for tests to complete with timeout + try: + await asyncio.wait_for(test_complete.wait(), timeout=8.0) + except TimeoutError: + pytest.fail("test timed out") + + # order may change, but all args must be present + for args in in_repeat_args.values(): + assert set(args) == expected + assert set(in_repeat_args.keys()) == {0, 1, 2} + assert set(after_wait_until_args) == expected, after_wait_until_args + assert set(after_script_wait_args) == expected, after_script_wait_args + assert set(after_repeat_args) == expected, after_repeat_args + assert set(after_while_args) == expected, after_while_args + assert dict(collections.Counter(in_while_args)) == { + 0: 5, + 1: 4, + 2: 3, + 3: 2, + 4: 1, + }, in_while_args diff --git a/tests/integration/test_api_custom_services.py b/tests/integration/test_api_custom_services.py index 9ae4cdcb5d..967c504112 100644 --- a/tests/integration/test_api_custom_services.py +++ b/tests/integration/test_api_custom_services.py @@ -33,12 +33,16 @@ async def test_api_custom_services( # Track log messages yaml_service_future = loop.create_future() + yaml_args_future = loop.create_future() + yaml_many_args_future = loop.create_future() custom_service_future = loop.create_future() custom_args_future = loop.create_future() custom_arrays_future = loop.create_future() # Patterns to match in logs yaml_service_pattern = re.compile(r"YAML service called") + yaml_args_pattern = re.compile(r"YAML service with args: 123, test_value") + yaml_many_args_pattern = re.compile(r"YAML service many args: 42, 3\.14, 1, hello") custom_service_pattern = re.compile(r"Custom test service called!") custom_args_pattern = re.compile( r"Custom service called with: test_string, 456, 1, 78\.90" @@ -51,6 +55,10 @@ async def test_api_custom_services( """Check log output for expected messages.""" if not yaml_service_future.done() and yaml_service_pattern.search(line): yaml_service_future.set_result(True) + elif not yaml_args_future.done() and yaml_args_pattern.search(line): + yaml_args_future.set_result(True) + elif not yaml_many_args_future.done() and yaml_many_args_pattern.search(line): + yaml_many_args_future.set_result(True) elif not custom_service_future.done() and custom_service_pattern.search(line): custom_service_future.set_result(True) elif not custom_args_future.done() and custom_args_pattern.search(line): @@ -71,11 +79,13 @@ async def test_api_custom_services( # List services _, services = await client.list_entities_services() - # Should have 4 services: 1 YAML + 3 CustomAPIDevice - assert len(services) == 4, f"Expected 4 services, found {len(services)}" + # Should have 6 services: 3 YAML + 3 CustomAPIDevice + assert len(services) == 6, f"Expected 6 services, found {len(services)}" # Find our services yaml_service: UserService | None = None + yaml_args_service: UserService | None = None + yaml_many_args_service: UserService | None = None custom_service: UserService | None = None custom_args_service: UserService | None = None custom_arrays_service: UserService | None = None @@ -83,6 +93,10 @@ async def test_api_custom_services( for service in services: if service.name == "test_yaml_service": yaml_service = service + elif service.name == "test_yaml_service_with_args": + yaml_args_service = service + elif service.name == "test_yaml_service_many_args": + yaml_many_args_service = service elif service.name == "custom_test_service": custom_service = service elif service.name == "custom_service_with_args": @@ -91,6 +105,10 @@ async def test_api_custom_services( custom_arrays_service = service assert yaml_service is not None, "test_yaml_service not found" + assert yaml_args_service is not None, "test_yaml_service_with_args not found" + assert yaml_many_args_service is not None, ( + "test_yaml_service_many_args not found" + ) assert custom_service is not None, "custom_test_service not found" assert custom_args_service is not None, "custom_service_with_args not found" assert custom_arrays_service is not None, "custom_service_with_arrays not found" @@ -99,6 +117,44 @@ async def test_api_custom_services( client.execute_service(yaml_service, {}) await asyncio.wait_for(yaml_service_future, timeout=5.0) + # Verify YAML service with args arguments + assert len(yaml_args_service.args) == 2 + yaml_args_types = {arg.name: arg.type for arg in yaml_args_service.args} + assert yaml_args_types["my_int"] == UserServiceArgType.INT + assert yaml_args_types["my_string"] == UserServiceArgType.STRING + + # Test YAML service with arguments + client.execute_service( + yaml_args_service, + { + "my_int": 123, + "my_string": "test_value", + }, + ) + await asyncio.wait_for(yaml_args_future, timeout=5.0) + + # Verify YAML service with many args arguments + assert len(yaml_many_args_service.args) == 4 + yaml_many_args_types = { + arg.name: arg.type for arg in yaml_many_args_service.args + } + assert yaml_many_args_types["arg1"] == UserServiceArgType.INT + assert yaml_many_args_types["arg2"] == UserServiceArgType.FLOAT + assert yaml_many_args_types["arg3"] == UserServiceArgType.BOOL + assert yaml_many_args_types["arg4"] == UserServiceArgType.STRING + + # Test YAML service with many arguments + client.execute_service( + yaml_many_args_service, + { + "arg1": 42, + "arg2": 3.14, + "arg3": True, + "arg4": "hello", + }, + ) + await asyncio.wait_for(yaml_many_args_future, timeout=5.0) + # Test simple CustomAPIDevice service client.execute_service(custom_service, {}) await asyncio.wait_for(custom_service_future, timeout=5.0) diff --git a/tests/integration/test_areas_and_devices.py b/tests/integration/test_areas_and_devices.py index 1af16c87e8..93326de0a9 100644 --- a/tests/integration/test_areas_and_devices.py +++ b/tests/integration/test_areas_and_devices.py @@ -132,6 +132,7 @@ async def test_areas_and_devices( "Temperature Sensor Reading": temp_sensor.device_id, "Motion Detector Status": motion_detector.device_id, "Smart Switch Power": smart_switch.device_id, + "Living Room Sensor": 0, # Main device } for entity in sensor_entities: @@ -160,6 +161,18 @@ async def test_areas_and_devices( "Should have a switch with device_id 0 (main device)" ) + # Verify extra switches with blank and none device_id are correctly available + extra_switches = [ + e for e in switch_entities if e.name.startswith("Living Room") + ] + assert len(extra_switches) == 2, ( + f"Expected 2 extra switches for Living Room, got {len(extra_switches)}" + ) + extra_switch_device_ids = [e.device_id for e in extra_switches] + assert all(d == 0 for d in extra_switch_device_ids), ( + "All extra switches should have device_id 0 (main device)" + ) + # Wait for initial states to be received for all switches await asyncio.wait_for(initial_states_future, timeout=2.0) diff --git a/tests/integration/test_automation_wait_actions.py b/tests/integration/test_automation_wait_actions.py new file mode 100644 index 0000000000..adcb8ba487 --- /dev/null +++ b/tests/integration/test_automation_wait_actions.py @@ -0,0 +1,104 @@ +"""Test concurrent execution of wait_until and script.wait in direct automation actions.""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_automation_wait_actions( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """ + Test that wait_until and script.wait correctly handle concurrent executions + when automation actions (not scripts) are triggered multiple times rapidly. + + This tests sensor.on_value automations being triggered 5 times before any complete. + """ + loop = asyncio.get_running_loop() + + # Track completion counts + test_results = { + "wait_until": 0, + "script_wait": 0, + "wait_until_timeout": 0, + } + + # Patterns for log messages + wait_until_complete = re.compile(r"wait_until automation completed") + script_wait_complete = re.compile(r"script\.wait automation completed") + timeout_complete = re.compile(r"timeout automation completed") + + # Test completion futures + test1_complete = loop.create_future() + test2_complete = loop.create_future() + test3_complete = loop.create_future() + + def check_output(line: str) -> None: + """Check log output for completion messages.""" + # Test 1: wait_until concurrent execution + if wait_until_complete.search(line): + test_results["wait_until"] += 1 + if test_results["wait_until"] == 5 and not test1_complete.done(): + test1_complete.set_result(True) + + # Test 2: script.wait concurrent execution + if script_wait_complete.search(line): + test_results["script_wait"] += 1 + if test_results["script_wait"] == 5 and not test2_complete.done(): + test2_complete.set_result(True) + + # Test 3: wait_until with timeout + if timeout_complete.search(line): + test_results["wait_until_timeout"] += 1 + if test_results["wait_until_timeout"] == 5 and not test3_complete.done(): + test3_complete.set_result(True) + + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Get services + _, services = await client.list_entities_services() + + # Test 1: wait_until in automation - trigger 5 times rapidly + test_service = next((s for s in services if s.name == "test_wait_until"), None) + assert test_service is not None, "test_wait_until service not found" + client.execute_service(test_service, {}) + await asyncio.wait_for(test1_complete, timeout=3.0) + + # Verify Test 1: All 5 triggers should complete + assert test_results["wait_until"] == 5, ( + f"Test 1: Expected 5 wait_until completions, got {test_results['wait_until']}" + ) + + # Test 2: script.wait in automation - trigger 5 times rapidly + test_service = next((s for s in services if s.name == "test_script_wait"), None) + assert test_service is not None, "test_script_wait service not found" + client.execute_service(test_service, {}) + await asyncio.wait_for(test2_complete, timeout=3.0) + + # Verify Test 2: All 5 triggers should complete + assert test_results["script_wait"] == 5, ( + f"Test 2: Expected 5 script.wait completions, got {test_results['script_wait']}" + ) + + # Test 3: wait_until with timeout in automation - trigger 5 times rapidly + test_service = next( + (s for s in services if s.name == "test_wait_timeout"), None + ) + assert test_service is not None, "test_wait_timeout service not found" + client.execute_service(test_service, {}) + await asyncio.wait_for(test3_complete, timeout=3.0) + + # Verify Test 3: All 5 triggers should timeout and complete + assert test_results["wait_until_timeout"] == 5, ( + f"Test 3: Expected 5 timeout completions, got {test_results['wait_until_timeout']}" + ) diff --git a/tests/integration/test_automations.py b/tests/integration/test_automations.py index bd2082e86b..83268c1eea 100644 --- a/tests/integration/test_automations.py +++ b/tests/integration/test_automations.py @@ -89,3 +89,73 @@ async def test_delay_action_cancellation( assert 0.4 < time_from_second_start < 0.6, ( f"Delay completed {time_from_second_start:.3f}s after second start, expected ~0.5s" ) + + +@pytest.mark.asyncio +async def test_parallel_script_delays( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that parallel scripts with delays don't interfere with each other.""" + loop = asyncio.get_running_loop() + + # Track script executions + script_starts: list[float] = [] + script_ends: list[float] = [] + + # Patterns to match + start_pattern = re.compile(r"Parallel script instance \d+ started") + end_pattern = re.compile(r"Parallel script instance \d+ completed after delay") + + # Future to track when all scripts have completed + all_scripts_completed = loop.create_future() + + def check_output(line: str) -> None: + """Check log output for parallel script messages.""" + current_time = loop.time() + + if start_pattern.search(line): + script_starts.append(current_time) + + if end_pattern.search(line): + script_ends.append(current_time) + # Check if we have all 3 completions + if len(script_ends) == 3 and not all_scripts_completed.done(): + all_scripts_completed.set_result(True) + + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Get services + entities, services = await client.list_entities_services() + + # Find our test service + test_service = next( + (s for s in services if s.name == "test_parallel_delays"), None + ) + assert test_service is not None, "test_parallel_delays service not found" + + # Execute the test - this will start 3 parallel scripts with 1 second delays + client.execute_service(test_service, {}) + + # Wait for all scripts to complete (should take ~1 second, not 3) + await asyncio.wait_for(all_scripts_completed, timeout=2.0) + + # Verify we had 3 starts and 3 ends + assert len(script_starts) == 3, ( + f"Expected 3 script starts, got {len(script_starts)}" + ) + assert len(script_ends) == 3, f"Expected 3 script ends, got {len(script_ends)}" + + # Verify they ran in parallel - all should complete within ~1.5 seconds + first_start = min(script_starts) + last_end = max(script_ends) + total_time = last_end - first_start + + # If running in parallel, total time should be close to 1 second + # If they were interfering (running sequentially), it would take 3+ seconds + assert total_time < 1.5, ( + f"Parallel scripts took {total_time:.2f}s total, should be ~1s if running in parallel" + ) diff --git a/tests/integration/test_climate_custom_modes.py b/tests/integration/test_climate_custom_modes.py new file mode 100644 index 0000000000..67a7b0581a --- /dev/null +++ b/tests/integration/test_climate_custom_modes.py @@ -0,0 +1,121 @@ +"""Integration test for climate custom presets.""" + +from __future__ import annotations + +import asyncio + +import aioesphomeapi +from aioesphomeapi import ClimateInfo, ClimatePreset, EntityState +import pytest + +from .state_utils import InitialStateHelper +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_climate_custom_fan_modes_and_presets( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that custom presets are properly exposed and can be changed.""" + loop = asyncio.get_running_loop() + async with run_compiled(yaml_config), api_client_connected() as client: + states: dict[int, EntityState] = {} + super_saver_future: asyncio.Future[EntityState] = loop.create_future() + vacation_future: asyncio.Future[EntityState] = loop.create_future() + + def on_state(state: EntityState) -> None: + states[state.key] = state + if isinstance(state, aioesphomeapi.ClimateState): + # Wait for Super Saver preset + if ( + state.custom_preset == "Super Saver" + and state.target_temperature_low == 20.0 + and state.target_temperature_high == 24.0 + and not super_saver_future.done() + ): + super_saver_future.set_result(state) + # Wait for Vacation Mode preset + elif ( + state.custom_preset == "Vacation Mode" + and state.target_temperature_low == 15.0 + and state.target_temperature_high == 18.0 + and not vacation_future.done() + ): + vacation_future.set_result(state) + + # Get entities and set up state synchronization + entities, services = await client.list_entities_services() + initial_state_helper = InitialStateHelper(entities) + climate_infos = [e for e in entities if isinstance(e, ClimateInfo)] + assert len(climate_infos) == 1, "Expected exactly 1 climate entity" + + test_climate = climate_infos[0] + + # Subscribe with the wrapper that filters initial states + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + + # Wait for all initial states to be broadcast + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + # Verify enum presets are exposed (from preset: config map) + assert ClimatePreset.AWAY in test_climate.supported_presets, ( + "Expected AWAY in enum presets" + ) + + # Verify custom string presets are exposed (non-standard preset names from preset map) + custom_presets = test_climate.supported_custom_presets + assert len(custom_presets) == 3, ( + f"Expected 3 custom presets, got {len(custom_presets)}: {custom_presets}" + ) + assert "Eco Plus" in custom_presets, "Expected 'Eco Plus' in custom presets" + assert "Super Saver" in custom_presets, ( + "Expected 'Super Saver' in custom presets" + ) + assert "Vacation Mode" in custom_presets, ( + "Expected 'Vacation Mode' in custom presets" + ) + + # Get initial state and verify default preset + initial_state = initial_state_helper.initial_states.get(test_climate.key) + assert initial_state is not None, "Climate initial state not found" + assert isinstance(initial_state, aioesphomeapi.ClimateState) + assert initial_state.custom_preset == "Eco Plus", ( + f"Expected default preset 'Eco Plus', got '{initial_state.custom_preset}'" + ) + assert initial_state.target_temperature_low == 18.0, ( + f"Expected low temp 18.0, got {initial_state.target_temperature_low}" + ) + assert initial_state.target_temperature_high == 22.0, ( + f"Expected high temp 22.0, got {initial_state.target_temperature_high}" + ) + + # Test changing to "Super Saver" custom preset + client.climate_command(test_climate.key, custom_preset="Super Saver") + + try: + super_saver_state = await asyncio.wait_for(super_saver_future, timeout=5.0) + except TimeoutError: + pytest.fail("Super Saver preset change not received within 5 seconds") + + assert isinstance(super_saver_state, aioesphomeapi.ClimateState) + assert super_saver_state.custom_preset == "Super Saver" + assert super_saver_state.target_temperature_low == 20.0 + assert super_saver_state.target_temperature_high == 24.0 + + # Test changing to "Vacation Mode" custom preset + client.climate_command(test_climate.key, custom_preset="Vacation Mode") + + try: + vacation_state = await asyncio.wait_for(vacation_future, timeout=5.0) + except TimeoutError: + pytest.fail("Vacation Mode preset change not received within 5 seconds") + + assert isinstance(vacation_state, aioesphomeapi.ClimateState) + assert vacation_state.custom_preset == "Vacation Mode" + assert vacation_state.target_temperature_low == 15.0 + assert vacation_state.target_temperature_high == 18.0 diff --git a/tests/integration/test_continuation_actions.py b/tests/integration/test_continuation_actions.py new file mode 100644 index 0000000000..1069ee7581 --- /dev/null +++ b/tests/integration/test_continuation_actions.py @@ -0,0 +1,235 @@ +"""Test continuation actions (ContinuationAction, WhileLoopContinuation, RepeatLoopContinuation).""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_continuation_actions( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """ + Test that continuation actions work correctly for if/while/repeat. + + These continuation classes replace LambdaAction with simple parent pointers, + saving 32-36 bytes per instance and eliminating std::function overhead. + """ + loop = asyncio.get_running_loop() + + # Track test completions + test_results = { + "if_then": False, + "if_else": False, + "if_complete": False, + "nested_both_true": False, + "nested_outer_true_inner_false": False, + "nested_outer_false": False, + "nested_complete": False, + "while_iterations": 0, + "while_complete": False, + "repeat_iterations": 0, + "repeat_complete": False, + "combined_iterations": 0, + "combined_complete": False, + "rapid_then": 0, + "rapid_else": 0, + "rapid_complete": 0, + } + + # Patterns for log messages + if_then_pattern = re.compile(r"if-then executed: value=(\d+)") + if_else_pattern = re.compile(r"if-else executed: value=(\d+)") + if_complete_pattern = re.compile(r"if completed") + nested_both_true_pattern = re.compile(r"nested-both-true") + nested_outer_true_inner_false_pattern = re.compile(r"nested-outer-true-inner-false") + nested_outer_false_pattern = re.compile(r"nested-outer-false") + nested_complete_pattern = re.compile(r"nested if completed") + while_iteration_pattern = re.compile(r"while-iteration-(\d+)") + while_complete_pattern = re.compile(r"while completed") + repeat_iteration_pattern = re.compile(r"repeat-iteration-(\d+)") + repeat_complete_pattern = re.compile(r"repeat completed") + combined_pattern = re.compile(r"combined-repeat(\d+)-while(\d+)") + combined_complete_pattern = re.compile(r"combined completed") + rapid_then_pattern = re.compile(r"rapid-if-then: value=(\d+)") + rapid_else_pattern = re.compile(r"rapid-if-else: value=(\d+)") + rapid_complete_pattern = re.compile(r"rapid-if-completed: value=(\d+)") + + # Test completion futures + test1_complete = loop.create_future() # if action + test2_complete = loop.create_future() # nested if + test3_complete = loop.create_future() # while + test4_complete = loop.create_future() # repeat + test5_complete = loop.create_future() # combined + test6_complete = loop.create_future() # rapid + + def check_output(line: str) -> None: + """Check log output for test messages.""" + # Test 1: IfAction + if if_then_pattern.search(line): + test_results["if_then"] = True + if if_else_pattern.search(line): + test_results["if_else"] = True + if if_complete_pattern.search(line): + test_results["if_complete"] = True + if not test1_complete.done(): + test1_complete.set_result(True) + + # Test 2: Nested IfAction + if nested_both_true_pattern.search(line): + test_results["nested_both_true"] = True + if nested_outer_true_inner_false_pattern.search(line): + test_results["nested_outer_true_inner_false"] = True + if nested_outer_false_pattern.search(line): + test_results["nested_outer_false"] = True + if nested_complete_pattern.search(line): + test_results["nested_complete"] = True + if not test2_complete.done(): + test2_complete.set_result(True) + + # Test 3: WhileAction + if match := while_iteration_pattern.search(line): + test_results["while_iterations"] = max( + test_results["while_iterations"], int(match.group(1)) + 1 + ) + if while_complete_pattern.search(line): + test_results["while_complete"] = True + if not test3_complete.done(): + test3_complete.set_result(True) + + # Test 4: RepeatAction + if match := repeat_iteration_pattern.search(line): + test_results["repeat_iterations"] = max( + test_results["repeat_iterations"], int(match.group(1)) + 1 + ) + if repeat_complete_pattern.search(line): + test_results["repeat_complete"] = True + if not test4_complete.done(): + test4_complete.set_result(True) + + # Test 5: Combined + if combined_pattern.search(line): + test_results["combined_iterations"] += 1 + if combined_complete_pattern.search(line): + test_results["combined_complete"] = True + if not test5_complete.done(): + test5_complete.set_result(True) + + # Test 6: Rapid triggers + if rapid_then_pattern.search(line): + test_results["rapid_then"] += 1 + if rapid_else_pattern.search(line): + test_results["rapid_else"] += 1 + if rapid_complete_pattern.search(line): + test_results["rapid_complete"] += 1 + if test_results["rapid_complete"] == 5 and not test6_complete.done(): + test6_complete.set_result(True) + + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Get services + _, services = await client.list_entities_services() + + # Test 1: IfAction with then branch + test_service = next((s for s in services if s.name == "test_if_action"), None) + assert test_service is not None, "test_if_action service not found" + client.execute_service(test_service, {"condition": True, "value": 42}) + await asyncio.wait_for(test1_complete, timeout=2.0) + assert test_results["if_then"], "IfAction then branch not executed" + assert test_results["if_complete"], "IfAction did not complete" + + # Test 1b: IfAction with else branch + test1_complete = loop.create_future() + test_results["if_complete"] = False + client.execute_service(test_service, {"condition": False, "value": 99}) + await asyncio.wait_for(test1_complete, timeout=2.0) + assert test_results["if_else"], "IfAction else branch not executed" + assert test_results["if_complete"], "IfAction did not complete" + + # Test 2: Nested IfAction - test all branches + test_service = next((s for s in services if s.name == "test_nested_if"), None) + assert test_service is not None, "test_nested_if service not found" + + # Both true + client.execute_service(test_service, {"outer": True, "inner": True}) + await asyncio.wait_for(test2_complete, timeout=2.0) + assert test_results["nested_both_true"], "Nested both true not executed" + + # Outer true, inner false + test2_complete = loop.create_future() + test_results["nested_complete"] = False + client.execute_service(test_service, {"outer": True, "inner": False}) + await asyncio.wait_for(test2_complete, timeout=2.0) + assert test_results["nested_outer_true_inner_false"], ( + "Nested outer true inner false not executed" + ) + + # Outer false + test2_complete = loop.create_future() + test_results["nested_complete"] = False + client.execute_service(test_service, {"outer": False, "inner": True}) + await asyncio.wait_for(test2_complete, timeout=2.0) + assert test_results["nested_outer_false"], "Nested outer false not executed" + + # Test 3: WhileAction + test_service = next( + (s for s in services if s.name == "test_while_action"), None + ) + assert test_service is not None, "test_while_action service not found" + client.execute_service(test_service, {"max_count": 3}) + await asyncio.wait_for(test3_complete, timeout=2.0) + assert test_results["while_iterations"] == 3, ( + f"WhileAction expected 3 iterations, got {test_results['while_iterations']}" + ) + assert test_results["while_complete"], "WhileAction did not complete" + + # Test 4: RepeatAction + test_service = next( + (s for s in services if s.name == "test_repeat_action"), None + ) + assert test_service is not None, "test_repeat_action service not found" + client.execute_service(test_service, {"count": 5}) + await asyncio.wait_for(test4_complete, timeout=2.0) + assert test_results["repeat_iterations"] == 5, ( + f"RepeatAction expected 5 iterations, got {test_results['repeat_iterations']}" + ) + assert test_results["repeat_complete"], "RepeatAction did not complete" + + # Test 5: Combined (if + repeat + while) + test_service = next((s for s in services if s.name == "test_combined"), None) + assert test_service is not None, "test_combined service not found" + client.execute_service(test_service, {"do_loop": True, "loop_count": 2}) + await asyncio.wait_for(test5_complete, timeout=2.0) + # Should execute: repeat 2 times, each iteration does while from iteration down to 0 + # iteration 0: while 0 times = 0 + # iteration 1: while 1 time = 1 + # Total: 1 combined log + assert test_results["combined_iterations"] >= 1, ( + f"Combined expected >=1 iterations, got {test_results['combined_iterations']}" + ) + assert test_results["combined_complete"], "Combined did not complete" + + # Test 6: Rapid triggers (tests memory efficiency of ContinuationAction) + test_service = next((s for s in services if s.name == "test_rapid_if"), None) + assert test_service is not None, "test_rapid_if service not found" + client.execute_service(test_service, {}) + await asyncio.wait_for(test6_complete, timeout=2.0) + # Values 1, 2 should hit else (<=2), values 3, 4, 5 should hit then (>2) + assert test_results["rapid_else"] == 2, ( + f"Rapid test expected 2 else, got {test_results['rapid_else']}" + ) + assert test_results["rapid_then"] == 3, ( + f"Rapid test expected 3 then, got {test_results['rapid_then']}" + ) + assert test_results["rapid_complete"] == 5, ( + f"Rapid test expected 5 completions, got {test_results['rapid_complete']}" + ) diff --git a/tests/integration/test_crc8_helper.py b/tests/integration/test_crc8_helper.py new file mode 100644 index 0000000000..ffe6244598 --- /dev/null +++ b/tests/integration/test_crc8_helper.py @@ -0,0 +1,100 @@ +"""Integration test for CRC8 helper function.""" + +from __future__ import annotations + +import asyncio +from pathlib import Path + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_crc8_helper( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test the CRC8 helper function through integration testing.""" + # Get the path to the external components directory + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + + # Replace the placeholder in the YAML config with the actual path + yaml_config = yaml_config.replace( + "EXTERNAL_COMPONENT_PATH", external_components_path + ) + + # Track test completion with asyncio.Event + test_complete = asyncio.Event() + + # Track test results + test_results = { + "dallas_maxim": False, + "sensirion": False, + "pec": False, + "parameter_equivalence": False, + "edge_cases": False, + "component_compatibility": False, + "setup_started": False, + } + + def on_log_line(line): + """Process log lines to track test progress and results.""" + # Track test start + if "CRC8 Helper Function Integration Test Starting" in line: + test_results["setup_started"] = True + + # Track test completion + elif "CRC8 Integration Test Complete" in line: + test_complete.set() + + # Track individual test results + elif "ALL TESTS PASSED" in line: + if "Dallas/Maxim CRC8" in line: + test_results["dallas_maxim"] = True + elif "Sensirion CRC8" in line: + test_results["sensirion"] = True + elif "PEC CRC8" in line: + test_results["pec"] = True + elif "Parameter equivalence" in line: + test_results["parameter_equivalence"] = True + elif "Edge cases" in line: + test_results["edge_cases"] = True + elif "Component compatibility" in line: + test_results["component_compatibility"] = True + + # Log failures for debugging + elif "TEST FAILED:" in line or "SUBTEST FAILED:" in line: + print(f"CRC8 Test Failure: {line}") + + # Compile and run the test + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + # Verify device info + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "crc8-helper-test" + + # Wait for tests to complete with timeout + try: + await asyncio.wait_for(test_complete.wait(), timeout=5.0) + except TimeoutError: + pytest.fail("CRC8 integration test timed out after 5 seconds") + + # Verify all tests passed + assert test_results["setup_started"], "CRC8 test setup never started" + assert test_results["dallas_maxim"], "Dallas/Maxim CRC8 test failed" + assert test_results["sensirion"], "Sensirion CRC8 test failed" + assert test_results["pec"], "PEC CRC8 test failed" + assert test_results["parameter_equivalence"], ( + "Parameter equivalence test failed" + ) + assert test_results["edge_cases"], "Edge cases test failed" + assert test_results["component_compatibility"], ( + "Component compatibility test failed" + ) diff --git a/tests/integration/test_gpio_expander_cache.py b/tests/integration/test_gpio_expander_cache.py new file mode 100644 index 0000000000..e5f0f2818f --- /dev/null +++ b/tests/integration/test_gpio_expander_cache.py @@ -0,0 +1,146 @@ +"""Integration test for CachedGPIOExpander to ensure correct behavior.""" + +from __future__ import annotations + +import asyncio +from pathlib import Path +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_gpio_expander_cache( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test gpio_expander::CachedGpioExpander correctly calls hardware functions.""" + # Get the path to the external components directory + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + + # Replace the placeholder in the YAML config with the actual path + yaml_config = yaml_config.replace( + "EXTERNAL_COMPONENT_PATH", external_components_path + ) + + logs_done = asyncio.Event() + + # Patterns to match in logs - match any variation of digital_read + read_hw_pattern = re.compile(r"(?:uint16_)?digital_read_hw pin=(\d+)") + read_cache_pattern = re.compile(r"(?:uint16_)?digital_read_cache pin=(\d+)") + + # Keep specific patterns for building the expected order + digital_read_hw_pattern = re.compile(r"^digital_read_hw pin=(\d+)") + digital_read_cache_pattern = re.compile(r"^digital_read_cache pin=(\d+)") + uint16_read_hw_pattern = re.compile(r"^uint16_digital_read_hw pin=(\d+)") + uint16_read_cache_pattern = re.compile(r"^uint16_digital_read_cache pin=(\d+)") + + # ensure logs are in the expected order + log_order = [ + (digital_read_hw_pattern, 0), + [(digital_read_cache_pattern, i) for i in range(0, 8)], + (digital_read_hw_pattern, 8), + [(digital_read_cache_pattern, i) for i in range(8, 16)], + (digital_read_hw_pattern, 16), + [(digital_read_cache_pattern, i) for i in range(16, 24)], + (digital_read_hw_pattern, 24), + [(digital_read_cache_pattern, i) for i in range(24, 32)], + (digital_read_hw_pattern, 3), + (digital_read_cache_pattern, 3), + (digital_read_hw_pattern, 3), + (digital_read_cache_pattern, 3), + (digital_read_cache_pattern, 4), + (digital_read_hw_pattern, 3), + (digital_read_cache_pattern, 3), + (digital_read_hw_pattern, 10), + (digital_read_cache_pattern, 10), + # full cache reset here for testing + (digital_read_hw_pattern, 15), + (digital_read_cache_pattern, 15), + (digital_read_cache_pattern, 14), + (digital_read_hw_pattern, 14), + (digital_read_cache_pattern, 14), + # uint16_t component tests (single bank of 16 pins) + (uint16_read_hw_pattern, 0), # First pin triggers hw read + [ + (uint16_read_cache_pattern, i) for i in range(0, 16) + ], # All 16 pins return via cache + # After cache reset + (uint16_read_hw_pattern, 5), # First read after reset triggers hw + (uint16_read_cache_pattern, 5), + (uint16_read_cache_pattern, 10), # These use cache (same bank) + (uint16_read_cache_pattern, 15), + (uint16_read_cache_pattern, 0), + ] + # Flatten the log order for easier processing + log_order: list[tuple[re.Pattern, int]] = [ + item + for sublist in log_order + for item in (sublist if isinstance(sublist, list) else [sublist]) + ] + + index = 0 + + def check_output(line: str) -> None: + """Check log output for expected messages.""" + nonlocal index + if logs_done.is_set(): + return + + clean_line = re.sub(r"\x1b\[[0-9;]*m", "", line) + + # Extract just the log message part (after the log level) + msg = clean_line.split(": ", 1)[-1] if ": " in clean_line else clean_line + + # Check if this line contains a read operation we're tracking + if read_hw_pattern.search(msg) or read_cache_pattern.search(msg): + if index >= len(log_order): + print(f"Received unexpected log line: {msg}") + logs_done.set() + return + + pattern, expected_pin = log_order[index] + match = pattern.search(msg) + + if not match: + print(f"Log line did not match next expected pattern: {msg}") + print(f"Expected pattern: {pattern.pattern}") + logs_done.set() + return + + pin = int(match.group(1)) + if pin != expected_pin: + print(f"Unexpected pin number. Expected {expected_pin}, got {pin}") + logs_done.set() + return + + index += 1 + + elif "DONE_UINT16" in clean_line: + # uint16 component is done, check if we've seen all expected logs + if index == len(log_order): + logs_done.set() + + # Run with log monitoring + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Verify device info + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "gpio-expander-cache" + + try: + await asyncio.wait_for(logs_done.wait(), timeout=5.0) + except TimeoutError: + pytest.fail("Timeout waiting for logs to complete") + + assert index == len(log_order), ( + f"Expected {len(log_order)} log entries, but got {index}" + ) diff --git a/tests/integration/test_host_mode_api_password.py b/tests/integration/test_host_mode_api_password.py index 825c2c55f2..5c5e689e45 100644 --- a/tests/integration/test_host_mode_api_password.py +++ b/tests/integration/test_host_mode_api_password.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio -from aioesphomeapi import APIConnectionError +from aioesphomeapi import APIConnectionError, InvalidAuthAPIError import pytest from .types import APIClientConnectedFactory, RunCompiledFunction @@ -48,6 +48,22 @@ async def test_host_mode_api_password( assert len(states) > 0 # Test with wrong password - should fail - with pytest.raises(APIConnectionError, match="Invalid password"): - async with api_client_connected(password="wrong_password"): - pass # Should not reach here + # Try connecting with wrong password + try: + async with api_client_connected( + password="wrong_password", timeout=5 + ) as client: + # If we get here without exception, try to use the connection + # which should fail if auth failed + await client.device_info_and_list_entities() + # If we successfully got device info and entities, auth didn't fail properly + pytest.fail("Connection succeeded with wrong password") + except (InvalidAuthAPIError, APIConnectionError) as e: + # Expected - auth should fail + # Accept either InvalidAuthAPIError or generic APIConnectionError + # since the client might not always distinguish + assert ( + "password" in str(e).lower() + or "auth" in str(e).lower() + or "invalid" in str(e).lower() + ) diff --git a/tests/integration/test_host_mode_climate_basic_state.py b/tests/integration/test_host_mode_climate_basic_state.py new file mode 100644 index 0000000000..7d871ed5a8 --- /dev/null +++ b/tests/integration/test_host_mode_climate_basic_state.py @@ -0,0 +1,49 @@ +"""Integration test for Host mode with climate.""" + +from __future__ import annotations + +import aioesphomeapi +from aioesphomeapi import ClimateAction, ClimateInfo, ClimateMode, ClimatePreset +import pytest + +from .state_utils import InitialStateHelper +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_host_mode_climate_basic_state( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test basic climate state reporting.""" + async with run_compiled(yaml_config), api_client_connected() as client: + # Get entities and set up state synchronization + entities, services = await client.list_entities_services() + initial_state_helper = InitialStateHelper(entities) + climate_infos = [e for e in entities if isinstance(e, ClimateInfo)] + assert len(climate_infos) >= 1, "Expected at least 1 climate entity" + + # Subscribe with the wrapper (no-op callback since we just want initial states) + client.subscribe_states(initial_state_helper.on_state_wrapper(lambda _: None)) + + # Wait for all initial states to be broadcast + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + # Get the climate entity and its initial state + test_climate = climate_infos[0] + climate_state = initial_state_helper.initial_states.get(test_climate.key) + + assert climate_state is not None, "Climate initial state not found" + assert isinstance(climate_state, aioesphomeapi.ClimateState) + assert climate_state.mode == ClimateMode.OFF + assert climate_state.action == ClimateAction.OFF + assert climate_state.current_temperature == 22.0 + assert climate_state.target_temperature_low == 18.0 + assert climate_state.target_temperature_high == 24.0 + assert climate_state.preset == ClimatePreset.HOME + assert climate_state.current_humidity == 42.0 + assert climate_state.target_humidity == 20.0 diff --git a/tests/integration/test_host_mode_climate_control.py b/tests/integration/test_host_mode_climate_control.py new file mode 100644 index 0000000000..96d15dfae0 --- /dev/null +++ b/tests/integration/test_host_mode_climate_control.py @@ -0,0 +1,76 @@ +"""Integration test for Host mode with climate.""" + +from __future__ import annotations + +import asyncio + +import aioesphomeapi +from aioesphomeapi import ClimateInfo, ClimateMode, EntityState +import pytest + +from .state_utils import InitialStateHelper +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_host_mode_climate_control( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test climate mode control.""" + loop = asyncio.get_running_loop() + async with run_compiled(yaml_config), api_client_connected() as client: + states: dict[int, EntityState] = {} + climate_future: asyncio.Future[EntityState] = loop.create_future() + + def on_state(state: EntityState) -> None: + states[state.key] = state + if ( + isinstance(state, aioesphomeapi.ClimateState) + and state.mode == ClimateMode.HEAT + and state.target_temperature_low == 21.5 + and state.target_temperature_high == 26.5 + and not climate_future.done() + ): + climate_future.set_result(state) + + # Get entities and set up state synchronization + entities, services = await client.list_entities_services() + initial_state_helper = InitialStateHelper(entities) + climate_infos = [e for e in entities if isinstance(e, ClimateInfo)] + assert len(climate_infos) >= 1, "Expected at least 1 climate entity" + + # Subscribe with the wrapper that filters initial states + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + + # Wait for all initial states to be broadcast + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + test_climate = next( + (c for c in climate_infos if c.name == "Dual-mode Thermostat"), None + ) + assert test_climate is not None, ( + "Dual-mode Thermostat thermostat climate not found" + ) + + # Adjust setpoints + client.climate_command( + test_climate.key, + mode=ClimateMode.HEAT, + target_temperature_low=21.5, + target_temperature_high=26.5, + ) + + try: + climate_state = await asyncio.wait_for(climate_future, timeout=5.0) + except TimeoutError: + pytest.fail("Climate state not received within 5 seconds") + + assert isinstance(climate_state, aioesphomeapi.ClimateState) + assert climate_state.mode == ClimateMode.HEAT + assert climate_state.target_temperature_low == 21.5 + assert climate_state.target_temperature_high == 26.5 diff --git a/tests/integration/test_host_mode_empty_string_options.py b/tests/integration/test_host_mode_empty_string_options.py index 242db2d40f..1180ce75fc 100644 --- a/tests/integration/test_host_mode_empty_string_options.py +++ b/tests/integration/test_host_mode_empty_string_options.py @@ -36,8 +36,8 @@ async def test_host_mode_empty_string_options( # Find our select entities select_entities = [e for e in entity_info if isinstance(e, SelectInfo)] - assert len(select_entities) == 3, ( - f"Expected 3 select entities, got {len(select_entities)}" + assert len(select_entities) == 4, ( + f"Expected 4 select entities, got {len(select_entities)}" ) # Verify each select entity by name and check their options @@ -71,6 +71,15 @@ async def test_host_mode_empty_string_options( assert empty_last.options[2] == "Choice Z" assert empty_last.options[3] == "" # Empty string at end + # Check "Select Initial Option Test" - verify non-default initial option + assert "Select Initial Option Test" in selects_by_name + initial_option_test = selects_by_name["Select Initial Option Test"] + assert len(initial_option_test.options) == 4 + assert initial_option_test.options[0] == "First" + assert initial_option_test.options[1] == "Second" + assert initial_option_test.options[2] == "Third" + assert initial_option_test.options[3] == "Fourth" + # If we got here without protobuf decoding errors, the fix is working # The bug would have caused "Invalid protobuf message" errors with trailing bytes @@ -78,7 +87,12 @@ async def test_host_mode_empty_string_options( # This ensures empty strings work properly in state messages too states: dict[int, EntityState] = {} states_received_future: asyncio.Future[None] = loop.create_future() - expected_select_keys = {empty_first.key, empty_middle.key, empty_last.key} + expected_select_keys = { + empty_first.key, + empty_middle.key, + empty_last.key, + initial_option_test.key, + } received_select_keys = set() def on_state(state: EntityState) -> None: @@ -109,6 +123,14 @@ async def test_host_mode_empty_string_options( assert empty_first.key in states assert empty_middle.key in states assert empty_last.key in states + assert initial_option_test.key in states + + # Verify the initial option is set correctly to "Third" (not the default "First") + initial_state = states[initial_option_test.key] + assert initial_state.state == "Third", ( + f"Expected initial state 'Third' but got '{initial_state.state}' - " + f"initial_option not correctly applied" + ) # The main test is that we got here without protobuf errors # The select entities with empty string options were properly encoded diff --git a/tests/integration/test_host_mode_many_entities.py b/tests/integration/test_host_mode_many_entities.py index aaca4555f6..299644d496 100644 --- a/tests/integration/test_host_mode_many_entities.py +++ b/tests/integration/test_host_mode_many_entities.py @@ -4,7 +4,20 @@ from __future__ import annotations import asyncio -from aioesphomeapi import ClimateInfo, EntityState, SensorState +from aioesphomeapi import ( + ClimateFanMode, + ClimateFeature, + ClimateInfo, + ClimateMode, + DateInfo, + DateState, + DateTimeInfo, + DateTimeState, + EntityState, + SensorState, + TimeInfo, + TimeState, +) import pytest from .types import APIClientConnectedFactory, RunCompiledFunction @@ -22,34 +35,56 @@ async def test_host_mode_many_entities( async with run_compiled(yaml_config), api_client_connected() as client: # Subscribe to state changes states: dict[int, EntityState] = {} - sensor_count_future: asyncio.Future[int] = loop.create_future() + minimum_states_future: asyncio.Future[None] = loop.create_future() def on_state(state: EntityState) -> None: states[state.key] = state - # Count sensor states specifically + # Check if we have received minimum expected states sensor_states = [ s for s in states.values() if isinstance(s, SensorState) and isinstance(s.state, float) ] - # When we have received states from at least 50 sensors, resolve the future - if len(sensor_states) >= 50 and not sensor_count_future.done(): - sensor_count_future.set_result(len(sensor_states)) + date_states = [s for s in states.values() if isinstance(s, DateState)] + time_states = [s for s in states.values() if isinstance(s, TimeState)] + datetime_states = [ + s for s in states.values() if isinstance(s, DateTimeState) + ] + + # We expect at least 50 sensors and 1 of each datetime entity type + if ( + len(sensor_states) >= 50 + and len(date_states) >= 1 + and len(time_states) >= 1 + and len(datetime_states) >= 1 + and not minimum_states_future.done() + ): + minimum_states_future.set_result(None) client.subscribe_states(on_state) - # Wait for states from at least 50 sensors with timeout + # Wait for minimum states with timeout try: - sensor_count = await asyncio.wait_for(sensor_count_future, timeout=10.0) + await asyncio.wait_for(minimum_states_future, timeout=10.0) except TimeoutError: sensor_states = [ s for s in states.values() if isinstance(s, SensorState) and isinstance(s.state, float) ] + date_states = [s for s in states.values() if isinstance(s, DateState)] + time_states = [s for s in states.values() if isinstance(s, TimeState)] + datetime_states = [ + s for s in states.values() if isinstance(s, DateTimeState) + ] + pytest.fail( - f"Did not receive states from at least 50 sensors within 10 seconds. " - f"Received {len(sensor_states)} sensor states out of {len(states)} total states" + f"Did not receive expected states within 10 seconds. " + f"Received: {len(sensor_states)} sensor states (expected >=50), " + f"{len(date_states)} date states (expected >=1), " + f"{len(time_states)} time states (expected >=1), " + f"{len(datetime_states)} datetime states (expected >=1). " + f"Total states: {len(states)}" ) # Verify we received a good number of entity states @@ -64,19 +99,71 @@ async def test_host_mode_many_entities( if isinstance(s, SensorState) and isinstance(s.state, float) ] - assert sensor_count >= 50, ( - f"Expected at least 50 sensor states, got {sensor_count}" - ) assert len(sensor_states) >= 50, ( f"Expected at least 50 sensor states, got {len(sensor_states)}" ) + # Verify we received datetime entity states + date_states = [s for s in states.values() if isinstance(s, DateState)] + time_states = [s for s in states.values() if isinstance(s, TimeState)] + datetime_states = [s for s in states.values() if isinstance(s, DateTimeState)] + + assert len(date_states) >= 1, ( + f"Expected at least 1 date state, got {len(date_states)}" + ) + assert len(time_states) >= 1, ( + f"Expected at least 1 time state, got {len(time_states)}" + ) + assert len(datetime_states) >= 1, ( + f"Expected at least 1 datetime state, got {len(datetime_states)}" + ) + # Get entity info to verify climate entity details entities = await client.list_entities_services() climate_infos = [e for e in entities[0] if isinstance(e, ClimateInfo)] assert len(climate_infos) >= 1, "Expected at least 1 climate entity" climate_info = climate_infos[0] + + # Verify feature flags set as expected + assert climate_info.feature_flags == ( + ClimateFeature.SUPPORTS_ACTION + | ClimateFeature.SUPPORTS_CURRENT_HUMIDITY + | ClimateFeature.SUPPORTS_CURRENT_TEMPERATURE + | ClimateFeature.SUPPORTS_TWO_POINT_TARGET_TEMPERATURE + | ClimateFeature.SUPPORTS_TARGET_HUMIDITY + ) + + # Verify modes + assert climate_info.supported_modes == [ + ClimateMode.OFF, + ClimateMode.COOL, + ClimateMode.HEAT, + ], f"Expected modes [OFF, COOL, HEAT], got {climate_info.supported_modes}" + + # Verify visual parameters + assert climate_info.visual_min_temperature == 15.0, ( + f"Expected min_temperature=15.0, got {climate_info.visual_min_temperature}" + ) + assert climate_info.visual_max_temperature == 32.0, ( + f"Expected max_temperature=32.0, got {climate_info.visual_max_temperature}" + ) + assert climate_info.visual_target_temperature_step == 0.1, ( + f"Expected temperature_step=0.1, got {climate_info.visual_target_temperature_step}" + ) + assert climate_info.visual_min_humidity == 20.0, ( + f"Expected min_humidity=20.0, got {climate_info.visual_min_humidity}" + ) + assert climate_info.visual_max_humidity == 70.0, ( + f"Expected max_humidity=70.0, got {climate_info.visual_max_humidity}" + ) + + # Verify fan modes + assert climate_info.supported_fan_modes == [ + ClimateFanMode.ON, + ClimateFanMode.AUTO, + ], f"Expected fan modes [ON, AUTO], got {climate_info.supported_fan_modes}" + # Verify the thermostat has presets assert len(climate_info.supported_presets) > 0, ( "Expected climate to have presets" @@ -89,3 +176,28 @@ async def test_host_mode_many_entities( assert "HOME" in preset_names, f"Expected 'HOME' preset, got {preset_names}" assert "AWAY" in preset_names, f"Expected 'AWAY' preset, got {preset_names}" assert "SLEEP" in preset_names, f"Expected 'SLEEP' preset, got {preset_names}" + + # Verify datetime entities exist + date_infos = [e for e in entities[0] if isinstance(e, DateInfo)] + time_infos = [e for e in entities[0] if isinstance(e, TimeInfo)] + datetime_infos = [e for e in entities[0] if isinstance(e, DateTimeInfo)] + + assert len(date_infos) >= 1, "Expected at least 1 date entity" + assert len(time_infos) >= 1, "Expected at least 1 time entity" + assert len(datetime_infos) >= 1, "Expected at least 1 datetime entity" + + # Verify the entity names + date_info = date_infos[0] + assert date_info.name == "Test Date", ( + f"Expected date entity name 'Test Date', got {date_info.name}" + ) + + time_info = time_infos[0] + assert time_info.name == "Test Time", ( + f"Expected time entity name 'Test Time', got {time_info.name}" + ) + + datetime_info = datetime_infos[0] + assert datetime_info.name == "Test DateTime", ( + f"Expected datetime entity name 'Test DateTime', got {datetime_info.name}" + ) diff --git a/tests/integration/test_host_preferences.py b/tests/integration/test_host_preferences.py new file mode 100644 index 0000000000..38c6460cf1 --- /dev/null +++ b/tests/integration/test_host_preferences.py @@ -0,0 +1,167 @@ +"""Test host preferences save and load functionality.""" + +from __future__ import annotations + +import asyncio +import re +from typing import Any + +from aioesphomeapi import ButtonInfo, EntityInfo, NumberInfo, SwitchInfo +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +def find_entity_by_name( + entities: list[EntityInfo], entity_type: type, name: str +) -> Any: + """Helper to find an entity by type and name.""" + return next( + (e for e in entities if isinstance(e, entity_type) and e.name == name), None + ) + + +@pytest.mark.asyncio +async def test_host_preferences_save_load( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that preferences are correctly saved and loaded after our optimization fix.""" + loop = asyncio.get_running_loop() + log_lines: list[str] = [] + preferences_saved = loop.create_future() + preferences_loaded = loop.create_future() + values_match = loop.create_future() + final_load_complete = loop.create_future() + + # Patterns to match preference logs + save_pattern = re.compile(r"Preference saved: key=(\w+), value=([0-9.]+)") + load_pattern = re.compile(r"Preference loaded: key=(\w+), value=([0-9.]+)") + verify_pattern = re.compile(r"Preferences verified: values match!") + final_load_success_pattern = re.compile( + r"Final load test: loaded \d+ preferences successfully" + ) + + saved_values: dict[str, float] = {} + loaded_values: dict[str, float] = {} + + def check_output(line: str) -> None: + """Check log output for preference operations.""" + log_lines.append(line) + + # Look for save operations + match = save_pattern.search(line) + if match: + key = match.group(1) + value = float(match.group(2)) + saved_values[key] = value + if len(saved_values) >= 2 and not preferences_saved.done(): + preferences_saved.set_result(True) + + # Look for load operations + match = load_pattern.search(line) + if match: + key = match.group(1) + value = float(match.group(2)) + loaded_values[key] = value + if len(loaded_values) >= 2 and not preferences_loaded.done(): + preferences_loaded.set_result(True) + + # Look for verification + if verify_pattern.search(line) and not values_match.done(): + values_match.set_result(True) + + # Look for final load test completion + if final_load_success_pattern.search(line) and not final_load_complete.done(): + final_load_complete.set_result(True) + + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Get entity list + entities, _ = await client.list_entities_services() + + # Find our test entities using helper + test_switch = find_entity_by_name(entities, SwitchInfo, "Test Switch") + test_number = find_entity_by_name(entities, NumberInfo, "Test Number") + save_button = find_entity_by_name(entities, ButtonInfo, "Save Preferences") + load_button = find_entity_by_name(entities, ButtonInfo, "Load Preferences") + verify_button = find_entity_by_name(entities, ButtonInfo, "Verify Preferences") + + assert test_switch is not None, "Test Switch not found" + assert test_number is not None, "Test Number not found" + assert save_button is not None, "Save Preferences button not found" + assert load_button is not None, "Load Preferences button not found" + assert verify_button is not None, "Verify Preferences button not found" + + # Set initial values + client.switch_command(test_switch.key, True) + client.number_command(test_number.key, 42.5) + + # Save preferences + client.button_command(save_button.key) + + # Wait for save to complete + try: + await asyncio.wait_for(preferences_saved, timeout=5.0) + except TimeoutError: + pytest.fail("Preferences not saved within timeout") + + # Verify we saved the expected values + assert "switch" in saved_values, f"Switch preference not saved: {saved_values}" + assert "number" in saved_values, f"Number preference not saved: {saved_values}" + assert saved_values["switch"] == 1.0, ( + f"Switch value incorrect: {saved_values['switch']}" + ) + assert saved_values["number"] == 42.5, ( + f"Number value incorrect: {saved_values['number']}" + ) + + # Change the values to something else + client.switch_command(test_switch.key, False) + client.number_command(test_number.key, 13.7) + + # Load preferences (should restore the saved values) + client.button_command(load_button.key) + + # Wait for load to complete + try: + await asyncio.wait_for(preferences_loaded, timeout=5.0) + except TimeoutError: + pytest.fail("Preferences not loaded within timeout") + + # Verify loaded values match saved values + assert "switch" in loaded_values, ( + f"Switch preference not loaded: {loaded_values}" + ) + assert "number" in loaded_values, ( + f"Number preference not loaded: {loaded_values}" + ) + assert loaded_values["switch"] == saved_values["switch"], ( + f"Loaded switch value {loaded_values['switch']} doesn't match saved {saved_values['switch']}" + ) + assert loaded_values["number"] == saved_values["number"], ( + f"Loaded number value {loaded_values['number']} doesn't match saved {saved_values['number']}" + ) + + # Verify the values were actually restored + client.button_command(verify_button.key) + + # Wait for verification + try: + await asyncio.wait_for(values_match, timeout=5.0) + except TimeoutError: + pytest.fail("Preference verification failed within timeout") + + # Test that non-existent preferences don't crash (tests our fix) + # This will trigger load attempts for keys that don't exist + # Our fix should prevent map entries from being created + client.button_command(load_button.key) + + # Wait for the final load test to complete + try: + await asyncio.wait_for(final_load_complete, timeout=5.0) + except TimeoutError: + pytest.fail("Final load test did not complete within timeout") diff --git a/tests/integration/test_light_calls.py b/tests/integration/test_light_calls.py index 1a0a9e553f..0eaf5af91b 100644 --- a/tests/integration/test_light_calls.py +++ b/tests/integration/test_light_calls.py @@ -8,6 +8,7 @@ import asyncio from typing import Any from aioesphomeapi import LightState +from aioesphomeapi.model import ColorMode import pytest from .types import APIClientConnectedFactory, RunCompiledFunction @@ -35,10 +36,51 @@ async def test_light_calls( # Get the light entities entities = await client.list_entities_services() lights = [e for e in entities[0] if e.object_id.startswith("test_")] - assert len(lights) >= 2 # Should have RGBCW and RGB lights + assert len(lights) >= 3 # Should have RGBCW, RGB, and Binary lights rgbcw_light = next(light for light in lights if "RGBCW" in light.name) rgb_light = next(light for light in lights if "RGB Light" in light.name) + binary_light = next(light for light in lights if "Binary" in light.name) + + # Test color mode encoding: Verify supported_color_modes contains actual ColorMode enum values + # not bit positions. This is critical - the iterator must convert bit positions to actual + # ColorMode enum values for API encoding. + + # RGBCW light (rgbww platform) should support RGB_COLD_WARM_WHITE mode + assert ColorMode.RGB_COLD_WARM_WHITE in rgbcw_light.supported_color_modes, ( + f"RGBCW light missing RGB_COLD_WARM_WHITE mode. Got: {rgbcw_light.supported_color_modes}" + ) + # Verify it's the actual enum value, not bit position + assert ColorMode.RGB_COLD_WARM_WHITE.value in [ + mode.value for mode in rgbcw_light.supported_color_modes + ], ( + f"RGBCW light has wrong color mode values. Expected {ColorMode.RGB_COLD_WARM_WHITE.value} " + f"(RGB_COLD_WARM_WHITE), got: {[mode.value for mode in rgbcw_light.supported_color_modes]}" + ) + + # RGB light should support RGB mode + assert ColorMode.RGB in rgb_light.supported_color_modes, ( + f"RGB light missing RGB color mode. Got: {rgb_light.supported_color_modes}" + ) + # Verify it's the actual enum value, not bit position + assert ColorMode.RGB.value in [ + mode.value for mode in rgb_light.supported_color_modes + ], ( + f"RGB light has wrong color mode values. Expected {ColorMode.RGB.value} (RGB), got: " + f"{[mode.value for mode in rgb_light.supported_color_modes]}" + ) + + # Binary light (on/off only) should support ON_OFF mode + assert ColorMode.ON_OFF in binary_light.supported_color_modes, ( + f"Binary light missing ON_OFF color mode. Got: {binary_light.supported_color_modes}" + ) + # Verify it's the actual enum value, not bit position + assert ColorMode.ON_OFF.value in [ + mode.value for mode in binary_light.supported_color_modes + ], ( + f"Binary light has wrong color mode values. Expected {ColorMode.ON_OFF.value} (ON_OFF), got: " + f"{[mode.value for mode in binary_light.supported_color_modes]}" + ) async def wait_for_state_change(key: int, timeout: float = 1.0) -> Any: """Wait for a state change for the given entity key.""" @@ -108,14 +150,51 @@ async def test_light_calls( # Wait for flash to end state = await wait_for_state_change(rgbcw_light.key) - # Test 13: effect only + # Test 13: effect only - test all random effects # First ensure light is on client.light_command(key=rgbcw_light.key, state=True) state = await wait_for_state_change(rgbcw_light.key) - # Now set effect - client.light_command(key=rgbcw_light.key, effect="Random Effect") + + # Test 13a: Default random effect (no name, gets default name "Random") + client.light_command(key=rgbcw_light.key, effect="Random") state = await wait_for_state_change(rgbcw_light.key) - assert state.effect == "Random Effect" + assert state.effect == "Random" + + # Test 13b: Slow random effect with long name + client.light_command( + key=rgbcw_light.key, effect="My Very Slow Random Effect With Long Name" + ) + state = await wait_for_state_change(rgbcw_light.key) + assert state.effect == "My Very Slow Random Effect With Long Name" + + # Test 13c: Fast random effect with long name + client.light_command( + key=rgbcw_light.key, effect="My Fast Random Effect That Changes Quickly" + ) + state = await wait_for_state_change(rgbcw_light.key) + assert state.effect == "My Fast Random Effect That Changes Quickly" + + # Test 13d: Random effect with medium length name + client.light_command( + key=rgbcw_light.key, effect="Random Effect With Medium Length Name Here" + ) + state = await wait_for_state_change(rgbcw_light.key) + assert state.effect == "Random Effect With Medium Length Name Here" + + # Test 13e: Another random effect + client.light_command( + key=rgbcw_light.key, + effect="Another Random Effect With Different Parameters", + ) + state = await wait_for_state_change(rgbcw_light.key) + assert state.effect == "Another Random Effect With Different Parameters" + + # Test 13f: Yet another random effect + client.light_command( + key=rgbcw_light.key, effect="Yet Another Random Effect To Test Memory" + ) + state = await wait_for_state_change(rgbcw_light.key) + assert state.effect == "Yet Another Random Effect To Test Memory" # Test 14: stop effect client.light_command(key=rgbcw_light.key, effect="None") diff --git a/tests/integration/test_multi_device_preferences.py b/tests/integration/test_multi_device_preferences.py new file mode 100644 index 0000000000..625f83f16e --- /dev/null +++ b/tests/integration/test_multi_device_preferences.py @@ -0,0 +1,144 @@ +"""Test multi-device preference storage functionality.""" + +from __future__ import annotations + +import asyncio +import re + +from aioesphomeapi import ButtonInfo, NumberInfo, SelectInfo, SwitchInfo +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_multi_device_preferences( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that entities with same names on different devices have unique preference storage.""" + loop = asyncio.get_running_loop() + log_lines: list[str] = [] + preferences_logged = loop.create_future() + + # Patterns to match preference hash logs + switch_hash_pattern_device = re.compile(r"Device ([AB]) Switch Pref Hash: (\d+)") + switch_hash_pattern_main = re.compile(r"Main Switch Pref Hash: (\d+)") + number_hash_pattern_device = re.compile(r"Device ([AB]) Number Pref Hash: (\d+)") + number_hash_pattern_main = re.compile(r"Main Number Pref Hash: (\d+)") + switch_hashes: dict[str, int] = {} + number_hashes: dict[str, int] = {} + + def check_output(line: str) -> None: + """Check log output for preference hash information.""" + log_lines.append(line) + + # Look for device switch preference hash logs + match = switch_hash_pattern_device.search(line) + if match: + device = match.group(1) + hash_value = int(match.group(2)) + switch_hashes[device] = hash_value + + # Look for main switch preference hash + match = switch_hash_pattern_main.search(line) + if match: + hash_value = int(match.group(1)) + switch_hashes["Main"] = hash_value + + # Look for device number preference hash logs + match = number_hash_pattern_device.search(line) + if match: + device = match.group(1) + hash_value = int(match.group(2)) + number_hashes[device] = hash_value + + # Look for main number preference hash + match = number_hash_pattern_main.search(line) + if match: + hash_value = int(match.group(1)) + number_hashes["Main"] = hash_value + + # If we have all hashes, complete the future + if ( + len(switch_hashes) == 3 + and len(number_hashes) == 3 + and not preferences_logged.done() + ): + preferences_logged.set_result(True) + + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Get entity list + entities, _ = await client.list_entities_services() + + # Verify we have the expected entities with duplicate names on different devices + + # Check switches (3 with name "Light") + switches = [ + e for e in entities if isinstance(e, SwitchInfo) and e.name == "Light" + ] + assert len(switches) == 3, f"Expected 3 'Light' switches, got {len(switches)}" + + # Check numbers (3 with name "Setpoint") + numbers = [ + e for e in entities if isinstance(e, NumberInfo) and e.name == "Setpoint" + ] + assert len(numbers) == 3, f"Expected 3 'Setpoint' numbers, got {len(numbers)}" + + # Check selects (3 with name "Mode") + selects = [ + e for e in entities if isinstance(e, SelectInfo) and e.name == "Mode" + ] + assert len(selects) == 3, f"Expected 3 'Mode' selects, got {len(selects)}" + + # Find the test button entity to trigger preference logging + buttons = [e for e in entities if isinstance(e, ButtonInfo)] + test_button = next((b for b in buttons if b.name == "Test Preferences"), None) + assert test_button is not None, "Test Preferences button not found" + + # Press the button to trigger logging + client.button_command(test_button.key) + + # Wait for preference hashes to be logged + try: + await asyncio.wait_for(preferences_logged, timeout=5.0) + except TimeoutError: + pytest.fail("Preference hashes not logged within timeout") + + # Verify all switch preference hashes are unique + assert len(switch_hashes) == 3, ( + f"Expected 3 devices with switches, got {switch_hashes}" + ) + switch_hash_values = list(switch_hashes.values()) + assert len(switch_hash_values) == len(set(switch_hash_values)), ( + f"Switch preference hashes are not unique: {switch_hashes}" + ) + + # Verify all number preference hashes are unique + assert len(number_hashes) == 3, ( + f"Expected 3 devices with numbers, got {number_hashes}" + ) + number_hash_values = list(number_hashes.values()) + assert len(number_hash_values) == len(set(number_hash_values)), ( + f"Number preference hashes are not unique: {number_hashes}" + ) + + # Verify Device A and Device B have different hashes (they have device_id set) + assert switch_hashes["A"] != switch_hashes["B"], ( + f"Device A and B switches should have different hashes: A={switch_hashes['A']}, B={switch_hashes['B']}" + ) + assert number_hashes["A"] != number_hashes["B"], ( + f"Device A and B numbers should have different hashes: A={number_hashes['A']}, B={number_hashes['B']}" + ) + + # Verify Main device hash is different from both A and B + assert switch_hashes["Main"] != switch_hashes["A"], ( + f"Main and Device A switches should have different hashes: Main={switch_hashes['Main']}, A={switch_hashes['A']}" + ) + assert switch_hashes["Main"] != switch_hashes["B"], ( + f"Main and Device B switches should have different hashes: Main={switch_hashes['Main']}, B={switch_hashes['B']}" + ) diff --git a/tests/integration/test_noise_encryption_key_protection.py b/tests/integration/test_noise_encryption_key_protection.py new file mode 100644 index 0000000000..37d32ce2b4 --- /dev/null +++ b/tests/integration/test_noise_encryption_key_protection.py @@ -0,0 +1,90 @@ +"""Integration test for noise encryption key protection from YAML.""" + +from __future__ import annotations + +import base64 + +from aioesphomeapi import InvalidEncryptionKeyAPIError +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_noise_encryption_key_protection( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that noise encryption key set in YAML cannot be changed via API.""" + # The key that's set in the YAML fixture + noise_psk = "zX9/JHxMKwpP0jUGsF0iESCm1wRvNgR6NkKVOhn7kSs=" + + # Keep ESPHome process running throughout all tests + async with run_compiled(yaml_config): + # First connection - test key change attempt + async with api_client_connected(noise_psk=noise_psk) as client: + # Verify connection is established + device_info = await client.device_info() + assert device_info is not None + + # Try to set a new encryption key via API + new_key = base64.b64encode( + b"x" * 32 + ) # Valid 32-byte key in base64 as bytes + + # This should fail since key was set in YAML + success = await client.noise_encryption_set_key(new_key) + assert success is False + + # Reconnect with the original key to verify it still works + async with api_client_connected(noise_psk=noise_psk) as client: + # Verify connection is still successful with original key + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "noise-key-test" + + # Verify that connecting with a wrong key fails + wrong_key = base64.b64encode(b"y" * 32).decode() # Different key + with pytest.raises(InvalidEncryptionKeyAPIError): + async with api_client_connected(noise_psk=wrong_key) as client: + await client.device_info() + + +@pytest.mark.asyncio +async def test_noise_encryption_key_clear_protection( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that noise encryption key set in YAML cannot be changed via API.""" + # The key that's set in the YAML fixture + noise_psk = "zX9/JHxMKwpP0jUGsF0iESCm1wRvNgR6NkKVOhn7kSs=" + + # Keep ESPHome process running throughout all tests + async with run_compiled(yaml_config): + # First connection - test key change attempt + async with api_client_connected(noise_psk=noise_psk) as client: + # Verify connection is established + device_info = await client.device_info() + assert device_info is not None + + # Try to set a new encryption key via API + new_key = b"" # Empty key to attempt to clear + + # This should fail since key was set in YAML + success = await client.noise_encryption_set_key(new_key) + assert success is False + + # Reconnect with the original key to verify it still works + async with api_client_connected(noise_psk=noise_psk) as client: + # Verify connection is still successful with original key + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "noise-key-test" + + # Verify that connecting with a wrong key fails + wrong_key = base64.b64encode(b"y" * 32).decode() # Different key + with pytest.raises(InvalidEncryptionKeyAPIError): + async with api_client_connected(noise_psk=wrong_key) as client: + await client.device_info() diff --git a/tests/integration/test_oversized_payloads.py b/tests/integration/test_oversized_payloads.py new file mode 100644 index 0000000000..8bf890261a --- /dev/null +++ b/tests/integration/test_oversized_payloads.py @@ -0,0 +1,341 @@ +"""Integration tests for oversized payloads and headers that should cause disconnection.""" + +from __future__ import annotations + +import asyncio + +import pytest + +from .types import APIClientConnectedWithDisconnectFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_oversized_payload_plaintext( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected_with_disconnect: APIClientConnectedWithDisconnectFactory, +) -> None: + """Test that oversized payloads (>32768 bytes) from client cause disconnection without crashing.""" + process_exited = False + helper_log_found = False + + def check_logs(line: str) -> None: + nonlocal process_exited, helper_log_found + # Check for signs that the process exited/crashed + if "Segmentation fault" in line or "core dumped" in line: + process_exited = True + # Check for HELPER_LOG message about message size exceeding maximum + if ( + "[VV]" in line + and "Bad packet: message size" in line + and "exceeds maximum" in line + ): + helper_log_found = True + + async with run_compiled(yaml_config, line_callback=check_logs): + async with api_client_connected_with_disconnect() as (client, disconnect_event): + # Verify basic connection works first + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "oversized-plaintext" + + # Create an oversized payload (>32768 bytes which is our new limit) + oversized_data = b"X" * 40000 # ~40KiB, exceeds the 32768 byte limit + + # Access the internal connection to send raw data + frame_helper = client._connection._frame_helper + # Create a message with oversized payload + # Using message type 1 (DeviceInfoRequest) as an example + message_type = 1 + frame_helper.write_packets([(message_type, oversized_data)], True) + + # Wait for the connection to be closed by ESPHome + await asyncio.wait_for(disconnect_event.wait(), timeout=5.0) + + # After disconnection, verify process didn't crash + assert not process_exited, "ESPHome process should not crash" + # Verify we saw the expected HELPER_LOG message + assert helper_log_found, ( + "Expected to see HELPER_LOG about message size exceeding maximum" + ) + + # Try to reconnect to verify the process is still running + async with api_client_connected_with_disconnect() as (client2, _): + device_info = await client2.device_info() + assert device_info is not None + assert device_info.name == "oversized-plaintext" + + +@pytest.mark.asyncio +async def test_oversized_protobuf_message_id_plaintext( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected_with_disconnect: APIClientConnectedWithDisconnectFactory, +) -> None: + """Test that protobuf messages with ID > UINT16_MAX cause disconnection without crashing. + + This tests the message type limit - message IDs must fit in a uint16_t (0-65535). + """ + process_exited = False + helper_log_found = False + + def check_logs(line: str) -> None: + nonlocal process_exited, helper_log_found + # Check for signs that the process exited/crashed + if "Segmentation fault" in line or "core dumped" in line: + process_exited = True + # Check for HELPER_LOG message about message type exceeding maximum + if ( + "[VV]" in line + and "Bad packet: message type" in line + and "exceeds maximum" in line + ): + helper_log_found = True + + async with run_compiled(yaml_config, line_callback=check_logs): + async with api_client_connected_with_disconnect() as (client, disconnect_event): + # Verify basic connection works first + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "oversized-protobuf-plaintext" + + # Access the internal connection to send raw message with large ID + frame_helper = client._connection._frame_helper + # Message ID that exceeds uint16_t limit (> 65535) + large_message_id = 65536 # 2^16, exceeds UINT16_MAX + # Small payload for the test + payload = b"test" + + # This should cause disconnection due to oversized varint + frame_helper.write_packets([(large_message_id, payload)], True) + + # Wait for the connection to be closed by ESPHome + await asyncio.wait_for(disconnect_event.wait(), timeout=5.0) + + # After disconnection, verify process didn't crash + assert not process_exited, "ESPHome process should not crash" + # Verify we saw the expected HELPER_LOG message + assert helper_log_found, ( + "Expected to see HELPER_LOG about message type exceeding maximum" + ) + + # Try to reconnect to verify the process is still running + async with api_client_connected_with_disconnect() as (client2, _): + device_info = await client2.device_info() + assert device_info is not None + assert device_info.name == "oversized-protobuf-plaintext" + + +@pytest.mark.asyncio +async def test_oversized_payload_noise( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected_with_disconnect: APIClientConnectedWithDisconnectFactory, +) -> None: + """Test that oversized payloads from client cause disconnection without crashing with noise encryption.""" + noise_key = "N4Yle5YirwZhPiHHsdZLdOA73ndj/84veVaLhTvxCuU=" + process_exited = False + helper_log_found = False + + def check_logs(line: str) -> None: + nonlocal process_exited, helper_log_found + # Check for signs that the process exited/crashed + if "Segmentation fault" in line or "core dumped" in line: + process_exited = True + # Check for HELPER_LOG message about message size exceeding maximum + # With our new protection, oversized messages are rejected at frame level + if ( + "[VV]" in line + and "Bad packet: message size" in line + and "exceeds maximum" in line + ): + helper_log_found = True + + async with run_compiled(yaml_config, line_callback=check_logs): + async with api_client_connected_with_disconnect(noise_psk=noise_key) as ( + client, + disconnect_event, + ): + # Verify basic connection works first + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "oversized-noise" + + # Create an oversized payload (>32768 bytes which is our new limit) + oversized_data = b"Y" * 40000 # ~40KiB, exceeds the 32768 byte limit + + # Access the internal connection to send raw data + frame_helper = client._connection._frame_helper + # For noise connections, we still send through write_packets + # but the frame helper will handle encryption + # Using message type 1 (DeviceInfoRequest) as an example + message_type = 1 + frame_helper.write_packets([(message_type, oversized_data)], True) + + # Wait for the connection to be closed by ESPHome + await asyncio.wait_for(disconnect_event.wait(), timeout=5.0) + + # After disconnection, verify process didn't crash + assert not process_exited, "ESPHome process should not crash" + # Verify we saw the expected HELPER_LOG message + assert helper_log_found, ( + "Expected to see HELPER_LOG about message size exceeding maximum" + ) + + # Try to reconnect to verify the process is still running + async with api_client_connected_with_disconnect(noise_psk=noise_key) as ( + client2, + _, + ): + device_info = await client2.device_info() + assert device_info is not None + assert device_info.name == "oversized-noise" + + +@pytest.mark.asyncio +async def test_oversized_protobuf_message_id_noise( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected_with_disconnect: APIClientConnectedWithDisconnectFactory, +) -> None: + """Test that the noise protocol handles unknown message types correctly. + + With noise encryption, message types are stored as uint16_t (2 bytes) after decryption. + Unknown message types should be ignored without disconnecting, as ESPHome needs to + read the full message to maintain encryption stream continuity. + """ + noise_key = "N4Yle5YirwZhPiHHsdZLdOA73ndj/84veVaLhTvxCuU=" + process_exited = False + + def check_logs(line: str) -> None: + nonlocal process_exited + # Check for signs that the process exited/crashed + if "Segmentation fault" in line or "core dumped" in line: + process_exited = True + + async with run_compiled(yaml_config, line_callback=check_logs): + async with api_client_connected_with_disconnect(noise_psk=noise_key) as ( + client, + disconnect_event, + ): + # Verify basic connection works first + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "oversized-noise" + + # With noise, message types are uint16_t, so we test with an unknown but valid value + frame_helper = client._connection._frame_helper + + # Test with an unknown message type (65535 is not used by ESPHome) + unknown_message_id = 65535 # Valid uint16_t but unknown to ESPHome + payload = b"test" + + # Send the unknown message type - ESPHome should read and ignore it + frame_helper.write_packets([(unknown_message_id, payload)], True) + + # Give ESPHome a moment to process (but expect no disconnection) + # The connection should stay alive as ESPHome ignores unknown message types + with pytest.raises(asyncio.TimeoutError): + await asyncio.wait_for(disconnect_event.wait(), timeout=0.5) + + # Connection should still be alive - unknown types are ignored, not fatal + assert client._connection.is_connected, ( + "Connection should remain open for unknown message types" + ) + + # Verify we can still communicate by sending a valid request + device_info2 = await client.device_info() + assert device_info2 is not None + assert device_info2.name == "oversized-noise" + + # After test, verify process didn't crash + assert not process_exited, "ESPHome process should not crash" + + # Verify we can still reconnect + async with api_client_connected_with_disconnect(noise_psk=noise_key) as ( + client2, + _, + ): + device_info = await client2.device_info() + assert device_info is not None + assert device_info.name == "oversized-noise" + + +@pytest.mark.asyncio +async def test_noise_corrupt_encrypted_frame( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected_with_disconnect: APIClientConnectedWithDisconnectFactory, +) -> None: + """Test that noise protocol properly handles corrupt encrypted frames. + + Send a frame with valid size but corrupt encrypted content (garbage bytes). + This should fail decryption and cause disconnection. + """ + noise_key = "N4Yle5YirwZhPiHHsdZLdOA73ndj/84veVaLhTvxCuU=" + process_exited = False + cipherstate_failed = False + + def check_logs(line: str) -> None: + nonlocal process_exited, cipherstate_failed + # Check for signs that the process exited/crashed + if "Segmentation fault" in line or "core dumped" in line: + process_exited = True + # Check for the expected log about decryption failure + # This can appear as either a VV-level log from noise or a W-level log from connection + if ( + "[VV][api.noise" in line + and "noise_cipherstate_decrypt failed: MAC_FAILURE" in line + ) or ( + "[W][api.connection" in line + and "Reading failed CIPHERSTATE_DECRYPT_FAILED" in line + ): + cipherstate_failed = True + + async with run_compiled(yaml_config, line_callback=check_logs): + async with api_client_connected_with_disconnect(noise_psk=noise_key) as ( + client, + disconnect_event, + ): + # Verify basic connection works first + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "oversized-noise" + + # Get the socket to send raw corrupt data + socket = client._connection._socket + + # Send a corrupt noise frame directly to the socket + # Format: [indicator=0x01][size_high][size_low][garbage_encrypted_data] + # Size of 32 bytes (reasonable size for a noise frame with MAC) + corrupt_frame = bytes( + [ + 0x01, # Noise indicator + 0x00, # Size high byte + 0x20, # Size low byte (32 bytes) + ] + ) + bytes(32) # 32 bytes of zeros (invalid encrypted data) + + # Send the corrupt frame + socket.sendall(corrupt_frame) + + # Wait for ESPHome to disconnect due to decryption failure + await asyncio.wait_for(disconnect_event.wait(), timeout=5.0) + + # After disconnection, verify process didn't crash + assert not process_exited, ( + "ESPHome process should not crash on corrupt encrypted frames" + ) + # Verify we saw the expected log message about decryption failure + assert cipherstate_failed, ( + "Expected to see log about noise_cipherstate_decrypt failure or CIPHERSTATE_DECRYPT_FAILED" + ) + + # Verify we can still reconnect after handling the corrupt frame + async with api_client_connected_with_disconnect(noise_psk=noise_key) as ( + client2, + _, + ): + device_info = await client2.device_info() + assert device_info is not None + assert device_info.name == "oversized-noise" diff --git a/tests/integration/test_scheduler_pool.py b/tests/integration/test_scheduler_pool.py new file mode 100644 index 0000000000..b5f9f12631 --- /dev/null +++ b/tests/integration/test_scheduler_pool.py @@ -0,0 +1,209 @@ +"""Integration test for scheduler memory pool functionality.""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_scheduler_pool( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that the scheduler memory pool is working correctly with realistic usage. + + This test simulates real-world scheduler usage patterns and verifies that: + 1. Items are recycled to the pool when timeouts complete naturally + 2. Items are recycled when intervals/timeouts are cancelled + 3. Items are reused from the pool for new scheduler operations + 4. The pool grows gradually based on actual usage patterns + 5. Pool operations are logged correctly with debug scheduler enabled + """ + # Track log messages to verify pool behavior + log_lines: list[str] = [] + pool_reuse_count = 0 + pool_recycle_count = 0 + pool_full_count = 0 + new_alloc_count = 0 + + # Patterns to match pool operations + reuse_pattern = re.compile(r"Reused item from pool \(pool size now: (\d+)\)") + recycle_pattern = re.compile(r"Recycled item to pool \(pool size now: (\d+)\)") + pool_full_pattern = re.compile(r"Pool full \(size: (\d+)\), deleting item") + new_alloc_pattern = re.compile(r"Allocated new item \(pool empty\)") + + # Futures to track when test phases complete + loop = asyncio.get_running_loop() + test_complete_future: asyncio.Future[bool] = loop.create_future() + phase_futures = { + 1: loop.create_future(), + 2: loop.create_future(), + 3: loop.create_future(), + 4: loop.create_future(), + 5: loop.create_future(), + 6: loop.create_future(), + 7: loop.create_future(), + } + + def check_output(line: str) -> None: + """Check log output for pool operations and phase completion.""" + nonlocal pool_reuse_count, pool_recycle_count, pool_full_count, new_alloc_count + log_lines.append(line) + + # Track pool operations + if reuse_pattern.search(line): + pool_reuse_count += 1 + + elif recycle_pattern.search(line): + pool_recycle_count += 1 + + elif pool_full_pattern.search(line): + pool_full_count += 1 + + elif new_alloc_pattern.search(line): + new_alloc_count += 1 + + # Track phase completion + for phase_num in range(1, 8): + if ( + f"Phase {phase_num} complete" in line + and phase_num in phase_futures + and not phase_futures[phase_num].done() + ): + phase_futures[phase_num].set_result(True) + + # Check for test completion + if "Pool recycling test complete" in line and not test_complete_future.done(): + test_complete_future.set_result(True) + + # Run the test with log monitoring + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Verify device is running + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "scheduler-pool-test" + + # Get list of services + entities, services = await client.list_entities_services() + service_names = {s.name for s in services} + + # Verify all test services are available + expected_services = { + "run_phase_1", + "run_phase_2", + "run_phase_3", + "run_phase_4", + "run_phase_5", + "run_phase_6", + "run_phase_7", + "run_complete", + } + assert expected_services.issubset(service_names), ( + f"Missing services: {expected_services - service_names}" + ) + + # Get service objects + phase_services = { + num: next(s for s in services if s.name == f"run_phase_{num}") + for num in range(1, 8) + } + complete_service = next(s for s in services if s.name == "run_complete") + + try: + # Phase 1: Component lifecycle + client.execute_service(phase_services[1], {}) + await asyncio.wait_for(phase_futures[1], timeout=1.0) + await asyncio.sleep(0.05) # Let timeouts complete + + # Phase 2: Sensor polling + client.execute_service(phase_services[2], {}) + await asyncio.wait_for(phase_futures[2], timeout=1.0) + await asyncio.sleep(0.1) # Let intervals run a bit + + # Phase 3: Communication patterns + client.execute_service(phase_services[3], {}) + await asyncio.wait_for(phase_futures[3], timeout=1.0) + await asyncio.sleep(0.1) # Let heartbeat run + + # Phase 4: Defer patterns + client.execute_service(phase_services[4], {}) + await asyncio.wait_for(phase_futures[4], timeout=1.0) + await asyncio.sleep(0.2) # Let everything settle and recycle + + # Phase 5: Pool reuse verification + client.execute_service(phase_services[5], {}) + await asyncio.wait_for(phase_futures[5], timeout=1.0) + await asyncio.sleep(0.1) # Let Phase 5 timeouts complete and recycle + + # Phase 6: Full pool reuse verification + client.execute_service(phase_services[6], {}) + await asyncio.wait_for(phase_futures[6], timeout=1.0) + await asyncio.sleep(0.1) # Let Phase 6 timeouts complete + + # Phase 7: Same-named defer optimization + client.execute_service(phase_services[7], {}) + await asyncio.wait_for(phase_futures[7], timeout=1.0) + await asyncio.sleep(0.05) # Let the single defer execute + + # Complete test + client.execute_service(complete_service, {}) + await asyncio.wait_for(test_complete_future, timeout=0.5) + + except TimeoutError as e: + # Print debug info if test times out + recent_logs = "\n".join(log_lines[-30:]) + phases_completed = [num for num, fut in phase_futures.items() if fut.done()] + pytest.fail( + f"Test timed out waiting for phase/completion. Error: {e}\n" + f" Phases completed: {phases_completed}\n" + f" Pool stats:\n" + f" Reuse count: {pool_reuse_count}\n" + f" Recycle count: {pool_recycle_count}\n" + f" Pool full count: {pool_full_count}\n" + f" New alloc count: {new_alloc_count}\n" + f"Recent logs:\n{recent_logs}" + ) + + # Verify all test phases ran + for phase_num in range(1, 8): + assert phase_futures[phase_num].done(), f"Phase {phase_num} did not complete" + + # Verify pool behavior + assert pool_recycle_count > 0, "Should have recycled items to pool" + + # Check pool metrics + if pool_recycle_count > 0: + max_pool_size = 0 + for line in log_lines: + if match := recycle_pattern.search(line): + size = int(match.group(1)) + max_pool_size = max(max_pool_size, size) + + # Pool can grow up to its maximum of 5 + assert max_pool_size <= 5, f"Pool grew beyond maximum ({max_pool_size})" + + # Log summary for debugging + print("\nScheduler Pool Test Summary (Python Orchestrated):") + print(f" Items recycled to pool: {pool_recycle_count}") + print(f" Items reused from pool: {pool_reuse_count}") + print(f" Pool full events: {pool_full_count}") + print(f" New allocations: {new_alloc_count}") + print(" All phases completed successfully") + + # Verify reuse happened + if pool_reuse_count == 0 and pool_recycle_count > 3: + pytest.fail("Pool had items recycled but none were reused") + + # Success - pool is working + assert pool_recycle_count > 0 or new_alloc_count < 15, ( + "Pool should either recycle items or limit new allocations" + ) diff --git a/tests/integration/test_scheduler_removed_item_race.py b/tests/integration/test_scheduler_removed_item_race.py new file mode 100644 index 0000000000..3e72bacc0d --- /dev/null +++ b/tests/integration/test_scheduler_removed_item_race.py @@ -0,0 +1,102 @@ +"""Test for scheduler race condition where removed items still execute.""" + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_scheduler_removed_item_race( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that items marked for removal don't execute. + + This test verifies the fix for a race condition where: + 1. cleanup_() only removes items from the front of the heap + 2. Items in the middle of the heap marked for removal still execute + 3. This causes cancelled timeouts to run when they shouldn't + """ + + loop = asyncio.get_running_loop() + test_complete_future: asyncio.Future[bool] = loop.create_future() + + # Track test results + test_passed = False + removed_executed = 0 + normal_executed = 0 + + # Patterns to match + race_pattern = re.compile(r"RACE: .* executed after being cancelled!") + passed_pattern = re.compile(r"TEST PASSED") + failed_pattern = re.compile(r"TEST FAILED") + complete_pattern = re.compile(r"=== Test Complete ===") + normal_count_pattern = re.compile(r"Normal items executed: (\d+)") + removed_count_pattern = re.compile(r"Removed items executed: (\d+)") + + def check_output(line: str) -> None: + """Check log output for test results.""" + nonlocal test_passed, removed_executed, normal_executed + + if race_pattern.search(line): + # Race condition detected - a cancelled item executed + test_passed = False + + if passed_pattern.search(line): + test_passed = True + elif failed_pattern.search(line): + test_passed = False + + normal_match = normal_count_pattern.search(line) + if normal_match: + normal_executed = int(normal_match.group(1)) + + removed_match = removed_count_pattern.search(line) + if removed_match: + removed_executed = int(removed_match.group(1)) + + if not test_complete_future.done() and complete_pattern.search(line): + test_complete_future.set_result(True) + + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Verify we can connect + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "scheduler-removed-item-race" + + # List services + _, services = await asyncio.wait_for( + client.list_entities_services(), timeout=5.0 + ) + + # Find run_test service + run_test_service = next((s for s in services if s.name == "run_test"), None) + assert run_test_service is not None, "run_test service not found" + + # Execute the test + client.execute_service(run_test_service, {}) + + # Wait for test completion + try: + await asyncio.wait_for(test_complete_future, timeout=5.0) + except TimeoutError: + pytest.fail("Test did not complete within timeout") + + # Verify results + assert test_passed, ( + f"Test failed! Removed items executed: {removed_executed}, " + f"Normal items executed: {normal_executed}" + ) + assert removed_executed == 0, ( + f"Cancelled items should not execute, but {removed_executed} did" + ) + assert normal_executed == 4, ( + f"Expected 4 normal items to execute, got {normal_executed}" + ) diff --git a/tests/integration/test_script_delay_params.py b/tests/integration/test_script_delay_params.py new file mode 100644 index 0000000000..1b5d70863b --- /dev/null +++ b/tests/integration/test_script_delay_params.py @@ -0,0 +1,121 @@ +"""Integration test for script.wait FIFO ordering (issues #12043, #12044). + +This test verifies that ScriptWaitAction processes queued items in FIFO order. + +PR #7972 introduced bugs in ScriptWaitAction: +- Used emplace_front() causing LIFO ordering instead of FIFO +- Called loop() synchronously causing reentrancy issues +- Used while loop processing entire queue causing infinite loops + +These bugs manifested as: +- Scripts becoming "zombies" (stuck in running state) +- script.wait hanging forever +- Incorrect execution order +""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_script_delay_with_params( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that script.wait processes queued items in FIFO order. + + This reproduces issues #12043 and #12044 where scripts would hang or become + zombies due to LIFO ordering bugs in ScriptWaitAction from PR #7972. + """ + test_complete = asyncio.Event() + + # Patterns to match in logs + father_calling_pattern = re.compile(r"Father iteration (\d+): calling son") + son_started_pattern = re.compile(r"Son script started with iteration (\d+)") + son_delaying_pattern = re.compile(r"Son script delaying for iteration (\d+)") + son_finished_pattern = re.compile(r"Son script finished with iteration (\d+)") + father_wait_returned_pattern = re.compile( + r"Father iteration (\d+): son finished, wait returned" + ) + + # Track which iterations completed + father_calling = set() + son_started = set() + son_delaying = set() + son_finished = set() + wait_returned = set() + + def check_output(line: str) -> None: + """Check log output for expected messages.""" + if test_complete.is_set(): + return + + if mo := father_calling_pattern.search(line): + father_calling.add(int(mo.group(1))) + elif mo := son_started_pattern.search(line): + son_started.add(int(mo.group(1))) + elif mo := son_delaying_pattern.search(line): + son_delaying.add(int(mo.group(1))) + elif mo := son_finished_pattern.search(line): + son_finished.add(int(mo.group(1))) + elif mo := father_wait_returned_pattern.search(line): + iteration = int(mo.group(1)) + wait_returned.add(iteration) + # Test completes when iteration 9 finishes + if iteration == 9: + test_complete.set() + + # Run with log monitoring + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Verify device info + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "test-script-delay-params" + + # Get services + _, services = await client.list_entities_services() + test_service = next( + (s for s in services if s.name == "test_repeat_with_delay"), None + ) + assert test_service is not None, "test_repeat_with_delay service not found" + + # Execute the test + client.execute_service(test_service, {}) + + # Wait for test to complete (10 iterations * ~100ms each + margin) + try: + await asyncio.wait_for(test_complete.wait(), timeout=5.0) + except TimeoutError: + pytest.fail( + f"Test timed out. Completed iterations: {sorted(wait_returned)}. " + f"This likely indicates the script became a zombie (issue #12044)." + ) + + # Verify all 10 iterations completed successfully + expected_iterations = set(range(10)) + assert father_calling == expected_iterations, "Not all iterations started" + assert son_started == expected_iterations, ( + "Son script not started for all iterations" + ) + assert son_finished == expected_iterations, ( + "Son script not finished for all iterations" + ) + assert wait_returned == expected_iterations, ( + "script.wait did not return for all iterations" + ) + + # Verify delays were triggered for iterations >= 5 + expected_delays = set(range(5, 10)) + assert son_delaying == expected_delays, ( + "Delays not triggered for iterations >= 5" + ) diff --git a/tests/integration/test_script_queued.py b/tests/integration/test_script_queued.py new file mode 100644 index 0000000000..ce1c25b649 --- /dev/null +++ b/tests/integration/test_script_queued.py @@ -0,0 +1,203 @@ +"""Test ESPHome queued script functionality.""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_script_queued( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test comprehensive queued script functionality.""" + loop = asyncio.get_running_loop() + + # Track all test results + test_results = { + "queue_depth": {"processed": [], "rejections": 0}, + "ring_buffer": {"start_order": [], "end_order": []}, + "stop": {"processed": [], "stop_logged": False}, + "rejection": {"processed": [], "rejections": 0}, + "no_params": {"executions": 0}, + } + + # Patterns for Test 1: Queue depth + queue_start = re.compile(r"Queue test: START item (\d+)") + queue_end = re.compile(r"Queue test: END item (\d+)") + queue_reject = re.compile(r"Script 'queue_depth_script' max instances") + + # Patterns for Test 2: Ring buffer + ring_start = re.compile(r"Ring buffer: START '([A-Z])'") + ring_end = re.compile(r"Ring buffer: END '([A-Z])'") + + # Patterns for Test 3: Stop + stop_start = re.compile(r"Stop test: START (\d+)") + stop_log = re.compile(r"STOPPING script now") + + # Patterns for Test 4: Rejection + reject_start = re.compile(r"Rejection test: START (\d+)") + reject_end = re.compile(r"Rejection test: END (\d+)") + reject_reject = re.compile(r"Script 'rejection_script' max instances") + + # Patterns for Test 5: No params + no_params_end = re.compile(r"No params: END") + + # Test completion futures + test1_complete = loop.create_future() + test2_complete = loop.create_future() + test3_complete = loop.create_future() + test4_complete = loop.create_future() + test5_complete = loop.create_future() + + def check_output(line: str) -> None: + """Check log output for all test messages.""" + # Test 1: Queue depth + if match := queue_start.search(line): + item = int(match.group(1)) + if item not in test_results["queue_depth"]["processed"]: + test_results["queue_depth"]["processed"].append(item) + + if match := queue_end.search(line): + item = int(match.group(1)) + if item == 5 and not test1_complete.done(): + test1_complete.set_result(True) + + if queue_reject.search(line): + test_results["queue_depth"]["rejections"] += 1 + + # Test 2: Ring buffer + if match := ring_start.search(line): + msg = match.group(1) + test_results["ring_buffer"]["start_order"].append(msg) + + if match := ring_end.search(line): + msg = match.group(1) + test_results["ring_buffer"]["end_order"].append(msg) + if ( + len(test_results["ring_buffer"]["end_order"]) == 3 + and not test2_complete.done() + ): + test2_complete.set_result(True) + + # Test 3: Stop + if match := stop_start.search(line): + item = int(match.group(1)) + if item not in test_results["stop"]["processed"]: + test_results["stop"]["processed"].append(item) + + if stop_log.search(line): + test_results["stop"]["stop_logged"] = True + # Give time for any queued items to be cleared + if not test3_complete.done(): + loop.call_later( + 0.3, + lambda: test3_complete.set_result(True) + if not test3_complete.done() + else None, + ) + + # Test 4: Rejection + if match := reject_start.search(line): + item = int(match.group(1)) + if item not in test_results["rejection"]["processed"]: + test_results["rejection"]["processed"].append(item) + + if match := reject_end.search(line): + item = int(match.group(1)) + if item == 3 and not test4_complete.done(): + test4_complete.set_result(True) + + if reject_reject.search(line): + test_results["rejection"]["rejections"] += 1 + + # Test 5: No params + if no_params_end.search(line): + test_results["no_params"]["executions"] += 1 + if ( + test_results["no_params"]["executions"] == 3 + and not test5_complete.done() + ): + test5_complete.set_result(True) + + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Get services + _, services = await client.list_entities_services() + + # Test 1: Queue depth limit + test_service = next((s for s in services if s.name == "test_queue_depth"), None) + assert test_service is not None, "test_queue_depth service not found" + client.execute_service(test_service, {}) + await asyncio.wait_for(test1_complete, timeout=2.0) + await asyncio.sleep(0.1) # Give time for rejections + + # Verify Test 1 + assert sorted(test_results["queue_depth"]["processed"]) == [1, 2, 3, 4, 5], ( + f"Test 1: Expected to process items 1-5 (max_runs=5 means 5 total), got {sorted(test_results['queue_depth']['processed'])}" + ) + assert test_results["queue_depth"]["rejections"] >= 2, ( + "Test 1: Expected at least 2 rejection warnings (items 6-7 should be rejected)" + ) + + # Test 2: Ring buffer order + test_service = next((s for s in services if s.name == "test_ring_buffer"), None) + assert test_service is not None, "test_ring_buffer service not found" + client.execute_service(test_service, {}) + await asyncio.wait_for(test2_complete, timeout=2.0) + + # Verify Test 2 + assert test_results["ring_buffer"]["start_order"] == ["A", "B", "C"], ( + f"Test 2: Expected start order [A, B, C], got {test_results['ring_buffer']['start_order']}" + ) + assert test_results["ring_buffer"]["end_order"] == ["A", "B", "C"], ( + f"Test 2: Expected end order [A, B, C], got {test_results['ring_buffer']['end_order']}" + ) + + # Test 3: Stop clears queue + test_service = next((s for s in services if s.name == "test_stop_clears"), None) + assert test_service is not None, "test_stop_clears service not found" + client.execute_service(test_service, {}) + await asyncio.wait_for(test3_complete, timeout=2.0) + + # Verify Test 3 + assert test_results["stop"]["stop_logged"], ( + "Test 3: Stop command was not logged" + ) + assert test_results["stop"]["processed"] == [1], ( + f"Test 3: Expected only item 1 to process, got {test_results['stop']['processed']}" + ) + + # Test 4: Rejection enforcement (max_runs=3) + test_service = next((s for s in services if s.name == "test_rejection"), None) + assert test_service is not None, "test_rejection service not found" + client.execute_service(test_service, {}) + await asyncio.wait_for(test4_complete, timeout=2.0) + await asyncio.sleep(0.1) # Give time for rejections + + # Verify Test 4 + assert sorted(test_results["rejection"]["processed"]) == [1, 2, 3], ( + f"Test 4: Expected to process items 1-3 (max_runs=3 means 3 total), got {sorted(test_results['rejection']['processed'])}" + ) + assert test_results["rejection"]["rejections"] == 5, ( + f"Test 4: Expected 5 rejections (items 4-8), got {test_results['rejection']['rejections']}" + ) + + # Test 5: No parameters + test_service = next((s for s in services if s.name == "test_no_params"), None) + assert test_service is not None, "test_no_params service not found" + client.execute_service(test_service, {}) + await asyncio.wait_for(test5_complete, timeout=2.0) + + # Verify Test 5 + assert test_results["no_params"]["executions"] == 3, ( + f"Test 5: Expected 3 executions, got {test_results['no_params']['executions']}" + ) diff --git a/tests/integration/test_script_wait_on_boot.py b/tests/integration/test_script_wait_on_boot.py new file mode 100644 index 0000000000..478090f782 --- /dev/null +++ b/tests/integration/test_script_wait_on_boot.py @@ -0,0 +1,130 @@ +"""Integration test for script.wait during on_boot (issue #12043). + +This test verifies that script.wait works correctly when triggered from on_boot. +The issue was that ScriptWaitAction::setup() unconditionally disabled the loop, +even if play_complex() had already been called (from an on_boot trigger at the +same priority level) and enabled it. + +The race condition occurs because: +1. on_boot's default priority is 600.0 (setup_priority::DATA) +2. ScriptWaitAction's default setup priority is also DATA (600.0) +3. When they have the same priority, if on_boot runs first and triggers a script, + ScriptWaitAction::play_complex() enables the loop +4. Then ScriptWaitAction::setup() runs and unconditionally disables the loop +5. The wait never completes because the loop is disabled + +The fix adds a conditional check (like WaitUntilAction has) to only disable the +loop in setup() if num_running_ is 0. +""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_script_wait_on_boot( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that script.wait works correctly when triggered from on_boot. + + This reproduces issue #12043 where script.wait would hang forever when + triggered from on_boot due to a race condition in ScriptWaitAction::setup(). + """ + test_complete = asyncio.Event() + + # Track progress through the boot sequence + boot_started = False + first_script_started = False + first_script_completed = False + first_wait_returned = False + second_script_started = False + second_script_completed = False + all_completed = False + + # Patterns for boot sequence logs + boot_start_pattern = re.compile(r"on_boot: Starting boot sequence") + show_start_pattern = re.compile(r"show_start_page: Starting") + show_complete_pattern = re.compile(r"show_start_page: Completed") + first_wait_pattern = re.compile(r"on_boot: First script completed") + flip_start_pattern = re.compile(r"flip_thru_pages: Starting") + flip_complete_pattern = re.compile(r"flip_thru_pages: Completed") + all_complete_pattern = re.compile(r"on_boot: All boot scripts completed") + + def check_output(line: str) -> None: + """Check log output for boot sequence progress.""" + nonlocal boot_started, first_script_started, first_script_completed + nonlocal first_wait_returned, second_script_started, second_script_completed + nonlocal all_completed + + if boot_start_pattern.search(line): + boot_started = True + elif show_start_pattern.search(line): + first_script_started = True + elif show_complete_pattern.search(line): + first_script_completed = True + elif first_wait_pattern.search(line): + first_wait_returned = True + elif flip_start_pattern.search(line): + second_script_started = True + elif flip_complete_pattern.search(line): + second_script_completed = True + elif all_complete_pattern.search(line): + all_completed = True + test_complete.set() + + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Verify device info + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "test-script-wait-on-boot" + + # Wait for on_boot sequence to complete + # The boot sequence should complete automatically + # Timeout is generous to allow for delays in the scripts + try: + await asyncio.wait_for(test_complete.wait(), timeout=5.0) + except TimeoutError: + # Build a detailed error message showing where the boot sequence got stuck + progress = [] + if boot_started: + progress.append("boot started") + if first_script_started: + progress.append("show_start_page started") + if first_script_completed: + progress.append("show_start_page completed") + if first_wait_returned: + progress.append("first script.wait returned") + if second_script_started: + progress.append("flip_thru_pages started") + if second_script_completed: + progress.append("flip_thru_pages completed") + + if not first_wait_returned and first_script_completed: + pytest.fail( + f"Test timed out - script.wait hung after show_start_page completed! " + f"This is the issue #12043 bug. Progress: {', '.join(progress)}" + ) + else: + pytest.fail( + f"Test timed out. Progress: {', '.join(progress) if progress else 'none'}" + ) + + # Verify the complete boot sequence executed in order + assert boot_started, "on_boot did not start" + assert first_script_started, "show_start_page did not start" + assert first_script_completed, "show_start_page did not complete" + assert first_wait_returned, "First script.wait did not return" + assert second_script_started, "flip_thru_pages did not start" + assert second_script_completed, "flip_thru_pages did not complete" + assert all_completed, "Boot sequence did not complete" diff --git a/tests/integration/test_sensor_filters_ring_buffer.py b/tests/integration/test_sensor_filters_ring_buffer.py new file mode 100644 index 0000000000..c8be8edce0 --- /dev/null +++ b/tests/integration/test_sensor_filters_ring_buffer.py @@ -0,0 +1,151 @@ +"""Test sensor ring buffer filter functionality (window_size != send_every).""" + +from __future__ import annotations + +import asyncio + +from aioesphomeapi import EntityState, SensorState +import pytest + +from .state_utils import InitialStateHelper, build_key_to_entity_mapping +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_sensor_filters_ring_buffer( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that ring buffer filters (window_size != send_every) work correctly.""" + loop = asyncio.get_running_loop() + + # Track state changes for each sensor + sensor_states: dict[str, list[float]] = { + "sliding_min": [], + "sliding_max": [], + "sliding_median": [], + "sliding_moving_avg": [], + } + + # Futures to track when we receive expected values + all_updates_received = loop.create_future() + + def on_state(state: EntityState) -> None: + """Track sensor state updates.""" + if not isinstance(state, SensorState): + return + + # Skip NaN values + if state.missing_state: + return + + # Get the sensor name from the key mapping + sensor_name = key_to_sensor.get(state.key) + if not sensor_name or sensor_name not in sensor_states: + return + + sensor_states[sensor_name].append(state.state) + + # Check if we've received enough updates from all sensors + # With send_every=2, send_first_at=1, we expect 5 outputs per sensor + if ( + len(sensor_states["sliding_min"]) >= 5 + and len(sensor_states["sliding_max"]) >= 5 + and len(sensor_states["sliding_median"]) >= 5 + and len(sensor_states["sliding_moving_avg"]) >= 5 + and not all_updates_received.done() + ): + all_updates_received.set_result(True) + + async with ( + run_compiled(yaml_config), + api_client_connected() as client, + ): + # Get entities first to build key mapping + entities, services = await client.list_entities_services() + + # Build key-to-sensor mapping + key_to_sensor = build_key_to_entity_mapping( + entities, + [ + "sliding_min", + "sliding_max", + "sliding_median", + "sliding_moving_avg", + ], + ) + + # Set up initial state helper with all entities + initial_state_helper = InitialStateHelper(entities) + + # Subscribe to state changes with wrapper + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + + # Wait for initial states to be sent before pressing button + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + # Find the publish button + publish_button = next( + (e for e in entities if "publish_values_button" in e.object_id.lower()), + None, + ) + assert publish_button is not None, "Publish Values Button not found" + + # Press the button to publish test values + client.button_command(publish_button.key) + + # Wait for all sensors to receive their values + try: + await asyncio.wait_for(all_updates_received, timeout=10.0) + except TimeoutError: + # Provide detailed failure info + pytest.fail( + f"Timeout waiting for updates. Received states:\n" + f" min: {sensor_states['sliding_min']}\n" + f" max: {sensor_states['sliding_max']}\n" + f" median: {sensor_states['sliding_median']}\n" + f" moving_avg: {sensor_states['sliding_moving_avg']}" + ) + + # Verify we got 5 outputs per sensor (positions 1, 3, 5, 7, 9) + assert len(sensor_states["sliding_min"]) == 5, ( + f"Min sensor should have 5 values, got {len(sensor_states['sliding_min'])}: {sensor_states['sliding_min']}" + ) + assert len(sensor_states["sliding_max"]) == 5 + assert len(sensor_states["sliding_median"]) == 5 + assert len(sensor_states["sliding_moving_avg"]) == 5 + + # Verify the values at each output position + # Position 1: window=[1] + assert sensor_states["sliding_min"][0] == pytest.approx(1.0) + assert sensor_states["sliding_max"][0] == pytest.approx(1.0) + assert sensor_states["sliding_median"][0] == pytest.approx(1.0) + assert sensor_states["sliding_moving_avg"][0] == pytest.approx(1.0) + + # Position 3: window=[1,2,3] + assert sensor_states["sliding_min"][1] == pytest.approx(1.0) + assert sensor_states["sliding_max"][1] == pytest.approx(3.0) + assert sensor_states["sliding_median"][1] == pytest.approx(2.0) + assert sensor_states["sliding_moving_avg"][1] == pytest.approx(2.0) + + # Position 5: window=[1,2,3,4,5] + assert sensor_states["sliding_min"][2] == pytest.approx(1.0) + assert sensor_states["sliding_max"][2] == pytest.approx(5.0) + assert sensor_states["sliding_median"][2] == pytest.approx(3.0) + assert sensor_states["sliding_moving_avg"][2] == pytest.approx(3.0) + + # Position 7: window=[3,4,5,6,7] (ring buffer wrapped) + assert sensor_states["sliding_min"][3] == pytest.approx(3.0) + assert sensor_states["sliding_max"][3] == pytest.approx(7.0) + assert sensor_states["sliding_median"][3] == pytest.approx(5.0) + assert sensor_states["sliding_moving_avg"][3] == pytest.approx(5.0) + + # Position 9: window=[5,6,7,8,9] (ring buffer wrapped) + assert sensor_states["sliding_min"][4] == pytest.approx(5.0) + assert sensor_states["sliding_max"][4] == pytest.approx(9.0) + assert sensor_states["sliding_median"][4] == pytest.approx(7.0) + assert sensor_states["sliding_moving_avg"][4] == pytest.approx(7.0) diff --git a/tests/integration/test_sensor_filters_sliding_window.py b/tests/integration/test_sensor_filters_sliding_window.py new file mode 100644 index 0000000000..b0688a6536 --- /dev/null +++ b/tests/integration/test_sensor_filters_sliding_window.py @@ -0,0 +1,395 @@ +"""Test sensor sliding window filter functionality.""" + +from __future__ import annotations + +import asyncio + +from aioesphomeapi import EntityState, SensorState +import pytest + +from .state_utils import InitialStateHelper, build_key_to_entity_mapping +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_sensor_filters_sliding_window( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that sliding window filters (min, max, median, quantile, moving_average) work correctly.""" + loop = asyncio.get_running_loop() + + # Track state changes for each sensor + sensor_states: dict[str, list[float]] = { + "min_sensor": [], + "max_sensor": [], + "median_sensor": [], + "quantile_sensor": [], + "moving_avg_sensor": [], + } + + # Futures to track when we receive expected values + min_received = loop.create_future() + max_received = loop.create_future() + median_received = loop.create_future() + quantile_received = loop.create_future() + moving_avg_received = loop.create_future() + + def on_state(state: EntityState) -> None: + """Track sensor state updates.""" + if not isinstance(state, SensorState): + return + + # Skip NaN values + if state.missing_state: + return + + # Get the sensor name from the key mapping + sensor_name = key_to_sensor.get(state.key) + if not sensor_name or sensor_name not in sensor_states: + return + + sensor_states[sensor_name].append(state.state) + + # Check if we received the expected final value + # After publishing 10 values [1.0, 2.0, ..., 10.0], the window has the last 5: [2, 3, 4, 5, 6] + # Filters send at position 1 and position 6 (send_every=5 means every 5th value after first) + if ( + sensor_name == "min_sensor" + and state.state == pytest.approx(2.0) + and not min_received.done() + ): + min_received.set_result(True) + elif ( + sensor_name == "max_sensor" + and state.state == pytest.approx(6.0) + and not max_received.done() + ): + max_received.set_result(True) + elif ( + sensor_name == "median_sensor" + and state.state == pytest.approx(4.0) + and not median_received.done() + ): + # Median of [2, 3, 4, 5, 6] = 4 + median_received.set_result(True) + elif ( + sensor_name == "quantile_sensor" + and state.state == pytest.approx(6.0) + and not quantile_received.done() + ): + # 90th percentile of [2, 3, 4, 5, 6] = 6 + quantile_received.set_result(True) + elif ( + sensor_name == "moving_avg_sensor" + and state.state == pytest.approx(4.0) + and not moving_avg_received.done() + ): + # Average of [2, 3, 4, 5, 6] = 4 + moving_avg_received.set_result(True) + + async with ( + run_compiled(yaml_config), + api_client_connected() as client, + ): + # Get entities first to build key mapping + entities, services = await client.list_entities_services() + + # Build key-to-sensor mapping + key_to_sensor = build_key_to_entity_mapping( + entities, + [ + "min_sensor", + "max_sensor", + "median_sensor", + "quantile_sensor", + "moving_avg_sensor", + ], + ) + + # Set up initial state helper with all entities + initial_state_helper = InitialStateHelper(entities) + + # Subscribe to state changes with wrapper + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + + # Wait for initial states to be sent before pressing button + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + # Find the publish button + publish_button = next( + (e for e in entities if "publish_values_button" in e.object_id.lower()), + None, + ) + assert publish_button is not None, "Publish Values Button not found" + + # Press the button to publish test values + client.button_command(publish_button.key) + + # Wait for all sensors to receive their final values + try: + await asyncio.wait_for( + asyncio.gather( + min_received, + max_received, + median_received, + quantile_received, + moving_avg_received, + ), + timeout=10.0, + ) + except TimeoutError: + # Provide detailed failure info + pytest.fail( + f"Timeout waiting for expected values. Received states:\n" + f" min: {sensor_states['min_sensor']}\n" + f" max: {sensor_states['max_sensor']}\n" + f" median: {sensor_states['median_sensor']}\n" + f" quantile: {sensor_states['quantile_sensor']}\n" + f" moving_avg: {sensor_states['moving_avg_sensor']}" + ) + + # Verify we got the expected values + # With batch_delay: 0ms, we should receive all outputs + # Filters output at positions 1 and 6 (send_every: 5) + assert len(sensor_states["min_sensor"]) == 2, ( + f"Min sensor should have 2 values, got {len(sensor_states['min_sensor'])}: {sensor_states['min_sensor']}" + ) + assert len(sensor_states["max_sensor"]) == 2, ( + f"Max sensor should have 2 values, got {len(sensor_states['max_sensor'])}: {sensor_states['max_sensor']}" + ) + assert len(sensor_states["median_sensor"]) == 2 + assert len(sensor_states["quantile_sensor"]) == 2 + assert len(sensor_states["moving_avg_sensor"]) == 2 + + # Verify the first output (after 1 value: [1]) + assert sensor_states["min_sensor"][0] == pytest.approx(1.0), ( + f"First min should be 1.0, got {sensor_states['min_sensor'][0]}" + ) + assert sensor_states["max_sensor"][0] == pytest.approx(1.0), ( + f"First max should be 1.0, got {sensor_states['max_sensor'][0]}" + ) + assert sensor_states["median_sensor"][0] == pytest.approx(1.0), ( + f"First median should be 1.0, got {sensor_states['median_sensor'][0]}" + ) + assert sensor_states["moving_avg_sensor"][0] == pytest.approx(1.0), ( + f"First moving avg should be 1.0, got {sensor_states['moving_avg_sensor'][0]}" + ) + + # Verify the second output (after 6 values, window has [2, 3, 4, 5, 6]) + assert sensor_states["min_sensor"][1] == pytest.approx(2.0), ( + f"Second min should be 2.0, got {sensor_states['min_sensor'][1]}" + ) + assert sensor_states["max_sensor"][1] == pytest.approx(6.0), ( + f"Second max should be 6.0, got {sensor_states['max_sensor'][1]}" + ) + assert sensor_states["median_sensor"][1] == pytest.approx(4.0), ( + f"Second median should be 4.0, got {sensor_states['median_sensor'][1]}" + ) + assert sensor_states["moving_avg_sensor"][1] == pytest.approx(4.0), ( + f"Second moving avg should be 4.0, got {sensor_states['moving_avg_sensor'][1]}" + ) + + +@pytest.mark.asyncio +async def test_sensor_filters_nan_handling( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that sliding window filters handle NaN values correctly.""" + loop = asyncio.get_running_loop() + + # Track states + min_states: list[float] = [] + max_states: list[float] = [] + + # Future to track completion + filters_completed = loop.create_future() + + def on_state(state: EntityState) -> None: + """Track sensor state updates.""" + if not isinstance(state, SensorState): + return + + # Skip NaN values + if state.missing_state: + return + + sensor_name = key_to_sensor.get(state.key) + + if sensor_name == "min_nan": + min_states.append(state.state) + elif sensor_name == "max_nan": + max_states.append(state.state) + + # Check if both have received their final values + # With batch_delay: 0ms, we should receive 2 outputs each + if ( + len(min_states) >= 2 + and len(max_states) >= 2 + and not filters_completed.done() + ): + filters_completed.set_result(True) + + async with ( + run_compiled(yaml_config), + api_client_connected() as client, + ): + # Get entities first to build key mapping + entities, services = await client.list_entities_services() + + # Build key-to-sensor mapping + key_to_sensor = build_key_to_entity_mapping(entities, ["min_nan", "max_nan"]) + + # Set up initial state helper with all entities + initial_state_helper = InitialStateHelper(entities) + + # Subscribe to state changes with wrapper + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + + # Wait for initial states + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + # Find the publish button + publish_button = next( + (e for e in entities if "publish_nan_values_button" in e.object_id.lower()), + None, + ) + assert publish_button is not None, "Publish NaN Values Button not found" + + # Press the button + client.button_command(publish_button.key) + + # Wait for filters to process + try: + await asyncio.wait_for(filters_completed, timeout=10.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for NaN handling. Received:\n" + f" min_states: {min_states}\n" + f" max_states: {max_states}" + ) + + # Verify NaN values were ignored + # With batch_delay: 0ms, we should receive both outputs (at positions 1 and 6) + # Position 1: window=[10], min=10, max=10 + # Position 6: window=[NaN, 5, NaN, 15, 8], ignoring NaN -> [5, 15, 8], min=5, max=15 + assert len(min_states) == 2, ( + f"Should have 2 min states, got {len(min_states)}: {min_states}" + ) + assert len(max_states) == 2, ( + f"Should have 2 max states, got {len(max_states)}: {max_states}" + ) + + # First output + assert min_states[0] == pytest.approx(10.0), ( + f"First min should be 10.0, got {min_states[0]}" + ) + assert max_states[0] == pytest.approx(10.0), ( + f"First max should be 10.0, got {max_states[0]}" + ) + + # Second output - verify NaN values were ignored + assert min_states[1] == pytest.approx(5.0), ( + f"Second min should ignore NaN and return 5.0, got {min_states[1]}" + ) + assert max_states[1] == pytest.approx(15.0), ( + f"Second max should ignore NaN and return 15.0, got {max_states[1]}" + ) + + +@pytest.mark.asyncio +async def test_sensor_filters_ring_buffer_wraparound( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that ring buffer correctly wraps around when window fills up.""" + loop = asyncio.get_running_loop() + + min_states: list[float] = [] + + test_completed = loop.create_future() + + def on_state(state: EntityState) -> None: + """Track min sensor states.""" + if not isinstance(state, SensorState): + return + + # Skip NaN values + if state.missing_state: + return + + sensor_name = key_to_sensor.get(state.key) + + if sensor_name == "wraparound_min": + min_states.append(state.state) + # With batch_delay: 0ms, we should receive all 3 outputs + if len(min_states) >= 3 and not test_completed.done(): + test_completed.set_result(True) + + async with ( + run_compiled(yaml_config), + api_client_connected() as client, + ): + # Get entities first to build key mapping + entities, services = await client.list_entities_services() + + # Build key-to-sensor mapping + key_to_sensor = build_key_to_entity_mapping(entities, ["wraparound_min"]) + + # Set up initial state helper with all entities + initial_state_helper = InitialStateHelper(entities) + + # Subscribe to state changes with wrapper + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + + # Wait for initial state + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial state") + + # Find the publish button + publish_button = next( + (e for e in entities if "publish_wraparound_button" in e.object_id.lower()), + None, + ) + assert publish_button is not None, "Publish Wraparound Button not found" + + # Press the button + # Will publish: 10, 20, 30, 5, 25, 15, 40, 35, 20 + client.button_command(publish_button.key) + + # Wait for completion + try: + await asyncio.wait_for(test_completed, timeout=10.0) + except TimeoutError: + pytest.fail(f"Timeout waiting for wraparound test. Received: {min_states}") + + # Verify outputs + # With window_size=3, send_every=3, we get outputs at positions 1, 4, 7 + # Position 1: window=[10], min=10 + # Position 4: window=[20, 30, 5], min=5 + # Position 7: window=[15, 40, 35], min=15 + # With batch_delay: 0ms, we should receive all 3 outputs + assert len(min_states) == 3, ( + f"Should have 3 states, got {len(min_states)}: {min_states}" + ) + assert min_states[0] == pytest.approx(10.0), ( + f"First min should be 10.0, got {min_states[0]}" + ) + assert min_states[1] == pytest.approx(5.0), ( + f"Second min should be 5.0, got {min_states[1]}" + ) + assert min_states[2] == pytest.approx(15.0), ( + f"Third min should be 15.0, got {min_states[2]}" + ) diff --git a/tests/integration/test_sensor_filters_value_list.py b/tests/integration/test_sensor_filters_value_list.py new file mode 100644 index 0000000000..87323fc730 --- /dev/null +++ b/tests/integration/test_sensor_filters_value_list.py @@ -0,0 +1,263 @@ +"""Test sensor ValueListFilter functionality (FilterOutValueFilter and ThrottleWithPriorityFilter).""" + +from __future__ import annotations + +import asyncio +import math + +from aioesphomeapi import ButtonInfo, EntityState, SensorState +import pytest + +from .state_utils import InitialStateHelper, build_key_to_entity_mapping +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_sensor_filters_value_list( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that ValueListFilter-based filters work correctly.""" + loop = asyncio.get_running_loop() + + # Track state changes for all sensors + sensor_values: dict[str, list[float]] = { + "filter_out_single": [], + "filter_out_multiple": [], + "throttle_priority_single": [], + "throttle_priority_multiple": [], + "filter_out_nan_test": [], + "filter_out_accuracy_2": [], + "throttle_priority_nan": [], + } + + # Futures for each test + filter_out_single_done = loop.create_future() + filter_out_multiple_done = loop.create_future() + throttle_single_done = loop.create_future() + throttle_multiple_done = loop.create_future() + filter_out_nan_done = loop.create_future() + filter_out_accuracy_2_done = loop.create_future() + throttle_nan_done = loop.create_future() + + def on_state(state: EntityState) -> None: + """Track sensor state updates.""" + if not isinstance(state, SensorState) or state.missing_state: + return + + sensor_name = key_to_sensor.get(state.key) + if sensor_name not in sensor_values: + return + + sensor_values[sensor_name].append(state.state) + + # Check completion conditions + if ( + sensor_name == "filter_out_single" + and len(sensor_values[sensor_name]) == 3 + and not filter_out_single_done.done() + ): + filter_out_single_done.set_result(True) + elif ( + sensor_name == "filter_out_multiple" + and len(sensor_values[sensor_name]) == 3 + and not filter_out_multiple_done.done() + ): + filter_out_multiple_done.set_result(True) + elif ( + sensor_name == "throttle_priority_single" + and len(sensor_values[sensor_name]) == 3 + and not throttle_single_done.done() + ): + throttle_single_done.set_result(True) + elif ( + sensor_name == "throttle_priority_multiple" + and len(sensor_values[sensor_name]) == 4 + and not throttle_multiple_done.done() + ): + throttle_multiple_done.set_result(True) + elif ( + sensor_name == "filter_out_nan_test" + and len(sensor_values[sensor_name]) == 3 + and not filter_out_nan_done.done() + ): + filter_out_nan_done.set_result(True) + elif ( + sensor_name == "filter_out_accuracy_2" + and len(sensor_values[sensor_name]) == 2 + and not filter_out_accuracy_2_done.done() + ): + filter_out_accuracy_2_done.set_result(True) + elif ( + sensor_name == "throttle_priority_nan" + and len(sensor_values[sensor_name]) == 3 + and not throttle_nan_done.done() + ): + throttle_nan_done.set_result(True) + + async with ( + run_compiled(yaml_config), + api_client_connected() as client, + ): + # Get entities and build key mapping + entities, _ = await client.list_entities_services() + key_to_sensor = build_key_to_entity_mapping( + entities, + { + "filter_out_single": "Filter Out Single", + "filter_out_multiple": "Filter Out Multiple", + "throttle_priority_single": "Throttle Priority Single", + "throttle_priority_multiple": "Throttle Priority Multiple", + "filter_out_nan_test": "Filter Out NaN Test", + "filter_out_accuracy_2": "Filter Out Accuracy 2", + "throttle_priority_nan": "Throttle Priority NaN", + }, + ) + + # Set up initial state helper with all entities + initial_state_helper = InitialStateHelper(entities) + + # Subscribe to state changes with wrapper + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + + # Wait for initial states + await initial_state_helper.wait_for_initial_states() + + # Find all buttons + button_name_map = { + "Test Filter Out Single": "filter_out_single", + "Test Filter Out Multiple": "filter_out_multiple", + "Test Throttle Priority Single": "throttle_priority_single", + "Test Throttle Priority Multiple": "throttle_priority_multiple", + "Test Filter Out NaN": "filter_out_nan", + "Test Filter Out Accuracy 2": "filter_out_accuracy_2", + "Test Throttle Priority NaN": "throttle_priority_nan", + } + buttons = {} + for entity in entities: + if isinstance(entity, ButtonInfo) and entity.name in button_name_map: + buttons[button_name_map[entity.name]] = entity.key + + assert len(buttons) == 7, f"Expected 7 buttons, found {len(buttons)}" + + # Test 1: FilterOutValueFilter - single value + sensor_values["filter_out_single"].clear() + client.button_command(buttons["filter_out_single"]) + try: + await asyncio.wait_for(filter_out_single_done, timeout=2.0) + except TimeoutError: + pytest.fail( + f"Test 1 timed out. Values: {sensor_values['filter_out_single']}" + ) + + expected = [1.0, 2.0, 3.0] + assert sensor_values["filter_out_single"] == pytest.approx(expected), ( + f"Test 1 failed: expected {expected}, got {sensor_values['filter_out_single']}" + ) + + # Test 2: FilterOutValueFilter - multiple values + sensor_values["filter_out_multiple"].clear() + filter_out_multiple_done = loop.create_future() + client.button_command(buttons["filter_out_multiple"]) + try: + await asyncio.wait_for(filter_out_multiple_done, timeout=2.0) + except TimeoutError: + pytest.fail( + f"Test 2 timed out. Values: {sensor_values['filter_out_multiple']}" + ) + + expected = [1.0, 2.0, 50.0] + assert sensor_values["filter_out_multiple"] == pytest.approx(expected), ( + f"Test 2 failed: expected {expected}, got {sensor_values['filter_out_multiple']}" + ) + + # Test 3: ThrottleWithPriorityFilter - single priority + sensor_values["throttle_priority_single"].clear() + throttle_single_done = loop.create_future() + client.button_command(buttons["throttle_priority_single"]) + try: + await asyncio.wait_for(throttle_single_done, timeout=2.0) + except TimeoutError: + pytest.fail( + f"Test 3 timed out. Values: {sensor_values['throttle_priority_single']}" + ) + + expected = [1.0, 42.0, 4.0] + assert sensor_values["throttle_priority_single"] == pytest.approx(expected), ( + f"Test 3 failed: expected {expected}, got {sensor_values['throttle_priority_single']}" + ) + + # Test 4: ThrottleWithPriorityFilter - multiple priorities + sensor_values["throttle_priority_multiple"].clear() + throttle_multiple_done = loop.create_future() + client.button_command(buttons["throttle_priority_multiple"]) + try: + await asyncio.wait_for(throttle_multiple_done, timeout=2.0) + except TimeoutError: + pytest.fail( + f"Test 4 timed out. Values: {sensor_values['throttle_priority_multiple']}" + ) + + expected = [1.0, 0.0, 42.0, 100.0] + assert sensor_values["throttle_priority_multiple"] == pytest.approx(expected), ( + f"Test 4 failed: expected {expected}, got {sensor_values['throttle_priority_multiple']}" + ) + + # Test 5: FilterOutValueFilter - NaN handling + sensor_values["filter_out_nan_test"].clear() + filter_out_nan_done = loop.create_future() + client.button_command(buttons["filter_out_nan"]) + try: + await asyncio.wait_for(filter_out_nan_done, timeout=2.0) + except TimeoutError: + pytest.fail( + f"Test 5 timed out. Values: {sensor_values['filter_out_nan_test']}" + ) + + expected = [1.0, 2.0, 3.0] + assert sensor_values["filter_out_nan_test"] == pytest.approx(expected), ( + f"Test 5 failed: expected {expected}, got {sensor_values['filter_out_nan_test']}" + ) + + # Test 6: FilterOutValueFilter - Accuracy decimals (2) + sensor_values["filter_out_accuracy_2"].clear() + filter_out_accuracy_2_done = loop.create_future() + client.button_command(buttons["filter_out_accuracy_2"]) + try: + await asyncio.wait_for(filter_out_accuracy_2_done, timeout=2.0) + except TimeoutError: + pytest.fail( + f"Test 6 timed out. Values: {sensor_values['filter_out_accuracy_2']}" + ) + + expected = [42.01, 42.15] + assert sensor_values["filter_out_accuracy_2"] == pytest.approx(expected), ( + f"Test 6 failed: expected {expected}, got {sensor_values['filter_out_accuracy_2']}" + ) + + # Test 7: ThrottleWithPriorityFilter - NaN priority + sensor_values["throttle_priority_nan"].clear() + throttle_nan_done = loop.create_future() + client.button_command(buttons["throttle_priority_nan"]) + try: + await asyncio.wait_for(throttle_nan_done, timeout=2.0) + except TimeoutError: + pytest.fail( + f"Test 7 timed out. Values: {sensor_values['throttle_priority_nan']}" + ) + + # First value (1.0) + two NaN priority values + # NaN values will be compared using math.isnan + assert len(sensor_values["throttle_priority_nan"]) == 3, ( + f"Test 7 failed: expected 3 values, got {len(sensor_values['throttle_priority_nan'])}" + ) + assert sensor_values["throttle_priority_nan"][0] == pytest.approx(1.0), ( + f"Test 7 failed: first value should be 1.0, got {sensor_values['throttle_priority_nan'][0]}" + ) + assert math.isnan(sensor_values["throttle_priority_nan"][1]), ( + f"Test 7 failed: second value should be NaN, got {sensor_values['throttle_priority_nan'][1]}" + ) + assert math.isnan(sensor_values["throttle_priority_nan"][2]), ( + f"Test 7 failed: third value should be NaN, got {sensor_values['throttle_priority_nan'][2]}" + ) diff --git a/tests/integration/test_sensor_timeout_filter.py b/tests/integration/test_sensor_timeout_filter.py new file mode 100644 index 0000000000..9b4704bb7b --- /dev/null +++ b/tests/integration/test_sensor_timeout_filter.py @@ -0,0 +1,185 @@ +"""Test sensor timeout filter functionality.""" + +from __future__ import annotations + +import asyncio + +from aioesphomeapi import EntityState, SensorState +import pytest + +from .state_utils import InitialStateHelper, build_key_to_entity_mapping +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_sensor_timeout_filter( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test TimeoutFilter and TimeoutFilterConfigured with all modes.""" + loop = asyncio.get_running_loop() + + # Track state changes for all sensors + timeout_last_states: list[float] = [] + timeout_reset_states: list[float] = [] + timeout_static_states: list[float] = [] + timeout_lambda_states: list[float] = [] + + # Futures for each test scenario + test1_complete = loop.create_future() # TimeoutFilter - last mode + test2_complete = loop.create_future() # TimeoutFilter - reset behavior + test3_complete = loop.create_future() # TimeoutFilterConfigured - static value + test4_complete = loop.create_future() # TimeoutFilterConfigured - lambda + + def on_state(state: EntityState) -> None: + """Track sensor state updates.""" + if not isinstance(state, SensorState): + return + + if state.missing_state: + return + + sensor_name = key_to_sensor.get(state.key) + + # Test 1: TimeoutFilter - last mode + if sensor_name == "timeout_last_sensor": + timeout_last_states.append(state.state) + # Expect 2 values: initial 42.0 + timeout fires with 42.0 + if len(timeout_last_states) >= 2 and not test1_complete.done(): + test1_complete.set_result(True) + + # Test 2: TimeoutFilter - reset behavior + elif sensor_name == "timeout_reset_sensor": + timeout_reset_states.append(state.state) + # Expect 4 values: 10.0, 20.0, 30.0, then timeout fires with 30.0 + if len(timeout_reset_states) >= 4 and not test2_complete.done(): + test2_complete.set_result(True) + + # Test 3: TimeoutFilterConfigured - static value + elif sensor_name == "timeout_static_sensor": + timeout_static_states.append(state.state) + # Expect 2 values: initial 55.5 + timeout fires with 99.9 + if len(timeout_static_states) >= 2 and not test3_complete.done(): + test3_complete.set_result(True) + + # Test 4: TimeoutFilterConfigured - lambda + elif sensor_name == "timeout_lambda_sensor": + timeout_lambda_states.append(state.state) + # Expect 2 values: initial 77.7 + timeout fires with -1.0 + if len(timeout_lambda_states) >= 2 and not test4_complete.done(): + test4_complete.set_result(True) + + async with ( + run_compiled(yaml_config), + api_client_connected() as client, + ): + entities, services = await client.list_entities_services() + + key_to_sensor = build_key_to_entity_mapping( + entities, + [ + "timeout_last_sensor", + "timeout_reset_sensor", + "timeout_static_sensor", + "timeout_lambda_sensor", + ], + ) + + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + # Helper to find buttons by object_id substring + def find_button(object_id_substring: str) -> int: + """Find a button by object_id substring and return its key.""" + button = next( + (e for e in entities if object_id_substring in e.object_id.lower()), + None, + ) + assert button is not None, f"Button '{object_id_substring}' not found" + return button.key + + # Find all test buttons + test1_button_key = find_button("test_timeout_last_button") + test2_button_key = find_button("test_timeout_reset_button") + test3_button_key = find_button("test_timeout_static_button") + test4_button_key = find_button("test_timeout_lambda_button") + + # === Test 1: TimeoutFilter - last mode === + client.button_command(test1_button_key) + try: + await asyncio.wait_for(test1_complete, timeout=2.0) + except TimeoutError: + pytest.fail(f"Test 1 timeout. Received states: {timeout_last_states}") + + assert len(timeout_last_states) == 2, ( + f"Test 1: Should have 2 states, got {len(timeout_last_states)}: {timeout_last_states}" + ) + assert timeout_last_states[0] == pytest.approx(42.0), ( + f"Test 1: First state should be 42.0, got {timeout_last_states[0]}" + ) + assert timeout_last_states[1] == pytest.approx(42.0), ( + f"Test 1: Timeout should output last value (42.0), got {timeout_last_states[1]}" + ) + + # === Test 2: TimeoutFilter - reset behavior === + client.button_command(test2_button_key) + try: + await asyncio.wait_for(test2_complete, timeout=2.0) + except TimeoutError: + pytest.fail(f"Test 2 timeout. Received states: {timeout_reset_states}") + + assert len(timeout_reset_states) == 4, ( + f"Test 2: Should have 4 states, got {len(timeout_reset_states)}: {timeout_reset_states}" + ) + assert timeout_reset_states[0] == pytest.approx(10.0), ( + f"Test 2: First state should be 10.0, got {timeout_reset_states[0]}" + ) + assert timeout_reset_states[1] == pytest.approx(20.0), ( + f"Test 2: Second state should be 20.0, got {timeout_reset_states[1]}" + ) + assert timeout_reset_states[2] == pytest.approx(30.0), ( + f"Test 2: Third state should be 30.0, got {timeout_reset_states[2]}" + ) + assert timeout_reset_states[3] == pytest.approx(30.0), ( + f"Test 2: Timeout should output last value (30.0), got {timeout_reset_states[3]}" + ) + + # === Test 3: TimeoutFilterConfigured - static value === + client.button_command(test3_button_key) + try: + await asyncio.wait_for(test3_complete, timeout=2.0) + except TimeoutError: + pytest.fail(f"Test 3 timeout. Received states: {timeout_static_states}") + + assert len(timeout_static_states) == 2, ( + f"Test 3: Should have 2 states, got {len(timeout_static_states)}: {timeout_static_states}" + ) + assert timeout_static_states[0] == pytest.approx(55.5), ( + f"Test 3: First state should be 55.5, got {timeout_static_states[0]}" + ) + assert timeout_static_states[1] == pytest.approx(99.9), ( + f"Test 3: Timeout should output configured value (99.9), got {timeout_static_states[1]}" + ) + + # === Test 4: TimeoutFilterConfigured - lambda === + client.button_command(test4_button_key) + try: + await asyncio.wait_for(test4_complete, timeout=2.0) + except TimeoutError: + pytest.fail(f"Test 4 timeout. Received states: {timeout_lambda_states}") + + assert len(timeout_lambda_states) == 2, ( + f"Test 4: Should have 2 states, got {len(timeout_lambda_states)}: {timeout_lambda_states}" + ) + assert timeout_lambda_states[0] == pytest.approx(77.7), ( + f"Test 4: First state should be 77.7, got {timeout_lambda_states[0]}" + ) + assert timeout_lambda_states[1] == pytest.approx(-1.0), ( + f"Test 4: Timeout should evaluate lambda (-1.0), got {timeout_lambda_states[1]}" + ) diff --git a/tests/integration/test_template_alarm_control_panel_many_sensors.py b/tests/integration/test_template_alarm_control_panel_many_sensors.py new file mode 100644 index 0000000000..856815c731 --- /dev/null +++ b/tests/integration/test_template_alarm_control_panel_many_sensors.py @@ -0,0 +1,118 @@ +"""Integration test for template alarm control panel with many sensors.""" + +from __future__ import annotations + +import aioesphomeapi +from aioesphomeapi.model import APIIntEnum +import pytest + +from .state_utils import InitialStateHelper +from .types import APIClientConnectedFactory, RunCompiledFunction + + +class EspHomeACPFeatures(APIIntEnum): + """ESPHome AlarmControlPanel feature numbers.""" + + ARM_HOME = 1 + ARM_AWAY = 2 + ARM_NIGHT = 4 + TRIGGER = 8 + ARM_CUSTOM_BYPASS = 16 + ARM_VACATION = 32 + + +@pytest.mark.asyncio +async def test_template_alarm_control_panel_many_sensors( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test template alarm control panel with 10 binary sensors using FixedVector.""" + async with run_compiled(yaml_config), api_client_connected() as client: + # Get entity info first + entities, _ = await client.list_entities_services() + + # Find the alarm control panel and binary sensors + alarm_info: aioesphomeapi.AlarmControlPanelInfo | None = None + binary_sensors: list[aioesphomeapi.BinarySensorInfo] = [] + + for entity in entities: + if isinstance(entity, aioesphomeapi.AlarmControlPanelInfo): + alarm_info = entity + elif isinstance(entity, aioesphomeapi.BinarySensorInfo): + binary_sensors.append(entity) + + assert alarm_info is not None, "Alarm control panel entity info not found" + assert alarm_info.name == "Test Alarm" + assert alarm_info.requires_code is True + assert alarm_info.requires_code_to_arm is True + + # Verify we have 10 binary sensors + assert len(binary_sensors) == 10, ( + f"Expected 10 binary sensors, got {len(binary_sensors)}" + ) + + # Verify sensor names + expected_sensor_names = { + "Door 1", + "Door 2", + "Window 1", + "Window 2", + "Motion 1", + "Motion 2", + "Glass Break 1", + "Glass Break 2", + "Smoke Detector", + "CO Detector", + } + actual_sensor_names = {sensor.name for sensor in binary_sensors} + assert actual_sensor_names == expected_sensor_names, ( + f"Sensor names mismatch. Expected: {expected_sensor_names}, " + f"Got: {actual_sensor_names}" + ) + + # Use InitialStateHelper to wait for all initial states + state_helper = InitialStateHelper(entities) + + def on_state(state: aioesphomeapi.EntityState) -> None: + # We'll receive subsequent states here after initial states + pass + + client.subscribe_states(state_helper.on_state_wrapper(on_state)) + + # Wait for all initial states + await state_helper.wait_for_initial_states(timeout=5.0) + + # Verify the alarm state is disarmed initially + alarm_state = state_helper.initial_states.get(alarm_info.key) + assert alarm_state is not None, "Alarm control panel initial state not received" + assert isinstance(alarm_state, aioesphomeapi.AlarmControlPanelEntityState) + assert alarm_state.state == aioesphomeapi.AlarmControlPanelState.DISARMED, ( + f"Expected initial state DISARMED, got {alarm_state.state}" + ) + + # Verify all 10 binary sensors have initial states + binary_sensor_states = [ + state_helper.initial_states.get(sensor.key) for sensor in binary_sensors + ] + assert all(state is not None for state in binary_sensor_states), ( + "Not all binary sensors have initial states" + ) + + # Verify all binary sensor states are BinarySensorState type + for i, state in enumerate(binary_sensor_states): + assert isinstance(state, aioesphomeapi.BinarySensorState), ( + f"Binary sensor {i} state is not BinarySensorState: {type(state)}" + ) + + # Verify supported features + expected_features = ( + EspHomeACPFeatures.ARM_HOME + | EspHomeACPFeatures.ARM_AWAY + | EspHomeACPFeatures.ARM_NIGHT + | EspHomeACPFeatures.TRIGGER + ) + assert alarm_info.supported_features == expected_features, ( + f"Expected supported_features={expected_features} (ARM_HOME|ARM_AWAY|ARM_NIGHT|TRIGGER), " + f"got {alarm_info.supported_features}" + ) diff --git a/tests/integration/test_wait_until_mid_loop_timing.py b/tests/integration/test_wait_until_mid_loop_timing.py new file mode 100644 index 0000000000..01cad747ae --- /dev/null +++ b/tests/integration/test_wait_until_mid_loop_timing.py @@ -0,0 +1,112 @@ +"""Integration test for PR #11676 mid-loop timing bug. + +This test validates that wait_until timeouts work correctly when triggered +mid-component-loop, where App.get_loop_component_start_time() is stale. + +The bug: When wait_until is triggered partway through a component's loop execution +(e.g., from a script or automation), the cached loop_component_start_time_ is stale +relative to when the action was actually triggered. This causes timeout calculations +to underflow and timeout immediately instead of waiting the specified duration. +""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_wait_until_mid_loop_timing( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that wait_until timeout works when triggered mid-component-loop. + + This test: + 1. Executes a script that delays 100ms (simulating component work) + 2. Then starts wait_until with 200ms timeout + 3. Verifies timeout takes ~200ms, not <50ms (immediate timeout bug) + """ + loop = asyncio.get_running_loop() + + # Track test results + test_results = { + "timeout_duration": None, + "passed": False, + "failed": False, + "bug_detected": False, + } + + # Patterns for log messages + timeout_duration = re.compile(r"wait_until completed after (\d+) ms") + test_pass = re.compile(r"✓ Timeout duration correct") + test_fail = re.compile(r"✗ Timeout duration WRONG") + bug_pattern = re.compile(r"Likely BUG: Immediate timeout") + test_passed = re.compile(r"✓ Test PASSED") + test_failed = re.compile(r"✗ Test FAILED") + + test_complete = loop.create_future() + + def check_output(line: str) -> None: + """Check log output for test results.""" + # Extract timeout duration + match = timeout_duration.search(line) + if match: + test_results["timeout_duration"] = int(match.group(1)) + + if test_pass.search(line): + test_results["passed"] = True + if test_fail.search(line): + test_results["failed"] = True + if bug_pattern.search(line): + test_results["bug_detected"] = True + + # Final test result + if ( + test_passed.search(line) + or test_failed.search(line) + and not test_complete.done() + ): + test_complete.set_result(True) + + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Get the test service + _, services = await client.list_entities_services() + test_service = next( + (s for s in services if s.name == "test_mid_loop_timeout"), None + ) + assert test_service is not None, "test_mid_loop_timeout service not found" + + # Execute the test + client.execute_service(test_service, {}) + + # Wait for test to complete (100ms delay + 200ms timeout + margins = ~500ms) + await asyncio.wait_for(test_complete, timeout=5.0) + + # Verify results + assert test_results["timeout_duration"] is not None, ( + "Timeout duration not reported" + ) + assert test_results["passed"], ( + f"Test failed: wait_until took {test_results['timeout_duration']}ms, expected ~200ms. " + f"Bug detected: {test_results['bug_detected']}" + ) + assert not test_results["bug_detected"], ( + f"BUG DETECTED: wait_until timed out immediately ({test_results['timeout_duration']}ms) " + "instead of waiting 200ms. This indicates stale loop_component_start_time." + ) + + # Additional validation: timeout should be ~200ms (150-250ms range) + duration = test_results["timeout_duration"] + assert 150 <= duration <= 250, ( + f"Timeout duration {duration}ms outside expected range (150-250ms). " + f"This suggests timing regression from PR #11676." + ) diff --git a/tests/integration/test_wait_until_on_boot.py b/tests/integration/test_wait_until_on_boot.py new file mode 100644 index 0000000000..b42c530c54 --- /dev/null +++ b/tests/integration/test_wait_until_on_boot.py @@ -0,0 +1,91 @@ +"""Integration test for wait_until in on_boot automation. + +This test validates that wait_until works correctly when triggered from on_boot, +which runs at the same setup priority as WaitUntilAction itself. This was broken +before the fix because WaitUntilAction::setup() would unconditionally disable_loop(), +even if play_complex() had already been called and enabled the loop. + +The bug: on_boot fires during StartupTrigger::setup(), which calls WaitUntilAction::play_complex() +before WaitUntilAction::setup() has run. Then when WaitUntilAction::setup() runs, it calls +disable_loop(), undoing the enable_loop() from play_complex(), causing wait_until to hang forever. +""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_wait_until_on_boot( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that wait_until works in on_boot automation with a condition that becomes true later.""" + loop = asyncio.get_running_loop() + + on_boot_started = False + on_boot_completed = False + + on_boot_started_pattern = re.compile(r"on_boot: Starting wait_until test") + on_boot_complete_pattern = re.compile(r"on_boot: wait_until completed successfully") + + on_boot_started_future = loop.create_future() + on_boot_complete_future = loop.create_future() + + def check_output(line: str) -> None: + """Check log output for test progress.""" + nonlocal on_boot_started, on_boot_completed + + if on_boot_started_pattern.search(line): + on_boot_started = True + if not on_boot_started_future.done(): + on_boot_started_future.set_result(True) + + if on_boot_complete_pattern.search(line): + on_boot_completed = True + if not on_boot_complete_future.done(): + on_boot_complete_future.set_result(True) + + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Wait for on_boot to start + await asyncio.wait_for(on_boot_started_future, timeout=10.0) + assert on_boot_started, "on_boot did not start" + + # At this point, on_boot is blocked in wait_until waiting for test_flag to become true + # If the bug exists, wait_until's loop is disabled and it will never complete + # even after we set the flag + + # Give a moment for setup to complete + await asyncio.sleep(0.5) + + # Now set the flag that wait_until is waiting for + _, services = await client.list_entities_services() + set_flag_service = next( + (s for s in services if s.name == "set_test_flag"), None + ) + assert set_flag_service is not None, "set_test_flag service not found" + + client.execute_service(set_flag_service, {}) + + # If the fix works, wait_until's loop() will check the condition and proceed + # If the bug exists, wait_until is stuck with disabled loop and will timeout + try: + await asyncio.wait_for(on_boot_complete_future, timeout=2.0) + assert on_boot_completed, ( + "on_boot wait_until did not complete after flag was set" + ) + except TimeoutError: + pytest.fail( + "wait_until in on_boot did not complete within 2s after condition became true. " + "This indicates the bug where WaitUntilAction::setup() disables the loop " + "after play_complex() has already enabled it." + ) diff --git a/tests/integration/test_wait_until_ordering.py b/tests/integration/test_wait_until_ordering.py new file mode 100644 index 0000000000..7c39913e5a --- /dev/null +++ b/tests/integration/test_wait_until_ordering.py @@ -0,0 +1,90 @@ +"""Integration test for wait_until FIFO ordering. + +This test verifies that when multiple wait_until actions are queued, +they execute in FIFO (First In First Out) order, not LIFO. + +PR #7972 introduced a bug where emplace_front() was used, causing +LIFO ordering which is incorrect. +""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_wait_until_fifo_ordering( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that wait_until executes queued items in FIFO order. + + With the bug (using emplace_front), the order would be 4,3,2,1,0 (LIFO). + With the fix (using emplace_back), the order should be 0,1,2,3,4 (FIFO). + """ + test_complete = asyncio.Event() + + # Track completion order + completed_order = [] + + # Patterns to match + queuing_pattern = re.compile(r"Queueing iteration (\d+)") + completed_pattern = re.compile(r"Completed iteration (\d+)") + + def check_output(line: str) -> None: + """Check log output for completion order.""" + if test_complete.is_set(): + return + + if mo := queuing_pattern.search(line): + iteration = int(mo.group(1)) + + elif mo := completed_pattern.search(line): + iteration = int(mo.group(1)) + completed_order.append(iteration) + + # Test completes when all 5 have completed + if len(completed_order) == 5: + test_complete.set() + + # Run with log monitoring + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Verify device info + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "test-wait-until-ordering" + + # Get services + _, services = await client.list_entities_services() + test_service = next( + (s for s in services if s.name == "test_wait_until_fifo"), None + ) + assert test_service is not None, "test_wait_until_fifo service not found" + + # Execute the test + client.execute_service(test_service, {}) + + # Wait for test to complete + try: + await asyncio.wait_for(test_complete.wait(), timeout=5.0) + except TimeoutError: + pytest.fail( + f"Test timed out. Completed order: {completed_order}. " + f"Expected 5 completions but got {len(completed_order)}." + ) + + # Verify FIFO order + expected_order = [0, 1, 2, 3, 4] + assert completed_order == expected_order, ( + f"Unexpected order: {completed_order}. " + f"Expected FIFO order: {expected_order}" + ) diff --git a/tests/integration/types.py b/tests/integration/types.py index 5e4bfaa29d..b6728a2fcb 100644 --- a/tests/integration/types.py +++ b/tests/integration/types.py @@ -54,3 +54,17 @@ class APIClientConnectedFactory(Protocol): client_info: str = "integration-test", timeout: float = 30, ) -> AbstractAsyncContextManager[APIClient]: ... + + +class APIClientConnectedWithDisconnectFactory(Protocol): + """Protocol for connected API client factory that returns disconnect event.""" + + def __call__( # noqa: E704 + self, + address: str = "localhost", + port: int | None = None, + password: str = "", + noise_psk: str | None = None, + client_info: str = "integration-test", + timeout: float = 30, + ) -> AbstractAsyncContextManager[tuple[APIClient, asyncio.Event]]: ... diff --git a/tests/script/test_clang_tidy_hash.py b/tests/script/test_clang_tidy_hash.py index 2f84d11a0d..b1690a6a2d 100644 --- a/tests/script/test_clang_tidy_hash.py +++ b/tests/script/test_clang_tidy_hash.py @@ -44,37 +44,53 @@ def test_get_clang_tidy_version_from_requirements( assert result == expected -def test_calculate_clang_tidy_hash() -> None: - """Test calculating hash from all configuration sources.""" +def test_calculate_clang_tidy_hash_with_sdkconfig(tmp_path: Path) -> None: + """Test calculating hash from all configuration sources including sdkconfig.defaults.""" clang_tidy_content = b"Checks: '-*,readability-*'\n" requirements_version = "clang-tidy==18.1.5" platformio_content = b"[env:esp32]\nplatform = espressif32\n" + sdkconfig_content = b"CONFIG_AUTOSTART_ARDUINO=y\n" + requirements_content = "clang-tidy==18.1.5\n" + + # Create temporary files + (tmp_path / ".clang-tidy").write_bytes(clang_tidy_content) + (tmp_path / "platformio.ini").write_bytes(platformio_content) + (tmp_path / "sdkconfig.defaults").write_bytes(sdkconfig_content) + (tmp_path / "requirements_dev.txt").write_text(requirements_content) # Expected hash calculation expected_hasher = hashlib.sha256() expected_hasher.update(clang_tidy_content) expected_hasher.update(requirements_version.encode()) expected_hasher.update(platformio_content) + expected_hasher.update(sdkconfig_content) expected_hash = expected_hasher.hexdigest() - # Mock the dependencies - with ( - patch("clang_tidy_hash.read_file_bytes") as mock_read_bytes, - patch( - "clang_tidy_hash.get_clang_tidy_version_from_requirements", - return_value=requirements_version, - ), - ): - # Set up mock to return different content based on the file being read - def read_file_mock(path: Path) -> bytes: - if ".clang-tidy" in str(path): - return clang_tidy_content - if "platformio.ini" in str(path): - return platformio_content - return b"" + result = clang_tidy_hash.calculate_clang_tidy_hash(repo_root=tmp_path) - mock_read_bytes.side_effect = read_file_mock - result = clang_tidy_hash.calculate_clang_tidy_hash() + assert result == expected_hash + + +def test_calculate_clang_tidy_hash_without_sdkconfig(tmp_path: Path) -> None: + """Test calculating hash without sdkconfig.defaults file.""" + clang_tidy_content = b"Checks: '-*,readability-*'\n" + requirements_version = "clang-tidy==18.1.5" + platformio_content = b"[env:esp32]\nplatform = espressif32\n" + requirements_content = "clang-tidy==18.1.5\n" + + # Create temporary files (without sdkconfig.defaults) + (tmp_path / ".clang-tidy").write_bytes(clang_tidy_content) + (tmp_path / "platformio.ini").write_bytes(platformio_content) + (tmp_path / "requirements_dev.txt").write_text(requirements_content) + + # Expected hash calculation (no sdkconfig) + expected_hasher = hashlib.sha256() + expected_hasher.update(clang_tidy_content) + expected_hasher.update(requirements_version.encode()) + expected_hasher.update(platformio_content) + expected_hash = expected_hasher.hexdigest() + + result = clang_tidy_hash.calculate_clang_tidy_hash(repo_root=tmp_path) assert result == expected_hash @@ -85,67 +101,63 @@ def test_read_stored_hash_exists(tmp_path: Path) -> None: hash_file = tmp_path / ".clang-tidy.hash" hash_file.write_text(f"{stored_hash}\n") - with ( - patch("clang_tidy_hash.Path") as mock_path_class, - patch("clang_tidy_hash.read_file_lines", return_value=[f"{stored_hash}\n"]), - ): - # Mock the path calculation and exists check - mock_hash_file = Mock() - mock_hash_file.exists.return_value = True - mock_path_class.return_value.parent.parent.__truediv__.return_value = ( - mock_hash_file - ) - - result = clang_tidy_hash.read_stored_hash() + result = clang_tidy_hash.read_stored_hash(repo_root=tmp_path) assert result == stored_hash -def test_read_stored_hash_not_exists() -> None: +def test_read_stored_hash_not_exists(tmp_path: Path) -> None: """Test reading hash when file doesn't exist.""" - with patch("clang_tidy_hash.Path") as mock_path_class: - # Mock the path calculation and exists check - mock_hash_file = Mock() - mock_hash_file.exists.return_value = False - mock_path_class.return_value.parent.parent.__truediv__.return_value = ( - mock_hash_file - ) - - result = clang_tidy_hash.read_stored_hash() + result = clang_tidy_hash.read_stored_hash(repo_root=tmp_path) assert result is None -def test_write_hash() -> None: +def test_write_hash(tmp_path: Path) -> None: """Test writing hash to file.""" hash_value = "abc123def456" + hash_file = tmp_path / ".clang-tidy.hash" - with patch("clang_tidy_hash.write_file_content") as mock_write: - clang_tidy_hash.write_hash(hash_value) + clang_tidy_hash.write_hash(hash_value, repo_root=tmp_path) - # Verify write_file_content was called with correct parameters - mock_write.assert_called_once() - args = mock_write.call_args[0] - assert str(args[0]).endswith(".clang-tidy.hash") - assert args[1] == hash_value.strip() + "\n" + assert hash_file.exists() + assert hash_file.read_text() == hash_value.strip() + "\n" @pytest.mark.parametrize( - ("args", "current_hash", "stored_hash", "expected_exit"), + ("args", "current_hash", "stored_hash", "hash_file_in_changed", "expected_exit"), [ - (["--check"], "abc123", "abc123", 1), # Hashes match, no scan needed - (["--check"], "abc123", "def456", 0), # Hashes differ, scan needed - (["--check"], "abc123", None, 0), # No stored hash, scan needed + (["--check"], "abc123", "abc123", False, 1), # Hashes match, no scan needed + (["--check"], "abc123", "def456", False, 0), # Hashes differ, scan needed + (["--check"], "abc123", None, False, 0), # No stored hash, scan needed + ( + ["--check"], + "abc123", + "abc123", + True, + 0, + ), # Hash file updated in PR, scan needed ], ) def test_main_check_mode( - args: list[str], current_hash: str, stored_hash: str | None, expected_exit: int + args: list[str], + current_hash: str, + stored_hash: str | None, + hash_file_in_changed: bool, + expected_exit: int, ) -> None: """Test main function in check mode.""" + changed = [".clang-tidy.hash"] if hash_file_in_changed else [] + + # Create a mock module that can be imported + mock_helpers = Mock() + mock_helpers.changed_files = Mock(return_value=changed) + with ( patch("sys.argv", ["clang_tidy_hash.py"] + args), patch("clang_tidy_hash.calculate_clang_tidy_hash", return_value=current_hash), patch("clang_tidy_hash.read_stored_hash", return_value=stored_hash), + patch.dict("sys.modules", {"helpers": mock_helpers}), pytest.raises(SystemExit) as exc_info, ): clang_tidy_hash.main() diff --git a/tests/script/test_determine_jobs.py b/tests/script/test_determine_jobs.py index 7200afc2ee..291a23967b 100644 --- a/tests/script/test_determine_jobs.py +++ b/tests/script/test_determine_jobs.py @@ -4,7 +4,7 @@ from collections.abc import Generator import importlib.util import json import os -import subprocess +from pathlib import Path import sys from unittest.mock import Mock, call, patch @@ -16,6 +16,11 @@ script_dir = os.path.abspath( ) sys.path.insert(0, script_dir) +# Import helpers module for patching +import helpers # noqa: E402 + +import script.helpers # noqa: E402 + spec = importlib.util.spec_from_file_location( "determine_jobs", os.path.join(script_dir, "determine-jobs.py") ) @@ -52,33 +57,94 @@ def mock_should_run_python_linters() -> Generator[Mock, None, None]: @pytest.fixture -def mock_subprocess_run() -> Generator[Mock, None, None]: - """Mock subprocess.run for list-components.py calls.""" - with patch.object(determine_jobs.subprocess, "run") as mock: +def mock_determine_cpp_unit_tests() -> Generator[Mock, None, None]: + """Mock determine_cpp_unit_tests from helpers.""" + with patch.object(determine_jobs, "determine_cpp_unit_tests") as mock: yield mock +@pytest.fixture +def mock_changed_files() -> Generator[Mock, None, None]: + """Mock changed_files for memory impact detection.""" + with patch.object(determine_jobs, "changed_files") as mock: + # Default to empty list + mock.return_value = [] + yield mock + + +@pytest.fixture +def mock_target_branch_dev() -> Generator[Mock, None, None]: + """Mock get_target_branch to return 'dev' for memory impact tests.""" + with patch.object(determine_jobs, "get_target_branch", return_value="dev") as mock: + yield mock + + +@pytest.fixture(autouse=True) +def clear_determine_jobs_caches() -> None: + """Clear all cached functions before each test.""" + determine_jobs._is_clang_tidy_full_scan.cache_clear() + determine_jobs._component_has_tests.cache_clear() + + def test_main_all_tests_should_run( mock_should_run_integration_tests: Mock, mock_should_run_clang_tidy: Mock, mock_should_run_clang_format: Mock, mock_should_run_python_linters: Mock, - mock_subprocess_run: Mock, + mock_changed_files: Mock, + mock_determine_cpp_unit_tests: Mock, capsys: pytest.CaptureFixture[str], + monkeypatch: pytest.MonkeyPatch, ) -> None: """Test when all tests should run.""" + # Ensure we're not in GITHUB_ACTIONS mode for this test + monkeypatch.delenv("GITHUB_ACTIONS", raising=False) + mock_should_run_integration_tests.return_value = True mock_should_run_clang_tidy.return_value = True mock_should_run_clang_format.return_value = True mock_should_run_python_linters.return_value = True + mock_determine_cpp_unit_tests.return_value = (False, ["wifi", "api", "sensor"]) - # Mock list-components.py output - mock_result = Mock() - mock_result.stdout = "wifi\napi\nsensor\n" - mock_subprocess_run.return_value = mock_result + # Mock changed_files to return non-component files (to avoid memory impact) + # Memory impact only runs when component C++ files change + mock_changed_files.return_value = [ + "esphome/config.py", + "esphome/helpers.py", + ] # Run main function with mocked argv - with patch("sys.argv", ["determine-jobs.py"]): + with ( + patch("sys.argv", ["determine-jobs.py"]), + patch.object(determine_jobs, "_is_clang_tidy_full_scan", return_value=False), + patch.object( + determine_jobs, + "get_changed_components", + return_value=["wifi", "api", "sensor"], + ), + patch.object( + determine_jobs, + "filter_component_and_test_files", + side_effect=lambda f: f.startswith("esphome/components/"), + ), + patch.object( + determine_jobs, + "get_components_with_dependencies", + side_effect=lambda files, deps: ( + ["wifi", "api"] if not deps else ["wifi", "api", "sensor"] + ), + ), + patch.object( + determine_jobs, + "detect_memory_impact_config", + return_value={"should_run": "false"}, + ), + patch.object( + determine_jobs, + "create_intelligent_batches", + return_value=([["wifi", "api", "sensor"]], {}), + ), + ): determine_jobs.main() # Check output @@ -87,10 +153,33 @@ def test_main_all_tests_should_run( assert output["integration_tests"] is True assert output["clang_tidy"] is True + assert output["clang_tidy_mode"] in ["nosplit", "split"] assert output["clang_format"] is True assert output["python_linters"] is True assert output["changed_components"] == ["wifi", "api", "sensor"] - assert output["component_test_count"] == 3 + # changed_components_with_tests will only include components that actually have test files + assert "changed_components_with_tests" in output + assert isinstance(output["changed_components_with_tests"], list) + # component_test_count matches number of components with tests + assert output["component_test_count"] == len( + output["changed_components_with_tests"] + ) + # changed_cpp_file_count should be present + assert "changed_cpp_file_count" in output + assert isinstance(output["changed_cpp_file_count"], int) + # memory_impact should be false (no component C++ files changed) + assert "memory_impact" in output + assert output["memory_impact"]["should_run"] == "false" + assert output["cpp_unit_tests_run_all"] is False + assert output["cpp_unit_tests_components"] == ["wifi", "api", "sensor"] + # component_test_batches should be present and be a list of space-separated strings + assert "component_test_batches" in output + assert isinstance(output["component_test_batches"], list) + # Each batch should be a space-separated string of component names + for batch in output["component_test_batches"]: + assert isinstance(batch, str) + # Should contain at least one component (no empty batches) + assert len(batch) > 0 def test_main_no_tests_should_run( @@ -98,22 +187,45 @@ def test_main_no_tests_should_run( mock_should_run_clang_tidy: Mock, mock_should_run_clang_format: Mock, mock_should_run_python_linters: Mock, - mock_subprocess_run: Mock, + mock_changed_files: Mock, + mock_determine_cpp_unit_tests: Mock, capsys: pytest.CaptureFixture[str], + monkeypatch: pytest.MonkeyPatch, ) -> None: """Test when no tests should run.""" + # Ensure we're not in GITHUB_ACTIONS mode for this test + monkeypatch.delenv("GITHUB_ACTIONS", raising=False) + mock_should_run_integration_tests.return_value = False mock_should_run_clang_tidy.return_value = False mock_should_run_clang_format.return_value = False mock_should_run_python_linters.return_value = False + mock_determine_cpp_unit_tests.return_value = (False, []) - # Mock empty list-components.py output - mock_result = Mock() - mock_result.stdout = "" - mock_subprocess_run.return_value = mock_result + # Mock changed_files to return no component files + mock_changed_files.return_value = [] # Run main function with mocked argv - with patch("sys.argv", ["determine-jobs.py"]): + with ( + patch("sys.argv", ["determine-jobs.py"]), + patch.object(determine_jobs, "get_changed_components", return_value=[]), + patch.object( + determine_jobs, "filter_component_and_test_files", return_value=False + ), + patch.object( + determine_jobs, "get_components_with_dependencies", return_value=[] + ), + patch.object( + determine_jobs, + "detect_memory_impact_config", + return_value={"should_run": "false"}, + ), + patch.object( + determine_jobs, + "create_intelligent_batches", + return_value=([], {}), + ), + ): determine_jobs.main() # Check output @@ -122,35 +234,22 @@ def test_main_no_tests_should_run( assert output["integration_tests"] is False assert output["clang_tidy"] is False + assert output["clang_tidy_mode"] == "disabled" assert output["clang_format"] is False assert output["python_linters"] is False assert output["changed_components"] == [] + assert output["changed_components_with_tests"] == [] assert output["component_test_count"] == 0 - - -def test_main_list_components_fails( - mock_should_run_integration_tests: Mock, - mock_should_run_clang_tidy: Mock, - mock_should_run_clang_format: Mock, - mock_should_run_python_linters: Mock, - mock_subprocess_run: Mock, - capsys: pytest.CaptureFixture[str], -) -> None: - """Test when list-components.py fails.""" - mock_should_run_integration_tests.return_value = True - mock_should_run_clang_tidy.return_value = True - mock_should_run_clang_format.return_value = True - mock_should_run_python_linters.return_value = True - - # Mock list-components.py failure - mock_subprocess_run.side_effect = subprocess.CalledProcessError(1, "cmd") - - # Run main function with mocked argv - should raise - with ( - patch("sys.argv", ["determine-jobs.py"]), - pytest.raises(subprocess.CalledProcessError), - ): - determine_jobs.main() + # changed_cpp_file_count should be 0 + assert output["changed_cpp_file_count"] == 0 + # memory_impact should be present + assert "memory_impact" in output + assert output["memory_impact"]["should_run"] == "false" + assert output["cpp_unit_tests_run_all"] is False + assert output["cpp_unit_tests_components"] == [] + # component_test_batches should be empty list + assert "component_test_batches" in output + assert output["component_test_batches"] == [] def test_main_with_branch_argument( @@ -158,21 +257,48 @@ def test_main_with_branch_argument( mock_should_run_clang_tidy: Mock, mock_should_run_clang_format: Mock, mock_should_run_python_linters: Mock, - mock_subprocess_run: Mock, + mock_changed_files: Mock, + mock_determine_cpp_unit_tests: Mock, capsys: pytest.CaptureFixture[str], + monkeypatch: pytest.MonkeyPatch, ) -> None: """Test with branch argument.""" + # Ensure we're not in GITHUB_ACTIONS mode for this test + monkeypatch.delenv("GITHUB_ACTIONS", raising=False) + mock_should_run_integration_tests.return_value = False mock_should_run_clang_tidy.return_value = True mock_should_run_clang_format.return_value = False mock_should_run_python_linters.return_value = True + mock_determine_cpp_unit_tests.return_value = (False, ["mqtt"]) - # Mock list-components.py output - mock_result = Mock() - mock_result.stdout = "mqtt\n" - mock_subprocess_run.return_value = mock_result + # Mock changed_files to return non-component files (to avoid memory impact) + # Memory impact only runs when component C++ files change + mock_changed_files.return_value = ["esphome/config.py"] - with patch("sys.argv", ["script.py", "-b", "main"]): + with ( + patch("sys.argv", ["script.py", "-b", "main"]), + patch.object(determine_jobs, "_is_clang_tidy_full_scan", return_value=False), + patch.object(determine_jobs, "get_changed_components", return_value=["mqtt"]), + patch.object( + determine_jobs, + "filter_component_and_test_files", + side_effect=lambda f: f.startswith("esphome/components/"), + ), + patch.object( + determine_jobs, "get_components_with_dependencies", return_value=["mqtt"] + ), + patch.object( + determine_jobs, + "detect_memory_impact_config", + return_value={"should_run": "false"}, + ), + patch.object( + determine_jobs, + "create_intelligent_batches", + return_value=([["mqtt"]], {}), + ), + ): determine_jobs.main() # Check that functions were called with branch @@ -181,23 +307,31 @@ def test_main_with_branch_argument( mock_should_run_clang_format.assert_called_once_with("main") mock_should_run_python_linters.assert_called_once_with("main") - # Check that list-components.py was called with branch - mock_subprocess_run.assert_called_once() - call_args = mock_subprocess_run.call_args[0][0] - assert "--changed" in call_args - assert "-b" in call_args - assert "main" in call_args - # Check output captured = capsys.readouterr() output = json.loads(captured.out) assert output["integration_tests"] is False assert output["clang_tidy"] is True + assert output["clang_tidy_mode"] in ["nosplit", "split"] assert output["clang_format"] is False assert output["python_linters"] is True assert output["changed_components"] == ["mqtt"] - assert output["component_test_count"] == 1 + # changed_components_with_tests will only include components that actually have test files + assert "changed_components_with_tests" in output + assert isinstance(output["changed_components_with_tests"], list) + # component_test_count matches number of components with tests + assert output["component_test_count"] == len( + output["changed_components_with_tests"] + ) + # changed_cpp_file_count should be present + assert "changed_cpp_file_count" in output + assert isinstance(output["changed_cpp_file_count"], int) + # memory_impact should be false (no component C++ files changed) + assert "memory_impact" in output + assert output["memory_impact"]["should_run"] == "false" + assert output["cpp_unit_tests_run_all"] is False + assert output["cpp_unit_tests_components"] == ["mqtt"] def test_should_run_integration_tests( @@ -298,16 +432,6 @@ def test_should_run_clang_tidy_hash_check_exception() -> None: result = determine_jobs.should_run_clang_tidy() assert result is True # Fail safe - run clang-tidy - # Even with C++ files, exception should trigger clang-tidy - with ( - patch.object( - determine_jobs, "changed_files", return_value=["esphome/core.cpp"] - ), - patch("subprocess.run", side_effect=Exception("Hash check failed")), - ): - result = determine_jobs.should_run_clang_tidy() - assert result is True - def test_should_run_clang_tidy_with_branch() -> None: """Test should_run_clang_tidy with branch argument.""" @@ -377,3 +501,1017 @@ def test_should_run_clang_format_with_branch() -> None: mock_changed.return_value = [] determine_jobs.should_run_clang_format("release") mock_changed.assert_called_once_with("release") + + +@pytest.mark.parametrize( + ("changed_files", "expected_count"), + [ + (["esphome/core.cpp"], 1), + (["esphome/core.h"], 1), + (["test.hpp"], 1), + (["test.cc"], 1), + (["test.cxx"], 1), + (["test.c"], 1), + (["test.tcc"], 1), + (["esphome/core.cpp", "esphome/core.h"], 2), + (["esphome/core.cpp", "esphome/core.h", "test.cc"], 3), + (["README.md"], 0), + (["esphome/config.py"], 0), + (["README.md", "esphome/config.py"], 0), + (["esphome/core.cpp", "README.md", "esphome/config.py"], 1), + ([], 0), + ], +) +def test_count_changed_cpp_files(changed_files: list[str], expected_count: int) -> None: + """Test count_changed_cpp_files function.""" + with patch.object(determine_jobs, "changed_files", return_value=changed_files): + result = determine_jobs.count_changed_cpp_files() + assert result == expected_count + + +def test_count_changed_cpp_files_with_branch() -> None: + """Test count_changed_cpp_files with branch argument.""" + with patch.object(determine_jobs, "changed_files") as mock_changed: + mock_changed.return_value = [] + determine_jobs.count_changed_cpp_files("release") + mock_changed.assert_called_once_with("release") + + +def test_main_filters_components_without_tests( + mock_should_run_integration_tests: Mock, + mock_should_run_clang_tidy: Mock, + mock_should_run_clang_format: Mock, + mock_should_run_python_linters: Mock, + mock_changed_files: Mock, + capsys: pytest.CaptureFixture[str], + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test that components without test files are filtered out.""" + # Ensure we're not in GITHUB_ACTIONS mode for this test + monkeypatch.delenv("GITHUB_ACTIONS", raising=False) + + mock_should_run_integration_tests.return_value = False + mock_should_run_clang_tidy.return_value = False + mock_should_run_clang_format.return_value = False + mock_should_run_python_linters.return_value = False + + # Mock changed_files to return component files + mock_changed_files.return_value = [ + "esphome/components/wifi/wifi.cpp", + "esphome/components/sensor/sensor.h", + ] + + # Create test directory structure + tests_dir = tmp_path / "tests" / "components" + + # wifi has tests + wifi_dir = tests_dir / "wifi" + wifi_dir.mkdir(parents=True) + (wifi_dir / "test.esp32.yaml").write_text("test: config") + + # sensor has tests + sensor_dir = tests_dir / "sensor" + sensor_dir.mkdir(parents=True) + (sensor_dir / "test.esp8266.yaml").write_text("test: config") + + # airthings_ble exists but has no test files + airthings_dir = tests_dir / "airthings_ble" + airthings_dir.mkdir(parents=True) + + # Mock root_path to use tmp_path (need to patch both determine_jobs and helpers) + with ( + patch.object(determine_jobs, "root_path", str(tmp_path)), + patch.object(helpers, "root_path", str(tmp_path)), + patch.object(helpers, "create_components_graph", return_value={}), + patch("sys.argv", ["determine-jobs.py"]), + patch.object( + determine_jobs, + "get_changed_components", + return_value=["wifi", "sensor", "airthings_ble"], + ), + patch.object( + determine_jobs, + "filter_component_and_test_files", + side_effect=lambda f: f.startswith("esphome/components/"), + ), + patch.object( + determine_jobs, + "get_components_with_dependencies", + side_effect=lambda files, deps: ( + ["wifi", "sensor"] if not deps else ["wifi", "sensor", "airthings_ble"] + ), + ), + patch.object(determine_jobs, "changed_files", return_value=[]), + patch.object( + determine_jobs, + "detect_memory_impact_config", + return_value={"should_run": "false"}, + ), + ): + # Clear the cache since we're mocking root_path + determine_jobs.main() + + # Check output + captured = capsys.readouterr() + output = json.loads(captured.out) + + # changed_components should have all components + assert set(output["changed_components"]) == {"wifi", "sensor", "airthings_ble"} + # changed_components_with_tests should only have components with test files + assert set(output["changed_components_with_tests"]) == {"wifi", "sensor"} + # component_test_count should be based on components with tests + assert output["component_test_count"] == 2 + # changed_cpp_file_count should be present + assert "changed_cpp_file_count" in output + assert isinstance(output["changed_cpp_file_count"], int) + # memory_impact should be present + assert "memory_impact" in output + assert output["memory_impact"]["should_run"] == "false" + + +def test_main_detects_components_with_variant_tests( + mock_should_run_integration_tests: Mock, + mock_should_run_clang_tidy: Mock, + mock_should_run_clang_format: Mock, + mock_should_run_python_linters: Mock, + mock_changed_files: Mock, + capsys: pytest.CaptureFixture[str], + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test that components with only variant test files (test-*.yaml) are detected. + + This test verifies the fix for components like improv_serial, ethernet, mdns, + improv_base, and safe_mode which only have variant test files (test-*.yaml) + instead of base test files (test.*.yaml). + """ + # Ensure we're not in GITHUB_ACTIONS mode for this test + monkeypatch.delenv("GITHUB_ACTIONS", raising=False) + + mock_should_run_integration_tests.return_value = False + mock_should_run_clang_tidy.return_value = False + mock_should_run_clang_format.return_value = False + mock_should_run_python_linters.return_value = False + + # Mock changed_files to return component files + mock_changed_files.return_value = [ + "esphome/components/improv_serial/improv_serial.cpp", + "esphome/components/ethernet/ethernet.cpp", + "esphome/components/no_tests/component.cpp", + ] + + # Create test directory structure + tests_dir = tmp_path / "tests" / "components" + + # improv_serial has only variant tests (like the real component) + improv_serial_dir = tests_dir / "improv_serial" + improv_serial_dir.mkdir(parents=True) + (improv_serial_dir / "test-uart0.esp32-idf.yaml").write_text("test: config") + (improv_serial_dir / "test-uart0.esp8266-ard.yaml").write_text("test: config") + (improv_serial_dir / "test-usb_cdc.esp32-s2-idf.yaml").write_text("test: config") + + # ethernet also has only variant tests + ethernet_dir = tests_dir / "ethernet" + ethernet_dir.mkdir(parents=True) + (ethernet_dir / "test-manual_ip.esp32-idf.yaml").write_text("test: config") + (ethernet_dir / "test-dhcp.esp32-idf.yaml").write_text("test: config") + + # no_tests component has no test files at all + no_tests_dir = tests_dir / "no_tests" + no_tests_dir.mkdir(parents=True) + + # Mock root_path to use tmp_path (need to patch both determine_jobs and helpers) + with ( + patch.object(determine_jobs, "root_path", str(tmp_path)), + patch.object(helpers, "root_path", str(tmp_path)), + patch.object(helpers, "create_components_graph", return_value={}), + patch("sys.argv", ["determine-jobs.py"]), + patch.object( + determine_jobs, + "get_changed_components", + return_value=["improv_serial", "ethernet", "no_tests"], + ), + patch.object( + determine_jobs, + "filter_component_and_test_files", + side_effect=lambda f: f.startswith("esphome/components/"), + ), + patch.object( + determine_jobs, + "get_components_with_dependencies", + side_effect=lambda files, deps: ( + ["improv_serial", "ethernet"] + if not deps + else ["improv_serial", "ethernet", "no_tests"] + ), + ), + patch.object(determine_jobs, "changed_files", return_value=[]), + patch.object( + determine_jobs, + "detect_memory_impact_config", + return_value={"should_run": "false"}, + ), + ): + # Clear the cache since we're mocking root_path + determine_jobs.main() + + # Check output + captured = capsys.readouterr() + output = json.loads(captured.out) + + # changed_components should have all components + assert set(output["changed_components"]) == { + "improv_serial", + "ethernet", + "no_tests", + } + # changed_components_with_tests should include components with variant tests + assert set(output["changed_components_with_tests"]) == {"improv_serial", "ethernet"} + # component_test_count should be 2 (improv_serial and ethernet) + assert output["component_test_count"] == 2 + # no_tests should be excluded since it has no test files + assert "no_tests" not in output["changed_components_with_tests"] + + +# Tests for detect_memory_impact_config function + + +@pytest.mark.usefixtures("mock_target_branch_dev") +def test_detect_memory_impact_config_with_common_platform(tmp_path: Path) -> None: + """Test memory impact detection when components share a common platform.""" + # Create test directory structure + tests_dir = tmp_path / "tests" / "components" + + # wifi component with esp32-idf test + wifi_dir = tests_dir / "wifi" + wifi_dir.mkdir(parents=True) + (wifi_dir / "test.esp32-idf.yaml").write_text("test: wifi") + + # api component with esp32-idf test + api_dir = tests_dir / "api" + api_dir.mkdir(parents=True) + (api_dir / "test.esp32-idf.yaml").write_text("test: api") + + # Mock changed_files to return wifi and api component changes + with ( + patch.object(determine_jobs, "root_path", str(tmp_path)), + patch.object(helpers, "root_path", str(tmp_path)), + patch.object(determine_jobs, "changed_files") as mock_changed_files, + ): + mock_changed_files.return_value = [ + "esphome/components/wifi/wifi.cpp", + "esphome/components/api/api.cpp", + ] + + result = determine_jobs.detect_memory_impact_config() + + assert result["should_run"] == "true" + assert set(result["components"]) == {"wifi", "api"} + assert result["platform"] == "esp32-idf" # Common platform + assert result["use_merged_config"] == "true" + + +@pytest.mark.usefixtures("mock_target_branch_dev") +def test_detect_memory_impact_config_core_only_changes(tmp_path: Path) -> None: + """Test memory impact detection with core C++ changes (no component changes).""" + # Create test directory structure with fallback component + tests_dir = tmp_path / "tests" / "components" + + # api component (fallback component) with esp32-idf test + api_dir = tests_dir / "api" + api_dir.mkdir(parents=True) + (api_dir / "test.esp32-idf.yaml").write_text("test: api") + + # Mock changed_files to return only core C++ files (no component files) + with ( + patch.object(determine_jobs, "root_path", str(tmp_path)), + patch.object(helpers, "root_path", str(tmp_path)), + patch.object(determine_jobs, "changed_files") as mock_changed_files, + ): + mock_changed_files.return_value = [ + "esphome/core/application.cpp", + "esphome/core/component.h", + ] + + result = determine_jobs.detect_memory_impact_config() + + assert result["should_run"] == "true" + assert result["components"] == ["api"] # Fallback component + assert result["platform"] == "esp32-idf" # Fallback platform + assert result["use_merged_config"] == "true" + + +def test_detect_memory_impact_config_core_python_only_changes(tmp_path: Path) -> None: + """Test that Python-only core changes don't trigger memory impact analysis.""" + # Create test directory structure with fallback component + tests_dir = tmp_path / "tests" / "components" + + # api component (fallback component) with esp32-idf test + api_dir = tests_dir / "api" + api_dir.mkdir(parents=True) + (api_dir / "test.esp32-idf.yaml").write_text("test: api") + + # Mock changed_files to return only core Python files (no C++ files) + with ( + patch.object(determine_jobs, "root_path", str(tmp_path)), + patch.object(helpers, "root_path", str(tmp_path)), + patch.object(determine_jobs, "changed_files") as mock_changed_files, + ): + mock_changed_files.return_value = [ + "esphome/__main__.py", + "esphome/config.py", + "esphome/core/config.py", + ] + + result = determine_jobs.detect_memory_impact_config() + + # Python-only changes should NOT trigger memory impact analysis + assert result["should_run"] == "false" + + +@pytest.mark.usefixtures("mock_target_branch_dev") +def test_detect_memory_impact_config_no_common_platform(tmp_path: Path) -> None: + """Test memory impact detection when components have no common platform.""" + # Create test directory structure + tests_dir = tmp_path / "tests" / "components" + + # wifi component only has esp32-idf test + wifi_dir = tests_dir / "wifi" + wifi_dir.mkdir(parents=True) + (wifi_dir / "test.esp32-idf.yaml").write_text("test: wifi") + + # logger component only has esp8266-ard test + logger_dir = tests_dir / "logger" + logger_dir.mkdir(parents=True) + (logger_dir / "test.esp8266-ard.yaml").write_text("test: logger") + + # Mock changed_files to return both components + with ( + patch.object(determine_jobs, "root_path", str(tmp_path)), + patch.object(helpers, "root_path", str(tmp_path)), + patch.object(determine_jobs, "changed_files") as mock_changed_files, + ): + mock_changed_files.return_value = [ + "esphome/components/wifi/wifi.cpp", + "esphome/components/logger/logger.cpp", + ] + + result = determine_jobs.detect_memory_impact_config() + + # Should pick the most frequently supported platform + assert result["should_run"] == "true" + assert set(result["components"]) == {"wifi", "logger"} + # When no common platform, picks most commonly supported + # esp8266-ard is preferred over esp32-idf in the preference list + assert result["platform"] in ["esp32-idf", "esp8266-ard"] + assert result["use_merged_config"] == "true" + + +def test_detect_memory_impact_config_no_changes(tmp_path: Path) -> None: + """Test memory impact detection when no files changed.""" + # Mock changed_files to return empty list + with ( + patch.object(determine_jobs, "root_path", str(tmp_path)), + patch.object(helpers, "root_path", str(tmp_path)), + patch.object(determine_jobs, "changed_files") as mock_changed_files, + ): + mock_changed_files.return_value = [] + + result = determine_jobs.detect_memory_impact_config() + + assert result["should_run"] == "false" + + +def test_detect_memory_impact_config_no_components_with_tests(tmp_path: Path) -> None: + """Test memory impact detection when changed components have no tests.""" + # Create test directory structure + tests_dir = tmp_path / "tests" / "components" + + # Create component directory but no test files + custom_component_dir = tests_dir / "my_custom_component" + custom_component_dir.mkdir(parents=True) + + # Mock changed_files to return component without tests + with ( + patch.object(determine_jobs, "root_path", str(tmp_path)), + patch.object(helpers, "root_path", str(tmp_path)), + patch.object(determine_jobs, "changed_files") as mock_changed_files, + ): + mock_changed_files.return_value = [ + "esphome/components/my_custom_component/component.cpp", + ] + + result = determine_jobs.detect_memory_impact_config() + + assert result["should_run"] == "false" + + +@pytest.mark.usefixtures("mock_target_branch_dev") +def test_detect_memory_impact_config_includes_base_bus_components( + tmp_path: Path, +) -> None: + """Test that base bus components (i2c, spi, uart) are included when directly changed. + + Base bus components should be analyzed for memory impact when they are directly + changed, even though they are often used as dependencies. This ensures that + optimizations to base components (like using move semantics or initializer_list) + are properly measured. + """ + # Create test directory structure + tests_dir = tmp_path / "tests" / "components" + + # uart component (base bus component that should be included) + uart_dir = tests_dir / "uart" + uart_dir.mkdir(parents=True) + (uart_dir / "test.esp32-idf.yaml").write_text("test: uart") + + # wifi component (regular component) + wifi_dir = tests_dir / "wifi" + wifi_dir.mkdir(parents=True) + (wifi_dir / "test.esp32-idf.yaml").write_text("test: wifi") + + # Mock changed_files to return both uart and wifi + with ( + patch.object(determine_jobs, "root_path", str(tmp_path)), + patch.object(helpers, "root_path", str(tmp_path)), + patch.object(determine_jobs, "changed_files") as mock_changed_files, + ): + mock_changed_files.return_value = [ + "esphome/components/uart/automation.h", # Header file with inline code + "esphome/components/wifi/wifi.cpp", + ] + + result = determine_jobs.detect_memory_impact_config() + + # Should include both uart and wifi + assert result["should_run"] == "true" + assert set(result["components"]) == {"uart", "wifi"} + assert result["platform"] == "esp32-idf" # Common platform + + +@pytest.mark.usefixtures("mock_target_branch_dev") +def test_detect_memory_impact_config_with_variant_tests(tmp_path: Path) -> None: + """Test memory impact detection for components with only variant test files. + + This verifies that memory impact analysis works correctly for components like + improv_serial, ethernet, mdns, etc. which only have variant test files + (test-*.yaml) instead of base test files (test.*.yaml). + """ + # Create test directory structure + tests_dir = tmp_path / "tests" / "components" + + # improv_serial with only variant tests + improv_serial_dir = tests_dir / "improv_serial" + improv_serial_dir.mkdir(parents=True) + (improv_serial_dir / "test-uart0.esp32-idf.yaml").write_text("test: improv") + (improv_serial_dir / "test-uart0.esp8266-ard.yaml").write_text("test: improv") + (improv_serial_dir / "test-usb_cdc.esp32-s2-idf.yaml").write_text("test: improv") + + # ethernet with only variant tests + ethernet_dir = tests_dir / "ethernet" + ethernet_dir.mkdir(parents=True) + (ethernet_dir / "test-manual_ip.esp32-idf.yaml").write_text("test: ethernet") + (ethernet_dir / "test-dhcp.esp32-c3-idf.yaml").write_text("test: ethernet") + + # Mock changed_files to return both components + with ( + patch.object(determine_jobs, "root_path", str(tmp_path)), + patch.object(helpers, "root_path", str(tmp_path)), + patch.object(determine_jobs, "changed_files") as mock_changed_files, + ): + mock_changed_files.return_value = [ + "esphome/components/improv_serial/improv_serial.cpp", + "esphome/components/ethernet/ethernet.cpp", + ] + + result = determine_jobs.detect_memory_impact_config() + + # Should detect both components even though they only have variant tests + assert result["should_run"] == "true" + assert set(result["components"]) == {"improv_serial", "ethernet"} + # Both components support esp32-idf + assert result["platform"] == "esp32-idf" + assert result["use_merged_config"] == "true" + + +# Tests for clang-tidy split mode logic + + +def test_clang_tidy_mode_full_scan( + mock_should_run_integration_tests: Mock, + mock_should_run_clang_tidy: Mock, + mock_should_run_clang_format: Mock, + mock_should_run_python_linters: Mock, + mock_changed_files: Mock, + capsys: pytest.CaptureFixture[str], + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test that full scan (hash changed) always uses split mode.""" + monkeypatch.delenv("GITHUB_ACTIONS", raising=False) + + mock_should_run_integration_tests.return_value = False + mock_should_run_clang_tidy.return_value = True + mock_should_run_clang_format.return_value = False + mock_should_run_python_linters.return_value = False + + # Mock changed_files to return no component files + mock_changed_files.return_value = [] + + # Mock full scan (hash changed) + with ( + patch("sys.argv", ["determine-jobs.py"]), + patch.object(determine_jobs, "_is_clang_tidy_full_scan", return_value=True), + patch.object(determine_jobs, "get_changed_components", return_value=[]), + patch.object( + determine_jobs, "filter_component_and_test_files", return_value=False + ), + patch.object( + determine_jobs, "get_components_with_dependencies", return_value=[] + ), + ): + determine_jobs.main() + + captured = capsys.readouterr() + output = json.loads(captured.out) + + # Full scan should always use split mode + assert output["clang_tidy_mode"] == "split" + + +@pytest.mark.parametrize( + ("component_count", "files_per_component", "expected_mode"), + [ + # Small PR: 5 files in 1 component -> nosplit + (1, 5, "nosplit"), + # Medium PR: 30 files in 2 components -> nosplit + (2, 15, "nosplit"), + # Medium PR: 64 files total -> nosplit (just under threshold) + (2, 32, "nosplit"), + # Large PR: 65 files total -> split (at threshold) + (2, 33, "split"), # 2 * 33 = 66 files + # Large PR: 100 files in 10 components -> split + (10, 10, "split"), + ], + ids=[ + "1_comp_5_files_nosplit", + "2_comp_30_files_nosplit", + "2_comp_64_files_nosplit_under_threshold", + "2_comp_66_files_split_at_threshold", + "10_comp_100_files_split", + ], +) +def test_clang_tidy_mode_targeted_scan( + component_count: int, + files_per_component: int, + expected_mode: str, + mock_should_run_integration_tests: Mock, + mock_should_run_clang_tidy: Mock, + mock_should_run_clang_format: Mock, + mock_should_run_python_linters: Mock, + mock_changed_files: Mock, + capsys: pytest.CaptureFixture[str], + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test clang-tidy mode selection based on files_to_check count.""" + monkeypatch.delenv("GITHUB_ACTIONS", raising=False) + + mock_should_run_integration_tests.return_value = False + mock_should_run_clang_tidy.return_value = True + mock_should_run_clang_format.return_value = False + mock_should_run_python_linters.return_value = False + + # Create component names + components = [f"comp{i}" for i in range(component_count)] + + # Mock changed_files to return component files + mock_changed_files.return_value = [ + f"esphome/components/{comp}/file.cpp" for comp in components + ] + + # Mock git_ls_files to return files for each component + cpp_files = { + f"esphome/components/{comp}/file{i}.cpp": 0 + for comp in components + for i in range(files_per_component) + } + + # Create a mock that returns the cpp_files dict for any call + def mock_git_ls_files(patterns=None): + return cpp_files + + with ( + patch("sys.argv", ["determine-jobs.py"]), + patch.object(determine_jobs, "_is_clang_tidy_full_scan", return_value=False), + patch.object(determine_jobs, "git_ls_files", side_effect=mock_git_ls_files), + patch.object(determine_jobs, "get_changed_components", return_value=components), + patch.object( + determine_jobs, + "filter_component_and_test_files", + side_effect=lambda f: f.startswith("esphome/components/"), + ), + patch.object( + determine_jobs, "get_components_with_dependencies", return_value=components + ), + ): + determine_jobs.main() + + captured = capsys.readouterr() + output = json.loads(captured.out) + + assert output["clang_tidy_mode"] == expected_mode + + +def test_main_core_files_changed_still_detects_components( + mock_should_run_integration_tests: Mock, + mock_should_run_clang_tidy: Mock, + mock_should_run_clang_format: Mock, + mock_should_run_python_linters: Mock, + mock_changed_files: Mock, + mock_determine_cpp_unit_tests: Mock, + capsys: pytest.CaptureFixture[str], + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test that component changes are detected even when core files change.""" + monkeypatch.delenv("GITHUB_ACTIONS", raising=False) + + mock_should_run_integration_tests.return_value = True + mock_should_run_clang_tidy.return_value = True + mock_should_run_clang_format.return_value = True + mock_should_run_python_linters.return_value = True + mock_determine_cpp_unit_tests.return_value = (True, []) + + mock_changed_files.return_value = [ + "esphome/core/helpers.h", + "esphome/components/select/select_traits.h", + "esphome/components/select/select_traits.cpp", + "esphome/components/api/api.proto", + ] + + with ( + patch("sys.argv", ["determine-jobs.py"]), + patch.object(determine_jobs, "_is_clang_tidy_full_scan", return_value=False), + patch.object(determine_jobs, "get_changed_components", return_value=None), + patch.object( + determine_jobs, + "filter_component_and_test_files", + side_effect=lambda f: f.startswith("esphome/components/"), + ), + patch.object( + determine_jobs, + "get_components_with_dependencies", + side_effect=lambda files, deps: ( + ["select", "api"] + if not deps + else ["select", "api", "bluetooth_proxy", "logger"] + ), + ), + patch.object( + determine_jobs, + "detect_memory_impact_config", + return_value={"should_run": "false"}, + ), + patch.object( + determine_jobs, + "create_intelligent_batches", + return_value=([["select", "api", "bluetooth_proxy", "logger"]], {}), + ), + ): + determine_jobs.main() + + captured = capsys.readouterr() + output = json.loads(captured.out) + + assert output["clang_tidy"] is True + assert output["clang_tidy_mode"] == "split" + assert "select" in output["changed_components"] + assert "api" in output["changed_components"] + assert len(output["changed_components"]) > 0 + + +@pytest.mark.usefixtures("mock_target_branch_dev") +def test_detect_memory_impact_config_filters_incompatible_esp32_on_esp8266( + tmp_path: Path, +) -> None: + """Test that ESP32 components are filtered out when ESP8266 platform is selected. + + This test verifies the fix for the issue where ESP32 components were being included + when ESP8266 was selected as the platform, causing build failures in PR 10387. + """ + # Create test directory structure + tests_dir = tmp_path / "tests" / "components" + + # esp32 component only has esp32-idf tests (NOT compatible with esp8266) + esp32_dir = tests_dir / "esp32" + esp32_dir.mkdir(parents=True) + (esp32_dir / "test.esp32-idf.yaml").write_text("test: esp32") + (esp32_dir / "test.esp32-s3-idf.yaml").write_text("test: esp32") + + # esp8266 component only has esp8266-ard test (NOT compatible with esp32) + esp8266_dir = tests_dir / "esp8266" + esp8266_dir.mkdir(parents=True) + (esp8266_dir / "test.esp8266-ard.yaml").write_text("test: esp8266") + + # Mock changed_files to return both esp32 and esp8266 component changes + # Include esp8266-specific filename to trigger esp8266 platform hint + with ( + patch.object(determine_jobs, "root_path", str(tmp_path)), + patch.object(helpers, "root_path", str(tmp_path)), + patch.object(determine_jobs, "changed_files") as mock_changed_files, + ): + mock_changed_files.return_value = [ + "tests/components/esp32/common.yaml", + "tests/components/esp8266/test.esp8266-ard.yaml", + "esphome/core/helpers_esp8266.h", # ESP8266-specific file to hint platform + ] + + result = determine_jobs.detect_memory_impact_config() + + # Memory impact should run + assert result["should_run"] == "true" + + # Platform should be esp8266-ard (due to ESP8266 filename hint) + assert result["platform"] == "esp8266-ard" + + # CRITICAL: Only esp8266 component should be included, not esp32 + # This prevents trying to build ESP32 components on ESP8266 platform + assert result["components"] == ["esp8266"], ( + "When esp8266-ard platform is selected, only esp8266 component should be included, " + "not esp32. This prevents trying to build ESP32 components on ESP8266 platform." + ) + + assert result["use_merged_config"] == "true" + + +@pytest.mark.usefixtures("mock_target_branch_dev") +def test_detect_memory_impact_config_filters_incompatible_esp8266_on_esp32( + tmp_path: Path, +) -> None: + """Test that ESP8266 components are filtered out when ESP32 platform is selected. + + This is the inverse of the ESP8266 test - ensures filtering works both ways. + """ + # Create test directory structure + tests_dir = tmp_path / "tests" / "components" + + # esp32 component only has esp32-idf tests (NOT compatible with esp8266) + esp32_dir = tests_dir / "esp32" + esp32_dir.mkdir(parents=True) + (esp32_dir / "test.esp32-idf.yaml").write_text("test: esp32") + (esp32_dir / "test.esp32-s3-idf.yaml").write_text("test: esp32") + + # esp8266 component only has esp8266-ard test (NOT compatible with esp32) + esp8266_dir = tests_dir / "esp8266" + esp8266_dir.mkdir(parents=True) + (esp8266_dir / "test.esp8266-ard.yaml").write_text("test: esp8266") + + # Mock changed_files to return both esp32 and esp8266 component changes + # Include MORE esp32-specific filenames to ensure esp32-idf wins the hint count + with ( + patch.object(determine_jobs, "root_path", str(tmp_path)), + patch.object(helpers, "root_path", str(tmp_path)), + patch.object(determine_jobs, "changed_files") as mock_changed_files, + ): + mock_changed_files.return_value = [ + "tests/components/esp32/common.yaml", + "tests/components/esp8266/test.esp8266-ard.yaml", + "esphome/components/wifi/wifi_component_esp_idf.cpp", # ESP-IDF hint + "esphome/components/ethernet/ethernet_esp32.cpp", # ESP32 hint + ] + + result = determine_jobs.detect_memory_impact_config() + + # Memory impact should run + assert result["should_run"] == "true" + + # Platform should be esp32-idf (due to more ESP32-IDF hints) + assert result["platform"] == "esp32-idf" + + # CRITICAL: Only esp32 component should be included, not esp8266 + # This prevents trying to build ESP8266 components on ESP32 platform + assert result["components"] == ["esp32"], ( + "When esp32-idf platform is selected, only esp32 component should be included, " + "not esp8266. This prevents trying to build ESP8266 components on ESP32 platform." + ) + + assert result["use_merged_config"] == "true" + + +def test_detect_memory_impact_config_skips_release_branch(tmp_path: Path) -> None: + """Test that memory impact analysis is skipped for release* branches.""" + # Create test directory structure with components that have tests + tests_dir = tmp_path / "tests" / "components" + wifi_dir = tests_dir / "wifi" + wifi_dir.mkdir(parents=True) + (wifi_dir / "test.esp32-idf.yaml").write_text("test: wifi") + + with ( + patch.object(determine_jobs, "root_path", str(tmp_path)), + patch.object(helpers, "root_path", str(tmp_path)), + patch.object(determine_jobs, "changed_files") as mock_changed_files, + patch.object(determine_jobs, "get_target_branch", return_value="release"), + ): + mock_changed_files.return_value = ["esphome/components/wifi/wifi.cpp"] + + result = determine_jobs.detect_memory_impact_config() + + # Memory impact should be skipped for release branch + assert result["should_run"] == "false" + + +def test_detect_memory_impact_config_skips_beta_branch(tmp_path: Path) -> None: + """Test that memory impact analysis is skipped for beta* branches.""" + # Create test directory structure with components that have tests + tests_dir = tmp_path / "tests" / "components" + wifi_dir = tests_dir / "wifi" + wifi_dir.mkdir(parents=True) + (wifi_dir / "test.esp32-idf.yaml").write_text("test: wifi") + + with ( + patch.object(determine_jobs, "root_path", str(tmp_path)), + patch.object(helpers, "root_path", str(tmp_path)), + patch.object(determine_jobs, "changed_files") as mock_changed_files, + patch.object(determine_jobs, "get_target_branch", return_value="beta"), + ): + mock_changed_files.return_value = ["esphome/components/wifi/wifi.cpp"] + + result = determine_jobs.detect_memory_impact_config() + + # Memory impact should be skipped for beta branch + assert result["should_run"] == "false" + + +def test_detect_memory_impact_config_runs_for_dev_branch(tmp_path: Path) -> None: + """Test that memory impact analysis runs for dev branch.""" + # Create test directory structure with components that have tests + tests_dir = tmp_path / "tests" / "components" + wifi_dir = tests_dir / "wifi" + wifi_dir.mkdir(parents=True) + (wifi_dir / "test.esp32-idf.yaml").write_text("test: wifi") + + with ( + patch.object(determine_jobs, "root_path", str(tmp_path)), + patch.object(helpers, "root_path", str(tmp_path)), + patch.object(determine_jobs, "changed_files") as mock_changed_files, + patch.object(determine_jobs, "get_target_branch", return_value="dev"), + ): + mock_changed_files.return_value = ["esphome/components/wifi/wifi.cpp"] + + result = determine_jobs.detect_memory_impact_config() + + # Memory impact should run for dev branch + assert result["should_run"] == "true" + assert result["components"] == ["wifi"] + + +def test_detect_memory_impact_config_skips_too_many_components( + tmp_path: Path, +) -> None: + """Test that memory impact analysis is skipped when more than 40 components changed.""" + # Create test directory structure with 41 components + tests_dir = tmp_path / "tests" / "components" + component_names = [f"component_{i}" for i in range(41)] + + for component_name in component_names: + comp_dir = tests_dir / component_name + comp_dir.mkdir(parents=True) + (comp_dir / "test.esp32-idf.yaml").write_text(f"test: {component_name}") + + with ( + patch.object(determine_jobs, "root_path", str(tmp_path)), + patch.object(helpers, "root_path", str(tmp_path)), + patch.object(determine_jobs, "changed_files") as mock_changed_files, + patch.object(determine_jobs, "get_target_branch", return_value="dev"), + ): + mock_changed_files.return_value = [ + f"esphome/components/{name}/{name}.cpp" for name in component_names + ] + + result = determine_jobs.detect_memory_impact_config() + + # Memory impact should be skipped for too many components (41 > 40) + assert result["should_run"] == "false" + + +def test_detect_memory_impact_config_runs_at_component_limit(tmp_path: Path) -> None: + """Test that memory impact analysis runs with exactly 40 components (at limit).""" + # Create test directory structure with exactly 40 components + tests_dir = tmp_path / "tests" / "components" + component_names = [f"component_{i}" for i in range(40)] + + for component_name in component_names: + comp_dir = tests_dir / component_name + comp_dir.mkdir(parents=True) + (comp_dir / "test.esp32-idf.yaml").write_text(f"test: {component_name}") + + with ( + patch.object(determine_jobs, "root_path", str(tmp_path)), + patch.object(helpers, "root_path", str(tmp_path)), + patch.object(determine_jobs, "changed_files") as mock_changed_files, + patch.object(determine_jobs, "get_target_branch", return_value="dev"), + ): + mock_changed_files.return_value = [ + f"esphome/components/{name}/{name}.cpp" for name in component_names + ] + + result = determine_jobs.detect_memory_impact_config() + + # Memory impact should run at exactly 40 components (at limit but not over) + assert result["should_run"] == "true" + assert len(result["components"]) == 40 + + +def test_component_batching_beta_branch_40_per_batch( + tmp_path: Path, + mock_should_run_integration_tests: Mock, + mock_should_run_clang_tidy: Mock, + mock_should_run_clang_format: Mock, + mock_should_run_python_linters: Mock, + mock_changed_files: Mock, + mock_determine_cpp_unit_tests: Mock, + capsys: pytest.CaptureFixture[str], +) -> None: + """Test that beta/release branches create batches with 40 actual components each. + + For beta/release branches, all components should be groupable (not isolated), + and each batch should contain 40 actual components with weight 1 each. + This matches the original behavior before consolidation. + """ + # Create 120 test components with test files + component_names = [f"comp_{i:03d}" for i in range(120)] + tests_dir = tmp_path / "tests" / "components" + + for comp in component_names: + comp_dir = tests_dir / comp + comp_dir.mkdir(parents=True) + (comp_dir / "test.esp32-idf.yaml").write_text(f"# Test for {comp}") + + # Setup mocks + mock_should_run_integration_tests.return_value = False + mock_should_run_clang_tidy.return_value = False + mock_should_run_clang_format.return_value = False + mock_should_run_python_linters.return_value = False + mock_determine_cpp_unit_tests.return_value = (False, []) + + # Mock changed_files to return all component files + changed_files = [ + f"esphome/components/{comp}/{comp}.cpp" for comp in component_names + ] + mock_changed_files.return_value = changed_files + + # Run main function with beta branch + # Don't mock create_intelligent_batches - that's what we're testing! + with ( + patch("sys.argv", ["determine-jobs.py", "--branch", "beta"]), + patch.object(determine_jobs, "root_path", str(tmp_path)), + patch.object(helpers, "root_path", str(tmp_path)), + patch.object(script.helpers, "root_path", str(tmp_path)), + patch.object(determine_jobs, "get_target_branch", return_value="beta"), + patch.object(determine_jobs, "_is_clang_tidy_full_scan", return_value=False), + patch.object( + determine_jobs, + "get_changed_components", + return_value=component_names, + ), + patch.object( + determine_jobs, + "filter_component_and_test_files", + side_effect=lambda f: f.startswith("esphome/components/"), + ), + patch.object( + determine_jobs, + "get_components_with_dependencies", + side_effect=lambda files, deps: component_names, + ), + patch.object( + determine_jobs, + "detect_memory_impact_config", + return_value={"should_run": "false"}, + ), + ): + determine_jobs.main() + + # Check output + captured = capsys.readouterr() + output = json.loads(captured.out) + + # Verify batches are present and properly sized + assert "component_test_batches" in output + batches = output["component_test_batches"] + + # Should have 3 batches (120 components / 40 per batch = 3) + assert len(batches) == 3, f"Expected 3 batches, got {len(batches)}" + + # Each batch should have approximately 40 components (all weight=1, groupable) + for i, batch_str in enumerate(batches): + batch_components = batch_str.split() + assert len(batch_components) == 40, ( + f"Batch {i} should have 40 components, got {len(batch_components)}" + ) + + # Verify all 120 components are in batches + all_components = [] + for batch_str in batches: + all_components.extend(batch_str.split()) + assert len(all_components) == 120 + assert set(all_components) == set(component_names) diff --git a/tests/script/test_helpers.py b/tests/script/test_helpers.py index 9730efd366..c51273f298 100644 --- a/tests/script/test_helpers.py +++ b/tests/script/test_helpers.py @@ -1,5 +1,6 @@ """Unit tests for script/helpers.py module.""" +from collections.abc import Generator import json import os from pathlib import Path @@ -30,6 +31,13 @@ print_file_list = helpers.print_file_list get_all_dependencies = helpers.get_all_dependencies +@pytest.fixture(autouse=True) +def clear_helpers_cache() -> None: + """Clear cached functions before each test.""" + helpers._get_github_event_data.cache_clear() + helpers._get_changed_files_github_actions.cache_clear() + + @pytest.mark.parametrize( ("github_ref", "expected_pr_number"), [ @@ -183,6 +191,61 @@ def test_get_changed_files_github_actions_pull_request( assert result == expected_files +def test_get_changed_files_github_actions_pull_request_large_pr( + monkeypatch: MonkeyPatch, +) -> None: + """Test _get_changed_files_github_actions fallback for PRs with >300 files.""" + monkeypatch.setenv("GITHUB_EVENT_NAME", "pull_request") + + expected_files = ["file1.py", "file2.cpp"] + + with ( + patch("helpers._get_pr_number_from_github_env", return_value="10214"), + patch("helpers._get_changed_files_from_command") as mock_get, + ): + # First call fails with too many files error, second succeeds with API method + mock_get.side_effect = [ + Exception("Sorry, the diff exceeded the maximum number of files (300)"), + expected_files, + ] + + result = _get_changed_files_github_actions() + + assert mock_get.call_count == 2 + mock_get.assert_any_call(["gh", "pr", "diff", "10214", "--name-only"]) + mock_get.assert_any_call( + [ + "gh", + "api", + "repos/esphome/esphome/pulls/10214/files", + "--paginate", + "--jq", + ".[].filename", + ] + ) + assert result == expected_files + + +def test_get_changed_files_github_actions_pull_request_other_error( + monkeypatch: MonkeyPatch, +) -> None: + """Test _get_changed_files_github_actions re-raises non-file-limit errors.""" + monkeypatch.setenv("GITHUB_EVENT_NAME", "pull_request") + + with ( + patch("helpers._get_pr_number_from_github_env", return_value="1234"), + patch("helpers._get_changed_files_from_command") as mock_get, + ): + # Error that is not about file limit + mock_get.side_effect = Exception("Command failed: authentication required") + + with pytest.raises(Exception, match="authentication required"): + _get_changed_files_github_actions() + + # Should only be called once (no retry with API) + mock_get.assert_called_once_with(["gh", "pr", "diff", "1234", "--name-only"]) + + def test_get_changed_files_github_actions_pull_request_no_pr_number( monkeypatch: MonkeyPatch, ) -> None: @@ -1010,3 +1073,303 @@ def test_parse_list_components_output(output: str, expected: list[str]) -> None: """Test parse_list_components_output function.""" result = helpers.parse_list_components_output(output) assert result == expected + + +@pytest.mark.parametrize( + ("file_path", "expected_component"), + [ + # Component files + ("esphome/components/wifi/wifi.cpp", "wifi"), + ("esphome/components/uart/uart.h", "uart"), + ("esphome/components/api/api_server.cpp", "api"), + ("esphome/components/sensor/sensor.cpp", "sensor"), + # Test files + ("tests/components/uart/test.esp32-idf.yaml", "uart"), + ("tests/components/wifi/test.esp8266-ard.yaml", "wifi"), + ("tests/components/sensor/test.esp32-idf.yaml", "sensor"), + ("tests/components/api/test_api.cpp", "api"), + ("tests/components/uart/common.h", "uart"), + # Non-component files + ("esphome/core/component.cpp", None), + ("esphome/core/helpers.h", None), + ("tests/integration/test_api.py", None), + ("tests/unit_tests/test_helpers.py", None), + ("README.md", None), + ("script/helpers.py", None), + # Edge cases + ("esphome/components/", None), # No component name + ("tests/components/", None), # No component name + ("esphome/components", None), # No trailing slash + ("tests/components", None), # No trailing slash + # Files in component directories that are not components + ("tests/components/.gitignore", None), # Hidden file + ("tests/components/README.md", None), # Documentation file + ("esphome/components/__init__.py", None), # Python init file + ("tests/components/main.cpp", None), # File with extension + ], +) +def test_get_component_from_path( + file_path: str, expected_component: str | None +) -> None: + """Test extraction of component names from file paths.""" + result = helpers.get_component_from_path(file_path) + assert result == expected_component + + +# Components graph cache tests + + +@pytest.fixture +def mock_git_output() -> str: + """Fixture for mock git ls-files output with realistic component files. + + Includes examples of AUTO_LOAD in sensor.py and binary_sensor.py files, + which is why we need to hash all .py files, not just __init__.py. + """ + return ( + "100644 abc123... 0 esphome/components/wifi/__init__.py\n" + "100644 def456... 0 esphome/components/api/__init__.py\n" + "100644 ghi789... 0 esphome/components/xiaomi_lywsd03mmc/__init__.py\n" + "100644 jkl012... 0 esphome/components/xiaomi_lywsd03mmc/sensor.py\n" + "100644 mno345... 0 esphome/components/xiaomi_cgpr1/__init__.py\n" + "100644 pqr678... 0 esphome/components/xiaomi_cgpr1/binary_sensor.py\n" + ) + + +@pytest.fixture +def mock_cache_file(tmp_path: Path) -> Path: + """Fixture for a temporary cache file path.""" + return tmp_path / "components_graph.json" + + +@pytest.fixture(autouse=True) +def clear_cache_key_cache() -> None: + """Clear the components graph cache key cache before each test.""" + helpers.get_components_graph_cache_key.cache_clear() + + +@pytest.fixture +def mock_subprocess_run() -> Generator[Mock, None, None]: + """Fixture to mock subprocess.run for git commands.""" + with patch("subprocess.run") as mock_run: + yield mock_run + + +def test_cache_key_generation(mock_git_output: str, mock_subprocess_run: Mock) -> None: + """Test that cache key is generated based on git file hashes.""" + mock_result = Mock() + mock_result.stdout = mock_git_output + mock_subprocess_run.return_value = mock_result + + key = helpers.get_components_graph_cache_key() + + # Should be a 64-character hex string (SHA256) + assert len(key) == 64 + assert all(c in "0123456789abcdef" for c in key) + + +def test_cache_key_consistent_for_same_files( + mock_git_output: str, mock_subprocess_run: Mock +) -> None: + """Test that same git output produces same cache key.""" + mock_result = Mock() + mock_result.stdout = mock_git_output + mock_subprocess_run.return_value = mock_result + + key1 = helpers.get_components_graph_cache_key() + key2 = helpers.get_components_graph_cache_key() + + assert key1 == key2 + + +def test_cache_key_different_for_changed_files(mock_subprocess_run: Mock) -> None: + """Test that different git output produces different cache key. + + This test demonstrates that changes to any .py file (not just __init__.py) + will invalidate the cache, which is important because AUTO_LOAD can be + defined in sensor.py, binary_sensor.py, etc. + """ + mock_result1 = Mock() + mock_result1.stdout = ( + "100644 abc123... 0 esphome/components/xiaomi_lywsd03mmc/sensor.py\n" + ) + + mock_result2 = Mock() + # Same file, different hash - simulates a change to AUTO_LOAD + mock_result2.stdout = ( + "100644 xyz789... 0 esphome/components/xiaomi_lywsd03mmc/sensor.py\n" + ) + + mock_subprocess_run.return_value = mock_result1 + key1 = helpers.get_components_graph_cache_key() + + helpers.get_components_graph_cache_key.cache_clear() + mock_subprocess_run.return_value = mock_result2 + key2 = helpers.get_components_graph_cache_key() + + assert key1 != key2 + + +def test_cache_key_uses_git_ls_files( + mock_git_output: str, mock_subprocess_run: Mock +) -> None: + """Test that git ls-files command is called correctly.""" + mock_result = Mock() + mock_result.stdout = mock_git_output + mock_subprocess_run.return_value = mock_result + + helpers.get_components_graph_cache_key() + + # Verify git ls-files was called with correct arguments + mock_subprocess_run.assert_called_once() + call_args = mock_subprocess_run.call_args + assert call_args[0][0] == [ + "git", + "ls-files", + "-s", + "esphome/components/**/*.py", + ] + assert call_args[1]["capture_output"] is True + assert call_args[1]["text"] is True + assert call_args[1]["check"] is True + assert call_args[1]["close_fds"] is False + + +def test_cache_hit_returns_cached_graph( + tmp_path: Path, mock_git_output: str, mock_subprocess_run: Mock +) -> None: + """Test that cache hit returns cached data without rebuilding.""" + mock_graph = {"wifi": ["network"], "api": ["socket"]} + cache_key = "a" * 64 + cache_data = { + "_version": helpers.COMPONENTS_GRAPH_CACHE_VERSION, + "_cache_key": cache_key, + "graph": mock_graph, + } + + # Write cache file + cache_file = tmp_path / "components_graph.json" + cache_file.write_text(json.dumps(cache_data)) + + mock_result = Mock() + mock_result.stdout = mock_git_output + mock_subprocess_run.return_value = mock_result + + with ( + patch("helpers.get_components_graph_cache_key", return_value=cache_key), + patch("helpers.temp_folder", str(tmp_path)), + ): + result = helpers.create_components_graph() + assert result == mock_graph + + +def test_cache_miss_no_cache_file( + tmp_path: Path, mock_git_output: str, mock_subprocess_run: Mock +) -> None: + """Test that cache miss rebuilds graph when no cache file exists.""" + mock_result = Mock() + mock_result.stdout = mock_git_output + mock_subprocess_run.return_value = mock_result + + # Create minimal components directory structure + components_dir = tmp_path / "esphome" / "components" + components_dir.mkdir(parents=True) + + with ( + patch("helpers.root_path", str(tmp_path)), + patch("helpers.temp_folder", str(tmp_path / ".temp")), + patch("helpers.get_components_graph_cache_key", return_value="test_key"), + ): + result = helpers.create_components_graph() + # Should return empty graph for empty components directory + assert result == {} + + +def test_cache_miss_version_mismatch( + tmp_path: Path, mock_git_output: str, mock_subprocess_run: Mock +) -> None: + """Test that cache miss rebuilds graph when version doesn't match.""" + cache_data = { + "_version": 999, # Wrong version + "_cache_key": "test_key", + "graph": {"old": ["data"]}, + } + + cache_file = tmp_path / ".temp" / "components_graph.json" + cache_file.parent.mkdir(parents=True) + cache_file.write_text(json.dumps(cache_data)) + + mock_result = Mock() + mock_result.stdout = mock_git_output + mock_subprocess_run.return_value = mock_result + + # Create minimal components directory structure + components_dir = tmp_path / "esphome" / "components" + components_dir.mkdir(parents=True) + + with ( + patch("helpers.root_path", str(tmp_path)), + patch("helpers.temp_folder", str(tmp_path / ".temp")), + patch("helpers.get_components_graph_cache_key", return_value="test_key"), + ): + result = helpers.create_components_graph() + # Should rebuild and return empty graph, not use cached data + assert result == {} + + +def test_cache_miss_key_mismatch( + tmp_path: Path, mock_git_output: str, mock_subprocess_run: Mock +) -> None: + """Test that cache miss rebuilds graph when cache key doesn't match.""" + cache_data = { + "_version": helpers.COMPONENTS_GRAPH_CACHE_VERSION, + "_cache_key": "old_key", + "graph": {"old": ["data"]}, + } + + cache_file = tmp_path / ".temp" / "components_graph.json" + cache_file.parent.mkdir(parents=True) + cache_file.write_text(json.dumps(cache_data)) + + mock_result = Mock() + mock_result.stdout = mock_git_output + mock_subprocess_run.return_value = mock_result + + # Create minimal components directory structure + components_dir = tmp_path / "esphome" / "components" + components_dir.mkdir(parents=True) + + with ( + patch("helpers.root_path", str(tmp_path)), + patch("helpers.temp_folder", str(tmp_path / ".temp")), + patch("helpers.get_components_graph_cache_key", return_value="new_key"), + ): + result = helpers.create_components_graph() + # Should rebuild and return empty graph, not use cached data with old key + assert result == {} + + +def test_cache_miss_corrupted_json( + tmp_path: Path, mock_git_output: str, mock_subprocess_run: Mock +) -> None: + """Test that cache miss rebuilds graph when cache file has invalid JSON.""" + cache_file = tmp_path / ".temp" / "components_graph.json" + cache_file.parent.mkdir(parents=True) + cache_file.write_text("{invalid json") + + mock_result = Mock() + mock_result.stdout = mock_git_output + mock_subprocess_run.return_value = mock_result + + # Create minimal components directory structure + components_dir = tmp_path / "esphome" / "components" + components_dir.mkdir(parents=True) + + with ( + patch("helpers.root_path", str(tmp_path)), + patch("helpers.temp_folder", str(tmp_path / ".temp")), + patch("helpers.get_components_graph_cache_key", return_value="test_key"), + ): + result = helpers.create_components_graph() + # Should handle corruption gracefully and rebuild + assert result == {} diff --git a/tests/test_build_components/build_components_base.bk72xx-ard.yaml b/tests/test_build_components/build_components_base.bk72xx-ard.yaml index 9a4e15d5cf..817acc3c39 100644 --- a/tests/test_build_components/build_components_base.bk72xx-ard.yaml +++ b/tests/test_build_components/build_components_base.bk72xx-ard.yaml @@ -3,7 +3,7 @@ esphome: friendly_name: $component_name bk72xx: - board: generic-bk7231n-qfn32-tuya + board: generic-bk7252 logger: level: VERY_VERBOSE diff --git a/tests/test_build_components/build_components_base.esp32-c3-idf.yaml b/tests/test_build_components/build_components_base.esp32-c3-idf.yaml index 18584497f4..73a85467d3 100644 --- a/tests/test_build_components/build_components_base.esp32-c3-idf.yaml +++ b/tests/test_build_components/build_components_base.esp32-c3-idf.yaml @@ -6,6 +6,9 @@ esp32: board: lolin_c3_mini framework: type: esp-idf + # Use custom partition table with larger app partition (3MB) + # Default IDF partitions only allow 1.75MB which is too small for grouped tests + partitions: ../partitions_testing.csv logger: level: VERY_VERBOSE diff --git a/tests/test_build_components/build_components_base.esp32-idf.yaml b/tests/test_build_components/build_components_base.esp32-idf.yaml index a62a995e68..dcb951c1ed 100644 --- a/tests/test_build_components/build_components_base.esp32-idf.yaml +++ b/tests/test_build_components/build_components_base.esp32-idf.yaml @@ -3,9 +3,13 @@ esphome: friendly_name: $component_name esp32: - board: nodemcu-32s + # Use board with 8MB flash for testing large component groups + board: esp32-pico-devkitm-2 framework: type: esp-idf + # Use custom partition table with larger app partitions (3MB each) + # Default IDF partitions only allow 1.75MB which is too small for grouped tests + partitions: ../partitions_testing.csv logger: level: VERY_VERBOSE diff --git a/tests/test_build_components/build_components_base.esp8266-ard.yaml b/tests/test_build_components/build_components_base.esp8266-ard.yaml index e4d6607c86..1e2d614392 100644 --- a/tests/test_build_components/build_components_base.esp8266-ard.yaml +++ b/tests/test_build_components/build_components_base.esp8266-ard.yaml @@ -3,7 +3,7 @@ esphome: friendly_name: $component_name esp8266: - board: d1_mini + board: d1_mini_pro logger: level: VERY_VERBOSE diff --git a/tests/test_build_components/build_components_base.nrf52-xiao-ble.yaml b/tests/test_build_components/build_components_base.nrf52-xiao-ble.yaml new file mode 100644 index 0000000000..2f3f91d957 --- /dev/null +++ b/tests/test_build_components/build_components_base.nrf52-xiao-ble.yaml @@ -0,0 +1,15 @@ +esphome: + name: componenttestnrf52 + friendly_name: $component_name + +nrf52: + board: xiao_ble + +logger: + level: VERY_VERBOSE + +packages: + component_under_test: !include + file: $component_test_file + vars: + component_test_file: $component_test_file diff --git a/tests/test_build_components/common/README.md b/tests/test_build_components/common/README.md new file mode 100644 index 0000000000..76f14b8664 --- /dev/null +++ b/tests/test_build_components/common/README.md @@ -0,0 +1,248 @@ +# Common Bus Configurations for Component Tests + +This directory contains standardized bus configurations (I2C, SPI, UART, Modbus, BLE) that component tests use, enabling multiple components to be tested together with intelligent grouping. + +## Purpose + +These common configs allow multiple components to **share a single bus**, dramatically reducing CI time by compiling multiple compatible components together. Components with identical bus configurations are automatically grouped and tested together. + +## Structure + +``` +common/ +├── i2c/ # Standard I2C (50kHz) +│ ├── esp32-idf.yaml +│ ├── esp32-ard.yaml +│ ├── esp32-c3-idf.yaml +│ ├── esp32-c3-ard.yaml +│ ├── esp32-s2-idf.yaml +│ ├── esp32-s2-ard.yaml +│ ├── esp32-s3-idf.yaml +│ ├── esp32-s3-ard.yaml +│ ├── esp8266-ard.yaml +│ ├── rp2040-ard.yaml +│ └── bk72xx-ard.yaml +├── i2c_low_freq/ # Low frequency I2C (10kHz) +│ └── (same platform variants) +├── spi/ +│ └── (same platform variants) +├── uart/ +│ ├── esp32-idf.yaml +│ ├── esp32-c3-idf.yaml +│ ├── esp8266-ard.yaml +│ └── rp2040-ard.yaml +├── modbus/ # Modbus (includes uart via packages) +│ ├── esp32-idf.yaml +│ ├── esp32-c3-idf.yaml +│ ├── esp8266-ard.yaml +│ └── rp2040-ard.yaml +└── ble/ + ├── esp32-idf.yaml + ├── esp32-ard.yaml + └── esp32-c3-idf.yaml +``` + +## How It Works + +### Component Test Structure +Each component test includes the common bus config: + +```yaml +# tests/components/bh1750/test.esp32-idf.yaml +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + +<<: !include common.yaml +``` + +The common config provides: +- Standardized pin assignments +- A shared bus instance (`i2c_bus`, `spi_bus`, `uart_bus`, `modbus_bus`, etc.) + +The component's `common.yaml` references the shared bus: +```yaml +# tests/components/bh1750/common.yaml +sensor: + - platform: bh1750 + i2c_id: i2c_bus + name: Living Room Brightness + address: 0x23 +``` + +### Intelligent Grouping (Implemented) +Components with identical bus configurations are automatically grouped and tested together: + +```yaml +# Auto-generated merged config (created by test_build_components.py) +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + +sensor: + - platform: bme280_i2c + i2c_id: i2c_bus + temperature: + name: BME280 Temperature + - platform: bh1750 + i2c_id: i2c_bus + name: BH1750 Illuminance + - platform: sht3xd + i2c_id: i2c_bus + temperature: + name: SHT3xD Temperature +``` + +**Result**: 3 components compile in one test instead of 3 separate tests! + +### Package Dependencies +Some packages include other packages to avoid duplication: + +```yaml +# tests/test_build_components/common/modbus/esp32-idf.yaml +packages: + uart: !include ../uart/esp32-idf.yaml # Modbus requires UART + +substitutions: + flow_control_pin: GPIO4 + +modbus: + - id: modbus_bus + uart_id: uart_bus + flow_control_pin: ${flow_control_pin} +``` + +Components using `modbus` packages automatically get `uart` as well. + +## Pin Allocations + +### I2C (Standard - 50kHz) +- **ESP32 IDF**: SCL=GPIO16, SDA=GPIO17 +- **ESP32 Arduino**: SCL=GPIO22, SDA=GPIO21 +- **ESP32-C3**: SCL=GPIO5, SDA=GPIO4 +- **ESP32-S2/S3**: SCL=GPIO9, SDA=GPIO8 +- **ESP8266**: SCL=GPIO5, SDA=GPIO4 +- **RP2040**: SCL=GPIO5, SDA=GPIO4 +- **BK72xx**: SCL=P20, SDA=P21 + +### I2C Low Frequency (10kHz) +Same pin allocations as standard I2C, but with 10kHz frequency for components requiring slower speeds. + +### SPI +- **ESP32**: CLK=GPIO18, MOSI=GPIO23, MISO=GPIO19 +- **ESP32-C3**: CLK=GPIO6, MOSI=GPIO7, MISO=GPIO5 +- **ESP32-S2**: CLK=GPIO36, MOSI=GPIO35, MISO=GPIO37 +- **ESP32-S3**: CLK=GPIO40, MOSI=GPIO6, MISO=GPIO41 +- **ESP8266**: CLK=GPIO14, MOSI=GPIO13, MISO=GPIO12 +- **RP2040**: CLK=GPIO18, MOSI=GPIO19, MISO=GPIO16 +- CS pins are component-specific (each SPI device needs unique CS) + +### UART +- **ESP32 IDF**: TX=GPIO17, RX=GPIO16 (baud: 19200) +- **ESP32-C3 IDF**: TX=GPIO7, RX=GPIO6 (baud: 19200) +- **ESP8266**: TX=GPIO1, RX=GPIO3 (baud: 19200) +- **RP2040**: TX=GPIO0, RX=GPIO1 (baud: 19200) + +### Modbus (includes UART) +Same UART pins as above, plus: +- **flow_control_pin**: GPIO4 (all platforms) + +### BLE +- **ESP32**: Shared `esp32_ble_tracker` infrastructure +- Each component defines unique `ble_client` with different MAC addresses + +## Benefits + +1. **Shared bus = less duplication** + - 200+ I2C components use common bus configs + - 60+ SPI components use common bus configs + - 80+ UART components use common bus configs + - 6 Modbus components use common modbus configs (which include UART) + +2. **Intelligent grouping reduces CI time** + - Components with identical bus configs are automatically grouped + - Typical reduction: 80-90% fewer builds + - Example: 3 I2C components → 1 merged build (saves 2 builds) + - CI runs 5 component batches in parallel (configurable via `max-parallel` in `.github/workflows/ci.yml`) + +3. **Easier maintenance** + - Change bus pins for a platform once, affects all component tests + - Consistent pin assignments across all components + - Centralized bus configuration + +## Component Compatibility + +### Groupable Components +Components are automatically grouped when they have: +- Identical bus package references (e.g., all use `i2c/esp32-idf.yaml`) +- No local file references (`$component_dir`) +- No `!extend` or `!remove` directives +- Proper bus ID references (`i2c_id: i2c_bus`, `spi_id: spi_bus`, etc.) + +### Non-Groupable Components +Components tested individually when they: +- Use different bus frequencies (e.g., `i2c` vs `i2c_low_freq`) +- Reference local files with `$component_dir` +- Are platform components (abstract base classes with `IS_PLATFORM_COMPONENT = True`) +- Define buses directly (not migrated to packages yet) +- Are in `ISOLATED_COMPONENTS` list (known build conflicts) + +### Bus Variants +- **i2c** (50kHz): Standard I2C frequency for most sensors +- **i2c_low_freq** (10kHz): For sensors requiring slower I2C speeds +- **modbus**: Includes UART via package dependencies + +### Making Components Groupable + +**WARNING**: Using `!extend` or `!remove` directives in component test files prevents automatic component grouping in CI, making builds slower. + +When platform-specific parameters are needed, inline the full configuration rather than using `!extend` or `!remove`. This allows the component to be grouped with other compatible components, reducing CI build time. + +**Note**: Some components legitimately require `!extend` or `!remove` for platform-specific features (e.g., `adc` removing ESP32-only `attenuation` parameter on ESP8266). These are correctly identified as non-groupable. + +## Testing Components + +### Testing Individual Components +Test specific components using `test_build_components`: +```bash +# Test a single component +./script/test_build_components -c bme280_i2c -t esp32-idf -e config + +# Test multiple components +./script/test_build_components -c bme280_i2c,bh1750,sht3xd -t esp32-idf -e compile +``` + +### Testing All Components Together +To verify that all components can be tested together without ID conflicts or configuration issues: +```bash +./script/test_component_grouping.py -e config --all +``` + +This tests all components in a single build to catch conflicts that might not appear when testing components individually. This is useful for: +- Detecting ID conflicts between components +- Validating that components can coexist in the same configuration +- Ensuring proper `i2c_id`, `spi_id`, `uart_id` specifications + +Use `-e config` for fast configuration validation, or `-e compile` for full compilation testing. + +### Testing Component Groups +Test specific groups of components by bus signature: +```bash +# Test all I2C components together +./script/test_component_grouping.py -s i2c -e config + +# Test with custom group sizes +./script/test_component_grouping.py --min-size 5 --max-size 20 --max-groups 3 +``` + +## Implementation Details + +### Scripts +- `script/analyze_component_buses.py`: Analyzes components to detect bus usage and grouping compatibility +- `script/merge_component_configs.py`: Merges multiple component configs into a single test file +- `script/test_build_components.py`: Main test runner with intelligent grouping +- `script/test_component_grouping.py`: Test component groups or all components together +- `script/split_components_for_ci.py`: Splits components into batches for parallel CI execution + +### Configuration +- `.github/workflows/ci.yml`: CI workflow with `max-parallel: 5` for component testing +- Package dependencies defined in `PACKAGE_DEPENDENCIES` (e.g., modbus → uart) +- Base bus components excluded from migration warnings: `i2c`, `spi`, `uart`, `modbus`, `canbus` diff --git a/tests/test_build_components/common/ble/esp32-ard.yaml b/tests/test_build_components/common/ble/esp32-ard.yaml new file mode 100644 index 0000000000..e1b5aec694 --- /dev/null +++ b/tests/test_build_components/common/ble/esp32-ard.yaml @@ -0,0 +1,9 @@ +# Common BLE tracker configuration for ESP32 Arduino tests +# BLE client components share this tracker infrastructure +# Each component defines its own ble_client with unique MAC address + +esp32_ble_tracker: + scan_parameters: + interval: 1100ms + window: 1100ms + active: true diff --git a/tests/test_build_components/common/ble/esp32-c3-idf.yaml b/tests/test_build_components/common/ble/esp32-c3-idf.yaml new file mode 100644 index 0000000000..6671722f21 --- /dev/null +++ b/tests/test_build_components/common/ble/esp32-c3-idf.yaml @@ -0,0 +1,9 @@ +# Common BLE tracker configuration for ESP32-C3 IDF tests +# BLE client components share this tracker infrastructure +# Each component defines its own ble_client with unique MAC address + +esp32_ble_tracker: + scan_parameters: + interval: 1100ms + window: 1100ms + active: true diff --git a/tests/test_build_components/common/ble/esp32-idf.yaml b/tests/test_build_components/common/ble/esp32-idf.yaml new file mode 100644 index 0000000000..7cdcea71f0 --- /dev/null +++ b/tests/test_build_components/common/ble/esp32-idf.yaml @@ -0,0 +1,9 @@ +# Common BLE tracker configuration for ESP32 IDF tests +# BLE client components share this tracker infrastructure +# Each component defines its own ble_client with unique MAC address + +esp32_ble_tracker: + scan_parameters: + interval: 1100ms + window: 1100ms + active: true diff --git a/tests/test_build_components/common/i2c/bk72xx-ard.yaml b/tests/test_build_components/common/i2c/bk72xx-ard.yaml new file mode 100644 index 0000000000..7d00546721 --- /dev/null +++ b/tests/test_build_components/common/i2c/bk72xx-ard.yaml @@ -0,0 +1,11 @@ +# Common I2C configuration for BK72XX Arduino tests + +substitutions: + scl_pin: P26 + sda_pin: P24 + +i2c: + - id: i2c_bus + scl: ${scl_pin} + sda: ${sda_pin} + scan: true diff --git a/tests/test_build_components/common/i2c/esp32-ard.yaml b/tests/test_build_components/common/i2c/esp32-ard.yaml new file mode 100644 index 0000000000..fff09ab85c --- /dev/null +++ b/tests/test_build_components/common/i2c/esp32-ard.yaml @@ -0,0 +1,11 @@ +# Common I2C configuration for ESP32 Arduino tests + +substitutions: + scl_pin: GPIO22 + sda_pin: GPIO21 + +i2c: + - id: i2c_bus + scl: ${scl_pin} + sda: ${sda_pin} + scan: true diff --git a/tests/test_build_components/common/i2c/esp32-c3-ard.yaml b/tests/test_build_components/common/i2c/esp32-c3-ard.yaml new file mode 100644 index 0000000000..0216dccbfa --- /dev/null +++ b/tests/test_build_components/common/i2c/esp32-c3-ard.yaml @@ -0,0 +1,11 @@ +# Common I2C configuration for ESP32-C3 Arduino tests + +substitutions: + scl_pin: GPIO8 + sda_pin: GPIO10 + +i2c: + - id: i2c_bus + scl: ${scl_pin} + sda: ${sda_pin} + scan: true diff --git a/tests/test_build_components/common/i2c/esp32-c3-idf.yaml b/tests/test_build_components/common/i2c/esp32-c3-idf.yaml new file mode 100644 index 0000000000..fd873b6db8 --- /dev/null +++ b/tests/test_build_components/common/i2c/esp32-c3-idf.yaml @@ -0,0 +1,11 @@ +# Common I2C configuration for ESP32-C3 IDF tests + +substitutions: + scl_pin: GPIO8 + sda_pin: GPIO10 + +i2c: + - id: i2c_bus + scl: ${scl_pin} + sda: ${sda_pin} + scan: true diff --git a/tests/test_build_components/common/i2c/esp32-idf.yaml b/tests/test_build_components/common/i2c/esp32-idf.yaml new file mode 100644 index 0000000000..bb2376e7d6 --- /dev/null +++ b/tests/test_build_components/common/i2c/esp32-idf.yaml @@ -0,0 +1,13 @@ +# Common I2C configuration for ESP32 IDF tests +# Provides a shared I2C bus that all components can use +# Components will auto-use this bus if they don't specify i2c_id + +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +i2c: + - id: i2c_bus + scl: ${scl_pin} + sda: ${sda_pin} + scan: true diff --git a/tests/test_build_components/common/i2c/esp32-p4-idf.yaml b/tests/test_build_components/common/i2c/esp32-p4-idf.yaml new file mode 100644 index 0000000000..0c7d7e3414 --- /dev/null +++ b/tests/test_build_components/common/i2c/esp32-p4-idf.yaml @@ -0,0 +1,13 @@ +# Common I2C configuration for ESP32-P4 IDF tests +# Provides a shared I2C bus that all components can use +# Components will auto-use this bus if they don't specify i2c_id + +substitutions: + scl_pin: GPIO8 + sda_pin: GPIO7 + +i2c: + - id: i2c_bus + scl: ${scl_pin} + sda: ${sda_pin} + scan: true diff --git a/tests/test_build_components/common/i2c/esp32-s2-ard.yaml b/tests/test_build_components/common/i2c/esp32-s2-ard.yaml new file mode 100644 index 0000000000..d8201db042 --- /dev/null +++ b/tests/test_build_components/common/i2c/esp32-s2-ard.yaml @@ -0,0 +1,11 @@ +# Common I2C configuration for ESP32-S2 Arduino tests + +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +i2c: + - id: i2c_bus + scl: ${scl_pin} + sda: ${sda_pin} + scan: true diff --git a/tests/test_build_components/common/i2c/esp32-s2-idf.yaml b/tests/test_build_components/common/i2c/esp32-s2-idf.yaml new file mode 100644 index 0000000000..f23f7e35cc --- /dev/null +++ b/tests/test_build_components/common/i2c/esp32-s2-idf.yaml @@ -0,0 +1,11 @@ +# Common I2C configuration for ESP32-S2 IDF tests + +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +i2c: + - id: i2c_bus + scl: ${scl_pin} + sda: ${sda_pin} + scan: true diff --git a/tests/test_build_components/common/i2c/esp32-s3-ard.yaml b/tests/test_build_components/common/i2c/esp32-s3-ard.yaml new file mode 100644 index 0000000000..3499770cb7 --- /dev/null +++ b/tests/test_build_components/common/i2c/esp32-s3-ard.yaml @@ -0,0 +1,11 @@ +# Common I2C configuration for ESP32-S3 Arduino tests + +substitutions: + scl_pin: GPIO9 + sda_pin: GPIO8 + +i2c: + - id: i2c_bus + scl: ${scl_pin} + sda: ${sda_pin} + scan: true diff --git a/tests/test_build_components/common/i2c/esp32-s3-idf.yaml b/tests/test_build_components/common/i2c/esp32-s3-idf.yaml new file mode 100644 index 0000000000..9f5b002eb3 --- /dev/null +++ b/tests/test_build_components/common/i2c/esp32-s3-idf.yaml @@ -0,0 +1,11 @@ +# Common I2C configuration for ESP32-S3 IDF tests + +substitutions: + scl_pin: GPIO9 + sda_pin: GPIO8 + +i2c: + - id: i2c_bus + scl: ${scl_pin} + sda: ${sda_pin} + scan: true diff --git a/tests/test_build_components/common/i2c/esp8266-ard.yaml b/tests/test_build_components/common/i2c/esp8266-ard.yaml new file mode 100644 index 0000000000..6aa28524b2 --- /dev/null +++ b/tests/test_build_components/common/i2c/esp8266-ard.yaml @@ -0,0 +1,11 @@ +# Common I2C configuration for ESP8266 Arduino tests + +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +i2c: + - id: i2c_bus + scl: ${scl_pin} + sda: ${sda_pin} + scan: true diff --git a/tests/test_build_components/common/i2c/nrf52.yaml b/tests/test_build_components/common/i2c/nrf52.yaml new file mode 100644 index 0000000000..b86cdf0d69 --- /dev/null +++ b/tests/test_build_components/common/i2c/nrf52.yaml @@ -0,0 +1,11 @@ +# Common I2C configuration for NRF52 tests + +substitutions: + scl_pin: P0.04 + sda_pin: P0.05 + +i2c: + - id: i2c_bus + scl: ${scl_pin} + sda: ${sda_pin} + scan: true diff --git a/tests/test_build_components/common/i2c/rp2040-ard.yaml b/tests/test_build_components/common/i2c/rp2040-ard.yaml new file mode 100644 index 0000000000..cb241aef76 --- /dev/null +++ b/tests/test_build_components/common/i2c/rp2040-ard.yaml @@ -0,0 +1,11 @@ +# Common I2C configuration for RP2040 Arduino tests + +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +i2c: + - id: i2c_bus + scl: ${scl_pin} + sda: ${sda_pin} + scan: true diff --git a/tests/test_build_components/common/i2c_camera/esp32-idf.yaml b/tests/test_build_components/common/i2c_camera/esp32-idf.yaml new file mode 100644 index 0000000000..443ebbebd9 --- /dev/null +++ b/tests/test_build_components/common/i2c_camera/esp32-idf.yaml @@ -0,0 +1,36 @@ +# I2C bus for camera sensor +psram: + +i2c: + - id: i2c_camera_bus + sda: 25 + scl: 23 + frequency: 400kHz + +esp32_camera: + name: ESP32 Camera + data_pins: + - number: 17 + - number: 35 + - number: 34 + - number: 5 + - number: 39 + - number: 18 + - number: 36 + - number: 19 + vsync_pin: 22 + href_pin: 26 + pixel_clock_pin: 21 + external_clock: + pin: 27 + frequency: 20MHz + i2c_id: i2c_camera_bus + reset_pin: 15 + power_down_pin: 1 + resolution: 640x480 + jpeg_quality: 10 + frame_buffer_location: PSRAM + on_image: + then: + - lambda: |- + ESP_LOGD("main", "image len=%d, data=%c", image.length, image.data[0]); diff --git a/tests/test_build_components/common/i2c_low_freq/esp32-ard.yaml b/tests/test_build_components/common/i2c_low_freq/esp32-ard.yaml new file mode 100644 index 0000000000..53d051f174 --- /dev/null +++ b/tests/test_build_components/common/i2c_low_freq/esp32-ard.yaml @@ -0,0 +1,12 @@ +# Common I2C configuration for ESP32 Arduino tests - Low Frequency (10kHz) + +substitutions: + scl_pin: GPIO22 + sda_pin: GPIO21 + +i2c: + - id: i2c_bus + scl: ${scl_pin} + sda: ${sda_pin} + frequency: 10kHz + scan: true diff --git a/tests/test_build_components/common/i2c_low_freq/esp32-c3-ard.yaml b/tests/test_build_components/common/i2c_low_freq/esp32-c3-ard.yaml new file mode 100644 index 0000000000..c9856c6b29 --- /dev/null +++ b/tests/test_build_components/common/i2c_low_freq/esp32-c3-ard.yaml @@ -0,0 +1,12 @@ +# Common I2C configuration for ESP32-C3 Arduino tests - Low Frequency (10kHz) + +substitutions: + scl_pin: GPIO6 + sda_pin: GPIO5 + +i2c: + - id: i2c_bus + scl: ${scl_pin} + sda: ${sda_pin} + frequency: 10kHz + scan: true diff --git a/tests/test_build_components/common/i2c_low_freq/esp32-c3-idf.yaml b/tests/test_build_components/common/i2c_low_freq/esp32-c3-idf.yaml new file mode 100644 index 0000000000..e9d44b585e --- /dev/null +++ b/tests/test_build_components/common/i2c_low_freq/esp32-c3-idf.yaml @@ -0,0 +1,12 @@ +# Common I2C configuration for ESP32-C3 IDF tests - Low Frequency (10kHz) + +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +i2c: + - id: i2c_bus + scl: ${scl_pin} + sda: ${sda_pin} + frequency: 10kHz + scan: true diff --git a/tests/test_build_components/common/i2c_low_freq/esp32-idf.yaml b/tests/test_build_components/common/i2c_low_freq/esp32-idf.yaml new file mode 100644 index 0000000000..4afe220315 --- /dev/null +++ b/tests/test_build_components/common/i2c_low_freq/esp32-idf.yaml @@ -0,0 +1,13 @@ +# Common I2C configuration for ESP32 IDF tests - Low Frequency (10kHz) +# For components that require I2C frequency <= 15kHz (ags10, ltr501, ltr_als_ps) + +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +i2c: + - id: i2c_bus + scl: ${scl_pin} + sda: ${sda_pin} + frequency: 10kHz + scan: true diff --git a/tests/test_build_components/common/i2c_low_freq/esp8266-ard.yaml b/tests/test_build_components/common/i2c_low_freq/esp8266-ard.yaml new file mode 100644 index 0000000000..281177ebbd --- /dev/null +++ b/tests/test_build_components/common/i2c_low_freq/esp8266-ard.yaml @@ -0,0 +1,12 @@ +# Common I2C configuration for ESP8266 Arduino tests - Low Frequency (10kHz) + +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +i2c: + - id: i2c_bus + scl: ${scl_pin} + sda: ${sda_pin} + frequency: 10kHz + scan: true diff --git a/tests/test_build_components/common/i2c_low_freq/rp2040-ard.yaml b/tests/test_build_components/common/i2c_low_freq/rp2040-ard.yaml new file mode 100644 index 0000000000..67d893e733 --- /dev/null +++ b/tests/test_build_components/common/i2c_low_freq/rp2040-ard.yaml @@ -0,0 +1,12 @@ +# Common I2C configuration for RP2040 Arduino tests - Low Frequency (10kHz) + +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +i2c: + - id: i2c_bus + scl: ${scl_pin} + sda: ${sda_pin} + frequency: 10kHz + scan: true diff --git a/tests/test_build_components/common/modbus/bk72xx-ard.yaml b/tests/test_build_components/common/modbus/bk72xx-ard.yaml new file mode 100644 index 0000000000..c428f0a7be --- /dev/null +++ b/tests/test_build_components/common/modbus/bk72xx-ard.yaml @@ -0,0 +1,12 @@ +# Common Modbus configuration for BK72XX Arduino tests + +packages: + uart: !include ../uart/bk72xx-ard.yaml + +substitutions: + flow_control_pin: P6 + +modbus: + - id: modbus_bus + uart_id: uart_bus + flow_control_pin: ${flow_control_pin} diff --git a/tests/test_build_components/common/modbus/esp32-ard.yaml b/tests/test_build_components/common/modbus/esp32-ard.yaml new file mode 100644 index 0000000000..f327cf266e --- /dev/null +++ b/tests/test_build_components/common/modbus/esp32-ard.yaml @@ -0,0 +1,12 @@ +# Common Modbus configuration for ESP32 Arduino tests + +packages: + uart: !include ../uart/esp32-ard.yaml + +substitutions: + flow_control_pin: GPIO4 + +modbus: + - id: modbus_bus + uart_id: uart_bus + flow_control_pin: ${flow_control_pin} diff --git a/tests/test_build_components/common/modbus/esp32-c3-ard.yaml b/tests/test_build_components/common/modbus/esp32-c3-ard.yaml new file mode 100644 index 0000000000..f12e5518c0 --- /dev/null +++ b/tests/test_build_components/common/modbus/esp32-c3-ard.yaml @@ -0,0 +1,12 @@ +# Common Modbus configuration for ESP32-C3 Arduino tests + +packages: + uart: !include ../uart/esp32-c3-ard.yaml + +substitutions: + flow_control_pin: GPIO4 + +modbus: + - id: modbus_bus + uart_id: uart_bus + flow_control_pin: ${flow_control_pin} diff --git a/tests/test_build_components/common/modbus/esp32-c3-idf.yaml b/tests/test_build_components/common/modbus/esp32-c3-idf.yaml new file mode 100644 index 0000000000..98fc11b0b7 --- /dev/null +++ b/tests/test_build_components/common/modbus/esp32-c3-idf.yaml @@ -0,0 +1,12 @@ +# Common Modbus configuration for ESP32-C3 IDF tests + +packages: + uart: !include ../uart/esp32-c3-idf.yaml + +substitutions: + flow_control_pin: GPIO4 + +modbus: + - id: modbus_bus + uart_id: uart_bus + flow_control_pin: ${flow_control_pin} diff --git a/tests/test_build_components/common/modbus/esp32-idf.yaml b/tests/test_build_components/common/modbus/esp32-idf.yaml new file mode 100644 index 0000000000..c2d777c3d7 --- /dev/null +++ b/tests/test_build_components/common/modbus/esp32-idf.yaml @@ -0,0 +1,13 @@ +# Common Modbus configuration for ESP32 IDF tests +# Provides a shared Modbus bus that all components can use + +packages: + uart: !include ../uart/esp32-idf.yaml + +substitutions: + flow_control_pin: GPIO4 + +modbus: + - id: modbus_bus + uart_id: uart_bus + flow_control_pin: ${flow_control_pin} diff --git a/tests/test_build_components/common/modbus/esp32-s2-ard.yaml b/tests/test_build_components/common/modbus/esp32-s2-ard.yaml new file mode 100644 index 0000000000..a47036f379 --- /dev/null +++ b/tests/test_build_components/common/modbus/esp32-s2-ard.yaml @@ -0,0 +1,12 @@ +# Common Modbus configuration for ESP32-S2 Arduino tests + +packages: + uart: !include ../uart/esp32-s2-ard.yaml + +substitutions: + flow_control_pin: GPIO4 + +modbus: + - id: modbus_bus + uart_id: uart_bus + flow_control_pin: ${flow_control_pin} diff --git a/tests/test_build_components/common/modbus/esp32-s2-idf.yaml b/tests/test_build_components/common/modbus/esp32-s2-idf.yaml new file mode 100644 index 0000000000..2cac82aa15 --- /dev/null +++ b/tests/test_build_components/common/modbus/esp32-s2-idf.yaml @@ -0,0 +1,12 @@ +# Common Modbus configuration for ESP32-S2 IDF tests + +packages: + uart: !include ../uart/esp32-s2-idf.yaml + +substitutions: + flow_control_pin: GPIO4 + +modbus: + - id: modbus_bus + uart_id: uart_bus + flow_control_pin: ${flow_control_pin} diff --git a/tests/test_build_components/common/modbus/esp32-s3-ard.yaml b/tests/test_build_components/common/modbus/esp32-s3-ard.yaml new file mode 100644 index 0000000000..3031f57159 --- /dev/null +++ b/tests/test_build_components/common/modbus/esp32-s3-ard.yaml @@ -0,0 +1,12 @@ +# Common Modbus configuration for ESP32-S3 Arduino tests + +packages: + uart: !include ../uart/esp32-s3-ard.yaml + +substitutions: + flow_control_pin: GPIO4 + +modbus: + - id: modbus_bus + uart_id: uart_bus + flow_control_pin: ${flow_control_pin} diff --git a/tests/test_build_components/common/modbus/esp32-s3-idf.yaml b/tests/test_build_components/common/modbus/esp32-s3-idf.yaml new file mode 100644 index 0000000000..0a0d4dbd07 --- /dev/null +++ b/tests/test_build_components/common/modbus/esp32-s3-idf.yaml @@ -0,0 +1,12 @@ +# Common Modbus configuration for ESP32-S3 IDF tests + +packages: + uart: !include ../uart/esp32-s3-idf.yaml + +substitutions: + flow_control_pin: GPIO4 + +modbus: + - id: modbus_bus + uart_id: uart_bus + flow_control_pin: ${flow_control_pin} diff --git a/tests/test_build_components/common/modbus/esp8266-ard.yaml b/tests/test_build_components/common/modbus/esp8266-ard.yaml new file mode 100644 index 0000000000..fce4c6df1d --- /dev/null +++ b/tests/test_build_components/common/modbus/esp8266-ard.yaml @@ -0,0 +1,12 @@ +# Common Modbus configuration for ESP8266 Arduino tests + +packages: + uart: !include ../uart/esp8266-ard.yaml + +substitutions: + flow_control_pin: GPIO5 + +modbus: + - id: modbus_bus + uart_id: uart_bus + flow_control_pin: ${flow_control_pin} diff --git a/tests/test_build_components/common/modbus/rp2040-ard.yaml b/tests/test_build_components/common/modbus/rp2040-ard.yaml new file mode 100644 index 0000000000..264ad8944f --- /dev/null +++ b/tests/test_build_components/common/modbus/rp2040-ard.yaml @@ -0,0 +1,12 @@ +# Common Modbus configuration for RP2040 Arduino tests + +packages: + uart: !include ../uart/rp2040-ard.yaml + +substitutions: + flow_control_pin: GPIO2 + +modbus: + - id: modbus_bus + uart_id: uart_bus + flow_control_pin: ${flow_control_pin} diff --git a/tests/test_build_components/common/qspi/esp32-s3-idf.yaml b/tests/test_build_components/common/qspi/esp32-s3-idf.yaml new file mode 100644 index 0000000000..22c98ef664 --- /dev/null +++ b/tests/test_build_components/common/qspi/esp32-s3-idf.yaml @@ -0,0 +1,13 @@ +# Common QSPI configuration for ESP32-S3 IDF tests +# For components that need QuadSPI (qspi_dbi displays) + +spi: + - id: quad_spi + type: quad + interface: spi3 + clk_pin: 47 + data_pins: + - 40 + - 41 + - 42 + - 43 diff --git a/tests/test_build_components/common/remote_receiver/esp32-ard.yaml b/tests/test_build_components/common/remote_receiver/esp32-ard.yaml new file mode 100644 index 0000000000..af5c2f2409 --- /dev/null +++ b/tests/test_build_components/common/remote_receiver/esp32-ard.yaml @@ -0,0 +1,12 @@ +# Common remote_receiver configuration for ESP32 Arduino tests +# Provides a shared remote receiver that all components can use +# Components will auto-use this receiver if they don't specify receiver_id + +substitutions: + remote_receiver_pin: GPIO32 + +remote_receiver: + - id: rcvr + pin: ${remote_receiver_pin} + dump: all + tolerance: 25% diff --git a/tests/test_build_components/common/remote_receiver/esp32-c3-ard.yaml b/tests/test_build_components/common/remote_receiver/esp32-c3-ard.yaml new file mode 100644 index 0000000000..26b288b427 --- /dev/null +++ b/tests/test_build_components/common/remote_receiver/esp32-c3-ard.yaml @@ -0,0 +1,12 @@ +# Common remote_receiver configuration for ESP32-C3 Arduino tests +# Provides a shared remote receiver that all components can use +# Components will auto-use this receiver if they don't specify receiver_id + +substitutions: + remote_receiver_pin: GPIO10 + +remote_receiver: + - id: rcvr + pin: ${remote_receiver_pin} + dump: all + tolerance: 25% diff --git a/tests/test_build_components/common/remote_receiver/esp32-c3-idf.yaml b/tests/test_build_components/common/remote_receiver/esp32-c3-idf.yaml new file mode 100644 index 0000000000..ad5acedf55 --- /dev/null +++ b/tests/test_build_components/common/remote_receiver/esp32-c3-idf.yaml @@ -0,0 +1,16 @@ +# Common remote_receiver configuration for ESP32-C3 IDF tests +# Provides a shared remote receiver that all components can use +# Components will auto-use this receiver if they don't specify receiver_id + +substitutions: + remote_receiver_pin: GPIO5 + +remote_receiver: + - id: rcvr + pin: ${remote_receiver_pin} + dump: all + tolerance: 25% + clock_resolution: 2000000 + filter_symbols: 2 + receive_symbols: 4 + rmt_symbols: 64 diff --git a/tests/test_build_components/common/remote_receiver/esp32-idf.yaml b/tests/test_build_components/common/remote_receiver/esp32-idf.yaml new file mode 100644 index 0000000000..2905e22233 --- /dev/null +++ b/tests/test_build_components/common/remote_receiver/esp32-idf.yaml @@ -0,0 +1,16 @@ +# Common remote_receiver configuration for ESP32 IDF tests +# Provides a shared remote receiver that all components can use +# Components will auto-use this receiver if they don't specify receiver_id + +substitutions: + remote_receiver_pin: GPIO32 + +remote_receiver: + - id: rcvr + pin: ${remote_receiver_pin} + dump: all + tolerance: 25% + clock_resolution: 2000000 + filter_symbols: 2 + receive_symbols: 4 + rmt_symbols: 64 diff --git a/tests/test_build_components/common/remote_receiver/esp8266-ard.yaml b/tests/test_build_components/common/remote_receiver/esp8266-ard.yaml new file mode 100644 index 0000000000..e2472d00c5 --- /dev/null +++ b/tests/test_build_components/common/remote_receiver/esp8266-ard.yaml @@ -0,0 +1,12 @@ +# Common remote_receiver configuration for ESP8266 Arduino tests +# Provides a shared remote receiver that all components can use +# Components will auto-use this receiver if they don't specify receiver_id + +substitutions: + remote_receiver_pin: GPIO5 + +remote_receiver: + id: rcvr + pin: ${remote_receiver_pin} + dump: all + tolerance: 25% diff --git a/tests/test_build_components/common/remote_transmitter/bk72xx-ard.yaml b/tests/test_build_components/common/remote_transmitter/bk72xx-ard.yaml new file mode 100644 index 0000000000..b951b8713f --- /dev/null +++ b/tests/test_build_components/common/remote_transmitter/bk72xx-ard.yaml @@ -0,0 +1,11 @@ +# Common remote_transmitter configuration for BK72XX Arduino tests +# Provides a shared remote transmitter that all components can use +# Components will auto-use this transmitter if they don't specify transmitter_id + +substitutions: + remote_transmitter_pin: GPIO6 + +remote_transmitter: + id: xmitr + pin: ${remote_transmitter_pin} + carrier_duty_percent: 50% diff --git a/tests/test_build_components/common/remote_transmitter/esp32-ard.yaml b/tests/test_build_components/common/remote_transmitter/esp32-ard.yaml new file mode 100644 index 0000000000..4378e328af --- /dev/null +++ b/tests/test_build_components/common/remote_transmitter/esp32-ard.yaml @@ -0,0 +1,11 @@ +# Common remote_transmitter configuration for ESP32 Arduino tests +# Provides a shared remote transmitter that all components can use +# Components will auto-use this transmitter if they don't specify transmitter_id + +substitutions: + remote_transmitter_pin: GPIO2 + +remote_transmitter: + id: xmitr + pin: ${remote_transmitter_pin} + carrier_duty_percent: 50% diff --git a/tests/test_build_components/common/remote_transmitter/esp32-c3-idf.yaml b/tests/test_build_components/common/remote_transmitter/esp32-c3-idf.yaml new file mode 100644 index 0000000000..b6b9a87fe1 --- /dev/null +++ b/tests/test_build_components/common/remote_transmitter/esp32-c3-idf.yaml @@ -0,0 +1,13 @@ +# Common remote_transmitter configuration for ESP32-C3 IDF tests +# Provides a shared remote transmitter that all components can use +# Components will auto-use this transmitter if they don't specify transmitter_id + +substitutions: + remote_transmitter_pin: GPIO2 + +remote_transmitter: + - id: xmitr + pin: ${remote_transmitter_pin} + carrier_duty_percent: 50% + clock_resolution: 2000000 + rmt_symbols: 64 diff --git a/tests/test_build_components/common/remote_transmitter/esp32-idf.yaml b/tests/test_build_components/common/remote_transmitter/esp32-idf.yaml new file mode 100644 index 0000000000..1d771b3edd --- /dev/null +++ b/tests/test_build_components/common/remote_transmitter/esp32-idf.yaml @@ -0,0 +1,13 @@ +# Common remote_transmitter configuration for ESP32 IDF tests +# Provides a shared remote transmitter that all components can use +# Components will auto-use this transmitter if they don't specify transmitter_id + +substitutions: + remote_transmitter_pin: GPIO2 + +remote_transmitter: + - id: xmitr + pin: ${remote_transmitter_pin} + carrier_duty_percent: 50% + clock_resolution: 2000000 + rmt_symbols: 64 diff --git a/tests/test_build_components/common/remote_transmitter/esp8266-ard.yaml b/tests/test_build_components/common/remote_transmitter/esp8266-ard.yaml new file mode 100644 index 0000000000..3be59c7997 --- /dev/null +++ b/tests/test_build_components/common/remote_transmitter/esp8266-ard.yaml @@ -0,0 +1,11 @@ +# Common remote_transmitter configuration for ESP8266 Arduino tests +# Provides a shared remote transmitter that all components can use +# Components will auto-use this transmitter if they don't specify transmitter_id + +substitutions: + remote_transmitter_pin: GPIO2 + +remote_transmitter: + id: xmitr + pin: ${remote_transmitter_pin} + carrier_duty_percent: 50% diff --git a/tests/test_build_components/common/spi/bk72xx-ard.yaml b/tests/test_build_components/common/spi/bk72xx-ard.yaml new file mode 100644 index 0000000000..471b147bfa --- /dev/null +++ b/tests/test_build_components/common/spi/bk72xx-ard.yaml @@ -0,0 +1,12 @@ +# Common SPI configuration for BK72XX Arduino tests + +substitutions: + clk_pin: P10 + mosi_pin: P11 + miso_pin: P6 + +spi: + - id: spi_bus + clk_pin: ${clk_pin} + mosi_pin: ${mosi_pin} + miso_pin: ${miso_pin} diff --git a/tests/test_build_components/common/spi/esp32-ard.yaml b/tests/test_build_components/common/spi/esp32-ard.yaml new file mode 100644 index 0000000000..816609688c --- /dev/null +++ b/tests/test_build_components/common/spi/esp32-ard.yaml @@ -0,0 +1,12 @@ +# Common SPI configuration for ESP32 Arduino tests + +substitutions: + clk_pin: GPIO18 + mosi_pin: GPIO23 + miso_pin: GPIO19 + +spi: + - id: spi_bus + clk_pin: ${clk_pin} + mosi_pin: ${mosi_pin} + miso_pin: ${miso_pin} diff --git a/tests/test_build_components/common/spi/esp32-c3-ard.yaml b/tests/test_build_components/common/spi/esp32-c3-ard.yaml new file mode 100644 index 0000000000..da3182f259 --- /dev/null +++ b/tests/test_build_components/common/spi/esp32-c3-ard.yaml @@ -0,0 +1,12 @@ +# Common SPI configuration for ESP32-C3 Arduino tests + +substitutions: + clk_pin: GPIO4 + mosi_pin: GPIO6 + miso_pin: GPIO5 + +spi: + - id: spi_bus + clk_pin: ${clk_pin} + mosi_pin: ${mosi_pin} + miso_pin: ${miso_pin} diff --git a/tests/test_build_components/common/spi/esp32-c3-idf.yaml b/tests/test_build_components/common/spi/esp32-c3-idf.yaml new file mode 100644 index 0000000000..6a8f76c38c --- /dev/null +++ b/tests/test_build_components/common/spi/esp32-c3-idf.yaml @@ -0,0 +1,15 @@ +# Common SPI configuration for ESP32-C3 IDF tests +# Provides a shared SPI bus that all components can use +# Components will auto-use this bus if they don't specify spi_id +# CS pins are component-specific + +substitutions: + clk_pin: GPIO4 + mosi_pin: GPIO6 + miso_pin: GPIO5 + +spi: + - id: spi_bus + clk_pin: ${clk_pin} + mosi_pin: ${mosi_pin} + miso_pin: ${miso_pin} diff --git a/tests/test_build_components/common/spi/esp32-idf.yaml b/tests/test_build_components/common/spi/esp32-idf.yaml new file mode 100644 index 0000000000..c2c39a2bc0 --- /dev/null +++ b/tests/test_build_components/common/spi/esp32-idf.yaml @@ -0,0 +1,15 @@ +# Common SPI configuration for ESP32 IDF tests +# Provides a shared SPI bus that all components can use +# Components will auto-use this bus if they don't specify spi_id +# CS pins are component-specific + +substitutions: + clk_pin: GPIO18 + mosi_pin: GPIO23 + miso_pin: GPIO19 + +spi: + - id: spi_bus + clk_pin: ${clk_pin} + mosi_pin: ${mosi_pin} + miso_pin: ${miso_pin} diff --git a/tests/test_build_components/common/spi/esp32-s2-ard.yaml b/tests/test_build_components/common/spi/esp32-s2-ard.yaml new file mode 100644 index 0000000000..7c8997ae7f --- /dev/null +++ b/tests/test_build_components/common/spi/esp32-s2-ard.yaml @@ -0,0 +1,12 @@ +# Common SPI configuration for ESP32-S2 Arduino tests + +substitutions: + clk_pin: GPIO36 + mosi_pin: GPIO35 + miso_pin: GPIO37 + +spi: + - id: spi_bus + clk_pin: ${clk_pin} + mosi_pin: ${mosi_pin} + miso_pin: ${miso_pin} diff --git a/tests/test_build_components/common/spi/esp32-s2-idf.yaml b/tests/test_build_components/common/spi/esp32-s2-idf.yaml new file mode 100644 index 0000000000..afcd83e94e --- /dev/null +++ b/tests/test_build_components/common/spi/esp32-s2-idf.yaml @@ -0,0 +1,12 @@ +# Common SPI configuration for ESP32-S2 IDF tests + +substitutions: + clk_pin: GPIO36 + mosi_pin: GPIO35 + miso_pin: GPIO37 + +spi: + - id: spi_bus + clk_pin: ${clk_pin} + mosi_pin: ${mosi_pin} + miso_pin: ${miso_pin} diff --git a/tests/test_build_components/common/spi/esp32-s3-ard.yaml b/tests/test_build_components/common/spi/esp32-s3-ard.yaml new file mode 100644 index 0000000000..06d5f65771 --- /dev/null +++ b/tests/test_build_components/common/spi/esp32-s3-ard.yaml @@ -0,0 +1,12 @@ +# Common SPI configuration for ESP32-S3 Arduino tests + +substitutions: + clk_pin: GPIO40 + mosi_pin: GPIO6 + miso_pin: GPIO41 + +spi: + - id: spi_bus + clk_pin: ${clk_pin} + mosi_pin: ${mosi_pin} + miso_pin: ${miso_pin} diff --git a/tests/test_build_components/common/spi/esp32-s3-idf.yaml b/tests/test_build_components/common/spi/esp32-s3-idf.yaml new file mode 100644 index 0000000000..ee47396ec7 --- /dev/null +++ b/tests/test_build_components/common/spi/esp32-s3-idf.yaml @@ -0,0 +1,12 @@ +# Common SPI configuration for ESP32-S3 IDF tests + +substitutions: + clk_pin: GPIO40 + mosi_pin: GPIO6 + miso_pin: GPIO41 + +spi: + - id: spi_bus + clk_pin: ${clk_pin} + mosi_pin: ${mosi_pin} + miso_pin: ${miso_pin} diff --git a/tests/test_build_components/common/spi/esp8266-ard.yaml b/tests/test_build_components/common/spi/esp8266-ard.yaml new file mode 100644 index 0000000000..4320afebb9 --- /dev/null +++ b/tests/test_build_components/common/spi/esp8266-ard.yaml @@ -0,0 +1,12 @@ +# Common SPI configuration for ESP8266 Arduino tests + +substitutions: + clk_pin: GPIO14 + mosi_pin: GPIO13 + miso_pin: GPIO12 + +spi: + - id: spi_bus + clk_pin: ${clk_pin} + mosi_pin: ${mosi_pin} + miso_pin: ${miso_pin} diff --git a/tests/test_build_components/common/spi/rp2040-ard.yaml b/tests/test_build_components/common/spi/rp2040-ard.yaml new file mode 100644 index 0000000000..916a636318 --- /dev/null +++ b/tests/test_build_components/common/spi/rp2040-ard.yaml @@ -0,0 +1,12 @@ +# Common SPI configuration for RP2040 Arduino tests + +substitutions: + clk_pin: GPIO18 + mosi_pin: GPIO19 + miso_pin: GPIO16 + +spi: + - id: spi_bus + clk_pin: ${clk_pin} + mosi_pin: ${mosi_pin} + miso_pin: ${miso_pin} diff --git a/tests/test_build_components/common/uart/bk72xx-ard.yaml b/tests/test_build_components/common/uart/bk72xx-ard.yaml new file mode 100644 index 0000000000..8d1abca70b --- /dev/null +++ b/tests/test_build_components/common/uart/bk72xx-ard.yaml @@ -0,0 +1,13 @@ +# Common UART configuration for BK72XX Arduino tests +# Provides a shared UART bus that components can use +# Components will auto-use this bus if they don't specify uart_id + +substitutions: + tx_pin: TX1 + rx_pin: RX1 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 9600 diff --git a/tests/test_build_components/common/uart/esp32-ard.yaml b/tests/test_build_components/common/uart/esp32-ard.yaml new file mode 100644 index 0000000000..805695def6 --- /dev/null +++ b/tests/test_build_components/common/uart/esp32-ard.yaml @@ -0,0 +1,11 @@ +# Common UART configuration for ESP32 Arduino tests + +substitutions: + tx_pin: GPIO17 + rx_pin: GPIO16 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 9600 diff --git a/tests/test_build_components/common/uart/esp32-c3-ard.yaml b/tests/test_build_components/common/uart/esp32-c3-ard.yaml new file mode 100644 index 0000000000..565b109f9a --- /dev/null +++ b/tests/test_build_components/common/uart/esp32-c3-ard.yaml @@ -0,0 +1,11 @@ +# Common UART configuration for ESP32-C3 Arduino tests + +substitutions: + tx_pin: GPIO20 + rx_pin: GPIO21 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 9600 diff --git a/tests/test_build_components/common/uart/esp32-c3-idf.yaml b/tests/test_build_components/common/uart/esp32-c3-idf.yaml new file mode 100644 index 0000000000..944aa013dd --- /dev/null +++ b/tests/test_build_components/common/uart/esp32-c3-idf.yaml @@ -0,0 +1,13 @@ +# Common UART configuration for ESP32-C3 IDF tests +# Provides a shared UART bus that components can use +# Components will auto-use this bus if they don't specify uart_id + +substitutions: + tx_pin: GPIO20 + rx_pin: GPIO21 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 9600 diff --git a/tests/test_build_components/common/uart/esp32-idf.yaml b/tests/test_build_components/common/uart/esp32-idf.yaml new file mode 100644 index 0000000000..95e5db9fb1 --- /dev/null +++ b/tests/test_build_components/common/uart/esp32-idf.yaml @@ -0,0 +1,13 @@ +# Common UART configuration for ESP32 IDF tests +# Provides a shared UART bus that components can use +# Components will auto-use this bus if they don't specify uart_id + +substitutions: + tx_pin: GPIO17 + rx_pin: GPIO16 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 9600 diff --git a/tests/test_build_components/common/uart/esp8266-ard.yaml b/tests/test_build_components/common/uart/esp8266-ard.yaml new file mode 100644 index 0000000000..e326f4fc0d --- /dev/null +++ b/tests/test_build_components/common/uart/esp8266-ard.yaml @@ -0,0 +1,11 @@ +# Common UART configuration for ESP8266 Arduino tests + +substitutions: + tx_pin: GPIO1 + rx_pin: GPIO3 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 9600 diff --git a/tests/test_build_components/common/uart/rp2040-ard.yaml b/tests/test_build_components/common/uart/rp2040-ard.yaml new file mode 100644 index 0000000000..cd1e54a13b --- /dev/null +++ b/tests/test_build_components/common/uart/rp2040-ard.yaml @@ -0,0 +1,11 @@ +# Common UART configuration for RP2040 Arduino tests + +substitutions: + tx_pin: GPIO0 + rx_pin: GPIO1 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 9600 diff --git a/tests/test_build_components/common/uart_115200/esp32-ard.yaml b/tests/test_build_components/common/uart_115200/esp32-ard.yaml new file mode 100644 index 0000000000..9102910f31 --- /dev/null +++ b/tests/test_build_components/common/uart_115200/esp32-ard.yaml @@ -0,0 +1,11 @@ +# Common UART configuration for ESP32 Arduino tests - 115200 baud + +substitutions: + tx_pin: GPIO17 + rx_pin: GPIO16 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 115200 diff --git a/tests/test_build_components/common/uart_115200/esp32-c3-ard.yaml b/tests/test_build_components/common/uart_115200/esp32-c3-ard.yaml new file mode 100644 index 0000000000..87a969c6a3 --- /dev/null +++ b/tests/test_build_components/common/uart_115200/esp32-c3-ard.yaml @@ -0,0 +1,11 @@ +# Common UART configuration for ESP32-C3 Arduino tests - 115200 baud + +substitutions: + tx_pin: GPIO20 + rx_pin: GPIO21 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 115200 diff --git a/tests/test_build_components/common/uart_115200/esp32-c3-idf.yaml b/tests/test_build_components/common/uart_115200/esp32-c3-idf.yaml new file mode 100644 index 0000000000..f3768592e5 --- /dev/null +++ b/tests/test_build_components/common/uart_115200/esp32-c3-idf.yaml @@ -0,0 +1,12 @@ +# Common UART configuration for ESP32-C3 IDF tests - 115200 baud +# For components that require UART baud rate 115200 (bl0906) + +substitutions: + tx_pin: GPIO20 + rx_pin: GPIO21 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 115200 diff --git a/tests/test_build_components/common/uart_115200/esp32-idf.yaml b/tests/test_build_components/common/uart_115200/esp32-idf.yaml new file mode 100644 index 0000000000..e405f74fe7 --- /dev/null +++ b/tests/test_build_components/common/uart_115200/esp32-idf.yaml @@ -0,0 +1,12 @@ +# Common UART configuration for ESP32 IDF tests - 115200 baud +# For components that require UART baud rate 115200 (bl0906) + +substitutions: + tx_pin: GPIO17 + rx_pin: GPIO16 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 115200 diff --git a/tests/test_build_components/common/uart_115200/esp8266-ard.yaml b/tests/test_build_components/common/uart_115200/esp8266-ard.yaml new file mode 100644 index 0000000000..2dcf1c4a5d --- /dev/null +++ b/tests/test_build_components/common/uart_115200/esp8266-ard.yaml @@ -0,0 +1,11 @@ +# Common UART configuration for ESP8266 Arduino tests - 115200 baud + +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 115200 diff --git a/tests/test_build_components/common/uart_115200/rp2040-ard.yaml b/tests/test_build_components/common/uart_115200/rp2040-ard.yaml new file mode 100644 index 0000000000..62a7b5aed2 --- /dev/null +++ b/tests/test_build_components/common/uart_115200/rp2040-ard.yaml @@ -0,0 +1,11 @@ +# Common UART configuration for RP2040 Arduino tests - 115200 baud + +substitutions: + tx_pin: GPIO0 + rx_pin: GPIO1 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 115200 diff --git a/tests/test_build_components/common/uart_1200/esp32-ard.yaml b/tests/test_build_components/common/uart_1200/esp32-ard.yaml new file mode 100644 index 0000000000..0ff5663d1f --- /dev/null +++ b/tests/test_build_components/common/uart_1200/esp32-ard.yaml @@ -0,0 +1,11 @@ +# Common UART configuration for ESP32 Arduino tests - 1200 baud + +substitutions: + tx_pin: GPIO17 + rx_pin: GPIO16 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 1200 diff --git a/tests/test_build_components/common/uart_1200/esp32-c3-ard.yaml b/tests/test_build_components/common/uart_1200/esp32-c3-ard.yaml new file mode 100644 index 0000000000..81cad70d3c --- /dev/null +++ b/tests/test_build_components/common/uart_1200/esp32-c3-ard.yaml @@ -0,0 +1,11 @@ +# Common UART configuration for ESP32-C3 Arduino tests - 1200 baud + +substitutions: + tx_pin: GPIO20 + rx_pin: GPIO21 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 1200 diff --git a/tests/test_build_components/common/uart_1200/esp32-c3-idf.yaml b/tests/test_build_components/common/uart_1200/esp32-c3-idf.yaml new file mode 100644 index 0000000000..8f1dace337 --- /dev/null +++ b/tests/test_build_components/common/uart_1200/esp32-c3-idf.yaml @@ -0,0 +1,11 @@ +# Common UART configuration for ESP32-C3 IDF tests - 1200 baud + +substitutions: + tx_pin: GPIO20 + rx_pin: GPIO21 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 1200 diff --git a/tests/test_build_components/common/uart_1200/esp32-idf.yaml b/tests/test_build_components/common/uart_1200/esp32-idf.yaml new file mode 100644 index 0000000000..38ad1b1459 --- /dev/null +++ b/tests/test_build_components/common/uart_1200/esp32-idf.yaml @@ -0,0 +1,11 @@ +# Common UART configuration for ESP32 IDF tests - 1200 baud + +substitutions: + tx_pin: GPIO17 + rx_pin: GPIO16 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 1200 diff --git a/tests/test_build_components/common/uart_1200/esp8266-ard.yaml b/tests/test_build_components/common/uart_1200/esp8266-ard.yaml new file mode 100644 index 0000000000..84907a3a42 --- /dev/null +++ b/tests/test_build_components/common/uart_1200/esp8266-ard.yaml @@ -0,0 +1,11 @@ +# Common UART configuration for ESP8266 Arduino tests - 1200 baud + +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 1200 diff --git a/tests/test_build_components/common/uart_1200/rp2040-ard.yaml b/tests/test_build_components/common/uart_1200/rp2040-ard.yaml new file mode 100644 index 0000000000..3a3b322ea8 --- /dev/null +++ b/tests/test_build_components/common/uart_1200/rp2040-ard.yaml @@ -0,0 +1,11 @@ +# Common UART configuration for RP2040 Arduino tests - 1200 baud + +substitutions: + tx_pin: GPIO0 + rx_pin: GPIO1 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 1200 diff --git a/tests/test_build_components/common/uart_1200_even/esp32-ard.yaml b/tests/test_build_components/common/uart_1200_even/esp32-ard.yaml new file mode 100644 index 0000000000..f5f7f0669f --- /dev/null +++ b/tests/test_build_components/common/uart_1200_even/esp32-ard.yaml @@ -0,0 +1,12 @@ +# Common UART configuration for ESP32 Arduino tests - 1200 baud even parity + +substitutions: + tx_pin: GPIO17 + rx_pin: GPIO16 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 1200 + parity: EVEN diff --git a/tests/test_build_components/common/uart_1200_even/esp32-c3-ard.yaml b/tests/test_build_components/common/uart_1200_even/esp32-c3-ard.yaml new file mode 100644 index 0000000000..0b1e3ba61b --- /dev/null +++ b/tests/test_build_components/common/uart_1200_even/esp32-c3-ard.yaml @@ -0,0 +1,12 @@ +# Common UART configuration for ESP32-C3 Arduino tests - 1200 baud even parity + +substitutions: + tx_pin: GPIO20 + rx_pin: GPIO21 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 1200 + parity: EVEN diff --git a/tests/test_build_components/common/uart_1200_even/esp32-c3-idf.yaml b/tests/test_build_components/common/uart_1200_even/esp32-c3-idf.yaml new file mode 100644 index 0000000000..1781babefb --- /dev/null +++ b/tests/test_build_components/common/uart_1200_even/esp32-c3-idf.yaml @@ -0,0 +1,12 @@ +# Common UART configuration for ESP32-C3 IDF tests - 1200 baud, EVEN parity + +substitutions: + tx_pin: GPIO20 + rx_pin: GPIO21 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 1200 + parity: EVEN diff --git a/tests/test_build_components/common/uart_1200_even/esp32-idf.yaml b/tests/test_build_components/common/uart_1200_even/esp32-idf.yaml new file mode 100644 index 0000000000..3b4b17f892 --- /dev/null +++ b/tests/test_build_components/common/uart_1200_even/esp32-idf.yaml @@ -0,0 +1,12 @@ +# Common UART configuration for ESP32 IDF tests - 1200 baud, EVEN parity + +substitutions: + tx_pin: GPIO17 + rx_pin: GPIO16 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 1200 + parity: EVEN diff --git a/tests/test_build_components/common/uart_1200_even/esp8266-ard.yaml b/tests/test_build_components/common/uart_1200_even/esp8266-ard.yaml new file mode 100644 index 0000000000..54f7de1757 --- /dev/null +++ b/tests/test_build_components/common/uart_1200_even/esp8266-ard.yaml @@ -0,0 +1,12 @@ +# Common UART configuration for ESP8266 Arduino tests - 1200 baud even parity + +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 1200 + parity: EVEN diff --git a/tests/test_build_components/common/uart_1200_even/rp2040-ard.yaml b/tests/test_build_components/common/uart_1200_even/rp2040-ard.yaml new file mode 100644 index 0000000000..0e8bdeae1f --- /dev/null +++ b/tests/test_build_components/common/uart_1200_even/rp2040-ard.yaml @@ -0,0 +1,12 @@ +# Common UART configuration for RP2040 Arduino tests - 1200 baud even parity + +substitutions: + tx_pin: GPIO0 + rx_pin: GPIO1 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 1200 + parity: EVEN diff --git a/tests/test_build_components/common/uart_19200/esp32-ard.yaml b/tests/test_build_components/common/uart_19200/esp32-ard.yaml new file mode 100644 index 0000000000..f4f04669da --- /dev/null +++ b/tests/test_build_components/common/uart_19200/esp32-ard.yaml @@ -0,0 +1,11 @@ +# Common UART configuration for ESP32 Arduino tests - 19200 baud + +substitutions: + tx_pin: GPIO17 + rx_pin: GPIO16 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 19200 diff --git a/tests/test_build_components/common/uart_19200/esp32-c3-ard.yaml b/tests/test_build_components/common/uart_19200/esp32-c3-ard.yaml new file mode 100644 index 0000000000..925acdc34c --- /dev/null +++ b/tests/test_build_components/common/uart_19200/esp32-c3-ard.yaml @@ -0,0 +1,11 @@ +# Common UART configuration for ESP32-C3 Arduino tests - 19200 baud + +substitutions: + tx_pin: GPIO20 + rx_pin: GPIO21 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 19200 diff --git a/tests/test_build_components/common/uart_19200/esp32-c3-idf.yaml b/tests/test_build_components/common/uart_19200/esp32-c3-idf.yaml new file mode 100644 index 0000000000..0d765a88a4 --- /dev/null +++ b/tests/test_build_components/common/uart_19200/esp32-c3-idf.yaml @@ -0,0 +1,12 @@ +# Common UART configuration for ESP32-C3 IDF tests - 19200 baud +# For components that require UART baud rate 19200 (bl0906) + +substitutions: + tx_pin: GPIO20 + rx_pin: GPIO21 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 19200 diff --git a/tests/test_build_components/common/uart_19200/esp32-idf.yaml b/tests/test_build_components/common/uart_19200/esp32-idf.yaml new file mode 100644 index 0000000000..e7849508c7 --- /dev/null +++ b/tests/test_build_components/common/uart_19200/esp32-idf.yaml @@ -0,0 +1,12 @@ +# Common UART configuration for ESP32 IDF tests - 19200 baud +# For components that require UART baud rate 19200 (bl0906) + +substitutions: + tx_pin: GPIO17 + rx_pin: GPIO16 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 19200 diff --git a/tests/test_build_components/common/uart_19200/esp8266-ard.yaml b/tests/test_build_components/common/uart_19200/esp8266-ard.yaml new file mode 100644 index 0000000000..f01bc4590c --- /dev/null +++ b/tests/test_build_components/common/uart_19200/esp8266-ard.yaml @@ -0,0 +1,11 @@ +# Common UART configuration for ESP8266 Arduino tests - 19200 baud + +substitutions: + tx_pin: GPIO1 + rx_pin: GPIO3 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 19200 diff --git a/tests/test_build_components/common/uart_19200/rp2040-ard.yaml b/tests/test_build_components/common/uart_19200/rp2040-ard.yaml new file mode 100644 index 0000000000..6ebd02d451 --- /dev/null +++ b/tests/test_build_components/common/uart_19200/rp2040-ard.yaml @@ -0,0 +1,11 @@ +# Common UART configuration for RP2040 Arduino tests - 19200 baud + +substitutions: + tx_pin: GPIO0 + rx_pin: GPIO1 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 19200 diff --git a/tests/test_build_components/common/uart_38400/esp32-ard.yaml b/tests/test_build_components/common/uart_38400/esp32-ard.yaml new file mode 100644 index 0000000000..15da771ccc --- /dev/null +++ b/tests/test_build_components/common/uart_38400/esp32-ard.yaml @@ -0,0 +1,11 @@ +# Common UART configuration for ESP32 Arduino tests - 38400 baud + +substitutions: + tx_pin: GPIO17 + rx_pin: GPIO16 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 38400 diff --git a/tests/test_build_components/common/uart_38400/esp32-c3-ard.yaml b/tests/test_build_components/common/uart_38400/esp32-c3-ard.yaml new file mode 100644 index 0000000000..8838f029dc --- /dev/null +++ b/tests/test_build_components/common/uart_38400/esp32-c3-ard.yaml @@ -0,0 +1,11 @@ +# Common UART configuration for ESP32-C3 Arduino tests - 38400 baud + +substitutions: + tx_pin: GPIO20 + rx_pin: GPIO21 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 38400 diff --git a/tests/test_build_components/common/uart_38400/esp32-c3-idf.yaml b/tests/test_build_components/common/uart_38400/esp32-c3-idf.yaml new file mode 100644 index 0000000000..d7d902af3d --- /dev/null +++ b/tests/test_build_components/common/uart_38400/esp32-c3-idf.yaml @@ -0,0 +1,11 @@ +# Common UART configuration for ESP32-C3 IDF tests - 38400 baud + +substitutions: + tx_pin: GPIO20 + rx_pin: GPIO21 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 38400 diff --git a/tests/test_build_components/common/uart_38400/esp32-idf.yaml b/tests/test_build_components/common/uart_38400/esp32-idf.yaml new file mode 100644 index 0000000000..f1c9587e27 --- /dev/null +++ b/tests/test_build_components/common/uart_38400/esp32-idf.yaml @@ -0,0 +1,11 @@ +# Common UART configuration for ESP32 IDF tests - 38400 baud + +substitutions: + tx_pin: GPIO17 + rx_pin: GPIO16 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 38400 diff --git a/tests/test_build_components/common/uart_38400/esp8266-ard.yaml b/tests/test_build_components/common/uart_38400/esp8266-ard.yaml new file mode 100644 index 0000000000..b1a046ea5e --- /dev/null +++ b/tests/test_build_components/common/uart_38400/esp8266-ard.yaml @@ -0,0 +1,11 @@ +# Common UART configuration for ESP8266 Arduino tests - 38400 baud + +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 38400 diff --git a/tests/test_build_components/common/uart_38400/rp2040-ard.yaml b/tests/test_build_components/common/uart_38400/rp2040-ard.yaml new file mode 100644 index 0000000000..01b5e58ed7 --- /dev/null +++ b/tests/test_build_components/common/uart_38400/rp2040-ard.yaml @@ -0,0 +1,11 @@ +# Common UART configuration for RP2040 Arduino tests - 38400 baud + +substitutions: + tx_pin: GPIO0 + rx_pin: GPIO1 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 38400 diff --git a/tests/test_build_components/common/uart_4800/esp32-ard.yaml b/tests/test_build_components/common/uart_4800/esp32-ard.yaml new file mode 100644 index 0000000000..7f7096e31d --- /dev/null +++ b/tests/test_build_components/common/uart_4800/esp32-ard.yaml @@ -0,0 +1,11 @@ +# Common UART configuration for ESP32 Arduino tests - 4800 baud + +substitutions: + tx_pin: GPIO17 + rx_pin: GPIO16 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 4800 diff --git a/tests/test_build_components/common/uart_4800/esp32-c3-ard.yaml b/tests/test_build_components/common/uart_4800/esp32-c3-ard.yaml new file mode 100644 index 0000000000..3c814b76e4 --- /dev/null +++ b/tests/test_build_components/common/uart_4800/esp32-c3-ard.yaml @@ -0,0 +1,11 @@ +# Common UART configuration for ESP32-C3 Arduino tests - 4800 baud + +substitutions: + tx_pin: GPIO20 + rx_pin: GPIO21 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 4800 diff --git a/tests/test_build_components/common/uart_4800/esp32-c3-idf.yaml b/tests/test_build_components/common/uart_4800/esp32-c3-idf.yaml new file mode 100644 index 0000000000..7b18789b19 --- /dev/null +++ b/tests/test_build_components/common/uart_4800/esp32-c3-idf.yaml @@ -0,0 +1,11 @@ +# Common UART configuration for ESP32-C3 IDF tests - 4800 baud + +substitutions: + tx_pin: GPIO20 + rx_pin: GPIO21 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 4800 diff --git a/tests/test_build_components/common/uart_4800/esp32-idf.yaml b/tests/test_build_components/common/uart_4800/esp32-idf.yaml new file mode 100644 index 0000000000..c2bd5b3edd --- /dev/null +++ b/tests/test_build_components/common/uart_4800/esp32-idf.yaml @@ -0,0 +1,11 @@ +# Common UART configuration for ESP32 IDF tests - 4800 baud + +substitutions: + tx_pin: GPIO17 + rx_pin: GPIO16 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 4800 diff --git a/tests/test_build_components/common/uart_4800/esp8266-ard.yaml b/tests/test_build_components/common/uart_4800/esp8266-ard.yaml new file mode 100644 index 0000000000..afdbf4a599 --- /dev/null +++ b/tests/test_build_components/common/uart_4800/esp8266-ard.yaml @@ -0,0 +1,11 @@ +# Common UART configuration for ESP8266 Arduino tests - 4800 baud + +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 4800 diff --git a/tests/test_build_components/common/uart_4800/rp2040-ard.yaml b/tests/test_build_components/common/uart_4800/rp2040-ard.yaml new file mode 100644 index 0000000000..3bf0d6ba47 --- /dev/null +++ b/tests/test_build_components/common/uart_4800/rp2040-ard.yaml @@ -0,0 +1,11 @@ +# Common UART configuration for RP2040 Arduino tests - 4800 baud + +substitutions: + tx_pin: GPIO0 + rx_pin: GPIO1 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 4800 diff --git a/tests/test_build_components/common/uart_4800_even/esp32-ard.yaml b/tests/test_build_components/common/uart_4800_even/esp32-ard.yaml new file mode 100644 index 0000000000..053848615b --- /dev/null +++ b/tests/test_build_components/common/uart_4800_even/esp32-ard.yaml @@ -0,0 +1,12 @@ +# Common UART configuration for ESP32 Arduino tests - 4800 baud even parity + +substitutions: + tx_pin: GPIO17 + rx_pin: GPIO16 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 4800 + parity: EVEN diff --git a/tests/test_build_components/common/uart_4800_even/esp32-c3-ard.yaml b/tests/test_build_components/common/uart_4800_even/esp32-c3-ard.yaml new file mode 100644 index 0000000000..b1370bc1cb --- /dev/null +++ b/tests/test_build_components/common/uart_4800_even/esp32-c3-ard.yaml @@ -0,0 +1,12 @@ +# Common UART configuration for ESP32-C3 Arduino tests - 4800 baud even parity + +substitutions: + tx_pin: GPIO20 + rx_pin: GPIO21 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 4800 + parity: EVEN diff --git a/tests/test_build_components/common/uart_4800_even/esp32-c3-idf.yaml b/tests/test_build_components/common/uart_4800_even/esp32-c3-idf.yaml new file mode 100644 index 0000000000..173c768937 --- /dev/null +++ b/tests/test_build_components/common/uart_4800_even/esp32-c3-idf.yaml @@ -0,0 +1,12 @@ +# Common UART configuration for ESP32-C3 IDF tests - 4800 baud, EVEN parity + +substitutions: + tx_pin: GPIO20 + rx_pin: GPIO21 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 4800 + parity: EVEN diff --git a/tests/test_build_components/common/uart_4800_even/esp32-idf.yaml b/tests/test_build_components/common/uart_4800_even/esp32-idf.yaml new file mode 100644 index 0000000000..eb850ec2dd --- /dev/null +++ b/tests/test_build_components/common/uart_4800_even/esp32-idf.yaml @@ -0,0 +1,12 @@ +# Common UART configuration for ESP32 IDF tests - 4800 baud, EVEN parity + +substitutions: + tx_pin: GPIO17 + rx_pin: GPIO16 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 4800 + parity: EVEN diff --git a/tests/test_build_components/common/uart_4800_even/esp8266-ard.yaml b/tests/test_build_components/common/uart_4800_even/esp8266-ard.yaml new file mode 100644 index 0000000000..0b6bd9eac1 --- /dev/null +++ b/tests/test_build_components/common/uart_4800_even/esp8266-ard.yaml @@ -0,0 +1,12 @@ +# Common UART configuration for ESP8266 Arduino tests - 4800 baud even parity + +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 4800 + parity: EVEN diff --git a/tests/test_build_components/common/uart_4800_even/rp2040-ard.yaml b/tests/test_build_components/common/uart_4800_even/rp2040-ard.yaml new file mode 100644 index 0000000000..c99421b791 --- /dev/null +++ b/tests/test_build_components/common/uart_4800_even/rp2040-ard.yaml @@ -0,0 +1,12 @@ +# Common UART configuration for RP2040 Arduino tests - 4800 baud even parity + +substitutions: + tx_pin: GPIO0 + rx_pin: GPIO1 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 4800 + parity: EVEN diff --git a/tests/test_build_components/common/uart_9600_even/esp32-ard.yaml b/tests/test_build_components/common/uart_9600_even/esp32-ard.yaml new file mode 100644 index 0000000000..0a04f10705 --- /dev/null +++ b/tests/test_build_components/common/uart_9600_even/esp32-ard.yaml @@ -0,0 +1,12 @@ +# Common UART configuration for ESP32 Arduino tests - 9600 baud even parity + +substitutions: + tx_pin: GPIO17 + rx_pin: GPIO16 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 9600 + parity: EVEN diff --git a/tests/test_build_components/common/uart_9600_even/esp32-c3-ard.yaml b/tests/test_build_components/common/uart_9600_even/esp32-c3-ard.yaml new file mode 100644 index 0000000000..1341a91b4e --- /dev/null +++ b/tests/test_build_components/common/uart_9600_even/esp32-c3-ard.yaml @@ -0,0 +1,12 @@ +# Common UART configuration for ESP32-C3 Arduino tests - 9600 baud even parity + +substitutions: + tx_pin: GPIO20 + rx_pin: GPIO21 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 9600 + parity: EVEN diff --git a/tests/test_build_components/common/uart_9600_even/esp32-c3-idf.yaml b/tests/test_build_components/common/uart_9600_even/esp32-c3-idf.yaml new file mode 100644 index 0000000000..5a7bce2198 --- /dev/null +++ b/tests/test_build_components/common/uart_9600_even/esp32-c3-idf.yaml @@ -0,0 +1,12 @@ +# Common UART configuration for ESP32-C3 IDF tests - 9600 baud, EVEN parity + +substitutions: + tx_pin: GPIO20 + rx_pin: GPIO21 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 9600 + parity: EVEN diff --git a/tests/test_build_components/common/uart_9600_even/esp32-idf.yaml b/tests/test_build_components/common/uart_9600_even/esp32-idf.yaml new file mode 100644 index 0000000000..b60cf71b17 --- /dev/null +++ b/tests/test_build_components/common/uart_9600_even/esp32-idf.yaml @@ -0,0 +1,12 @@ +# Common UART configuration for ESP32 IDF tests - 9600 baud, EVEN parity + +substitutions: + tx_pin: GPIO17 + rx_pin: GPIO16 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 9600 + parity: EVEN diff --git a/tests/test_build_components/common/uart_9600_even/esp8266-ard.yaml b/tests/test_build_components/common/uart_9600_even/esp8266-ard.yaml new file mode 100644 index 0000000000..300ec842df --- /dev/null +++ b/tests/test_build_components/common/uart_9600_even/esp8266-ard.yaml @@ -0,0 +1,12 @@ +# Common UART configuration for ESP8266 Arduino tests - 9600 baud even parity + +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 9600 + parity: EVEN diff --git a/tests/test_build_components/common/uart_9600_even/rp2040-ard.yaml b/tests/test_build_components/common/uart_9600_even/rp2040-ard.yaml new file mode 100644 index 0000000000..c281ae84b5 --- /dev/null +++ b/tests/test_build_components/common/uart_9600_even/rp2040-ard.yaml @@ -0,0 +1,12 @@ +# Common UART configuration for RP2040 Arduino tests - 9600 baud even parity + +substitutions: + tx_pin: GPIO0 + rx_pin: GPIO1 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 9600 + parity: EVEN diff --git a/tests/test_build_components/common/uart_bridge_2/esp32-idf.yaml b/tests/test_build_components/common/uart_bridge_2/esp32-idf.yaml new file mode 100644 index 0000000000..ff8a2f8d13 --- /dev/null +++ b/tests/test_build_components/common/uart_bridge_2/esp32-idf.yaml @@ -0,0 +1,11 @@ +# Common configuration for 2-channel UART bridge/expander chips +# Used by components like wk2132 that create 2 UART channels +# Defines standardized UART IDs: uart_id_0, uart_id_1 + +substitutions: + # These will be overridden by component-specific values + uart_bridge_address: "0x70" + +# Note: The actual UART instances are created by the bridge component +# This package just ensures all bridge components use the same ID naming convention +# so they can be grouped together without conflicts diff --git a/tests/test_build_components/common/uart_bridge_2/esp32-s3-idf.yaml b/tests/test_build_components/common/uart_bridge_2/esp32-s3-idf.yaml new file mode 100644 index 0000000000..ff8a2f8d13 --- /dev/null +++ b/tests/test_build_components/common/uart_bridge_2/esp32-s3-idf.yaml @@ -0,0 +1,11 @@ +# Common configuration for 2-channel UART bridge/expander chips +# Used by components like wk2132 that create 2 UART channels +# Defines standardized UART IDs: uart_id_0, uart_id_1 + +substitutions: + # These will be overridden by component-specific values + uart_bridge_address: "0x70" + +# Note: The actual UART instances are created by the bridge component +# This package just ensures all bridge components use the same ID naming convention +# so they can be grouped together without conflicts diff --git a/tests/test_build_components/common/uart_bridge_4/esp32-idf.yaml b/tests/test_build_components/common/uart_bridge_4/esp32-idf.yaml new file mode 100644 index 0000000000..bb88037947 --- /dev/null +++ b/tests/test_build_components/common/uart_bridge_4/esp32-idf.yaml @@ -0,0 +1,11 @@ +# Common configuration for 4-channel UART bridge/expander chips +# Used by components like wk2168, wk2204, wk2212 that create 4 UART channels +# Defines standardized UART IDs: uart_id_0, uart_id_1, uart_id_2, uart_id_3 + +substitutions: + # These will be overridden by component-specific values + uart_bridge_address: "0x70" + +# Note: The actual UART instances are created by the bridge component +# This package just ensures all bridge components use the same ID naming convention +# so they can be grouped together without conflicts diff --git a/tests/test_build_components/common/uart_bridge_4/esp32-s3-idf.yaml b/tests/test_build_components/common/uart_bridge_4/esp32-s3-idf.yaml new file mode 100644 index 0000000000..bb88037947 --- /dev/null +++ b/tests/test_build_components/common/uart_bridge_4/esp32-s3-idf.yaml @@ -0,0 +1,11 @@ +# Common configuration for 4-channel UART bridge/expander chips +# Used by components like wk2168, wk2204, wk2212 that create 4 UART channels +# Defines standardized UART IDs: uart_id_0, uart_id_1, uart_id_2, uart_id_3 + +substitutions: + # These will be overridden by component-specific values + uart_bridge_address: "0x70" + +# Note: The actual UART instances are created by the bridge component +# This package just ensures all bridge components use the same ID naming convention +# so they can be grouped together without conflicts diff --git a/tests/test_build_components/partitions_testing.csv b/tests/test_build_components/partitions_testing.csv new file mode 100644 index 0000000000..0ca8c24e05 --- /dev/null +++ b/tests/test_build_components/partitions_testing.csv @@ -0,0 +1,10 @@ +# ESP-IDF Partition Table for ESPHome Component Testing +# Single app partition to maximize space for large component group testing +# Fits in 4MB flash +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x4000, +otadata, data, ota, , 0x2000, +phy_init, data, phy, , 0x1000, +factory, app, factory, 0x10000, 0x300000, +nvs_key, data, nvs_keys,, 0x1000, +coredump, data, coredump,, 0xEB000, diff --git a/tests/unit_tests/build_gen/test_platformio.py b/tests/unit_tests/build_gen/test_platformio.py new file mode 100644 index 0000000000..a124dbc128 --- /dev/null +++ b/tests/unit_tests/build_gen/test_platformio.py @@ -0,0 +1,188 @@ +"""Tests for esphome.build_gen.platformio module.""" + +from __future__ import annotations + +from collections.abc import Generator +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest + +from esphome.build_gen import platformio +from esphome.core import CORE + + +@pytest.fixture +def mock_update_storage_json() -> Generator[MagicMock]: + """Mock update_storage_json for all tests.""" + with patch("esphome.build_gen.platformio.update_storage_json") as mock: + yield mock + + +@pytest.fixture +def mock_write_file_if_changed() -> Generator[MagicMock]: + """Mock write_file_if_changed for tests.""" + with patch("esphome.build_gen.platformio.write_file_if_changed") as mock: + yield mock + + +def test_write_ini_creates_new_file( + tmp_path: Path, mock_update_storage_json: MagicMock +) -> None: + """Test write_ini creates a new platformio.ini file.""" + CORE.build_path = str(tmp_path) + + content = """ +[env:test] +platform = espressif32 +board = esp32dev +framework = arduino +""" + + platformio.write_ini(content) + + ini_file = tmp_path / "platformio.ini" + assert ini_file.exists() + + file_content = ini_file.read_text() + assert content in file_content + assert platformio.INI_AUTO_GENERATE_BEGIN in file_content + assert platformio.INI_AUTO_GENERATE_END in file_content + + +def test_write_ini_updates_existing_file( + tmp_path: Path, mock_update_storage_json: MagicMock +) -> None: + """Test write_ini updates existing platformio.ini file.""" + CORE.build_path = str(tmp_path) + + # Create existing file with custom content + ini_file = tmp_path / "platformio.ini" + existing_content = f""" +; Custom header +[platformio] +default_envs = test + +{platformio.INI_AUTO_GENERATE_BEGIN} +; Old auto-generated content +[env:old] +platform = old +{platformio.INI_AUTO_GENERATE_END} + +; Custom footer +""" + ini_file.write_text(existing_content) + + # New content to write + new_content = """ +[env:test] +platform = espressif32 +board = esp32dev +framework = arduino +""" + + platformio.write_ini(new_content) + + file_content = ini_file.read_text() + + # Check that custom parts are preserved + assert "; Custom header" in file_content + assert "[platformio]" in file_content + assert "default_envs = test" in file_content + assert "; Custom footer" in file_content + + # Check that new content replaced old auto-generated content + assert new_content in file_content + assert "[env:old]" not in file_content + assert "platform = old" not in file_content + + +def test_write_ini_preserves_custom_sections( + tmp_path: Path, mock_update_storage_json: MagicMock +) -> None: + """Test write_ini preserves custom sections outside auto-generate markers.""" + CORE.build_path = str(tmp_path) + + # Create existing file with multiple custom sections + ini_file = tmp_path / "platformio.ini" + existing_content = f""" +[platformio] +src_dir = . +include_dir = . + +[common] +lib_deps = + Wire + SPI + +{platformio.INI_AUTO_GENERATE_BEGIN} +[env:old] +platform = old +{platformio.INI_AUTO_GENERATE_END} + +[env:custom] +upload_speed = 921600 +monitor_speed = 115200 +""" + ini_file.write_text(existing_content) + + new_content = "[env:auto]\nplatform = new" + + platformio.write_ini(new_content) + + file_content = ini_file.read_text() + + # All custom sections should be preserved + assert "[platformio]" in file_content + assert "src_dir = ." in file_content + assert "[common]" in file_content + assert "lib_deps" in file_content + assert "[env:custom]" in file_content + assert "upload_speed = 921600" in file_content + + # New auto-generated content should replace old + assert "[env:auto]" in file_content + assert "platform = new" in file_content + assert "[env:old]" not in file_content + + +def test_write_ini_no_change_when_content_same( + tmp_path: Path, + mock_update_storage_json: MagicMock, + mock_write_file_if_changed: MagicMock, +) -> None: + """Test write_ini doesn't rewrite file when content is unchanged.""" + CORE.build_path = str(tmp_path) + + content = "[env:test]\nplatform = esp32" + full_content = ( + f"{platformio.INI_BASE_FORMAT[0]}" + f"{platformio.INI_AUTO_GENERATE_BEGIN}\n" + f"{content}" + f"{platformio.INI_AUTO_GENERATE_END}" + f"{platformio.INI_BASE_FORMAT[1]}" + ) + + ini_file = tmp_path / "platformio.ini" + ini_file.write_text(full_content) + + mock_write_file_if_changed.return_value = False # Indicate no change + platformio.write_ini(content) + + # write_file_if_changed should be called with the same content + mock_write_file_if_changed.assert_called_once() + call_args = mock_write_file_if_changed.call_args[0] + assert call_args[0] == ini_file + assert content in call_args[1] + + +def test_write_ini_calls_update_storage_json( + tmp_path: Path, mock_update_storage_json: MagicMock +) -> None: + """Test write_ini calls update_storage_json.""" + CORE.build_path = str(tmp_path) + + content = "[env:test]\nplatform = esp32" + + platformio.write_ini(content) + mock_update_storage_json.assert_called_once() diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py index aac5a642f6..fc61841500 100644 --- a/tests/unit_tests/conftest.py +++ b/tests/unit_tests/conftest.py @@ -9,8 +9,10 @@ not be part of a unit test suite. """ +from collections.abc import Generator from pathlib import Path import sys +from unittest.mock import Mock, patch import pytest @@ -36,3 +38,80 @@ def fixture_path() -> Path: Location of all fixture files. """ return here / "fixtures" + + +@pytest.fixture +def setup_core(tmp_path: Path) -> Path: + """Set up CORE with test paths.""" + CORE.config_path = tmp_path / "test.yaml" + return tmp_path + + +@pytest.fixture +def mock_write_file_if_changed() -> Generator[Mock, None, None]: + """Mock write_file_if_changed for storage_json.""" + with patch("esphome.storage_json.write_file_if_changed") as mock: + yield mock + + +@pytest.fixture +def mock_copy_file_if_changed() -> Generator[Mock, None, None]: + """Mock copy_file_if_changed for core.config.""" + with patch("esphome.core.config.copy_file_if_changed") as mock: + yield mock + + +@pytest.fixture +def mock_run_platformio_cli() -> Generator[Mock, None, None]: + """Mock run_platformio_cli for platformio_api.""" + with patch("esphome.platformio_api.run_platformio_cli") as mock: + yield mock + + +@pytest.fixture +def mock_run_platformio_cli_run() -> Generator[Mock, None, None]: + """Mock run_platformio_cli_run for platformio_api.""" + with patch("esphome.platformio_api.run_platformio_cli_run") as mock: + yield mock + + +@pytest.fixture +def mock_decode_pc() -> Generator[Mock, None, None]: + """Mock _decode_pc for platformio_api.""" + with patch("esphome.platformio_api._decode_pc") as mock: + yield mock + + +@pytest.fixture +def mock_run_external_command() -> Generator[Mock, None, None]: + """Mock run_external_command for platformio_api.""" + with patch("esphome.platformio_api.run_external_command") as mock: + yield mock + + +@pytest.fixture +def mock_run_git_command() -> Generator[Mock, None, None]: + """Mock run_git_command for git module.""" + with patch("esphome.git.run_git_command") as mock: + yield mock + + +@pytest.fixture +def mock_subprocess_run() -> Generator[Mock, None, None]: + """Mock subprocess.run for testing.""" + with patch("subprocess.run") as mock: + yield mock + + +@pytest.fixture +def mock_get_idedata() -> Generator[Mock, None, None]: + """Mock get_idedata for platformio_api.""" + with patch("esphome.platformio_api.get_idedata") as mock: + yield mock + + +@pytest.fixture +def mock_get_component() -> Generator[Mock, None, None]: + """Mock get_component for config module.""" + with patch("esphome.config.get_component") as mock: + yield mock diff --git a/tests/unit_tests/core/common.py b/tests/unit_tests/core/common.py index 1848d5397b..daa429dc96 100644 --- a/tests/unit_tests/core/common.py +++ b/tests/unit_tests/core/common.py @@ -10,7 +10,7 @@ from esphome.core import CORE def load_config_from_yaml( - yaml_file: Callable[[str], str], yaml_content: str + yaml_file: Callable[[str], Path], yaml_content: str ) -> Config | None: """Load configuration from YAML content.""" yaml_path = yaml_file(yaml_content) @@ -25,7 +25,7 @@ def load_config_from_yaml( def load_config_from_fixture( - yaml_file: Callable[[str], str], fixture_name: str, fixtures_dir: Path + yaml_file: Callable[[str], Path], fixture_name: str, fixtures_dir: Path ) -> Config | None: """Load configuration from a fixture file.""" fixture_path = fixtures_dir / fixture_name diff --git a/tests/unit_tests/core/conftest.py b/tests/unit_tests/core/conftest.py index 60d6738ce9..42e59c15e6 100644 --- a/tests/unit_tests/core/conftest.py +++ b/tests/unit_tests/core/conftest.py @@ -7,12 +7,12 @@ import pytest @pytest.fixture -def yaml_file(tmp_path: Path) -> Callable[[str], str]: +def yaml_file(tmp_path: Path) -> Callable[[str], Path]: """Create a temporary YAML file for testing.""" - def _yaml_file(content: str) -> str: + def _yaml_file(content: str) -> Path: yaml_path = tmp_path / "test.yaml" yaml_path.write_text(content) - return str(yaml_path) + return yaml_path return _yaml_file diff --git a/tests/unit_tests/core/test_config.py b/tests/unit_tests/core/test_config.py index 46e3b513d7..90b2f5edba 100644 --- a/tests/unit_tests/core/test_config.py +++ b/tests/unit_tests/core/test_config.py @@ -1,20 +1,56 @@ """Unit tests for core config functionality including areas and devices.""" from collections.abc import Callable +import os from pathlib import Path +import types from typing import Any +from unittest.mock import MagicMock, Mock, patch import pytest from esphome import config_validation as cv, core -from esphome.const import CONF_AREA, CONF_AREAS, CONF_DEVICES -from esphome.core.config import Area, validate_area_config +from esphome.const import ( + CONF_AREA, + CONF_AREAS, + CONF_BUILD_PATH, + CONF_DEVICES, + CONF_ESPHOME, + CONF_NAME, + CONF_NAME_ADD_MAC_SUFFIX, + KEY_CORE, +) +from esphome.core import CORE, config +from esphome.core.config import ( + Area, + preload_core_config, + valid_include, + valid_project_name, + validate_area_config, + validate_hostname, +) from .common import load_config_from_fixture FIXTURES_DIR = Path(__file__).parent.parent / "fixtures" / "core" / "config" +@pytest.fixture +def mock_cg_with_include_capture() -> tuple[Mock, list[str]]: + """Mock code generation with include capture.""" + includes_added: list[str] = [] + + with patch("esphome.core.config.cg") as mock_cg: + mock_raw_statement = MagicMock() + + def capture_include(text: str) -> MagicMock: + includes_added.append(text) + return mock_raw_statement + + mock_cg.RawStatement.side_effect = capture_include + yield mock_cg, includes_added + + def test_validate_area_config_with_string() -> None: """Test that string area config is converted to structured format.""" result = validate_area_config("Living Room") @@ -223,3 +259,636 @@ def test_device_duplicate_id( # Check for the specific error message from IDPassValidationStep captured = capsys.readouterr() assert "ID duplicate_device redefined!" in captured.out + + +def test_substitution_with_id( + yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str] +) -> None: + """Test that a ids coming from substitutions do not cause false positive ID redefinition.""" + load_config_from_fixture( + yaml_file, "id_collision_with_substitution.yaml", FIXTURES_DIR + ) + captured = capsys.readouterr() + assert "ID some_switch_id redefined!" not in captured.out + + +def test_add_platform_defines_priority() -> None: + """Test that _add_platform_defines runs after globals. + + This ensures the fix for issue #10431 where sensor counts were incorrect + when lambdas were present. The function must run at a lower priority than + globals (-100.0) to ensure all components (including those using globals + in lambdas) have registered their entities before the count defines are + generated. + + Regression test for https://github.com/esphome/esphome/issues/10431 + """ + # Import globals to check its priority + from esphome.components.globals import to_code as globals_to_code + + # _add_platform_defines must run AFTER globals (lower priority number = runs later) + assert config._add_platform_defines.priority < globals_to_code.priority, ( + f"_add_platform_defines priority ({config._add_platform_defines.priority}) must be lower than " + f"globals priority ({globals_to_code.priority}) to fix issue #10431 (sensor count bug with lambdas)" + ) + + +def test_valid_include_with_angle_brackets() -> None: + """Test valid_include accepts angle bracket includes.""" + assert valid_include("") == "" + + +def test_valid_include_with_valid_file(tmp_path: Path) -> None: + """Test valid_include accepts valid include files.""" + CORE.config_path = tmp_path / "test.yaml" + include_file = tmp_path / "include.h" + include_file.touch() + + assert valid_include(str(include_file)) == str(include_file) + + +def test_valid_include_with_valid_directory(tmp_path: Path) -> None: + """Test valid_include accepts valid directories.""" + CORE.config_path = tmp_path / "test.yaml" + include_dir = tmp_path / "includes" + include_dir.mkdir() + + assert valid_include(str(include_dir)) == str(include_dir) + + +def test_valid_include_invalid_extension(tmp_path: Path) -> None: + """Test valid_include rejects files with invalid extensions.""" + CORE.config_path = tmp_path / "test.yaml" + invalid_file = tmp_path / "file.txt" + invalid_file.touch() + + with pytest.raises(cv.Invalid, match="Include has invalid file extension"): + valid_include(str(invalid_file)) + + +def test_valid_project_name_valid() -> None: + """Test valid_project_name accepts valid project names.""" + assert valid_project_name("esphome.my_project") == "esphome.my_project" + + +def test_valid_project_name_no_namespace() -> None: + """Test valid_project_name rejects names without namespace.""" + with pytest.raises(cv.Invalid, match="project name needs to have a namespace"): + valid_project_name("my_project") + + +def test_valid_project_name_multiple_dots() -> None: + """Test valid_project_name rejects names with multiple dots.""" + with pytest.raises(cv.Invalid, match="project name needs to have a namespace"): + valid_project_name("esphome.my.project") + + +def test_validate_hostname_valid() -> None: + """Test validate_hostname accepts valid hostnames.""" + config = {CONF_NAME: "my-device", CONF_NAME_ADD_MAC_SUFFIX: False} + assert validate_hostname(config) == config + + +def test_validate_hostname_too_long() -> None: + """Test validate_hostname rejects hostnames that are too long.""" + config = { + CONF_NAME: "a" * 32, # 32 chars, max is 31 + CONF_NAME_ADD_MAC_SUFFIX: False, + } + with pytest.raises(cv.Invalid, match="Hostnames can only be 31 characters long"): + validate_hostname(config) + + +def test_validate_hostname_too_long_with_mac_suffix() -> None: + """Test validate_hostname accounts for MAC suffix length.""" + config = { + CONF_NAME: "a" * 25, # 25 chars, max is 24 with MAC suffix + CONF_NAME_ADD_MAC_SUFFIX: True, + } + with pytest.raises(cv.Invalid, match="Hostnames can only be 24 characters long"): + validate_hostname(config) + + +def test_validate_hostname_with_underscore(caplog) -> None: + """Test validate_hostname warns about underscores.""" + config = {CONF_NAME: "my_device", CONF_NAME_ADD_MAC_SUFFIX: False} + assert validate_hostname(config) == config + assert ( + "Using the '_' (underscore) character in the hostname is discouraged" + in caplog.text + ) + + +def test_preload_core_config_basic(setup_core: Path) -> None: + """Test preload_core_config sets basic CORE attributes.""" + config = { + CONF_ESPHOME: { + CONF_NAME: "test_device", + }, + "esp32": {}, + } + result = {} + + platform = preload_core_config(config, result) + + assert CORE.name == "test_device" + assert platform == "esp32" + assert KEY_CORE in CORE.data + assert CONF_BUILD_PATH in config[CONF_ESPHOME] + # Verify default build path is "build/" + build_path = config[CONF_ESPHOME][CONF_BUILD_PATH] + assert build_path.endswith(os.path.join("build", "test_device")) + + +def test_preload_core_config_with_build_path(setup_core: Path) -> None: + """Test preload_core_config uses provided build path.""" + config = { + CONF_ESPHOME: { + CONF_NAME: "test_device", + CONF_BUILD_PATH: "/custom/build/path", + }, + "esp8266": {}, + } + result = {} + + platform = preload_core_config(config, result) + + assert config[CONF_ESPHOME][CONF_BUILD_PATH] == "/custom/build/path" + assert platform == "esp8266" + + +def test_preload_core_config_env_build_path(setup_core: Path) -> None: + """Test preload_core_config uses ESPHOME_BUILD_PATH env var.""" + config = { + CONF_ESPHOME: { + CONF_NAME: "test_device", + }, + "rp2040": {}, + } + result = {} + + with patch.dict(os.environ, {"ESPHOME_BUILD_PATH": "/env/build"}): + platform = preload_core_config(config, result) + + assert CONF_BUILD_PATH in config[CONF_ESPHOME] + assert "test_device" in config[CONF_ESPHOME][CONF_BUILD_PATH] + # Verify it uses the env var path with device name appended + build_path = config[CONF_ESPHOME][CONF_BUILD_PATH] + expected_path = os.path.join("/env/build", "test_device") + assert build_path == expected_path or build_path == expected_path.replace( + "/", os.sep + ) + assert platform == "rp2040" + + +def test_preload_core_config_no_platform(setup_core: Path) -> None: + """Test preload_core_config raises when no platform is specified.""" + config = { + CONF_ESPHOME: { + CONF_NAME: "test_device", + }, + } + result = {} + + # Mock _is_target_platform to avoid expensive component loading + with patch("esphome.core.config._is_target_platform") as mock_is_platform: + # Return True for known platforms + mock_is_platform.side_effect = lambda name: name in [ + "esp32", + "esp8266", + "rp2040", + ] + + with pytest.raises(cv.Invalid, match="Platform missing"): + preload_core_config(config, result) + + +def test_preload_core_config_multiple_platforms(setup_core: Path) -> None: + """Test preload_core_config raises when multiple platforms are specified.""" + config = { + CONF_ESPHOME: { + CONF_NAME: "test_device", + }, + "esp32": {}, + "esp8266": {}, + } + result = {} + + # Mock _is_target_platform to avoid expensive component loading + with patch("esphome.core.config._is_target_platform") as mock_is_platform: + # Return True for known platforms + mock_is_platform.side_effect = lambda name: name in [ + "esp32", + "esp8266", + "rp2040", + ] + + with pytest.raises(cv.Invalid, match="Found multiple target platform blocks"): + preload_core_config(config, result) + + +def test_include_file_header(tmp_path: Path, mock_copy_file_if_changed: Mock) -> None: + """Test include_file adds include statement for header files.""" + src_file = tmp_path / "source.h" + src_file.write_text("// Header content") + + CORE.build_path = tmp_path / "build" + + with patch("esphome.core.config.cg") as mock_cg: + # Mock RawStatement to capture the text + mock_raw_statement = MagicMock() + mock_raw_statement.text = "" + + def raw_statement_side_effect(text): + mock_raw_statement.text = text + return mock_raw_statement + + mock_cg.RawStatement.side_effect = raw_statement_side_effect + + config.include_file(src_file, Path("test.h")) + + mock_copy_file_if_changed.assert_called_once() + mock_cg.add_global.assert_called_once() + # Check that include statement was added + assert '#include "test.h"' in mock_raw_statement.text + + +def test_include_file_cpp(tmp_path: Path, mock_copy_file_if_changed: Mock) -> None: + """Test include_file does not add include for cpp files.""" + src_file = tmp_path / "source.cpp" + src_file.write_text("// CPP content") + + CORE.build_path = tmp_path / "build" + + with patch("esphome.core.config.cg") as mock_cg: + config.include_file(src_file, Path("test.cpp")) + + mock_copy_file_if_changed.assert_called_once() + # Should not add include statement for .cpp files + mock_cg.add_global.assert_not_called() + + +def test_include_file_with_c_header( + tmp_path: Path, mock_copy_file_if_changed: Mock +) -> None: + """Test include_file wraps header in extern C block when is_c_header is True.""" + src_file = tmp_path / "c_library.h" + src_file.write_text("// C library header") + + CORE.build_path = tmp_path / "build" + + with patch("esphome.core.config.cg") as mock_cg: + # Mock RawStatement to capture the text + mock_raw_statement = MagicMock() + mock_raw_statement.text = "" + + def raw_statement_side_effect(text): + mock_raw_statement.text = text + return mock_raw_statement + + mock_cg.RawStatement.side_effect = raw_statement_side_effect + + config.include_file(src_file, Path("c_library.h"), is_c_header=True) + + mock_copy_file_if_changed.assert_called_once() + mock_cg.add_global.assert_called_once() + # Check that include statement is wrapped in extern "C" block + assert 'extern "C"' in mock_raw_statement.text + assert '#include "c_library.h"' in mock_raw_statement.text + + +def test_get_usable_cpu_count() -> None: + """Test get_usable_cpu_count returns CPU count.""" + count = config.get_usable_cpu_count() + assert isinstance(count, int) + assert count > 0 + + +def test_get_usable_cpu_count_with_process_cpu_count() -> None: + """Test get_usable_cpu_count uses process_cpu_count when available.""" + # Test with process_cpu_count (Python 3.13+) + # Create a mock os module with process_cpu_count + + mock_os = types.SimpleNamespace(process_cpu_count=lambda: 8, cpu_count=lambda: 4) + + with patch("esphome.core.config.os", mock_os): + # When process_cpu_count exists, it should be used + count = config.get_usable_cpu_count() + assert count == 8 + + # Test fallback to cpu_count when process_cpu_count not available + mock_os_no_process = types.SimpleNamespace(cpu_count=lambda: 4) + + with patch("esphome.core.config.os", mock_os_no_process): + count = config.get_usable_cpu_count() + assert count == 4 + + +def test_list_target_platforms(tmp_path: Path) -> None: + """Test _list_target_platforms returns available platforms.""" + # Create mock components directory structure + components_dir = tmp_path / "components" + components_dir.mkdir() + + # Create platform and non-platform directories with __init__.py + platforms = ["esp32", "esp8266", "rp2040", "libretiny", "host"] + non_platforms = ["sensor"] + + for component in platforms + non_platforms: + component_dir = components_dir / component + component_dir.mkdir() + (component_dir / "__init__.py").touch() + + # Create a file (not a directory) + (components_dir / "README.md").touch() + + # Create a directory without __init__.py + (components_dir / "no_init").mkdir() + + # Mock Path(__file__).parents[1] to return our tmp_path + with patch("esphome.core.config.Path") as mock_path: + mock_file_path = MagicMock() + mock_file_path.parents = [MagicMock(), tmp_path] + mock_path.return_value = mock_file_path + + platforms = config._list_target_platforms() + + assert isinstance(platforms, list) + # Should include platform components + assert "esp32" in platforms + assert "esp8266" in platforms + assert "rp2040" in platforms + assert "libretiny" in platforms + assert "host" in platforms + # Should not include non-platform components + assert "sensor" not in platforms + assert "README.md" not in platforms + assert "no_init" not in platforms + + +def test_is_target_platform() -> None: + """Test _is_target_platform identifies valid platforms.""" + assert config._is_target_platform("esp32") is True + assert config._is_target_platform("esp8266") is True + assert config._is_target_platform("rp2040") is True + assert config._is_target_platform("invalid_platform") is False + assert config._is_target_platform("api") is False # Component but not platform + + +@pytest.mark.asyncio +async def test_add_includes_with_single_file( + tmp_path: Path, + mock_copy_file_if_changed: Mock, + mock_cg_with_include_capture: tuple[Mock, list[str]], +) -> None: + """Test add_includes copies a single header file to build directory.""" + CORE.config_path = tmp_path / "config.yaml" + CORE.build_path = tmp_path / "build" + os.makedirs(CORE.build_path, exist_ok=True) + + # Create include file + include_file = tmp_path / "my_header.h" + include_file.write_text("#define MY_CONSTANT 42") + + mock_cg, includes_added = mock_cg_with_include_capture + + await config.add_includes([str(include_file)]) + + # Verify copy_file_if_changed was called to copy the file + # Note: add_includes adds files to a src/ subdirectory + mock_copy_file_if_changed.assert_called_once_with( + include_file, CORE.build_path / "src" / "my_header.h" + ) + + # Verify include statement was added + assert any('#include "my_header.h"' in inc for inc in includes_added) + + +@pytest.mark.asyncio +@pytest.mark.skipif(os.name == "nt", reason="Unix-specific test") +async def test_add_includes_with_directory_unix( + tmp_path: Path, + mock_copy_file_if_changed: Mock, + mock_cg_with_include_capture: tuple[Mock, list[str]], +) -> None: + """Test add_includes copies all files from a directory on Unix.""" + CORE.config_path = tmp_path / "config.yaml" + CORE.build_path = tmp_path / "build" + os.makedirs(CORE.build_path, exist_ok=True) + + # Create include directory with files + include_dir = tmp_path / "includes" + include_dir.mkdir() + (include_dir / "header1.h").write_text("#define HEADER1") + (include_dir / "header2.hpp").write_text("#define HEADER2") + (include_dir / "source.cpp").write_text("// Implementation") + (include_dir / "README.md").write_text( + "# Documentation" + ) # Should be copied but not included + + # Create subdirectory with files + subdir = include_dir / "subdir" + subdir.mkdir() + (subdir / "nested.h").write_text("#define NESTED") + + mock_cg, includes_added = mock_cg_with_include_capture + + await config.add_includes([str(include_dir)]) + + # Verify copy_file_if_changed was called for all files + assert mock_copy_file_if_changed.call_count == 5 # 4 code files + 1 README + + # Verify include statements were added for valid extensions + include_strings = " ".join(includes_added) + assert "includes/header1.h" in include_strings + assert "includes/header2.hpp" in include_strings + assert "includes/subdir/nested.h" in include_strings + # CPP files are copied but not included + assert "source.cpp" not in include_strings or "#include" not in include_strings + # README.md should not have an include statement + assert "README.md" not in include_strings + + +@pytest.mark.asyncio +@pytest.mark.skipif(os.name != "nt", reason="Windows-specific test") +async def test_add_includes_with_directory_windows( + tmp_path: Path, + mock_copy_file_if_changed: Mock, + mock_cg_with_include_capture: tuple[Mock, list[str]], +) -> None: + """Test add_includes copies all files from a directory on Windows.""" + CORE.config_path = tmp_path / "config.yaml" + CORE.build_path = tmp_path / "build" + os.makedirs(CORE.build_path, exist_ok=True) + + # Create include directory with files + include_dir = tmp_path / "includes" + include_dir.mkdir() + (include_dir / "header1.h").write_text("#define HEADER1") + (include_dir / "header2.hpp").write_text("#define HEADER2") + (include_dir / "source.cpp").write_text("// Implementation") + (include_dir / "README.md").write_text( + "# Documentation" + ) # Should be copied but not included + + # Create subdirectory with files + subdir = include_dir / "subdir" + subdir.mkdir() + (subdir / "nested.h").write_text("#define NESTED") + + mock_cg, includes_added = mock_cg_with_include_capture + + await config.add_includes([str(include_dir)]) + + # Verify copy_file_if_changed was called for all files + assert mock_copy_file_if_changed.call_count == 5 # 4 code files + 1 README + + # Verify include statements were added for valid extensions + include_strings = " ".join(includes_added) + assert "includes\\header1.h" in include_strings + assert "includes\\header2.hpp" in include_strings + assert "includes\\subdir\\nested.h" in include_strings + # CPP files are copied but not included + assert "source.cpp" not in include_strings or "#include" not in include_strings + # README.md should not have an include statement + assert "README.md" not in include_strings + + +@pytest.mark.asyncio +async def test_add_includes_with_multiple_sources( + tmp_path: Path, mock_copy_file_if_changed: Mock +) -> None: + """Test add_includes with multiple files and directories.""" + CORE.config_path = tmp_path / "config.yaml" + CORE.build_path = tmp_path / "build" + os.makedirs(CORE.build_path, exist_ok=True) + + # Create various include sources + single_file = tmp_path / "single.h" + single_file.write_text("#define SINGLE") + + dir1 = tmp_path / "dir1" + dir1.mkdir() + (dir1 / "file1.h").write_text("#define FILE1") + + dir2 = tmp_path / "dir2" + dir2.mkdir() + (dir2 / "file2.cpp").write_text("// File2") + + with patch("esphome.core.config.cg"): + await config.add_includes([str(single_file), str(dir1), str(dir2)]) + + # Verify copy_file_if_changed was called for all files + assert mock_copy_file_if_changed.call_count == 3 # 3 files total + + +@pytest.mark.asyncio +async def test_add_includes_empty_directory( + tmp_path: Path, mock_copy_file_if_changed: Mock +) -> None: + """Test add_includes with an empty directory doesn't fail.""" + CORE.config_path = tmp_path / "config.yaml" + CORE.build_path = tmp_path / "build" + os.makedirs(CORE.build_path, exist_ok=True) + + # Create empty directory + empty_dir = tmp_path / "empty" + empty_dir.mkdir() + + with patch("esphome.core.config.cg"): + # Should not raise any errors + await config.add_includes([str(empty_dir)]) + + # No files to copy from empty directory + mock_copy_file_if_changed.assert_not_called() + + +@pytest.mark.asyncio +@pytest.mark.skipif(os.name == "nt", reason="Unix-specific test") +async def test_add_includes_preserves_directory_structure_unix( + tmp_path: Path, mock_copy_file_if_changed: Mock +) -> None: + """Test that add_includes preserves relative directory structure on Unix.""" + CORE.config_path = tmp_path / "config.yaml" + CORE.build_path = tmp_path / "build" + os.makedirs(CORE.build_path, exist_ok=True) + + # Create nested directory structure + lib_dir = tmp_path / "lib" + lib_dir.mkdir() + + src_dir = lib_dir / "src" + src_dir.mkdir() + (src_dir / "core.h").write_text("#define CORE") + + utils_dir = lib_dir / "utils" + utils_dir.mkdir() + (utils_dir / "helper.h").write_text("#define HELPER") + + with patch("esphome.core.config.cg"): + await config.add_includes([str(lib_dir)]) + + # Verify copy_file_if_changed was called with correct paths + calls = mock_copy_file_if_changed.call_args_list + dest_paths = [call[0][1] for call in calls] + + # Check that relative paths are preserved + assert any("lib/src/core.h" in str(path) for path in dest_paths) + assert any("lib/utils/helper.h" in str(path) for path in dest_paths) + + +@pytest.mark.asyncio +@pytest.mark.skipif(os.name != "nt", reason="Windows-specific test") +async def test_add_includes_preserves_directory_structure_windows( + tmp_path: Path, mock_copy_file_if_changed: Mock +) -> None: + """Test that add_includes preserves relative directory structure on Windows.""" + CORE.config_path = tmp_path / "config.yaml" + CORE.build_path = tmp_path / "build" + os.makedirs(CORE.build_path, exist_ok=True) + + # Create nested directory structure + lib_dir = tmp_path / "lib" + lib_dir.mkdir() + + src_dir = lib_dir / "src" + src_dir.mkdir() + (src_dir / "core.h").write_text("#define CORE") + + utils_dir = lib_dir / "utils" + utils_dir.mkdir() + (utils_dir / "helper.h").write_text("#define HELPER") + + with patch("esphome.core.config.cg"): + await config.add_includes([str(lib_dir)]) + + # Verify copy_file_if_changed was called with correct paths + calls = mock_copy_file_if_changed.call_args_list + dest_paths = [call[0][1] for call in calls] + + # Check that relative paths are preserved + assert any("lib\\src\\core.h" in str(path) for path in dest_paths) + assert any("lib\\utils\\helper.h" in str(path) for path in dest_paths) + + +@pytest.mark.asyncio +async def test_add_includes_overwrites_existing_files( + tmp_path: Path, mock_copy_file_if_changed: Mock +) -> None: + """Test that add_includes overwrites existing files in build directory.""" + CORE.config_path = tmp_path / "config.yaml" + CORE.build_path = tmp_path / "build" + os.makedirs(CORE.build_path, exist_ok=True) + + # Create include file + include_file = tmp_path / "header.h" + include_file.write_text("#define NEW_VALUE 42") + + with patch("esphome.core.config.cg"): + await config.add_includes([str(include_file)]) + + # Verify copy_file_if_changed was called (it handles overwriting) + # Note: add_includes adds files to a src/ subdirectory + mock_copy_file_if_changed.assert_called_once_with( + include_file, CORE.build_path / "src" / "header.h" + ) diff --git a/tests/unit_tests/core/test_entity_helpers.py b/tests/unit_tests/core/test_entity_helpers.py index c639ad94b2..01de0f27f9 100644 --- a/tests/unit_tests/core/test_entity_helpers.py +++ b/tests/unit_tests/core/test_entity_helpers.py @@ -12,6 +12,7 @@ from esphome.const import ( CONF_DEVICE_ID, CONF_DISABLED_BY_DEFAULT, CONF_ICON, + CONF_ID, CONF_INTERNAL, CONF_NAME, ) @@ -26,8 +27,13 @@ from esphome.helpers import sanitize, snake_case from .common import load_config_from_fixture -# Pre-compiled regex pattern for extracting object IDs from expressions +# Pre-compiled regex patterns for extracting object IDs from expressions +# Matches both old format: .set_object_id("obj_id") +# and new format: .set_name_and_object_id("name", "obj_id") OBJECT_ID_PATTERN = re.compile(r'\.set_object_id\(["\'](.*?)["\']\)') +COMBINED_PATTERN = re.compile( + r'\.set_name_and_object_id\(["\'].*?["\']\s*,\s*["\'](.*?)["\']\)' +) FIXTURES_DIR = Path(__file__).parent.parent / "fixtures" / "core" / "entity_helpers" @@ -272,8 +278,10 @@ def setup_test_environment() -> Generator[list[str], None, None]: def extract_object_id_from_expressions(expressions: list[str]) -> str | None: """Extract the object ID that was set from the generated expressions.""" for expr in expressions: - # Look for set_object_id calls with regex to handle various formats - # Matches: var.set_object_id("temperature_2") or var.set_object_id('temperature_2') + # First try new combined format: .set_name_and_object_id("name", "obj_id") + if match := COMBINED_PATTERN.search(expr): + return match.group(1) + # Fall back to old format: .set_object_id("obj_id") if match := OBJECT_ID_PATTERN.search(expr): return match.group(1) return None @@ -511,12 +519,18 @@ def test_entity_duplicate_validator() -> None: validated1 = validator(config1) assert validated1 == config1 assert ("", "sensor", "temperature") in CORE.unique_ids + # Check metadata was stored + metadata = CORE.unique_ids[("", "sensor", "temperature")] + assert metadata["name"] == "Temperature" + assert metadata["platform"] == "sensor" # Second entity with different name should pass config2 = {CONF_NAME: "Humidity"} validated2 = validator(config2) assert validated2 == config2 assert ("", "sensor", "humidity") in CORE.unique_ids + metadata2 = CORE.unique_ids[("", "sensor", "humidity")] + assert metadata2["name"] == "Humidity" # Duplicate entity should fail config3 = {CONF_NAME: "Temperature"} @@ -540,11 +554,15 @@ def test_entity_duplicate_validator_with_devices() -> None: validated1 = validator(config1) assert validated1 == config1 assert ("device1", "sensor", "temperature") in CORE.unique_ids + metadata1 = CORE.unique_ids[("device1", "sensor", "temperature")] + assert metadata1["device_id"] == "device1" config2 = {CONF_NAME: "Temperature", CONF_DEVICE_ID: device2} validated2 = validator(config2) assert validated2 == config2 assert ("device2", "sensor", "temperature") in CORE.unique_ids + metadata2 = CORE.unique_ids[("device2", "sensor", "temperature")] + assert metadata2["device_id"] == "device2" # Duplicate on same device should fail config3 = {CONF_NAME: "Temperature", CONF_DEVICE_ID: device1} @@ -595,6 +613,54 @@ def test_entity_different_platforms_yaml_validation( assert result is not None +def test_entity_duplicate_validator_error_message() -> None: + """Test that duplicate entity error messages include helpful metadata.""" + # Create validator for sensor platform + validator = entity_duplicate_validator("sensor") + + # Set current component to simulate validation context for uptime sensor + CORE.current_component = "sensor.uptime" + + # First entity should pass + config1 = {CONF_NAME: "Battery", CONF_ID: ID("battery_1")} + validated1 = validator(config1) + assert validated1 == config1 + + # Reset component to simulate template sensor + CORE.current_component = "sensor.template" + + # Duplicate entity should fail with detailed error + config2 = {CONF_NAME: "Battery", CONF_ID: ID("battery_2")} + with pytest.raises( + Invalid, + match=r"Duplicate sensor entity with name 'Battery' found.*" + r"Conflicts with entity 'Battery' \(id: battery_1\) from component 'sensor\.uptime'", + ): + validator(config2) + + # Clean up + CORE.current_component = None + + +def test_entity_conflict_between_components_yaml( + yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str] +) -> None: + """Test that conflicts between different components show helpful error messages.""" + result = load_config_from_fixture( + yaml_file, "entity_conflict_components.yaml", FIXTURES_DIR + ) + assert result is None + + # Check for the enhanced error message + captured = capsys.readouterr() + # The error should mention both the conflict and which component created it + assert "Duplicate sensor entity with name 'Battery' found" in captured.out + # Should mention it conflicts with an entity from a specific sensor platform + assert "from component 'sensor." in captured.out + # Should show it's a conflict between wifi_signal and template + assert "sensor.wifi_signal" in captured.out or "sensor.template" in captured.out + + def test_entity_duplicate_validator_internal_entities() -> None: """Test that internal entities are excluded from duplicate name validation.""" # Create validator for sensor platform @@ -612,14 +678,17 @@ def test_entity_duplicate_validator_internal_entities() -> None: validated2 = validator(config2) assert validated2 == config2 # Internal entity should not be added to unique_ids - assert len([k for k in CORE.unique_ids if k == ("", "sensor", "temperature")]) == 1 + # Count how many times the key appears (should still be 1) + count = sum(1 for k in CORE.unique_ids if k == ("", "sensor", "temperature")) + assert count == 1 # Another internal entity with same name should also pass config3 = {CONF_NAME: "Temperature", CONF_INTERNAL: True} validated3 = validator(config3) assert validated3 == config3 # Still only one entry in unique_ids (from the non-internal entity) - assert len([k for k in CORE.unique_ids if k == ("", "sensor", "temperature")]) == 1 + count = sum(1 for k in CORE.unique_ids if k == ("", "sensor", "temperature")) + assert count == 1 # Non-internal entity with same name should fail config4 = {CONF_NAME: "Temperature"} @@ -627,3 +696,64 @@ def test_entity_duplicate_validator_internal_entities() -> None: Invalid, match=r"Duplicate sensor entity with name 'Temperature' found" ): validator(config4) + + +def test_empty_or_null_device_id_on_entity() -> None: + """Test that empty or null device IDs are handled correctly.""" + # Create validator for sensor platform + validator = entity_duplicate_validator("sensor") + + # Entity with empty device_id should pass + config1 = {CONF_NAME: "Battery", CONF_DEVICE_ID: ""} + validated1 = validator(config1) + assert validated1 == config1 + + # Entity with None device_id should pass + config2 = {CONF_NAME: "Temperature", CONF_DEVICE_ID: None} + validated2 = validator(config2) + assert validated2 == config2 + + +def test_entity_duplicate_validator_non_ascii_names() -> None: + """Test that non-ASCII names show helpful error messages.""" + # Create validator for binary_sensor platform + validator = entity_duplicate_validator("binary_sensor") + + # First Russian sensor should pass + config1 = {CONF_NAME: "Датчик открытия основного крана"} + validated1 = validator(config1) + assert validated1 == config1 + + # Second Russian sensor with different text but same ASCII conversion should fail + config2 = {CONF_NAME: "Датчик закрытия основного крана"} + with pytest.raises( + Invalid, + match=re.compile( + r"Duplicate binary_sensor entity with name 'Датчик закрытия основного крана' found.*" + r"Original names: 'Датчик закрытия основного крана' and 'Датчик открытия основного крана'.*" + r"Both convert to ASCII ID: '_______________________________'.*" + r"To fix: Add unique ASCII characters \(e\.g\., '1', '2', or 'A', 'B'\)", + re.DOTALL, + ), + ): + validator(config2) + + +def test_entity_duplicate_validator_same_name_no_enhanced_message() -> None: + """Test that identical names don't show the enhanced message.""" + # Create validator for sensor platform + validator = entity_duplicate_validator("sensor") + + # First entity should pass + config1 = {CONF_NAME: "Temperature"} + validated1 = validator(config1) + assert validated1 == config1 + + # Second entity with exact same name should fail without enhanced message + config2 = {CONF_NAME: "Temperature"} + with pytest.raises( + Invalid, + match=r"Duplicate sensor entity with name 'Temperature' found.*" + r"Each entity on a device must have a unique name within its platform\.$", + ): + validator(config2) diff --git a/tests/unit_tests/fixtures/auto_load_dynamic.yaml b/tests/unit_tests/fixtures/auto_load_dynamic.yaml new file mode 100644 index 0000000000..b604a2a42b --- /dev/null +++ b/tests/unit_tests/fixtures/auto_load_dynamic.yaml @@ -0,0 +1,10 @@ +esphome: + name: test-device + +esp32: + board: esp32dev + +# Test component with dynamic AUTO_LOAD +test_component: + enable_logger: true + enable_api: false diff --git a/tests/unit_tests/fixtures/auto_load_static.yaml b/tests/unit_tests/fixtures/auto_load_static.yaml new file mode 100644 index 0000000000..c8f9e6222a --- /dev/null +++ b/tests/unit_tests/fixtures/auto_load_static.yaml @@ -0,0 +1,8 @@ +esphome: + name: test-device + +esp32: + board: esp32dev + +# Test component with static AUTO_LOAD +test_component: diff --git a/tests/unit_tests/fixtures/core/config/id_collision_with_substitution.yaml b/tests/unit_tests/fixtures/core/config/id_collision_with_substitution.yaml new file mode 100644 index 0000000000..840d9ac925 --- /dev/null +++ b/tests/unit_tests/fixtures/core/config/id_collision_with_substitution.yaml @@ -0,0 +1,12 @@ +esphome: + name: test + +host: + +substitutions: + support_switches: + - platform: gpio + id: some_switch_id + pin: 12 + +switch: $support_switches diff --git a/tests/unit_tests/fixtures/core/entity_helpers/entity_conflict_components.yaml b/tests/unit_tests/fixtures/core/entity_helpers/entity_conflict_components.yaml new file mode 100644 index 0000000000..6a1df0f7b4 --- /dev/null +++ b/tests/unit_tests/fixtures/core/entity_helpers/entity_conflict_components.yaml @@ -0,0 +1,20 @@ +esphome: + name: test-device + +esp32: + board: esp32dev + +# Uptime sensor +sensor: + - platform: uptime + name: "Battery" + id: uptime_battery + +# Template sensor also named "Battery" - this should conflict + - platform: template + name: "Battery" + id: template_battery + lambda: |- + return 95.0; + unit_of_measurement: "%" + update_interval: 60s diff --git a/tests/unit_tests/fixtures/ota_empty_dict.yaml b/tests/unit_tests/fixtures/ota_empty_dict.yaml new file mode 100644 index 0000000000..cf9b166afa --- /dev/null +++ b/tests/unit_tests/fixtures/ota_empty_dict.yaml @@ -0,0 +1,17 @@ +esphome: + name: test-device2 + +esp32: + board: esp32dev + framework: + type: esp-idf + +# OTA with empty dict - should be normalized +ota: {} + +wifi: + ssid: "test" + password: "test" + +# Captive portal auto-loads ota.web_server which triggers the issue +captive_portal: diff --git a/tests/unit_tests/fixtures/ota_no_platform.yaml b/tests/unit_tests/fixtures/ota_no_platform.yaml new file mode 100644 index 0000000000..0b09c836fb --- /dev/null +++ b/tests/unit_tests/fixtures/ota_no_platform.yaml @@ -0,0 +1,17 @@ +esphome: + name: test-device + +esp32: + board: esp32dev + framework: + type: esp-idf + +# OTA with no value - this should be normalized to empty list +ota: + +wifi: + ssid: "test" + password: "test" + +# Captive portal auto-loads ota.web_server which triggers the issue +captive_portal: diff --git a/tests/unit_tests/fixtures/ota_with_platform_list.yaml b/tests/unit_tests/fixtures/ota_with_platform_list.yaml new file mode 100644 index 0000000000..b1b03743ae --- /dev/null +++ b/tests/unit_tests/fixtures/ota_with_platform_list.yaml @@ -0,0 +1,19 @@ +esphome: + name: test-device3 + +esp32: + board: esp32dev + framework: + type: esp-idf + +# OTA with proper list format +ota: + - platform: esphome + password: "test123" + +wifi: + ssid: "test" + password: "test" + +# Captive portal auto-loads ota.web_server +captive_portal: diff --git a/tests/unit_tests/fixtures/substitutions/00-simple_var.approved.yaml b/tests/unit_tests/fixtures/substitutions/00-simple_var.approved.yaml index f5d2f8aa20..6f3bae1ac4 100644 --- a/tests/unit_tests/fixtures/substitutions/00-simple_var.approved.yaml +++ b/tests/unit_tests/fixtures/substitutions/00-simple_var.approved.yaml @@ -1,7 +1,14 @@ substitutions: + substituted: 99 var1: '1' var2: '2' var21: '79' + value: 33 + values: 44 + position: + x: 79 + y: 82 + esphome: name: test test_list: @@ -19,3 +26,11 @@ test_list: - ${ undefined_var } - key1: 1 key2: 2 + - Literal $values ${are not substituted} + - ["list $value", "${is not}", "${substituted}"] + - {"$dictionary": "$value", "${is not}": "${substituted}"} + - |- + {{{ "x", "79"}, { "y", "82"}}} + - '{{{"AA"}}}' + - '"HELLO"' + - '{ 79, 82 }' diff --git a/tests/unit_tests/fixtures/substitutions/00-simple_var.input.yaml b/tests/unit_tests/fixtures/substitutions/00-simple_var.input.yaml index 5717433c7e..306119b753 100644 --- a/tests/unit_tests/fixtures/substitutions/00-simple_var.input.yaml +++ b/tests/unit_tests/fixtures/substitutions/00-simple_var.input.yaml @@ -2,9 +2,15 @@ esphome: name: test substitutions: + substituted: 99 var1: "1" var2: "2" var21: "79" + value: 33 + values: 44 + position: + x: 79 + y: 82 test_list: - "$var1" @@ -21,3 +27,11 @@ test_list: - ${ undefined_var } - key${var1}: 1 key${var2}: 2 + - !literal Literal $values ${are not substituted} + - !literal ["list $value", "${is not}", "${substituted}"] + - !literal {"$dictionary": "$value", "${is not}": "${substituted}"} + - |- # Test parsing things that look like a python set of sets when rendered: + {{{ "x", "${ position.x }"}, { "y", "${ position.y }"}}} + - ${ '{{{"AA"}}}' } + - ${ '"HELLO"' } + - '{ ${position.x}, ${position.y} }' diff --git a/tests/unit_tests/fixtures/substitutions/02-expressions.approved.yaml b/tests/unit_tests/fixtures/substitutions/02-expressions.approved.yaml index 9e401ec5d6..1a51fc44cf 100644 --- a/tests/unit_tests/fixtures/substitutions/02-expressions.approved.yaml +++ b/tests/unit_tests/fixtures/substitutions/02-expressions.approved.yaml @@ -8,6 +8,7 @@ substitutions: area: 25 numberOne: 1 var1: 79 + double_width: 14 test_list: - The area is 56 - 56 @@ -22,3 +23,7 @@ test_list: - The pin number is 18 - The square root is: 5.0 - The number is 80 + - ord("a") = 97 + - chr(97) = a + - len([1,2,3]) = 3 + - width = 7, double_width = 14 diff --git a/tests/unit_tests/fixtures/substitutions/02-expressions.input.yaml b/tests/unit_tests/fixtures/substitutions/02-expressions.input.yaml index 1777b46f67..4612f581b5 100644 --- a/tests/unit_tests/fixtures/substitutions/02-expressions.input.yaml +++ b/tests/unit_tests/fixtures/substitutions/02-expressions.input.yaml @@ -8,6 +8,7 @@ substitutions: area: 25 numberOne: 1 var1: 79 + double_width: ${width * 2} test_list: - "The area is ${width * height}" @@ -20,3 +21,7 @@ test_list: - The pin number is ${pin.number} - The square root is: ${math.sqrt(area)} - The number is ${var${numberOne} + 1} + - ord("a") = ${ ord("a") } + - chr(97) = ${ chr(97) } + - len([1,2,3]) = ${ len([1,2,3]) } + - width = ${width}, double_width = ${double_width} diff --git a/tests/unit_tests/fixtures/substitutions/05-extend-remove.approved.yaml b/tests/unit_tests/fixtures/substitutions/05-extend-remove.approved.yaml new file mode 100644 index 0000000000..773a124f25 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/05-extend-remove.approved.yaml @@ -0,0 +1,39 @@ +substitutions: + A: component1 + B: component2 + C: component3 +some_component: + - id: component1 + value: 2 + - id: component2 + value: 5 +lvgl: + pages: + - id: page1 + widgets: + - obj: + id: object1 + x: 3 + y: 2 + width: 4 + - obj: + id: object3 + x: 6 + y: 12 + widgets: + - obj: + id: object4 + x: 14 + y: 9 + width: 15 + height: 13 + - obj: + id: object5 + x: 10 + y: 11 + - obj: + id: + - Invalid ID + - obj: + id: + invalid: id diff --git a/tests/unit_tests/fixtures/substitutions/05-extend-remove.input.yaml b/tests/unit_tests/fixtures/substitutions/05-extend-remove.input.yaml new file mode 100644 index 0000000000..e6d46d6dc4 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/05-extend-remove.input.yaml @@ -0,0 +1,70 @@ +substitutions: + A: component1 + B: component2 + C: component3 + +packages: + - some_component: + - id: component1 + value: 1 + - id: !extend ${B} + value: 4 + - id: !extend ${B} + value: 5 + - id: component3 + value: 6 + - lvgl: + pages: + - id: page1 + widgets: + - obj: + id: object1 + x: 1 + y: 2 + - obj: + id: object2 + x: 5 + - obj: + id: object3 + x: 6 + y: 7 + widgets: + - obj: + id: object4 + x: 8 + y: 9 + - obj: + id: object5 + x: 10 + y: 11 + - obj: + id: ["Invalid ID"] + - obj: + id: {"invalid": "id"} + +some_component: + - id: !extend ${A} + value: 2 + - id: component2 + value: 3 + - id: !remove ${C} + +lvgl: + pages: + - id: !extend page1 + widgets: + - obj: + id: !extend object1 + x: 3 + width: 4 + - obj: + id: !remove object2 + - obj: + id: !extend object3 + y: 12 + height: 13 + widgets: + - obj: + id: !extend object4 + x: 14 + width: 15 diff --git a/tests/unit_tests/fixtures/yaml_util/named_dir/.hidden.yaml b/tests/unit_tests/fixtures/yaml_util/named_dir/.hidden.yaml new file mode 100644 index 0000000000..75eb989ea5 --- /dev/null +++ b/tests/unit_tests/fixtures/yaml_util/named_dir/.hidden.yaml @@ -0,0 +1,3 @@ +# This file should be ignored +platform: template +name: "Hidden Sensor" diff --git a/tests/unit_tests/fixtures/yaml_util/named_dir/not_yaml.txt b/tests/unit_tests/fixtures/yaml_util/named_dir/not_yaml.txt new file mode 100644 index 0000000000..98efb74b0f --- /dev/null +++ b/tests/unit_tests/fixtures/yaml_util/named_dir/not_yaml.txt @@ -0,0 +1 @@ +This is not a YAML file and should be ignored diff --git a/tests/unit_tests/fixtures/yaml_util/named_dir/sensor1.yaml b/tests/unit_tests/fixtures/yaml_util/named_dir/sensor1.yaml new file mode 100644 index 0000000000..a4b0a11916 --- /dev/null +++ b/tests/unit_tests/fixtures/yaml_util/named_dir/sensor1.yaml @@ -0,0 +1,4 @@ +platform: template +name: "Sensor 1" +lambda: |- + return 42.0; diff --git a/tests/unit_tests/fixtures/yaml_util/named_dir/sensor2.yaml b/tests/unit_tests/fixtures/yaml_util/named_dir/sensor2.yaml new file mode 100644 index 0000000000..72d4b714b6 --- /dev/null +++ b/tests/unit_tests/fixtures/yaml_util/named_dir/sensor2.yaml @@ -0,0 +1,4 @@ +platform: template +name: "Sensor 2" +lambda: |- + return 100.0; diff --git a/tests/unit_tests/fixtures/yaml_util/named_dir/subdir/sensor3.yaml b/tests/unit_tests/fixtures/yaml_util/named_dir/subdir/sensor3.yaml new file mode 100644 index 0000000000..bcb8dd320d --- /dev/null +++ b/tests/unit_tests/fixtures/yaml_util/named_dir/subdir/sensor3.yaml @@ -0,0 +1,4 @@ +platform: template +name: "Sensor 3 in subdir" +lambda: |- + return 200.0; diff --git a/tests/unit_tests/fixtures/yaml_util/secrets.yaml b/tests/unit_tests/fixtures/yaml_util/secrets.yaml new file mode 100644 index 0000000000..4eef570926 --- /dev/null +++ b/tests/unit_tests/fixtures/yaml_util/secrets.yaml @@ -0,0 +1,4 @@ +test_secret: "my_secret_value" +another_secret: "another_value" +wifi_password: "super_secret_wifi" +api_key: "0123456789abcdef" diff --git a/tests/unit_tests/fixtures/yaml_util/test_secret.yaml b/tests/unit_tests/fixtures/yaml_util/test_secret.yaml new file mode 100644 index 0000000000..c23afaee94 --- /dev/null +++ b/tests/unit_tests/fixtures/yaml_util/test_secret.yaml @@ -0,0 +1,17 @@ +esphome: + name: test_device + platform: ESP32 + board: esp32dev + +wifi: + ssid: "TestNetwork" + password: !secret wifi_password + +api: + encryption: + key: !secret api_key + +sensor: + - platform: template + name: "Test Sensor" + id: !secret test_secret diff --git a/tests/unit_tests/test_address_cache.py b/tests/unit_tests/test_address_cache.py new file mode 100644 index 0000000000..de43830d53 --- /dev/null +++ b/tests/unit_tests/test_address_cache.py @@ -0,0 +1,305 @@ +"""Tests for the address_cache module.""" + +from __future__ import annotations + +import logging + +import pytest +from pytest import LogCaptureFixture + +from esphome.address_cache import AddressCache, normalize_hostname + + +def test_normalize_simple_hostname() -> None: + """Test normalizing a simple hostname.""" + assert normalize_hostname("device") == "device" + assert normalize_hostname("device.local") == "device.local" + assert normalize_hostname("server.example.com") == "server.example.com" + + +def test_normalize_removes_trailing_dots() -> None: + """Test that trailing dots are removed.""" + assert normalize_hostname("device.") == "device" + assert normalize_hostname("device.local.") == "device.local" + assert normalize_hostname("server.example.com.") == "server.example.com" + assert normalize_hostname("device...") == "device" + + +def test_normalize_converts_to_lowercase() -> None: + """Test that hostnames are converted to lowercase.""" + assert normalize_hostname("DEVICE") == "device" + assert normalize_hostname("Device.Local") == "device.local" + assert normalize_hostname("Server.Example.COM") == "server.example.com" + + +def test_normalize_combined() -> None: + """Test combination of trailing dots and case conversion.""" + assert normalize_hostname("DEVICE.LOCAL.") == "device.local" + assert normalize_hostname("Server.Example.COM...") == "server.example.com" + + +def test_init_empty() -> None: + """Test initialization with empty caches.""" + cache = AddressCache() + assert cache.mdns_cache == {} + assert cache.dns_cache == {} + assert not cache.has_cache() + + +def test_init_with_caches() -> None: + """Test initialization with provided caches.""" + mdns_cache: dict[str, list[str]] = {"device.local": ["192.168.1.10"]} + dns_cache: dict[str, list[str]] = {"server.com": ["10.0.0.1"]} + cache = AddressCache(mdns_cache=mdns_cache, dns_cache=dns_cache) + assert cache.mdns_cache == mdns_cache + assert cache.dns_cache == dns_cache + assert cache.has_cache() + + +def test_get_mdns_addresses() -> None: + """Test getting mDNS addresses.""" + cache = AddressCache(mdns_cache={"device.local": ["192.168.1.10", "192.168.1.11"]}) + + # Direct lookup + assert cache.get_mdns_addresses("device.local") == [ + "192.168.1.10", + "192.168.1.11", + ] + + # Case insensitive lookup + assert cache.get_mdns_addresses("Device.Local") == [ + "192.168.1.10", + "192.168.1.11", + ] + + # With trailing dot + assert cache.get_mdns_addresses("device.local.") == [ + "192.168.1.10", + "192.168.1.11", + ] + + # Not found + assert cache.get_mdns_addresses("unknown.local") is None + + +def test_get_dns_addresses() -> None: + """Test getting DNS addresses.""" + cache = AddressCache(dns_cache={"server.com": ["10.0.0.1", "10.0.0.2"]}) + + # Direct lookup + assert cache.get_dns_addresses("server.com") == ["10.0.0.1", "10.0.0.2"] + + # Case insensitive lookup + assert cache.get_dns_addresses("Server.COM") == ["10.0.0.1", "10.0.0.2"] + + # With trailing dot + assert cache.get_dns_addresses("server.com.") == ["10.0.0.1", "10.0.0.2"] + + # Not found + assert cache.get_dns_addresses("unknown.com") is None + + +def test_get_addresses_auto_detection() -> None: + """Test automatic cache selection based on hostname.""" + cache = AddressCache( + mdns_cache={"device.local": ["192.168.1.10"]}, + dns_cache={"server.com": ["10.0.0.1"]}, + ) + + # Should use mDNS cache for .local domains + assert cache.get_addresses("device.local") == ["192.168.1.10"] + assert cache.get_addresses("device.local.") == ["192.168.1.10"] + assert cache.get_addresses("Device.Local") == ["192.168.1.10"] + + # Should use DNS cache for non-.local domains + assert cache.get_addresses("server.com") == ["10.0.0.1"] + assert cache.get_addresses("server.com.") == ["10.0.0.1"] + assert cache.get_addresses("Server.COM") == ["10.0.0.1"] + + # Not found + assert cache.get_addresses("unknown.local") is None + assert cache.get_addresses("unknown.com") is None + + +def test_has_cache() -> None: + """Test checking if cache has entries.""" + # Empty cache + cache = AddressCache() + assert not cache.has_cache() + + # Only mDNS cache + cache = AddressCache(mdns_cache={"device.local": ["192.168.1.10"]}) + assert cache.has_cache() + + # Only DNS cache + cache = AddressCache(dns_cache={"server.com": ["10.0.0.1"]}) + assert cache.has_cache() + + # Both caches + cache = AddressCache( + mdns_cache={"device.local": ["192.168.1.10"]}, + dns_cache={"server.com": ["10.0.0.1"]}, + ) + assert cache.has_cache() + + +def test_from_cli_args_empty() -> None: + """Test creating cache from empty CLI arguments.""" + cache = AddressCache.from_cli_args([], []) + assert cache.mdns_cache == {} + assert cache.dns_cache == {} + + +def test_from_cli_args_single_entry() -> None: + """Test creating cache from single CLI argument.""" + mdns_args: list[str] = ["device.local=192.168.1.10"] + dns_args: list[str] = ["server.com=10.0.0.1"] + + cache = AddressCache.from_cli_args(mdns_args, dns_args) + + assert cache.mdns_cache == {"device.local": ["192.168.1.10"]} + assert cache.dns_cache == {"server.com": ["10.0.0.1"]} + + +def test_from_cli_args_multiple_ips() -> None: + """Test creating cache with multiple IPs per host.""" + mdns_args: list[str] = ["device.local=192.168.1.10,192.168.1.11"] + dns_args: list[str] = ["server.com=10.0.0.1,10.0.0.2,10.0.0.3"] + + cache = AddressCache.from_cli_args(mdns_args, dns_args) + + assert cache.mdns_cache == {"device.local": ["192.168.1.10", "192.168.1.11"]} + assert cache.dns_cache == {"server.com": ["10.0.0.1", "10.0.0.2", "10.0.0.3"]} + + +def test_from_cli_args_multiple_entries() -> None: + """Test creating cache with multiple host entries.""" + mdns_args: list[str] = [ + "device1.local=192.168.1.10", + "device2.local=192.168.1.20,192.168.1.21", + ] + dns_args: list[str] = ["server1.com=10.0.0.1", "server2.com=10.0.0.2"] + + cache = AddressCache.from_cli_args(mdns_args, dns_args) + + assert cache.mdns_cache == { + "device1.local": ["192.168.1.10"], + "device2.local": ["192.168.1.20", "192.168.1.21"], + } + assert cache.dns_cache == { + "server1.com": ["10.0.0.1"], + "server2.com": ["10.0.0.2"], + } + + +def test_from_cli_args_normalization() -> None: + """Test that CLI arguments are normalized.""" + mdns_args: list[str] = ["Device1.Local.=192.168.1.10", "DEVICE2.LOCAL=192.168.1.20"] + dns_args: list[str] = ["Server1.COM.=10.0.0.1", "SERVER2.com=10.0.0.2"] + + cache = AddressCache.from_cli_args(mdns_args, dns_args) + + # Hostnames should be normalized (lowercase, no trailing dots) + assert cache.mdns_cache == { + "device1.local": ["192.168.1.10"], + "device2.local": ["192.168.1.20"], + } + assert cache.dns_cache == { + "server1.com": ["10.0.0.1"], + "server2.com": ["10.0.0.2"], + } + + +def test_from_cli_args_whitespace_handling() -> None: + """Test that whitespace in IPs is handled.""" + mdns_args: list[str] = ["device.local= 192.168.1.10 , 192.168.1.11 "] + dns_args: list[str] = ["server.com= 10.0.0.1 , 10.0.0.2 "] + + cache = AddressCache.from_cli_args(mdns_args, dns_args) + + assert cache.mdns_cache == {"device.local": ["192.168.1.10", "192.168.1.11"]} + assert cache.dns_cache == {"server.com": ["10.0.0.1", "10.0.0.2"]} + + +def test_from_cli_args_invalid_format(caplog: LogCaptureFixture) -> None: + """Test handling of invalid argument format.""" + mdns_args: list[str] = ["invalid_format", "device.local=192.168.1.10"] + dns_args: list[str] = ["server.com=10.0.0.1", "also_invalid"] + + cache = AddressCache.from_cli_args(mdns_args, dns_args) + + # Valid entries should still be processed + assert cache.mdns_cache == {"device.local": ["192.168.1.10"]} + assert cache.dns_cache == {"server.com": ["10.0.0.1"]} + + # Check that warnings were logged for invalid entries + assert "Invalid cache format: invalid_format" in caplog.text + assert "Invalid cache format: also_invalid" in caplog.text + + +def test_from_cli_args_ipv6() -> None: + """Test handling of IPv6 addresses.""" + mdns_args: list[str] = ["device.local=fe80::1,2001:db8::1"] + dns_args: list[str] = ["server.com=2001:db8::2,::1"] + + cache = AddressCache.from_cli_args(mdns_args, dns_args) + + assert cache.mdns_cache == {"device.local": ["fe80::1", "2001:db8::1"]} + assert cache.dns_cache == {"server.com": ["2001:db8::2", "::1"]} + + +def test_logging_output(caplog: LogCaptureFixture) -> None: + """Test that appropriate debug logging occurs.""" + caplog.set_level(logging.DEBUG) + + cache = AddressCache( + mdns_cache={"device.local": ["192.168.1.10"]}, + dns_cache={"server.com": ["10.0.0.1"]}, + ) + + # Test successful lookups log at debug level + result: list[str] | None = cache.get_mdns_addresses("device.local") + assert result == ["192.168.1.10"] + assert "Using mDNS cache for device.local" in caplog.text + + caplog.clear() + result = cache.get_dns_addresses("server.com") + assert result == ["10.0.0.1"] + assert "Using DNS cache for server.com" in caplog.text + + # Test that failed lookups don't log + caplog.clear() + result = cache.get_mdns_addresses("unknown.local") + assert result is None + assert "Using mDNS cache" not in caplog.text + + +@pytest.mark.parametrize( + "hostname,expected", + [ + ("test.local", "test.local"), + ("Test.Local.", "test.local"), + ("TEST.LOCAL...", "test.local"), + ("example.com", "example.com"), + ("EXAMPLE.COM.", "example.com"), + ], +) +def test_normalize_hostname_parametrized(hostname: str, expected: str) -> None: + """Test hostname normalization with various inputs.""" + assert normalize_hostname(hostname) == expected + + +@pytest.mark.parametrize( + "mdns_arg,expected", + [ + ("host=1.2.3.4", {"host": ["1.2.3.4"]}), + ("Host.Local=1.2.3.4,5.6.7.8", {"host.local": ["1.2.3.4", "5.6.7.8"]}), + ("HOST.LOCAL.=::1", {"host.local": ["::1"]}), + ], +) +def test_parse_cache_args_parametrized( + mdns_arg: str, expected: dict[str, list[str]] +) -> None: + """Test parsing of cache arguments with various formats.""" + cache = AddressCache.from_cli_args([mdns_arg], []) + assert cache.mdns_cache == expected diff --git a/tests/unit_tests/test_config_auto_load.py b/tests/unit_tests/test_config_auto_load.py new file mode 100644 index 0000000000..d31b17eeec --- /dev/null +++ b/tests/unit_tests/test_config_auto_load.py @@ -0,0 +1,131 @@ +"""Tests for AUTO_LOAD functionality including dynamic AUTO_LOAD.""" + +from pathlib import Path +from typing import Any +from unittest.mock import Mock + +import pytest + +from esphome import config, config_validation as cv, yaml_util +from esphome.core import CORE + + +@pytest.fixture +def fixtures_dir() -> Path: + """Get the fixtures directory.""" + return Path(__file__).parent / "fixtures" + + +@pytest.fixture +def default_component() -> Mock: + """Create a default mock component for unmocked components.""" + return Mock( + auto_load=[], + is_platform_component=False, + is_platform=False, + multi_conf=False, + multi_conf_no_default=False, + dependencies=[], + conflicts_with=[], + config_schema=cv.Schema({}, extra=cv.ALLOW_EXTRA), + ) + + +@pytest.fixture +def static_auto_load_component() -> Mock: + """Create a mock component with static AUTO_LOAD.""" + return Mock( + auto_load=["logger"], + is_platform_component=False, + is_platform=False, + multi_conf=False, + multi_conf_no_default=False, + dependencies=[], + conflicts_with=[], + config_schema=cv.Schema({}, extra=cv.ALLOW_EXTRA), + ) + + +def test_static_auto_load_adds_components( + mock_get_component: Mock, + fixtures_dir: Path, + static_auto_load_component: Mock, + default_component: Mock, +) -> None: + """Test that static AUTO_LOAD triggers loading of specified components.""" + CORE.config_path = fixtures_dir / "auto_load_static.yaml" + + config_file = fixtures_dir / "auto_load_static.yaml" + raw_config = yaml_util.load_yaml(config_file) + + component_mocks = {"test_component": static_auto_load_component} + mock_get_component.side_effect = lambda name: component_mocks.get( + name, default_component + ) + + result = config.validate_config(raw_config, {}) + + # Check for validation errors + assert not result.errors, f"Validation errors: {result.errors}" + + # Logger should have been auto-loaded by test_component + assert "logger" in result + assert "test_component" in result + + +def test_dynamic_auto_load_with_config_param( + mock_get_component: Mock, + fixtures_dir: Path, + default_component: Mock, +) -> None: + """Test that dynamic AUTO_LOAD evaluates based on configuration.""" + CORE.config_path = fixtures_dir / "auto_load_dynamic.yaml" + + config_file = fixtures_dir / "auto_load_dynamic.yaml" + raw_config = yaml_util.load_yaml(config_file) + + # Track if auto_load was called with config + auto_load_calls = [] + + def dynamic_auto_load(conf: dict[str, Any]) -> list[str]: + """Dynamically load components based on config.""" + auto_load_calls.append(conf) + component_map = { + "enable_logger": "logger", + "enable_api": "api", + } + return [comp for key, comp in component_map.items() if conf.get(key)] + + dynamic_component = Mock( + auto_load=dynamic_auto_load, + is_platform_component=False, + is_platform=False, + multi_conf=False, + multi_conf_no_default=False, + dependencies=[], + conflicts_with=[], + config_schema=cv.Schema({}, extra=cv.ALLOW_EXTRA), + ) + + component_mocks = {"test_component": dynamic_component} + mock_get_component.side_effect = lambda name: component_mocks.get( + name, default_component + ) + + result = config.validate_config(raw_config, {}) + + # Check for validation errors + assert not result.errors, f"Validation errors: {result.errors}" + + # Verify auto_load was called with the validated config + assert len(auto_load_calls) == 1, "auto_load should be called exactly once" + assert auto_load_calls[0].get("enable_logger") is True + assert auto_load_calls[0].get("enable_api") is False + + # Only logger should be auto-loaded (enable_logger=true in YAML) + assert "logger" in result, ( + f"Logger not found in result. Result keys: {list(result.keys())}" + ) + # API should NOT be auto-loaded (enable_api=false in YAML) + assert "api" not in result + assert "test_component" in result diff --git a/tests/unit_tests/test_config_normalization.py b/tests/unit_tests/test_config_normalization.py new file mode 100644 index 0000000000..d70f3c24e0 --- /dev/null +++ b/tests/unit_tests/test_config_normalization.py @@ -0,0 +1,115 @@ +"""Unit tests for esphome.config module.""" + +from collections.abc import Generator +from pathlib import Path +from unittest.mock import MagicMock, Mock, patch + +import pytest + +from esphome import config, yaml_util +from esphome.core import CORE + + +@pytest.fixture +def mock_get_platform() -> Generator[Mock, None, None]: + """Fixture for mocking get_platform.""" + with patch("esphome.config.get_platform") as mock_get_platform: + # Default mock platform + mock_get_platform.return_value = MagicMock() + yield mock_get_platform + + +@pytest.fixture +def fixtures_dir() -> Path: + """Get the fixtures directory.""" + return Path(__file__).parent / "fixtures" + + +def test_ota_component_configs_with_proper_platform_list( + mock_get_component: Mock, + mock_get_platform: Mock, +) -> None: + """Test iter_component_configs handles OTA properly configured as a list.""" + test_config = { + "ota": [ + {"platform": "esphome", "password": "test123", "id": "my_ota"}, + ], + } + + mock_get_component.return_value = MagicMock( + is_platform_component=True, multi_conf=False + ) + + configs = list(config.iter_component_configs(test_config)) + assert len(configs) == 2 + + assert configs[0][0] == "ota" + assert configs[0][2] == test_config["ota"] # The list itself + + assert configs[1][0] == "ota.esphome" + assert configs[1][2]["platform"] == "esphome" + assert configs[1][2]["password"] == "test123" + + +def test_iter_component_configs_with_multi_conf(mock_get_component: Mock) -> None: + """Test that iter_component_configs handles multi_conf components correctly.""" + test_config = { + "switch": [ + {"name": "Switch 1"}, + {"name": "Switch 2"}, + ], + } + + mock_get_component.return_value = MagicMock( + is_platform_component=False, multi_conf=True + ) + + configs = list(config.iter_component_configs(test_config)) + assert len(configs) == 2 + + for domain, component, conf in configs: + assert domain == "switch" + assert "name" in conf + + +def test_ota_no_platform_with_captive_portal(fixtures_dir: Path) -> None: + """Test OTA with no platform (ota:) gets normalized when captive_portal auto-loads.""" + CORE.config_path = fixtures_dir / "dummy.yaml" + + config_file = fixtures_dir / "ota_no_platform.yaml" + raw_config = yaml_util.load_yaml(config_file) + result = config.validate_config(raw_config, {}) + + assert "ota" in result + assert isinstance(result["ota"], list), f"Expected list, got {type(result['ota'])}" + platforms = {p.get("platform") for p in result["ota"]} + assert "web_server" in platforms, f"Expected web_server platform in {platforms}" + + +def test_ota_empty_dict_with_captive_portal(fixtures_dir: Path) -> None: + """Test OTA with empty dict ({}) gets normalized when captive_portal auto-loads.""" + CORE.config_path = fixtures_dir / "dummy.yaml" + + config_file = fixtures_dir / "ota_empty_dict.yaml" + raw_config = yaml_util.load_yaml(config_file) + result = config.validate_config(raw_config, {}) + + assert "ota" in result + assert isinstance(result["ota"], list), f"Expected list, got {type(result['ota'])}" + platforms = {p.get("platform") for p in result["ota"]} + assert "web_server" in platforms, f"Expected web_server platform in {platforms}" + + +def test_ota_with_platform_list_and_captive_portal(fixtures_dir: Path) -> None: + """Test OTA with proper platform list remains valid when captive_portal auto-loads.""" + CORE.config_path = fixtures_dir / "dummy.yaml" + + config_file = fixtures_dir / "ota_with_platform_list.yaml" + raw_config = yaml_util.load_yaml(config_file) + result = config.validate_config(raw_config, {}) + + assert "ota" in result + assert isinstance(result["ota"], list), f"Expected list, got {type(result['ota'])}" + platforms = {p.get("platform") for p in result["ota"]} + assert "esphome" in platforms, f"Expected esphome platform in {platforms}" + assert "web_server" in platforms, f"Expected web_server platform in {platforms}" diff --git a/tests/unit_tests/test_config_validation.py b/tests/unit_tests/test_config_validation.py index 2928c5c83a..104cdc2b7a 100644 --- a/tests/unit_tests/test_config_validation.py +++ b/tests/unit_tests/test_config_validation.py @@ -3,6 +3,7 @@ import string from hypothesis import example, given from hypothesis.strategies import builds, integers, ip_addresses, one_of, text import pytest +import voluptuous as vol from esphome import config_validation from esphome.components.esp32.const import ( @@ -301,8 +302,6 @@ def test_split_default(framework, platform, variant, full, idf, arduino, simple) ], ) def test_require_framework_version(framework, platform, message): - import voluptuous as vol - from esphome.const import ( KEY_CORE, KEY_FRAMEWORK_VERSION, @@ -377,3 +376,129 @@ def test_require_framework_version(framework, platform, message): config_validation.require_framework_version( extra_message="test 5", )("test") + + +def test_only_with_single_component_loaded() -> None: + """Test OnlyWith with single component when component is loaded.""" + CORE.loaded_integrations = {"mqtt"} + + schema = config_validation.Schema( + { + config_validation.OnlyWith("mqtt_id", "mqtt", default="test_mqtt"): str, + } + ) + + result = schema({}) + assert result.get("mqtt_id") == "test_mqtt" + + +def test_only_with_single_component_not_loaded() -> None: + """Test OnlyWith with single component when component is not loaded.""" + CORE.loaded_integrations = set() + + schema = config_validation.Schema( + { + config_validation.OnlyWith("mqtt_id", "mqtt", default="test_mqtt"): str, + } + ) + + result = schema({}) + assert "mqtt_id" not in result + + +def test_only_with_list_all_components_loaded() -> None: + """Test OnlyWith with list when all components are loaded.""" + CORE.loaded_integrations = {"zigbee", "nrf52"} + + schema = config_validation.Schema( + { + config_validation.OnlyWith( + "zigbee_id", ["zigbee", "nrf52"], default="test_zigbee" + ): str, + } + ) + + result = schema({}) + assert result.get("zigbee_id") == "test_zigbee" + + +def test_only_with_list_partial_components_loaded() -> None: + """Test OnlyWith with list when only some components are loaded.""" + CORE.loaded_integrations = {"zigbee"} # Only zigbee, not nrf52 + + schema = config_validation.Schema( + { + config_validation.OnlyWith( + "zigbee_id", ["zigbee", "nrf52"], default="test_zigbee" + ): str, + } + ) + + result = schema({}) + assert "zigbee_id" not in result + + +def test_only_with_list_no_components_loaded() -> None: + """Test OnlyWith with list when no components are loaded.""" + CORE.loaded_integrations = set() + + schema = config_validation.Schema( + { + config_validation.OnlyWith( + "zigbee_id", ["zigbee", "nrf52"], default="test_zigbee" + ): str, + } + ) + + result = schema({}) + assert "zigbee_id" not in result + + +def test_only_with_list_multiple_components() -> None: + """Test OnlyWith with list requiring three components.""" + CORE.loaded_integrations = {"comp1", "comp2", "comp3"} + + schema = config_validation.Schema( + { + config_validation.OnlyWith( + "test_id", ["comp1", "comp2", "comp3"], default="test_value" + ): str, + } + ) + + result = schema({}) + assert result.get("test_id") == "test_value" + + # Test with one missing + CORE.loaded_integrations = {"comp1", "comp2"} + result = schema({}) + assert "test_id" not in result + + +def test_only_with_empty_list() -> None: + """Test OnlyWith with empty list (edge case).""" + CORE.loaded_integrations = set() + + schema = config_validation.Schema( + { + config_validation.OnlyWith("test_id", [], default="test_value"): str, + } + ) + + # all([]) returns True, so default should be applied + result = schema({}) + assert result.get("test_id") == "test_value" + + +def test_only_with_user_value_overrides_default() -> None: + """Test OnlyWith respects user-provided values over defaults.""" + CORE.loaded_integrations = {"mqtt"} + + schema = config_validation.Schema( + { + config_validation.OnlyWith("mqtt_id", "mqtt", default="default_id"): str, + } + ) + + result = schema({"mqtt_id": "custom_id"}) + assert result.get("mqtt_id") == "custom_id" diff --git a/tests/unit_tests/test_config_validation_paths.py b/tests/unit_tests/test_config_validation_paths.py new file mode 100644 index 0000000000..f327e9c443 --- /dev/null +++ b/tests/unit_tests/test_config_validation_paths.py @@ -0,0 +1,187 @@ +"""Tests for config_validation.py path-related functions.""" + +from pathlib import Path + +import pytest +import voluptuous as vol + +from esphome import config_validation as cv + + +def test_directory_valid_path(setup_core: Path) -> None: + """Test directory validator with valid directory.""" + test_dir = setup_core / "test_directory" + test_dir.mkdir() + + result = cv.directory("test_directory") + + assert result == test_dir + + +def test_directory_absolute_path(setup_core: Path) -> None: + """Test directory validator with absolute path.""" + test_dir = setup_core / "test_directory" + test_dir.mkdir() + + result = cv.directory(str(test_dir)) + + assert result == test_dir + + +def test_directory_nonexistent_path(setup_core: Path) -> None: + """Test directory validator raises error for non-existent directory.""" + with pytest.raises( + vol.Invalid, match="Could not find directory.*nonexistent_directory" + ): + cv.directory("nonexistent_directory") + + +def test_directory_file_instead_of_directory(setup_core: Path) -> None: + """Test directory validator raises error when path is a file.""" + test_file = setup_core / "test_file.txt" + test_file.write_text("content") + + with pytest.raises(vol.Invalid, match="is not a directory"): + cv.directory("test_file.txt") + + +def test_directory_with_parent_directory(setup_core: Path) -> None: + """Test directory validator with nested directory structure.""" + nested_dir = setup_core / "parent" / "child" / "grandchild" + nested_dir.mkdir(parents=True) + + result = cv.directory("parent/child/grandchild") + + assert result == nested_dir + + +def test_file_valid_path(setup_core: Path) -> None: + """Test file_ validator with valid file.""" + test_file = setup_core / "test_file.yaml" + test_file.write_text("test content") + + result = cv.file_("test_file.yaml") + + assert result == test_file + + +def test_file_absolute_path(setup_core: Path) -> None: + """Test file_ validator with absolute path.""" + test_file = setup_core / "test_file.yaml" + test_file.write_text("test content") + + result = cv.file_(str(test_file)) + + assert result == test_file + + +def test_file_nonexistent_path(setup_core: Path) -> None: + """Test file_ validator raises error for non-existent file.""" + with pytest.raises(vol.Invalid, match="Could not find file.*nonexistent_file.yaml"): + cv.file_("nonexistent_file.yaml") + + +def test_file_directory_instead_of_file(setup_core: Path) -> None: + """Test file_ validator raises error when path is a directory.""" + test_dir = setup_core / "test_directory" + test_dir.mkdir() + + with pytest.raises(vol.Invalid, match="is not a file"): + cv.file_("test_directory") + + +def test_file_with_parent_directory(setup_core: Path) -> None: + """Test file_ validator with file in nested directory.""" + nested_dir = setup_core / "configs" / "sensors" + nested_dir.mkdir(parents=True) + test_file = nested_dir / "temperature.yaml" + test_file.write_text("sensor config") + + result = cv.file_("configs/sensors/temperature.yaml") + + assert result == test_file + + +def test_directory_handles_trailing_slash(setup_core: Path) -> None: + """Test directory validator handles trailing slashes correctly.""" + test_dir = setup_core / "test_dir" + test_dir.mkdir() + + result = cv.directory("test_dir/") + assert result == test_dir + + result = cv.directory("test_dir") + assert result == test_dir + + +def test_file_handles_various_extensions(setup_core: Path) -> None: + """Test file_ validator works with different file extensions.""" + yaml_file = setup_core / "config.yaml" + yaml_file.write_text("yaml content") + assert cv.file_("config.yaml") == yaml_file + + yml_file = setup_core / "config.yml" + yml_file.write_text("yml content") + assert cv.file_("config.yml") == yml_file + + txt_file = setup_core / "readme.txt" + txt_file.write_text("text content") + assert cv.file_("readme.txt") == txt_file + + no_ext_file = setup_core / "LICENSE" + no_ext_file.write_text("license content") + assert cv.file_("LICENSE") == no_ext_file + + +def test_directory_with_symlink(setup_core: Path) -> None: + """Test directory validator follows symlinks.""" + actual_dir = setup_core / "actual_directory" + actual_dir.mkdir() + + symlink_dir = setup_core / "symlink_directory" + symlink_dir.symlink_to(actual_dir) + + result = cv.directory("symlink_directory") + assert result == symlink_dir + + +def test_file_with_symlink(setup_core: Path) -> None: + """Test file_ validator follows symlinks.""" + actual_file = setup_core / "actual_file.txt" + actual_file.write_text("content") + + symlink_file = setup_core / "symlink_file.txt" + symlink_file.symlink_to(actual_file) + + result = cv.file_("symlink_file.txt") + assert result == symlink_file + + +def test_directory_error_shows_full_path(setup_core: Path) -> None: + """Test directory validator error message includes full path.""" + with pytest.raises(vol.Invalid, match=".*missing_dir.*full path:.*"): + cv.directory("missing_dir") + + +def test_file_error_shows_full_path(setup_core: Path) -> None: + """Test file_ validator error message includes full path.""" + with pytest.raises(vol.Invalid, match=".*missing_file.yaml.*full path:.*"): + cv.file_("missing_file.yaml") + + +def test_directory_with_spaces_in_name(setup_core: Path) -> None: + """Test directory validator handles spaces in directory names.""" + dir_with_spaces = setup_core / "my test directory" + dir_with_spaces.mkdir() + + result = cv.directory("my test directory") + assert result == dir_with_spaces + + +def test_file_with_spaces_in_name(setup_core: Path) -> None: + """Test file_ validator handles spaces in file names.""" + file_with_spaces = setup_core / "my test file.yaml" + file_with_spaces.write_text("content") + + result = cv.file_("my test file.yaml") + assert result == file_with_spaces diff --git a/tests/unit_tests/test_core.py b/tests/unit_tests/test_core.py index f7dda9fb95..e52cb24831 100644 --- a/tests/unit_tests/test_core.py +++ b/tests/unit_tests/test_core.py @@ -1,3 +1,7 @@ +import os +from pathlib import Path +from unittest.mock import patch + from hypothesis import given import pytest from strategies import mac_addr_strings @@ -533,8 +537,8 @@ class TestEsphomeCore: @pytest.fixture def target(self, fixture_path): target = core.EsphomeCore() - target.build_path = "foo/build" - target.config_path = "foo/config" + target.build_path = Path("foo/build") + target.config_path = Path("foo/config") return target def test_reset(self, target): @@ -566,6 +570,15 @@ class TestEsphomeCore: assert target.address == "4.3.2.1" + def test_address__openthread(self, target): + target.config = {} + target.config[const.CONF_OPENTHREAD] = { + const.CONF_USE_ADDRESS: "test-device.local" + } + target.name = "test-device" + + assert target.address == "test-device.local" + def test_is_esp32(self, target): target.data[const.KEY_CORE] = {const.KEY_TARGET_PLATFORM: "esp32"} @@ -577,3 +590,131 @@ class TestEsphomeCore: assert target.is_esp32 is False assert target.is_esp8266 is True + + @pytest.mark.skipif(os.name == "nt", reason="Unix-specific test") + def test_data_dir_default_unix(self, target): + """Test data_dir returns .esphome in config directory by default on Unix.""" + target.config_path = Path("/home/user/config.yaml") + assert target.data_dir == Path("/home/user/.esphome") + + @pytest.mark.skipif(os.name != "nt", reason="Windows-specific test") + def test_data_dir_default_windows(self, target): + """Test data_dir returns .esphome in config directory by default on Windows.""" + target.config_path = Path("D:\\home\\user\\config.yaml") + assert target.data_dir == Path("D:\\home\\user\\.esphome") + + def test_data_dir_ha_addon(self, target): + """Test data_dir returns /data when running as Home Assistant addon.""" + target.config_path = Path("/config/test.yaml") + + with patch.dict(os.environ, {"ESPHOME_IS_HA_ADDON": "true"}): + assert target.data_dir == Path("/data") + + def test_data_dir_env_override(self, target): + """Test data_dir uses ESPHOME_DATA_DIR environment variable when set.""" + target.config_path = Path("/home/user/config.yaml") + + with patch.dict(os.environ, {"ESPHOME_DATA_DIR": "/custom/data/path"}): + assert target.data_dir == Path("/custom/data/path") + + @pytest.mark.skipif(os.name == "nt", reason="Unix-specific test") + def test_data_dir_priority_unix(self, target): + """Test data_dir priority on Unix: HA addon > env var > default.""" + target.config_path = Path("/config/test.yaml") + expected_default = "/config/.esphome" + + # Test HA addon takes priority over env var + with patch.dict( + os.environ, + {"ESPHOME_IS_HA_ADDON": "true", "ESPHOME_DATA_DIR": "/custom/path"}, + ): + assert target.data_dir == Path("/data") + + # Test env var is used when not HA addon + with patch.dict( + os.environ, + {"ESPHOME_IS_HA_ADDON": "false", "ESPHOME_DATA_DIR": "/custom/path"}, + ): + assert target.data_dir == Path("/custom/path") + + # Test default when neither is set + with patch.dict(os.environ, {}, clear=True): + # Ensure these env vars are not set + os.environ.pop("ESPHOME_IS_HA_ADDON", None) + os.environ.pop("ESPHOME_DATA_DIR", None) + assert target.data_dir == Path(expected_default) + + @pytest.mark.skipif(os.name != "nt", reason="Windows-specific test") + def test_data_dir_priority_windows(self, target): + """Test data_dir priority on Windows: HA addon > env var > default.""" + target.config_path = Path("D:\\config\\test.yaml") + expected_default = "D:\\config\\.esphome" + + # Test HA addon takes priority over env var + with patch.dict( + os.environ, + {"ESPHOME_IS_HA_ADDON": "true", "ESPHOME_DATA_DIR": "/custom/path"}, + ): + assert target.data_dir == Path("/data") + + # Test env var is used when not HA addon + with patch.dict( + os.environ, + {"ESPHOME_IS_HA_ADDON": "false", "ESPHOME_DATA_DIR": "/custom/path"}, + ): + assert target.data_dir == Path("/custom/path") + + # Test default when neither is set + with patch.dict(os.environ, {}, clear=True): + # Ensure these env vars are not set + os.environ.pop("ESPHOME_IS_HA_ADDON", None) + os.environ.pop("ESPHOME_DATA_DIR", None) + assert target.data_dir == Path(expected_default) + + def test_web_port__none(self, target): + """Test web_port returns None when web_server is not configured.""" + target.config = {} + assert target.web_port is None + + def test_web_port__explicit_web_server_default_port(self, target): + """Test web_port returns 80 when web_server is explicitly configured without port.""" + target.config = {const.CONF_WEB_SERVER: {}} + assert target.web_port == 80 + + def test_web_port__explicit_web_server_custom_port(self, target): + """Test web_port returns custom port when web_server is configured with port.""" + target.config = {const.CONF_WEB_SERVER: {const.CONF_PORT: 8080}} + assert target.web_port == 8080 + + def test_web_port__ota_web_server_platform_only(self, target): + """ + Test web_port returns None when ota.web_server platform is explicitly configured. + + This is a critical test for Dashboard Issue #766: + https://github.com/esphome/dashboard/issues/766 + + When ota: platform: web_server is explicitly configured (or auto-loaded by captive_portal): + - "web_server" appears in loaded_integrations (platform name added to integrations) + - "ota/web_server" appears in loaded_platforms + - But CONF_WEB_SERVER is NOT in config (only the platform is loaded, not the component) + - web_port MUST return None (no web UI available) + - Dashboard should NOT show VISIT button + + This test ensures web_port only checks CONF_WEB_SERVER in config, not loaded_integrations. + """ + # Simulate config with ota.web_server platform but no web_server component + # This happens when: + # 1. User explicitly configures: ota: - platform: web_server + # 2. OR captive_portal auto-loads ota.web_server + target.config = { + const.CONF_OTA: [ + { + "platform": "web_server", + # OTA web_server platform config would be here + } + ], + # Note: CONF_WEB_SERVER is NOT in config - only the OTA platform + } + # Even though "web_server" is in loaded_integrations due to the platform, + # web_port must return None because the full web_server component is not configured + assert target.web_port is None diff --git a/tests/unit_tests/test_coroutine.py b/tests/unit_tests/test_coroutine.py new file mode 100644 index 0000000000..e12c273294 --- /dev/null +++ b/tests/unit_tests/test_coroutine.py @@ -0,0 +1,219 @@ +"""Tests for the coroutine module.""" + +import pytest + +from esphome.coroutine import CoroPriority, FakeEventLoop, coroutine_with_priority + + +def test_coro_priority_enum_values() -> None: + """Test that CoroPriority enum values match expected priorities.""" + assert CoroPriority.PLATFORM == 1000 + assert CoroPriority.NETWORK == 201 + assert CoroPriority.NETWORK_TRANSPORT == 200 + assert CoroPriority.CORE == 100 + assert CoroPriority.DIAGNOSTICS == 90 + assert CoroPriority.STATUS == 80 + assert CoroPriority.WEB_SERVER_BASE == 65 + assert CoroPriority.CAPTIVE_PORTAL == 64 + assert CoroPriority.COMMUNICATION == 60 + assert CoroPriority.NETWORK_SERVICES == 55 + assert CoroPriority.OTA_UPDATES == 54 + assert CoroPriority.WEB_SERVER_OTA == 52 + assert CoroPriority.APPLICATION == 50 + assert CoroPriority.WEB == 40 + assert CoroPriority.AUTOMATION == 30 + assert CoroPriority.BUS == 1 + assert CoroPriority.COMPONENT == 0 + assert CoroPriority.LATE == -100 + assert CoroPriority.WORKAROUNDS == -999 + assert CoroPriority.FINAL == -1000 + + +def test_coroutine_with_priority_accepts_float() -> None: + """Test that coroutine_with_priority accepts float values.""" + + @coroutine_with_priority(100.0) + def test_func() -> None: + pass + + assert hasattr(test_func, "priority") + assert test_func.priority == 100.0 + + +def test_coroutine_with_priority_accepts_enum() -> None: + """Test that coroutine_with_priority accepts CoroPriority enum values.""" + + @coroutine_with_priority(CoroPriority.CORE) + def test_func() -> None: + pass + + assert hasattr(test_func, "priority") + assert test_func.priority == 100.0 + + +def test_float_and_enum_are_interchangeable() -> None: + """Test that float and CoroPriority enum values produce the same priority.""" + + @coroutine_with_priority(100.0) + def func_with_float() -> None: + pass + + @coroutine_with_priority(CoroPriority.CORE) + def func_with_enum() -> None: + pass + + assert func_with_float.priority == func_with_enum.priority + assert func_with_float.priority == 100.0 + + +@pytest.mark.parametrize( + ("enum_value", "float_value"), + [ + (CoroPriority.PLATFORM, 1000.0), + (CoroPriority.NETWORK, 201.0), + (CoroPriority.NETWORK_TRANSPORT, 200.0), + (CoroPriority.CORE, 100.0), + (CoroPriority.DIAGNOSTICS, 90.0), + (CoroPriority.STATUS, 80.0), + (CoroPriority.WEB_SERVER_BASE, 65.0), + (CoroPriority.CAPTIVE_PORTAL, 64.0), + (CoroPriority.COMMUNICATION, 60.0), + (CoroPriority.NETWORK_SERVICES, 55.0), + (CoroPriority.OTA_UPDATES, 54.0), + (CoroPriority.WEB_SERVER_OTA, 52.0), + (CoroPriority.APPLICATION, 50.0), + (CoroPriority.WEB, 40.0), + (CoroPriority.AUTOMATION, 30.0), + (CoroPriority.BUS, 1.0), + (CoroPriority.COMPONENT, 0.0), + (CoroPriority.LATE, -100.0), + (CoroPriority.WORKAROUNDS, -999.0), + (CoroPriority.FINAL, -1000.0), + ], +) +def test_all_priority_values_are_interchangeable( + enum_value: CoroPriority, float_value: float +) -> None: + """Test that all CoroPriority values work correctly with coroutine_with_priority.""" + + @coroutine_with_priority(enum_value) + def func_with_enum() -> None: + pass + + @coroutine_with_priority(float_value) + def func_with_float() -> None: + pass + + assert func_with_enum.priority == float_value + assert func_with_float.priority == float_value + assert func_with_enum.priority == func_with_float.priority + + +def test_execution_order_with_enum_priorities() -> None: + """Test that execution order is correct when using enum priorities.""" + execution_order: list[str] = [] + + @coroutine_with_priority(CoroPriority.PLATFORM) + async def platform_func() -> None: + execution_order.append("platform") + + @coroutine_with_priority(CoroPriority.CORE) + async def core_func() -> None: + execution_order.append("core") + + @coroutine_with_priority(CoroPriority.FINAL) + async def final_func() -> None: + execution_order.append("final") + + # Create event loop and add jobs + loop = FakeEventLoop() + loop.add_job(platform_func) + loop.add_job(core_func) + loop.add_job(final_func) + + # Run all tasks + loop.flush_tasks() + + # Check execution order (higher priority runs first) + assert execution_order == ["platform", "core", "final"] + + +def test_mixed_float_and_enum_priorities() -> None: + """Test that mixing float and enum priorities works correctly.""" + execution_order: list[str] = [] + + @coroutine_with_priority(1000.0) # Same as PLATFORM + async def func1() -> None: + execution_order.append("func1") + + @coroutine_with_priority(CoroPriority.CORE) + async def func2() -> None: + execution_order.append("func2") + + @coroutine_with_priority(-1000.0) # Same as FINAL + async def func3() -> None: + execution_order.append("func3") + + # Create event loop and add jobs + loop = FakeEventLoop() + loop.add_job(func2) + loop.add_job(func3) + loop.add_job(func1) + + # Run all tasks + loop.flush_tasks() + + # Check execution order + assert execution_order == ["func1", "func2", "func3"] + + +def test_enum_priority_comparison() -> None: + """Test that enum priorities can be compared directly.""" + assert CoroPriority.PLATFORM > CoroPriority.NETWORK + assert CoroPriority.NETWORK > CoroPriority.NETWORK_TRANSPORT + assert CoroPriority.NETWORK_TRANSPORT > CoroPriority.CORE + assert CoroPriority.CORE > CoroPriority.DIAGNOSTICS + assert CoroPriority.DIAGNOSTICS > CoroPriority.STATUS + assert CoroPriority.STATUS > CoroPriority.WEB_SERVER_BASE + assert CoroPriority.WEB_SERVER_BASE > CoroPriority.CAPTIVE_PORTAL + assert CoroPriority.CAPTIVE_PORTAL > CoroPriority.COMMUNICATION + assert CoroPriority.COMMUNICATION > CoroPriority.NETWORK_SERVICES + assert CoroPriority.NETWORK_SERVICES > CoroPriority.OTA_UPDATES + assert CoroPriority.OTA_UPDATES > CoroPriority.WEB_SERVER_OTA + assert CoroPriority.WEB_SERVER_OTA > CoroPriority.APPLICATION + assert CoroPriority.APPLICATION > CoroPriority.WEB + assert CoroPriority.WEB > CoroPriority.AUTOMATION + assert CoroPriority.AUTOMATION > CoroPriority.BUS + assert CoroPriority.BUS > CoroPriority.COMPONENT + assert CoroPriority.COMPONENT > CoroPriority.LATE + assert CoroPriority.LATE > CoroPriority.WORKAROUNDS + assert CoroPriority.WORKAROUNDS > CoroPriority.FINAL + + +def test_custom_priority_between_enum_values() -> None: + """Test that custom float priorities between enum values work correctly.""" + execution_order: list[str] = [] + + @coroutine_with_priority(CoroPriority.CORE) # 100 + async def core_func() -> None: + execution_order.append("core") + + @coroutine_with_priority(95.0) # Between CORE and DIAGNOSTICS + async def custom_func() -> None: + execution_order.append("custom") + + @coroutine_with_priority(CoroPriority.DIAGNOSTICS) # 90 + async def diag_func() -> None: + execution_order.append("diagnostics") + + # Create event loop and add jobs + loop = FakeEventLoop() + loop.add_job(diag_func) + loop.add_job(core_func) + loop.add_job(custom_func) + + # Run all tasks + loop.flush_tasks() + + # Check execution order + assert execution_order == ["core", "custom", "diagnostics"] diff --git a/tests/unit_tests/test_cpp_generator.py b/tests/unit_tests/test_cpp_generator.py index 95633ca0c6..2c9f760c8e 100644 --- a/tests/unit_tests/test_cpp_generator.py +++ b/tests/unit_tests/test_cpp_generator.py @@ -173,6 +173,61 @@ class TestLambdaExpression: "}" ) + def test_str__stateless_no_return(self): + """Test stateless lambda (empty capture) generates correctly""" + target = cg.LambdaExpression( + ('ESP_LOGD("main", "Test message");',), + (), # No parameters + "", # Empty capture (stateless) + ) + + actual = str(target) + + assert actual == ('[]() {\n ESP_LOGD("main", "Test message");\n}') + + def test_str__stateless_with_return(self): + """Test stateless lambda with return type generates correctly""" + target = cg.LambdaExpression( + ("return global_value > 0;",), + (), # No parameters + "", # Empty capture (stateless) + bool, # Return type + ) + + actual = str(target) + + assert actual == ("[]() -> bool {\n return global_value > 0;\n}") + + def test_str__stateless_with_params(self): + """Test stateless lambda with parameters generates correctly""" + target = cg.LambdaExpression( + ("return foo + bar;",), + ((int, "foo"), (float, "bar")), + "", # Empty capture (stateless) + float, + ) + + actual = str(target) + + assert actual == ( + "[](int32_t foo, float bar) -> float {\n return foo + bar;\n}" + ) + + def test_str__with_capture(self): + """Test lambda with capture generates correctly""" + target = cg.LambdaExpression( + ("return captured_var + x;",), + ((int, "x"),), + "captured_var", # Has capture (not stateless) + int, + ) + + actual = str(target) + + assert actual == ( + "[captured_var](int32_t x) -> int32_t {\n return captured_var + x;\n}" + ) + class TestLiterals: @pytest.mark.parametrize( diff --git a/tests/unit_tests/test_espota2.py b/tests/unit_tests/test_espota2.py new file mode 100644 index 0000000000..02f965782b --- /dev/null +++ b/tests/unit_tests/test_espota2.py @@ -0,0 +1,738 @@ +"""Unit tests for esphome.espota2 module.""" + +from __future__ import annotations + +from collections.abc import Generator +import gzip +import hashlib +import io +from pathlib import Path +import socket +import struct +from unittest.mock import Mock, call, patch + +import pytest +from pytest import CaptureFixture + +from esphome import espota2 +from esphome.core import EsphomeError + +# Test constants +MOCK_RANDOM_VALUE = 0.123456 +MOCK_RANDOM_BYTES = b"0.123456" +MOCK_MD5_NONCE = b"12345678901234567890123456789012" # 32 char nonce for MD5 +MOCK_SHA256_NONCE = b"1234567890123456789012345678901234567890123456789012345678901234" # 64 char nonce for SHA256 + + +@pytest.fixture +def mock_socket() -> Mock: + """Create a mock socket for testing.""" + socket_mock = Mock() + socket_mock.close = Mock() + socket_mock.recv = Mock() + socket_mock.sendall = Mock() + socket_mock.settimeout = Mock() + socket_mock.connect = Mock() + socket_mock.setsockopt = Mock() + return socket_mock + + +@pytest.fixture +def mock_file() -> io.BytesIO: + """Create a mock firmware file for testing.""" + return io.BytesIO(b"firmware content here") + + +@pytest.fixture +def mock_time() -> Generator[None]: + """Mock time-related functions for consistent testing.""" + # Provide enough values for multiple calls (tests may call perform_ota multiple times) + with ( + patch("time.sleep"), + patch("time.perf_counter", side_effect=[0, 1, 0, 1, 0, 1]), + ): + yield + + +@pytest.fixture +def mock_random() -> Generator[Mock]: + """Mock random for predictable test values.""" + with patch("random.random", return_value=MOCK_RANDOM_VALUE) as mock_rand: + yield mock_rand + + +@pytest.fixture +def mock_resolve_ip() -> Generator[Mock]: + """Mock resolve_ip_address for testing.""" + with patch("esphome.espota2.resolve_ip_address") as mock: + mock.return_value = [ + (socket.AF_INET, socket.SOCK_STREAM, 0, "", ("192.168.1.100", 3232)) + ] + yield mock + + +@pytest.fixture +def mock_perform_ota() -> Generator[Mock]: + """Mock perform_ota function for testing.""" + with patch("esphome.espota2.perform_ota") as mock: + yield mock + + +@pytest.fixture +def mock_run_ota_impl() -> Generator[Mock]: + """Mock run_ota_impl_ function for testing.""" + with patch("esphome.espota2.run_ota_impl_") as mock: + mock.return_value = (0, "192.168.1.100") + yield mock + + +@pytest.fixture +def mock_socket_constructor(mock_socket: Mock) -> Generator[Mock]: + """Mock socket.socket constructor to return our mock socket.""" + with patch("socket.socket", return_value=mock_socket) as mock_constructor: + yield mock_constructor + + +def test_recv_decode_with_decode(mock_socket: Mock) -> None: + """Test recv_decode with decode=True returns list.""" + mock_socket.recv.return_value = b"\x01\x02\x03" + + result = espota2.recv_decode(mock_socket, 3, decode=True) + + assert result == [1, 2, 3] + mock_socket.recv.assert_called_once_with(3) + + +def test_recv_decode_without_decode(mock_socket: Mock) -> None: + """Test recv_decode with decode=False returns bytes.""" + mock_socket.recv.return_value = b"\x01\x02\x03" + + result = espota2.recv_decode(mock_socket, 3, decode=False) + + assert result == b"\x01\x02\x03" + mock_socket.recv.assert_called_once_with(3) + + +def test_receive_exactly_success(mock_socket: Mock) -> None: + """Test receive_exactly successfully receives expected data.""" + mock_socket.recv.side_effect = [b"\x00", b"\x01\x02"] + + result = espota2.receive_exactly(mock_socket, 3, "test", espota2.RESPONSE_OK) + + assert result == [0, 1, 2] + assert mock_socket.recv.call_count == 2 + + +def test_receive_exactly_with_error_response(mock_socket: Mock) -> None: + """Test receive_exactly raises OTAError on error response.""" + mock_socket.recv.return_value = bytes([espota2.RESPONSE_ERROR_AUTH_INVALID]) + + with pytest.raises(espota2.OTAError, match="Error auth:.*Authentication invalid"): + espota2.receive_exactly(mock_socket, 1, "auth", [espota2.RESPONSE_OK]) + + mock_socket.close.assert_called_once() + + +def test_receive_exactly_socket_error(mock_socket: Mock) -> None: + """Test receive_exactly handles socket errors.""" + mock_socket.recv.side_effect = OSError("Connection reset") + + with pytest.raises(espota2.OTAError, match="Error receiving acknowledge test"): + espota2.receive_exactly(mock_socket, 1, "test", espota2.RESPONSE_OK) + + +@pytest.mark.parametrize( + ("error_code", "expected_msg"), + [ + (espota2.RESPONSE_ERROR_MAGIC, "Error: Invalid magic byte"), + (espota2.RESPONSE_ERROR_UPDATE_PREPARE, "Error: Couldn't prepare flash memory"), + (espota2.RESPONSE_ERROR_AUTH_INVALID, "Error: Authentication invalid"), + ( + espota2.RESPONSE_ERROR_WRITING_FLASH, + "Error: Writing OTA data to flash memory failed", + ), + (espota2.RESPONSE_ERROR_UPDATE_END, "Error: Finishing update failed"), + ( + espota2.RESPONSE_ERROR_INVALID_BOOTSTRAPPING, + "Error: Please press the reset button", + ), + ( + espota2.RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG, + "Error: ESP has been flashed with wrong flash size", + ), + ( + espota2.RESPONSE_ERROR_WRONG_NEW_FLASH_CONFIG, + "Error: ESP does not have the requested flash size", + ), + ( + espota2.RESPONSE_ERROR_ESP8266_NOT_ENOUGH_SPACE, + "Error: ESP does not have enough space", + ), + ( + espota2.RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE, + "Error: The OTA partition on the ESP is too small", + ), + ( + espota2.RESPONSE_ERROR_NO_UPDATE_PARTITION, + "Error: The OTA partition on the ESP couldn't be found", + ), + (espota2.RESPONSE_ERROR_MD5_MISMATCH, "Error: Application MD5 code mismatch"), + (espota2.RESPONSE_ERROR_UNKNOWN, "Unknown error from ESP"), + ], +) +def test_check_error_with_various_errors(error_code: int, expected_msg: str) -> None: + """Test check_error raises appropriate errors for different error codes.""" + with pytest.raises(espota2.OTAError, match=expected_msg): + espota2.check_error([error_code], [espota2.RESPONSE_OK]) + + +def test_check_error_unexpected_response() -> None: + """Test check_error raises error for unexpected response.""" + with pytest.raises(espota2.OTAError, match="Unexpected response from ESP: 0x7F"): + espota2.check_error([0x7F], [espota2.RESPONSE_OK, espota2.RESPONSE_AUTH_OK]) + + +def test_send_check_with_various_data_types(mock_socket: Mock) -> None: + """Test send_check handles different data types.""" + + # Test with list/tuple + espota2.send_check(mock_socket, [0x01, 0x02], "list") + mock_socket.sendall.assert_called_with(b"\x01\x02") + + # Test with int + espota2.send_check(mock_socket, 0x42, "int") + mock_socket.sendall.assert_called_with(b"\x42") + + # Test with string + espota2.send_check(mock_socket, "hello", "string") + mock_socket.sendall.assert_called_with(b"hello") + + # Test with bytes (should pass through) + espota2.send_check(mock_socket, b"\xaa\xbb", "bytes") + mock_socket.sendall.assert_called_with(b"\xaa\xbb") + + +def test_send_check_socket_error(mock_socket: Mock) -> None: + """Test send_check handles socket errors.""" + mock_socket.sendall.side_effect = OSError("Broken pipe") + + with pytest.raises(espota2.OTAError, match="Error sending test"): + espota2.send_check(mock_socket, b"data", "test") + + +@pytest.mark.usefixtures("mock_time") +def test_perform_ota_successful_md5_auth( + mock_socket: Mock, mock_file: io.BytesIO, mock_random: Mock +) -> None: + """Test successful OTA with MD5 authentication.""" + # Setup socket responses for recv calls + recv_responses = [ + bytes([espota2.RESPONSE_OK]), # First byte of version response + bytes([espota2.OTA_VERSION_2_0]), # Version number + bytes([espota2.RESPONSE_HEADER_OK]), # Features response + bytes([espota2.RESPONSE_REQUEST_AUTH]), # Auth request + MOCK_MD5_NONCE, # 32 char hex nonce + bytes([espota2.RESPONSE_AUTH_OK]), # Auth result + bytes([espota2.RESPONSE_UPDATE_PREPARE_OK]), # Binary size OK + bytes([espota2.RESPONSE_BIN_MD5_OK]), # MD5 checksum OK + bytes([espota2.RESPONSE_CHUNK_OK]), # Chunk OK + bytes([espota2.RESPONSE_RECEIVE_OK]), # Receive OK + bytes([espota2.RESPONSE_UPDATE_END_OK]), # Update end OK + ] + + mock_socket.recv.side_effect = recv_responses + + # Run OTA + espota2.perform_ota(mock_socket, "testpass", mock_file, "test.bin") + + # Verify magic bytes were sent + assert mock_socket.sendall.call_args_list[0] == call(bytes(espota2.MAGIC_BYTES)) + + # Verify features were sent (compression + SHA256 support) + assert mock_socket.sendall.call_args_list[1] == call( + bytes( + [ + espota2.FEATURE_SUPPORTS_COMPRESSION + | espota2.FEATURE_SUPPORTS_SHA256_AUTH + ] + ) + ) + + # Verify cnonce was sent (MD5 of random.random()) + cnonce = hashlib.md5(MOCK_RANDOM_BYTES).hexdigest() + assert mock_socket.sendall.call_args_list[2] == call(cnonce.encode()) + + # Verify auth result was computed correctly + expected_hash = hashlib.md5() + expected_hash.update(b"testpass") + expected_hash.update(MOCK_MD5_NONCE) + expected_hash.update(cnonce.encode()) + expected_result = expected_hash.hexdigest() + assert mock_socket.sendall.call_args_list[3] == call(expected_result.encode()) + + +@pytest.mark.usefixtures("mock_time") +def test_perform_ota_no_auth(mock_socket: Mock, mock_file: io.BytesIO) -> None: + """Test OTA without authentication.""" + recv_responses = [ + bytes([espota2.RESPONSE_OK]), # First byte of version response + bytes([espota2.OTA_VERSION_1_0]), # Version number + bytes([espota2.RESPONSE_HEADER_OK]), # Features response + bytes([espota2.RESPONSE_AUTH_OK]), # No auth required + bytes([espota2.RESPONSE_UPDATE_PREPARE_OK]), # Binary size OK + bytes([espota2.RESPONSE_BIN_MD5_OK]), # MD5 checksum OK + bytes([espota2.RESPONSE_RECEIVE_OK]), # Receive OK + bytes([espota2.RESPONSE_UPDATE_END_OK]), # Update end OK + ] + + mock_socket.recv.side_effect = recv_responses + + espota2.perform_ota(mock_socket, None, mock_file, "test.bin") + + # Should not send any auth-related data + auth_calls = [ + call + for call in mock_socket.sendall.call_args_list + if "cnonce" in str(call) or "result" in str(call) + ] + assert len(auth_calls) == 0 + + +@pytest.mark.usefixtures("mock_time") +def test_perform_ota_with_compression(mock_socket: Mock) -> None: + """Test OTA with compression support.""" + original_content = b"firmware" * 100 # Repeating content for compression + mock_file = io.BytesIO(original_content) + recv_responses = [ + bytes([espota2.RESPONSE_OK]), # First byte of version response + bytes([espota2.OTA_VERSION_2_0]), # Version number + bytes([espota2.RESPONSE_SUPPORTS_COMPRESSION]), # Device supports compression + bytes([espota2.RESPONSE_AUTH_OK]), # No auth required + bytes([espota2.RESPONSE_UPDATE_PREPARE_OK]), # Binary size OK + bytes([espota2.RESPONSE_BIN_MD5_OK]), # MD5 checksum OK + bytes([espota2.RESPONSE_CHUNK_OK]), # Chunk OK + bytes([espota2.RESPONSE_RECEIVE_OK]), # Receive OK + bytes([espota2.RESPONSE_UPDATE_END_OK]), # Update end OK + ] + + mock_socket.recv.side_effect = recv_responses + + espota2.perform_ota(mock_socket, None, mock_file, "test.bin") + + # Verify compressed content was sent + # Get the binary size that was sent (4 bytes after features) + size_bytes = mock_socket.sendall.call_args_list[2][0][0] + sent_size = struct.unpack(">I", size_bytes)[0] + + # Size should be less than original due to compression + assert sent_size < len(original_content) + + # Verify the content sent was gzipped + compressed = gzip.compress(original_content, compresslevel=9) + assert sent_size == len(compressed) + + +def test_perform_ota_auth_without_password(mock_socket: Mock) -> None: + """Test OTA fails when auth is required but no password provided.""" + mock_file = io.BytesIO(b"firmware") + + responses = [ + bytes([espota2.RESPONSE_OK, espota2.OTA_VERSION_2_0]), + bytes([espota2.RESPONSE_HEADER_OK]), + bytes([espota2.RESPONSE_REQUEST_AUTH]), + ] + + mock_socket.recv.side_effect = responses + + with pytest.raises( + espota2.OTAError, match="ESP requests password, but no password given" + ): + espota2.perform_ota(mock_socket, None, mock_file, "test.bin") + + +@pytest.mark.usefixtures("mock_time") +def test_perform_ota_md5_auth_wrong_password( + mock_socket: Mock, mock_file: io.BytesIO, mock_random: Mock +) -> None: + """Test OTA fails when MD5 authentication is rejected due to wrong password.""" + # Setup socket responses for recv calls + recv_responses = [ + bytes([espota2.RESPONSE_OK]), # First byte of version response + bytes([espota2.OTA_VERSION_2_0]), # Version number + bytes([espota2.RESPONSE_HEADER_OK]), # Features response + bytes([espota2.RESPONSE_REQUEST_AUTH]), # Auth request + MOCK_MD5_NONCE, # 32 char hex nonce + bytes([espota2.RESPONSE_ERROR_AUTH_INVALID]), # Auth rejected! + ] + + mock_socket.recv.side_effect = recv_responses + + with pytest.raises(espota2.OTAError, match="Error auth.*Authentication invalid"): + espota2.perform_ota(mock_socket, "wrongpassword", mock_file, "test.bin") + + # Verify the socket was closed after auth failure + mock_socket.close.assert_called() + + +@pytest.mark.usefixtures("mock_time") +def test_perform_ota_sha256_auth_wrong_password( + mock_socket: Mock, mock_file: io.BytesIO, mock_random: Mock +) -> None: + """Test OTA fails when SHA256 authentication is rejected due to wrong password.""" + # Setup socket responses for recv calls + recv_responses = [ + bytes([espota2.RESPONSE_OK]), # First byte of version response + bytes([espota2.OTA_VERSION_2_0]), # Version number + bytes([espota2.RESPONSE_HEADER_OK]), # Features response + bytes([espota2.RESPONSE_REQUEST_SHA256_AUTH]), # SHA256 Auth request + MOCK_SHA256_NONCE, # 64 char hex nonce + bytes([espota2.RESPONSE_ERROR_AUTH_INVALID]), # Auth rejected! + ] + + mock_socket.recv.side_effect = recv_responses + + with pytest.raises(espota2.OTAError, match="Error auth.*Authentication invalid"): + espota2.perform_ota(mock_socket, "wrongpassword", mock_file, "test.bin") + + # Verify the socket was closed after auth failure + mock_socket.close.assert_called() + + +def test_perform_ota_sha256_auth_without_password(mock_socket: Mock) -> None: + """Test OTA fails when SHA256 auth is required but no password provided.""" + mock_file = io.BytesIO(b"firmware") + + responses = [ + bytes([espota2.RESPONSE_OK, espota2.OTA_VERSION_2_0]), + bytes([espota2.RESPONSE_HEADER_OK]), + bytes([espota2.RESPONSE_REQUEST_SHA256_AUTH]), + ] + + mock_socket.recv.side_effect = responses + + with pytest.raises( + espota2.OTAError, match="ESP requests password, but no password given" + ): + espota2.perform_ota(mock_socket, None, mock_file, "test.bin") + + +def test_perform_ota_unexpected_auth_response(mock_socket: Mock) -> None: + """Test OTA fails when device sends an unexpected auth response.""" + mock_file = io.BytesIO(b"firmware") + + # Use 0x03 which is not in the expected auth responses + # This will be caught by check_error and raise "Unexpected response from ESP" + UNKNOWN_AUTH_METHOD = 0x03 + + responses = [ + bytes([espota2.RESPONSE_OK, espota2.OTA_VERSION_2_0]), + bytes([espota2.RESPONSE_HEADER_OK]), + bytes([UNKNOWN_AUTH_METHOD]), # Unknown auth method + ] + + mock_socket.recv.side_effect = responses + + # This will actually raise "Unexpected response from ESP" from check_error + with pytest.raises( + espota2.OTAError, match=r"Error auth: Unexpected response from ESP: 0x03" + ): + espota2.perform_ota(mock_socket, "password", mock_file, "test.bin") + + +def test_perform_ota_unsupported_version(mock_socket: Mock) -> None: + """Test OTA fails with unsupported version.""" + mock_file = io.BytesIO(b"firmware") + + responses = [ + bytes([espota2.RESPONSE_OK, 99]), # Unsupported version + ] + + mock_socket.recv.side_effect = responses + + with pytest.raises(espota2.OTAError, match="Device uses unsupported OTA version"): + espota2.perform_ota(mock_socket, None, mock_file, "test.bin") + + +@pytest.mark.usefixtures("mock_time") +def test_perform_ota_upload_error(mock_socket: Mock, mock_file: io.BytesIO) -> None: + """Test OTA handles upload errors.""" + # Setup responses - provide enough for the recv calls + recv_responses = [ + bytes([espota2.RESPONSE_OK]), # First byte of version response + bytes([espota2.OTA_VERSION_2_0]), # Version number + bytes([espota2.RESPONSE_HEADER_OK]), # Features response + bytes([espota2.RESPONSE_AUTH_OK]), # No auth required + bytes([espota2.RESPONSE_UPDATE_PREPARE_OK]), # Binary size OK + bytes([espota2.RESPONSE_BIN_MD5_OK]), # MD5 checksum OK + ] + # Add OSError to recv to simulate connection loss during chunk read + recv_responses.append(OSError("Connection lost")) + + mock_socket.recv.side_effect = recv_responses + + with pytest.raises(espota2.OTAError, match="Error receiving acknowledge chunk OK"): + espota2.perform_ota(mock_socket, None, mock_file, "test.bin") + + +@pytest.mark.usefixtures("mock_socket_constructor", "mock_resolve_ip") +def test_run_ota_impl_successful( + mock_socket: Mock, tmp_path: Path, mock_perform_ota: Mock +) -> None: + """Test run_ota_impl_ with successful upload.""" + # Create a real firmware file + firmware_file = tmp_path / "firmware.bin" + firmware_file.write_bytes(b"firmware content") + + # Run OTA with real file path + result_code, result_host = espota2.run_ota_impl_( + "test.local", 3232, "password", str(firmware_file) + ) + + # Verify success + assert result_code == 0 + assert result_host == "192.168.1.100" + + # Verify socket was configured correctly + mock_socket.settimeout.assert_called_with(20.0) + mock_socket.connect.assert_called_once_with(("192.168.1.100", 3232)) + mock_socket.close.assert_called_once() + + # Verify perform_ota was called with real file + mock_perform_ota.assert_called_once() + call_args = mock_perform_ota.call_args[0] + assert call_args[0] == mock_socket + assert call_args[1] == "password" + # Verify the file object is a proper file handle + assert isinstance(call_args[2], io.IOBase) + assert call_args[3] == str(firmware_file) + + +@pytest.mark.usefixtures("mock_socket_constructor", "mock_resolve_ip") +def test_run_ota_impl_connection_failed(mock_socket: Mock, tmp_path: Path) -> None: + """Test run_ota_impl_ when connection fails.""" + mock_socket.connect.side_effect = OSError("Connection refused") + + # Create a real firmware file + firmware_file = tmp_path / "firmware.bin" + firmware_file.write_bytes(b"firmware content") + + result_code, result_host = espota2.run_ota_impl_( + "test.local", 3232, "password", str(firmware_file) + ) + + assert result_code == 1 + assert result_host is None + mock_socket.close.assert_called_once() + + +def test_run_ota_impl_resolve_failed(tmp_path: Path, mock_resolve_ip: Mock) -> None: + """Test run_ota_impl_ when DNS resolution fails.""" + # Create a real firmware file + firmware_file = tmp_path / "firmware.bin" + firmware_file.write_bytes(b"firmware content") + + mock_resolve_ip.side_effect = EsphomeError("DNS resolution failed") + + with pytest.raises(espota2.OTAError, match="DNS resolution failed"): + result_code, result_host = espota2.run_ota_impl_( + "unknown.host", 3232, "password", str(firmware_file) + ) + + +def test_run_ota_wrapper(mock_run_ota_impl: Mock) -> None: + """Test run_ota wrapper function.""" + # Test successful case + mock_run_ota_impl.return_value = (0, "192.168.1.100") + result = espota2.run_ota("test.local", 3232, "pass", "fw.bin") + assert result == (0, "192.168.1.100") + + # Test error case + mock_run_ota_impl.side_effect = espota2.OTAError("Test error") + result = espota2.run_ota("test.local", 3232, "pass", "fw.bin") + assert result == (1, None) + + +def test_progress_bar(capsys: CaptureFixture[str]) -> None: + """Test ProgressBar functionality.""" + progress = espota2.ProgressBar() + + # Test initial update + progress.update(0.0) + captured = capsys.readouterr() + assert "0%" in captured.err + assert "[" in captured.err + + # Test progress update + progress.update(0.5) + captured = capsys.readouterr() + assert "50%" in captured.err + + # Test completion + progress.update(1.0) + captured = capsys.readouterr() + assert "100%" in captured.err + assert "Done" in captured.err + + # Test done method + progress.done() + captured = capsys.readouterr() + assert captured.err == "\n" + + # Test same progress doesn't update + progress.update(0.5) + progress.update(0.5) + captured = capsys.readouterr() + # Should only see one update (second call shouldn't write) + assert captured.err.count("50%") == 1 + + +# Tests for SHA256 authentication +@pytest.mark.usefixtures("mock_time") +def test_perform_ota_successful_sha256_auth( + mock_socket: Mock, mock_file: io.BytesIO, mock_random: Mock +) -> None: + """Test successful OTA with SHA256 authentication.""" + # Setup socket responses for recv calls + recv_responses = [ + bytes([espota2.RESPONSE_OK]), # First byte of version response + bytes([espota2.OTA_VERSION_2_0]), # Version number + bytes([espota2.RESPONSE_HEADER_OK]), # Features response + bytes([espota2.RESPONSE_REQUEST_SHA256_AUTH]), # SHA256 Auth request + MOCK_SHA256_NONCE, # 64 char hex nonce + bytes([espota2.RESPONSE_AUTH_OK]), # Auth result + bytes([espota2.RESPONSE_UPDATE_PREPARE_OK]), # Binary size OK + bytes([espota2.RESPONSE_BIN_MD5_OK]), # MD5 checksum OK + bytes([espota2.RESPONSE_CHUNK_OK]), # Chunk OK + bytes([espota2.RESPONSE_RECEIVE_OK]), # Receive OK + bytes([espota2.RESPONSE_UPDATE_END_OK]), # Update end OK + ] + + mock_socket.recv.side_effect = recv_responses + + # Run OTA + espota2.perform_ota(mock_socket, "testpass", mock_file, "test.bin") + + # Verify magic bytes were sent + assert mock_socket.sendall.call_args_list[0] == call(bytes(espota2.MAGIC_BYTES)) + + # Verify features were sent (compression + SHA256 support) + assert mock_socket.sendall.call_args_list[1] == call( + bytes( + [ + espota2.FEATURE_SUPPORTS_COMPRESSION + | espota2.FEATURE_SUPPORTS_SHA256_AUTH + ] + ) + ) + + # Verify cnonce was sent (SHA256 of random.random()) + cnonce = hashlib.sha256(MOCK_RANDOM_BYTES).hexdigest() + assert mock_socket.sendall.call_args_list[2] == call(cnonce.encode()) + + # Verify auth result was computed correctly with SHA256 + expected_hash = hashlib.sha256() + expected_hash.update(b"testpass") + expected_hash.update(MOCK_SHA256_NONCE) + expected_hash.update(cnonce.encode()) + expected_result = expected_hash.hexdigest() + assert mock_socket.sendall.call_args_list[3] == call(expected_result.encode()) + + +@pytest.mark.usefixtures("mock_time") +def test_perform_ota_sha256_fallback_to_md5( + mock_socket: Mock, mock_file: io.BytesIO, mock_random: Mock +) -> None: + """Test SHA256-capable client falls back to MD5 for compatibility.""" + # This test verifies the temporary backward compatibility + # where a SHA256-capable client can still authenticate with MD5 + # This compatibility will be removed in 2026.1.0 + recv_responses = [ + bytes([espota2.RESPONSE_OK]), # First byte of version response + bytes([espota2.OTA_VERSION_2_0]), # Version number + bytes([espota2.RESPONSE_HEADER_OK]), # Features response + bytes( + [espota2.RESPONSE_REQUEST_AUTH] + ), # MD5 Auth request (device doesn't support SHA256) + MOCK_MD5_NONCE, # 32 char hex nonce for MD5 + bytes([espota2.RESPONSE_AUTH_OK]), # Auth result + bytes([espota2.RESPONSE_UPDATE_PREPARE_OK]), # Binary size OK + bytes([espota2.RESPONSE_BIN_MD5_OK]), # MD5 checksum OK + bytes([espota2.RESPONSE_CHUNK_OK]), # Chunk OK + bytes([espota2.RESPONSE_RECEIVE_OK]), # Receive OK + bytes([espota2.RESPONSE_UPDATE_END_OK]), # Update end OK + ] + + mock_socket.recv.side_effect = recv_responses + + # Run OTA - should work even though device requested MD5 + espota2.perform_ota(mock_socket, "testpass", mock_file, "test.bin") + + # Verify client still advertised SHA256 support + assert mock_socket.sendall.call_args_list[1] == call( + bytes( + [ + espota2.FEATURE_SUPPORTS_COMPRESSION + | espota2.FEATURE_SUPPORTS_SHA256_AUTH + ] + ) + ) + + # But authentication was done with MD5 + cnonce = hashlib.md5(MOCK_RANDOM_BYTES).hexdigest() + expected_hash = hashlib.md5() + expected_hash.update(b"testpass") + expected_hash.update(MOCK_MD5_NONCE) + expected_hash.update(cnonce.encode()) + expected_result = expected_hash.hexdigest() + assert mock_socket.sendall.call_args_list[3] == call(expected_result.encode()) + + +@pytest.mark.usefixtures("mock_time") +def test_perform_ota_version_differences( + mock_socket: Mock, mock_file: io.BytesIO +) -> None: + """Test OTA behavior differences between version 1.0 and 2.0.""" + # Test version 1.0 - no chunk acknowledgments + recv_responses = [ + bytes([espota2.RESPONSE_OK]), # First byte of version response + bytes([espota2.OTA_VERSION_1_0]), # Version number + bytes([espota2.RESPONSE_HEADER_OK]), # Features response + bytes([espota2.RESPONSE_AUTH_OK]), # No auth required + bytes([espota2.RESPONSE_UPDATE_PREPARE_OK]), # Binary size OK + bytes([espota2.RESPONSE_BIN_MD5_OK]), # MD5 checksum OK + # No RESPONSE_CHUNK_OK for v1 + bytes([espota2.RESPONSE_RECEIVE_OK]), # Receive OK + bytes([espota2.RESPONSE_UPDATE_END_OK]), # Update end OK + ] + + mock_socket.recv.side_effect = recv_responses + espota2.perform_ota(mock_socket, None, mock_file, "test.bin") + + # For v1.0, verify that we only get the expected number of recv calls + # v1.0 doesn't have chunk acknowledgments, so fewer recv calls + assert mock_socket.recv.call_count == 8 # v1.0 has 8 recv calls + + # Reset mock for v2.0 test + mock_socket.reset_mock() + + # Reset file position for second test + mock_file.seek(0) + + # Test version 2.0 - with chunk acknowledgments + recv_responses_v2 = [ + bytes([espota2.RESPONSE_OK]), # First byte of version response + bytes([espota2.OTA_VERSION_2_0]), # Version number + bytes([espota2.RESPONSE_HEADER_OK]), # Features response + bytes([espota2.RESPONSE_AUTH_OK]), # No auth required + bytes([espota2.RESPONSE_UPDATE_PREPARE_OK]), # Binary size OK + bytes([espota2.RESPONSE_BIN_MD5_OK]), # MD5 checksum OK + bytes([espota2.RESPONSE_CHUNK_OK]), # v2.0 has chunk acknowledgment + bytes([espota2.RESPONSE_RECEIVE_OK]), # Receive OK + bytes([espota2.RESPONSE_UPDATE_END_OK]), # Update end OK + ] + + mock_socket.recv.side_effect = recv_responses_v2 + espota2.perform_ota(mock_socket, None, mock_file, "test.bin") + + # For v2.0, verify more recv calls due to chunk acknowledgments + assert mock_socket.recv.call_count == 9 # v2.0 has 9 recv calls (includes chunk OK) diff --git a/tests/unit_tests/test_external_files.py b/tests/unit_tests/test_external_files.py new file mode 100644 index 0000000000..05e0bd3523 --- /dev/null +++ b/tests/unit_tests/test_external_files.py @@ -0,0 +1,200 @@ +"""Tests for external_files.py functions.""" + +from pathlib import Path +import time +from unittest.mock import MagicMock, patch + +import pytest +import requests + +from esphome import external_files +from esphome.config_validation import Invalid +from esphome.core import CORE, TimePeriod + + +def test_compute_local_file_dir(setup_core: Path) -> None: + """Test compute_local_file_dir creates and returns correct path.""" + domain = "font" + + result = external_files.compute_local_file_dir(domain) + + assert isinstance(result, Path) + assert result == Path(CORE.data_dir) / domain + assert result.exists() + assert result.is_dir() + + +def test_compute_local_file_dir_nested(setup_core: Path) -> None: + """Test compute_local_file_dir works with nested domains.""" + domain = "images/icons" + + result = external_files.compute_local_file_dir(domain) + + assert result == Path(CORE.data_dir) / "images" / "icons" + assert result.exists() + assert result.is_dir() + + +def test_is_file_recent_with_recent_file(setup_core: Path) -> None: + """Test is_file_recent returns True for recently created file.""" + test_file = setup_core / "recent.txt" + test_file.write_text("content") + + refresh = TimePeriod(seconds=3600) + + result = external_files.is_file_recent(test_file, refresh) + + assert result is True + + +def test_is_file_recent_with_old_file(setup_core: Path) -> None: + """Test is_file_recent returns False for old file.""" + test_file = setup_core / "old.txt" + test_file.write_text("content") + + old_time = time.time() - 7200 + mock_stat = MagicMock() + mock_stat.st_ctime = old_time + + with patch.object(Path, "stat", return_value=mock_stat): + refresh = TimePeriod(seconds=3600) + + result = external_files.is_file_recent(test_file, refresh) + + assert result is False + + +def test_is_file_recent_nonexistent_file(setup_core: Path) -> None: + """Test is_file_recent returns False for non-existent file.""" + test_file = setup_core / "nonexistent.txt" + refresh = TimePeriod(seconds=3600) + + result = external_files.is_file_recent(test_file, refresh) + + assert result is False + + +def test_is_file_recent_with_zero_refresh(setup_core: Path) -> None: + """Test is_file_recent with zero refresh period returns False.""" + test_file = setup_core / "test.txt" + test_file.write_text("content") + + # Mock stat to return a time 10 seconds ago + mock_stat = MagicMock() + mock_stat.st_ctime = time.time() - 10 + with patch.object(Path, "stat", return_value=mock_stat): + refresh = TimePeriod(seconds=0) + result = external_files.is_file_recent(test_file, refresh) + assert result is False + + +@patch("esphome.external_files.requests.head") +def test_has_remote_file_changed_not_modified( + mock_head: MagicMock, setup_core: Path +) -> None: + """Test has_remote_file_changed returns False when file not modified.""" + test_file = setup_core / "cached.txt" + test_file.write_text("cached content") + + mock_response = MagicMock() + mock_response.status_code = 304 + mock_head.return_value = mock_response + + url = "https://example.com/file.txt" + result = external_files.has_remote_file_changed(url, test_file) + + assert result is False + mock_head.assert_called_once() + + call_args = mock_head.call_args + headers = call_args[1]["headers"] + assert external_files.IF_MODIFIED_SINCE in headers + assert external_files.CACHE_CONTROL in headers + + +@patch("esphome.external_files.requests.head") +def test_has_remote_file_changed_modified( + mock_head: MagicMock, setup_core: Path +) -> None: + """Test has_remote_file_changed returns True when file modified.""" + test_file = setup_core / "cached.txt" + test_file.write_text("cached content") + + mock_response = MagicMock() + mock_response.status_code = 200 + mock_head.return_value = mock_response + + url = "https://example.com/file.txt" + result = external_files.has_remote_file_changed(url, test_file) + + assert result is True + + +def test_has_remote_file_changed_no_local_file(setup_core: Path) -> None: + """Test has_remote_file_changed returns True when local file doesn't exist.""" + test_file = setup_core / "nonexistent.txt" + + url = "https://example.com/file.txt" + result = external_files.has_remote_file_changed(url, test_file) + + assert result is True + + +@patch("esphome.external_files.requests.head") +def test_has_remote_file_changed_network_error( + mock_head: MagicMock, setup_core: Path +) -> None: + """Test has_remote_file_changed handles network errors gracefully.""" + test_file = setup_core / "cached.txt" + test_file.write_text("cached content") + + mock_head.side_effect = requests.exceptions.RequestException("Network error") + + url = "https://example.com/file.txt" + + with pytest.raises(Invalid, match="Could not check if.*Network error"): + external_files.has_remote_file_changed(url, test_file) + + +@patch("esphome.external_files.requests.head") +def test_has_remote_file_changed_timeout( + mock_head: MagicMock, setup_core: Path +) -> None: + """Test has_remote_file_changed respects timeout.""" + test_file = setup_core / "cached.txt" + test_file.write_text("cached content") + + mock_response = MagicMock() + mock_response.status_code = 304 + mock_head.return_value = mock_response + + url = "https://example.com/file.txt" + external_files.has_remote_file_changed(url, test_file) + + call_args = mock_head.call_args + assert call_args[1]["timeout"] == external_files.NETWORK_TIMEOUT + + +def test_compute_local_file_dir_creates_parent_dirs(setup_core: Path) -> None: + """Test compute_local_file_dir creates parent directories.""" + domain = "level1/level2/level3/level4" + + result = external_files.compute_local_file_dir(domain) + + assert result.exists() + assert result.is_dir() + assert result.parent.name == "level3" + assert result.parent.parent.name == "level2" + assert result.parent.parent.parent.name == "level1" + + +def test_is_file_recent_handles_float_seconds(setup_core: Path) -> None: + """Test is_file_recent works with float seconds in TimePeriod.""" + test_file = setup_core / "test.txt" + test_file.write_text("content") + + refresh = TimePeriod(seconds=3600.5) + + result = external_files.is_file_recent(test_file, refresh) + + assert result is True diff --git a/tests/unit_tests/test_git.py b/tests/unit_tests/test_git.py new file mode 100644 index 0000000000..0411fe5e43 --- /dev/null +++ b/tests/unit_tests/test_git.py @@ -0,0 +1,673 @@ +"""Tests for git.py module.""" + +from datetime import datetime, timedelta +import os +from pathlib import Path +from typing import Any +from unittest.mock import Mock + +import pytest + +from esphome import git +from esphome.core import CORE, TimePeriodSeconds +from esphome.git import GitCommandError + + +def _compute_repo_dir(url: str, ref: str | None, domain: str) -> Path: + """Helper to compute the expected repo directory path using git module's logic.""" + key = f"{url}@{ref}" + return git._compute_destination_path(key, domain) + + +def _setup_old_repo(repo_dir: Path, days_old: int = 2) -> None: + """Helper to set up a git repo directory structure with an old timestamp. + + Args: + repo_dir: The repository directory path to create. + days_old: Number of days old to make the FETCH_HEAD file (default: 2). + """ + # Create repo directory + repo_dir.mkdir(parents=True) + git_dir = repo_dir / ".git" + git_dir.mkdir() + + # Create FETCH_HEAD file with old timestamp + fetch_head = git_dir / "FETCH_HEAD" + fetch_head.write_text("test") + old_time = datetime.now() - timedelta(days=days_old) + fetch_head.touch() + os.utime(fetch_head, (old_time.timestamp(), old_time.timestamp())) + + +def _get_git_command_type(cmd: list[str]) -> str | None: + """Helper to determine the type of git command from a command list. + + Args: + cmd: The git command list (e.g., ["git", "rev-parse", "HEAD"]). + + Returns: + The command type ("rev-parse", "stash", "fetch", "reset", "clone") or None. + """ + # Git commands are always in format ["git", "command", ...], so check index 1 + if len(cmd) > 1: + return cmd[1] + return None + + +def test_run_git_command_success(tmp_path: Path) -> None: + """Test that run_git_command returns output on success.""" + # Create a simple git repo to test with + repo_dir = tmp_path / "test_repo" + repo_dir.mkdir() + + # Initialize a git repo + result = git.run_git_command(["git", "init"], str(repo_dir)) + assert "Initialized empty Git repository" in result or result == "" + + # Verify we can run a command and get output + result = git.run_git_command(["git", "status", "--porcelain"], str(repo_dir)) + # Empty repo should have empty status + assert isinstance(result, str) + + +def test_run_git_command_with_git_dir_isolation( + tmp_path: Path, mock_subprocess_run: Mock +) -> None: + """Test that git_dir parameter properly isolates git operations.""" + repo_dir = tmp_path / "test_repo" + repo_dir.mkdir() + git_dir = repo_dir / ".git" + git_dir.mkdir() + + # Configure mock to return success + mock_subprocess_run.return_value = Mock( + returncode=0, + stdout=b"test output", + stderr=b"", + ) + + result = git.run_git_command( + ["git", "rev-parse", "HEAD"], + git_dir=repo_dir, + ) + + # Verify subprocess.run was called + assert mock_subprocess_run.called + call_args = mock_subprocess_run.call_args + + # Verify environment was set + env = call_args[1]["env"] + assert "GIT_DIR" in env + assert "GIT_WORK_TREE" in env + assert env["GIT_DIR"] == str(repo_dir / ".git") + assert env["GIT_WORK_TREE"] == str(repo_dir) + + assert result == "test output" + + +def test_run_git_command_raises_git_not_installed_error( + tmp_path: Path, mock_subprocess_run: Mock +) -> None: + """Test that FileNotFoundError is converted to GitNotInstalledError.""" + from esphome.git import GitNotInstalledError + + repo_dir = tmp_path / "test_repo" + + # Configure mock to raise FileNotFoundError + mock_subprocess_run.side_effect = FileNotFoundError("git not found") + + with pytest.raises(GitNotInstalledError, match="git is not installed"): + git.run_git_command(["git", "status"], git_dir=repo_dir) + + +def test_run_git_command_raises_git_command_error_on_failure( + tmp_path: Path, mock_subprocess_run: Mock +) -> None: + """Test that failed git commands raise GitCommandError.""" + repo_dir = tmp_path / "test_repo" + + # Configure mock to return non-zero exit code + mock_subprocess_run.return_value = Mock( + returncode=1, + stdout=b"", + stderr=b"fatal: not a git repository", + ) + + with pytest.raises(GitCommandError, match="not a git repository"): + git.run_git_command(["git", "status"], git_dir=repo_dir) + + +def test_run_git_command_strips_fatal_prefix( + tmp_path: Path, mock_subprocess_run: Mock +) -> None: + """Test that 'fatal: ' prefix is stripped from error messages.""" + repo_dir = tmp_path / "test_repo" + + # Configure mock to return error with "fatal: " prefix + mock_subprocess_run.return_value = Mock( + returncode=128, + stdout=b"", + stderr=b"fatal: repository not found\n", + ) + + with pytest.raises(GitCommandError) as exc_info: + git.run_git_command(["git", "clone", "invalid-url"], git_dir=repo_dir) + + # Error message should NOT include "fatal: " prefix + assert "fatal:" not in str(exc_info.value) + assert "repository not found" in str(exc_info.value) + + +def test_run_git_command_without_git_dir(mock_subprocess_run: Mock) -> None: + """Test that run_git_command works without git_dir (clone case).""" + # Configure mock to return success + mock_subprocess_run.return_value = Mock( + returncode=0, + stdout=b"Cloning into 'test_repo'...", + stderr=b"", + ) + + result = git.run_git_command(["git", "clone", "https://github.com/test/repo"]) + + # Verify subprocess.run was called + assert mock_subprocess_run.called + call_args = mock_subprocess_run.call_args + + # Verify environment does NOT have GIT_DIR or GIT_WORK_TREE set + # (it should use the default environment or None) + env = call_args[1].get("env") + if env is not None: + assert "GIT_DIR" not in env + assert "GIT_WORK_TREE" not in env + + # Verify cwd is None (default) + assert call_args[1].get("cwd") is None + + assert result == "Cloning into 'test_repo'..." + + +def test_run_git_command_without_git_dir_raises_error( + mock_subprocess_run: Mock, +) -> None: + """Test that run_git_command without git_dir can still raise errors.""" + # Configure mock to return error + mock_subprocess_run.return_value = Mock( + returncode=128, + stdout=b"", + stderr=b"fatal: repository not found\n", + ) + + with pytest.raises(GitCommandError, match="repository not found"): + git.run_git_command(["git", "clone", "https://invalid.url/repo.git"]) + + +def test_clone_or_update_with_never_refresh( + tmp_path: Path, mock_run_git_command: Mock +) -> None: + """Test that NEVER_REFRESH skips updates for existing repos.""" + # Set up CORE.config_path so data_dir uses tmp_path + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + ref = None + domain = "test" + repo_dir = _compute_repo_dir(url, ref, domain) + + # Create the git repo directory structure + repo_dir.mkdir(parents=True) + git_dir = repo_dir / ".git" + git_dir.mkdir() + + # Create FETCH_HEAD file with current timestamp + fetch_head = git_dir / "FETCH_HEAD" + fetch_head.write_text("test") + + # Call with NEVER_REFRESH + result_dir, revert = git.clone_or_update( + url=url, + ref=ref, + refresh=git.NEVER_REFRESH, + domain=domain, + ) + + # Should NOT call git commands since NEVER_REFRESH and repo exists + mock_run_git_command.assert_not_called() + assert result_dir == repo_dir + assert revert is None + + +def test_clone_or_update_with_refresh_updates_old_repo( + tmp_path: Path, mock_run_git_command: Mock +) -> None: + """Test that refresh triggers update for old repos.""" + # Set up CORE.config_path so data_dir uses tmp_path + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + ref = None + domain = "test" + repo_dir = _compute_repo_dir(url, ref, domain) + + # Create the git repo directory structure + repo_dir.mkdir(parents=True) + git_dir = repo_dir / ".git" + git_dir.mkdir() + + # Create FETCH_HEAD file with old timestamp (2 days ago) + fetch_head = git_dir / "FETCH_HEAD" + fetch_head.write_text("test") + old_time = datetime.now() - timedelta(days=2) + fetch_head.touch() # Create the file + # Set modification time to 2 days ago + os.utime(fetch_head, (old_time.timestamp(), old_time.timestamp())) + + # Mock git command responses + mock_run_git_command.return_value = "abc123" # SHA for rev-parse + + # Call with refresh=1d (1 day) + refresh = TimePeriodSeconds(days=1) + result_dir, revert = git.clone_or_update( + url=url, + ref=ref, + refresh=refresh, + domain=domain, + ) + + # Should call git fetch and update commands since repo is older than refresh + assert mock_run_git_command.called + # Check for fetch command + fetch_calls = [ + call + for call in mock_run_git_command.call_args_list + if len(call[0]) > 0 and "fetch" in call[0][0] + ] + assert len(fetch_calls) > 0 + + +def test_clone_or_update_with_refresh_skips_fresh_repo( + tmp_path: Path, mock_run_git_command: Mock +) -> None: + """Test that refresh doesn't update fresh repos.""" + # Set up CORE.config_path so data_dir uses tmp_path + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + ref = None + domain = "test" + repo_dir = _compute_repo_dir(url, ref, domain) + + # Create the git repo directory structure + repo_dir.mkdir(parents=True) + git_dir = repo_dir / ".git" + git_dir.mkdir() + + # Create FETCH_HEAD file with recent timestamp (1 hour ago) + fetch_head = git_dir / "FETCH_HEAD" + fetch_head.write_text("test") + recent_time = datetime.now() - timedelta(hours=1) + fetch_head.touch() # Create the file + # Set modification time to 1 hour ago + os.utime(fetch_head, (recent_time.timestamp(), recent_time.timestamp())) + + # Call with refresh=1d (1 day) + refresh = TimePeriodSeconds(days=1) + result_dir, revert = git.clone_or_update( + url=url, + ref=ref, + refresh=refresh, + domain=domain, + ) + + # Should NOT call git fetch since repo is fresh + mock_run_git_command.assert_not_called() + assert result_dir == repo_dir + assert revert is None + + +def test_clone_or_update_clones_missing_repo( + tmp_path: Path, mock_run_git_command: Mock +) -> None: + """Test that missing repos are cloned regardless of refresh setting.""" + # Set up CORE.config_path so data_dir uses tmp_path + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + ref = None + domain = "test" + repo_dir = _compute_repo_dir(url, ref, domain) + + # Create base directory but NOT the repo itself + base_dir = tmp_path / ".esphome" / domain + base_dir.mkdir(parents=True) + # repo_dir should NOT exist + assert not repo_dir.exists() + + # Test with NEVER_REFRESH - should still clone since repo doesn't exist + result_dir, revert = git.clone_or_update( + url=url, + ref=ref, + refresh=git.NEVER_REFRESH, + domain=domain, + ) + + # Should call git clone + assert mock_run_git_command.called + clone_calls = [ + call + for call in mock_run_git_command.call_args_list + if len(call[0]) > 0 and "clone" in call[0][0] + ] + assert len(clone_calls) > 0 + + +def test_clone_or_update_with_none_refresh_always_updates( + tmp_path: Path, mock_run_git_command: Mock +) -> None: + """Test that refresh=None always updates existing repos.""" + # Set up CORE.config_path so data_dir uses tmp_path + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + ref = None + domain = "test" + repo_dir = _compute_repo_dir(url, ref, domain) + + # Create the git repo directory structure + repo_dir.mkdir(parents=True) + git_dir = repo_dir / ".git" + git_dir.mkdir() + + # Create FETCH_HEAD file with very recent timestamp (1 second ago) + fetch_head = git_dir / "FETCH_HEAD" + fetch_head.write_text("test") + recent_time = datetime.now() - timedelta(seconds=1) + fetch_head.touch() # Create the file + # Set modification time to 1 second ago + os.utime(fetch_head, (recent_time.timestamp(), recent_time.timestamp())) + + # Mock git command responses + mock_run_git_command.return_value = "abc123" # SHA for rev-parse + + # Call with refresh=None (default behavior) + result_dir, revert = git.clone_or_update( + url=url, + ref=ref, + refresh=None, + domain=domain, + ) + + # Should call git fetch and update commands since refresh=None means always update + assert mock_run_git_command.called + # Check for fetch command + fetch_calls = [ + call + for call in mock_run_git_command.call_args_list + if len(call[0]) > 0 and "fetch" in call[0][0] + ] + assert len(fetch_calls) > 0 + + +@pytest.mark.parametrize( + ("fail_command", "error_message"), + [ + ( + "rev-parse", + "ambiguous argument 'HEAD': unknown revision or path not in the working tree.", + ), + ("stash", "fatal: unable to write new index file"), + ( + "fetch", + "fatal: unable to access 'https://github.com/test/repo/': Could not resolve host", + ), + ("reset", "fatal: Could not reset index file to revision 'FETCH_HEAD'"), + ], +) +def test_clone_or_update_recovers_from_git_failures( + tmp_path: Path, mock_run_git_command: Mock, fail_command: str, error_message: str +) -> None: + """Test that repos are re-cloned when various git commands fail.""" + # Set up CORE.config_path so data_dir uses tmp_path + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + ref = "main" + domain = "test" + repo_dir = _compute_repo_dir(url, ref, domain) + + # Use helper to set up old repo + _setup_old_repo(repo_dir) + + # Track command call counts to make first call fail, subsequent calls succeed + call_counts: dict[str, int] = {} + + def git_command_side_effect( + cmd: list[str], cwd: str | None = None, **kwargs: Any + ) -> str: + # Determine which command this is + cmd_type = _get_git_command_type(cmd) + + # Track call count for this command type + if cmd_type: + call_counts[cmd_type] = call_counts.get(cmd_type, 0) + 1 + + # Fail on first call to the specified command, succeed on subsequent calls + if cmd_type == fail_command and call_counts[cmd_type] == 1: + raise GitCommandError(error_message) + + # Default successful responses + if cmd_type == "rev-parse": + return "abc123" + return "" + + mock_run_git_command.side_effect = git_command_side_effect + + refresh = TimePeriodSeconds(days=1) + result_dir, revert = git.clone_or_update( + url=url, + ref=ref, + refresh=refresh, + domain=domain, + ) + + # Verify recovery happened + call_list = mock_run_git_command.call_args_list + + # Should have attempted the failing command + assert any(fail_command in str(c) for c in call_list) + + # Should have called clone for recovery + assert any("clone" in str(c) for c in call_list) + + # Verify the repo directory path is returned + assert result_dir == repo_dir + + +def test_clone_or_update_fails_when_recovery_also_fails( + tmp_path: Path, mock_run_git_command: Mock +) -> None: + """Test that we don't infinitely recurse when recovery also fails.""" + # Set up CORE.config_path so data_dir uses tmp_path + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + ref = "main" + domain = "test" + repo_dir = _compute_repo_dir(url, ref, domain) + + # Use helper to set up old repo + _setup_old_repo(repo_dir) + + # Mock git command to fail on clone (simulating network failure during recovery) + def git_command_side_effect( + cmd: list[str], cwd: str | None = None, **kwargs: Any + ) -> str: + cmd_type = _get_git_command_type(cmd) + if cmd_type == "rev-parse": + # First time fails (broken repo) + raise GitCommandError( + "ambiguous argument 'HEAD': unknown revision or path not in the working tree." + ) + if cmd_type == "clone": + # Clone also fails (recovery fails) + raise GitCommandError("fatal: unable to access repository") + return "" + + mock_run_git_command.side_effect = git_command_side_effect + + refresh = TimePeriodSeconds(days=1) + + # Should raise after one recovery attempt fails + with pytest.raises(GitCommandError, match="fatal: unable to access repository"): + git.clone_or_update( + url=url, + ref=ref, + refresh=refresh, + domain=domain, + ) + + # Verify we only tried to clone once (no infinite recursion) + call_list = mock_run_git_command.call_args_list + clone_calls = [c for c in call_list if "clone" in c[0][0]] + # Should have exactly one clone call (the recovery attempt that failed) + assert len(clone_calls) == 1 + # Should have tried rev-parse once (which failed and triggered recovery) + rev_parse_calls = [c for c in call_list if "rev-parse" in c[0][0]] + assert len(rev_parse_calls) == 1 + + +def test_clone_or_update_recover_broken_flag_prevents_second_recovery( + tmp_path: Path, mock_run_git_command: Mock +) -> None: + """Test that _recover_broken=False prevents a second recovery attempt (tests the raise path).""" + # Set up CORE.config_path so data_dir uses tmp_path + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + ref = "main" + domain = "test" + repo_dir = _compute_repo_dir(url, ref, domain) + + # Use helper to set up old repo + _setup_old_repo(repo_dir) + + # Track fetch calls to differentiate between first (in clone) and second (in recovery update) + call_counts: dict[str, int] = {} + + # Mock git command to fail on fetch during recovery's ref checkout + def git_command_side_effect( + cmd: list[str], cwd: str | None = None, **kwargs: Any + ) -> str: + cmd_type = _get_git_command_type(cmd) + + if cmd_type: + call_counts[cmd_type] = call_counts.get(cmd_type, 0) + 1 + + # First attempt: rev-parse fails (broken repo) + if cmd_type == "rev-parse" and call_counts[cmd_type] == 1: + raise GitCommandError( + "ambiguous argument 'HEAD': unknown revision or path not in the working tree." + ) + + # Recovery: clone succeeds + if cmd_type == "clone": + return "" + + # Recovery: fetch for ref checkout fails + # This happens in the clone path when ref is not None (line 80 in git.py) + if cmd_type == "fetch" and call_counts[cmd_type] == 1: + raise GitCommandError("fatal: couldn't find remote ref main") + + # Default success + return "abc123" if cmd_type == "rev-parse" else "" + + mock_run_git_command.side_effect = git_command_side_effect + + refresh = TimePeriodSeconds(days=1) + + # Should raise on the fetch during recovery (when _recover_broken=False) + # This tests the critical "if not _recover_broken: raise" path + with pytest.raises(GitCommandError, match="fatal: couldn't find remote ref main"): + git.clone_or_update( + url=url, + ref=ref, + refresh=refresh, + domain=domain, + ) + + # Verify the sequence of events + call_list = mock_run_git_command.call_args_list + + # Should have: rev-parse (fail, triggers recovery), clone (success), + # fetch (fail during ref checkout, raises because _recover_broken=False) + rev_parse_calls = [c for c in call_list if "rev-parse" in c[0][0]] + # Should have exactly one rev-parse call that failed + assert len(rev_parse_calls) == 1 + + clone_calls = [c for c in call_list if "clone" in c[0][0]] + # Should have exactly one clone call (the recovery attempt) + assert len(clone_calls) == 1 + + fetch_calls = [c for c in call_list if "fetch" in c[0][0]] + # Should have exactly one fetch call that failed (during ref checkout in recovery) + assert len(fetch_calls) == 1 + + +def test_clone_or_update_recover_broken_flag_prevents_infinite_loop( + tmp_path: Path, mock_run_git_command: Mock +) -> None: + """Test that _recover_broken=False prevents infinite recursion when repo persists.""" + # This tests the critical "if not _recover_broken: raise" path at line 124-125 + # Set up CORE.config_path so data_dir uses tmp_path + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + ref = "main" + domain = "test" + repo_dir = _compute_repo_dir(url, ref, domain) + + # Use helper to set up old repo + _setup_old_repo(repo_dir) + + # Mock shutil.rmtree to NOT actually delete the directory + # This simulates a scenario where deletion fails (permissions, etc.) + import unittest.mock + + def mock_rmtree(path, *args, **kwargs): + # Don't actually delete - this causes the recursive call to still see the repo + pass + + # Mock git commands to always fail on stash + def git_command_side_effect( + cmd: list[str], cwd: str | None = None, **kwargs: Any + ) -> str: + cmd_type = _get_git_command_type(cmd) + if cmd_type == "rev-parse": + return "abc123" + if cmd_type == "stash": + # Always fails + raise GitCommandError("fatal: unable to write new index file") + return "" + + mock_run_git_command.side_effect = git_command_side_effect + + refresh = TimePeriodSeconds(days=1) + + # Mock shutil.rmtree and test + # Should raise on the second attempt when _recover_broken=False + # This hits the "if not _recover_broken: raise" path + with ( + unittest.mock.patch("esphome.git.shutil.rmtree", side_effect=mock_rmtree), + pytest.raises(GitCommandError, match="fatal: unable to write new index file"), + ): + git.clone_or_update( + url=url, + ref=ref, + refresh=refresh, + domain=domain, + ) + + # Verify the sequence: stash fails twice (once triggering recovery, once raising) + call_list = mock_run_git_command.call_args_list + stash_calls = [c for c in call_list if "stash" in c[0][0]] + # Should have exactly two stash calls + assert len(stash_calls) == 2 diff --git a/tests/unit_tests/test_helpers.py b/tests/unit_tests/test_helpers.py index b353d1aa99..47b945e0eb 100644 --- a/tests/unit_tests/test_helpers.py +++ b/tests/unit_tests/test_helpers.py @@ -1,8 +1,18 @@ +import logging +import os +from pathlib import Path +import socket +import stat +from unittest.mock import patch + +from aioesphomeapi.host_resolver import AddrInfo, IPv4Sockaddr, IPv6Sockaddr from hypothesis import given from hypothesis.strategies import ip_addresses import pytest from esphome import helpers +from esphome.address_cache import AddressCache +from esphome.core import EsphomeError @pytest.mark.parametrize( @@ -144,11 +154,11 @@ def test_walk_files(fixture_path): actual = list(helpers.walk_files(path)) # Ensure paths start with the root - assert all(p.startswith(str(path)) for p in actual) + assert all(p.is_relative_to(path) for p in actual) class Test_write_file_if_changed: - def test_src_and_dst_match(self, tmp_path): + def test_src_and_dst_match(self, tmp_path: Path): text = "A files are unique.\n" initial = text dst = tmp_path / "file-a.txt" @@ -158,7 +168,7 @@ class Test_write_file_if_changed: assert dst.read_text() == text - def test_src_and_dst_do_not_match(self, tmp_path): + def test_src_and_dst_do_not_match(self, tmp_path: Path): text = "A files are unique.\n" initial = "B files are unique.\n" dst = tmp_path / "file-a.txt" @@ -168,7 +178,7 @@ class Test_write_file_if_changed: assert dst.read_text() == text - def test_dst_does_not_exist(self, tmp_path): + def test_dst_does_not_exist(self, tmp_path: Path): text = "A files are unique.\n" dst = tmp_path / "file-a.txt" @@ -178,7 +188,7 @@ class Test_write_file_if_changed: class Test_copy_file_if_changed: - def test_src_and_dst_match(self, tmp_path, fixture_path): + def test_src_and_dst_match(self, tmp_path: Path, fixture_path: Path): src = fixture_path / "helpers" / "file-a.txt" initial = fixture_path / "helpers" / "file-a.txt" dst = tmp_path / "file-a.txt" @@ -187,7 +197,7 @@ class Test_copy_file_if_changed: helpers.copy_file_if_changed(src, dst) - def test_src_and_dst_do_not_match(self, tmp_path, fixture_path): + def test_src_and_dst_do_not_match(self, tmp_path: Path, fixture_path: Path): src = fixture_path / "helpers" / "file-a.txt" initial = fixture_path / "helpers" / "file-c.txt" dst = tmp_path / "file-a.txt" @@ -198,7 +208,7 @@ class Test_copy_file_if_changed: assert src.read_text() == dst.read_text() - def test_dst_does_not_exist(self, tmp_path, fixture_path): + def test_dst_does_not_exist(self, tmp_path: Path, fixture_path: Path): src = fixture_path / "helpers" / "file-a.txt" dst = tmp_path / "file-a.txt" @@ -277,3 +287,624 @@ def test_sort_ip_addresses(text: list[str], expected: list[str]) -> None: actual = helpers.sort_ip_addresses(text) assert actual == expected + + +# DNS resolution tests +def test_is_ip_address_ipv4() -> None: + """Test is_ip_address with IPv4 addresses.""" + assert helpers.is_ip_address("192.168.1.1") is True + assert helpers.is_ip_address("127.0.0.1") is True + assert helpers.is_ip_address("255.255.255.255") is True + assert helpers.is_ip_address("0.0.0.0") is True + + +def test_is_ip_address_ipv6() -> None: + """Test is_ip_address with IPv6 addresses.""" + assert helpers.is_ip_address("::1") is True + assert helpers.is_ip_address("2001:db8::1") is True + assert helpers.is_ip_address("fe80::1") is True + assert helpers.is_ip_address("::") is True + + +def test_is_ip_address_invalid() -> None: + """Test is_ip_address with non-IP strings.""" + assert helpers.is_ip_address("hostname") is False + assert helpers.is_ip_address("hostname.local") is False + assert helpers.is_ip_address("256.256.256.256") is False + assert helpers.is_ip_address("192.168.1") is False + assert helpers.is_ip_address("") is False + + +def test_resolve_ip_address_single_ipv4() -> None: + """Test resolving a single IPv4 address (fast path).""" + result = helpers.resolve_ip_address("192.168.1.100", 6053) + + assert len(result) == 1 + assert result[0][0] == socket.AF_INET # family + assert result[0][1] in ( + 0, + socket.SOCK_STREAM, + ) # type (0 on Windows with AI_NUMERICHOST) + assert result[0][2] in ( + 0, + socket.IPPROTO_TCP, + ) # proto (0 on Windows with AI_NUMERICHOST) + assert result[0][3] == "" # canonname + assert result[0][4] == ("192.168.1.100", 6053) # sockaddr + + +def test_resolve_ip_address_single_ipv6() -> None: + """Test resolving a single IPv6 address (fast path).""" + result = helpers.resolve_ip_address("::1", 6053) + + assert len(result) == 1 + assert result[0][0] == socket.AF_INET6 # family + assert result[0][1] in ( + 0, + socket.SOCK_STREAM, + ) # type (0 on Windows with AI_NUMERICHOST) + assert result[0][2] in ( + 0, + socket.IPPROTO_TCP, + ) # proto (0 on Windows with AI_NUMERICHOST) + assert result[0][3] == "" # canonname + # IPv6 sockaddr has 4 elements + assert len(result[0][4]) == 4 + assert result[0][4][0] == "::1" # address + assert result[0][4][1] == 6053 # port + + +def test_resolve_ip_address_list_of_ips() -> None: + """Test resolving a list of IP addresses (fast path).""" + ips = ["192.168.1.100", "10.0.0.1", "::1"] + result = helpers.resolve_ip_address(ips, 6053) + + # Should return results sorted by preference (IPv6 first, then IPv4) + assert len(result) >= 2 # At least IPv4 addresses should work + + # Check that results are properly formatted + for addr_info in result: + assert addr_info[0] in (socket.AF_INET, socket.AF_INET6) + assert addr_info[1] in ( + 0, + socket.SOCK_STREAM, + ) # 0 on Windows with AI_NUMERICHOST + assert addr_info[2] in ( + 0, + socket.IPPROTO_TCP, + ) # 0 on Windows with AI_NUMERICHOST + assert addr_info[3] == "" + + +def test_resolve_ip_address_with_getaddrinfo_failure(caplog) -> None: + """Test that getaddrinfo OSError is handled gracefully in fast path.""" + with ( + caplog.at_level(logging.DEBUG), + patch("socket.getaddrinfo") as mock_getaddrinfo, + ): + # First IP succeeds + mock_getaddrinfo.side_effect = [ + [ + ( + socket.AF_INET, + socket.SOCK_STREAM, + socket.IPPROTO_TCP, + "", + ("192.168.1.100", 6053), + ) + ], + OSError("Failed to resolve"), # Second IP fails + ] + + # Should continue despite one failure + result = helpers.resolve_ip_address(["192.168.1.100", "192.168.1.101"], 6053) + + # Should have result from first IP only + assert len(result) == 1 + assert result[0][4][0] == "192.168.1.100" + + # Verify both IPs were attempted + assert mock_getaddrinfo.call_count == 2 + mock_getaddrinfo.assert_any_call( + "192.168.1.100", 6053, proto=socket.IPPROTO_TCP, flags=socket.AI_NUMERICHOST + ) + mock_getaddrinfo.assert_any_call( + "192.168.1.101", 6053, proto=socket.IPPROTO_TCP, flags=socket.AI_NUMERICHOST + ) + + # Verify the debug log was called for the failed IP + assert "Failed to parse IP address '192.168.1.101'" in caplog.text + + +def test_resolve_ip_address_hostname() -> None: + """Test resolving a hostname (async resolver path).""" + mock_addr_info = AddrInfo( + family=socket.AF_INET, + type=socket.SOCK_STREAM, + proto=socket.IPPROTO_TCP, + sockaddr=IPv4Sockaddr(address="192.168.1.100", port=6053), + ) + + with patch("esphome.resolver.AsyncResolver") as MockResolver: + mock_resolver = MockResolver.return_value + mock_resolver.resolve.return_value = [mock_addr_info] + + result = helpers.resolve_ip_address("test.local", 6053) + + assert len(result) == 1 + assert result[0][0] == socket.AF_INET + assert result[0][4] == ("192.168.1.100", 6053) + MockResolver.assert_called_once_with(["test.local"], 6053) + mock_resolver.resolve.assert_called_once() + + +def test_resolve_ip_address_mixed_list() -> None: + """Test resolving a mix of IPs and hostnames.""" + mock_addr_info = AddrInfo( + family=socket.AF_INET, + type=socket.SOCK_STREAM, + proto=socket.IPPROTO_TCP, + sockaddr=IPv4Sockaddr(address="192.168.1.200", port=6053), + ) + + with patch("esphome.resolver.AsyncResolver") as MockResolver: + mock_resolver = MockResolver.return_value + mock_resolver.resolve.return_value = [mock_addr_info] + + # Mix of IP and hostname - should use async resolver + result = helpers.resolve_ip_address(["192.168.1.100", "test.local"], 6053) + + assert len(result) == 2 + assert result[0][4][0] == "192.168.1.100" + assert result[1][4][0] == "192.168.1.200" + MockResolver.assert_called_once_with(["test.local"], 6053) + mock_resolver.resolve.assert_called_once() + + +def test_resolve_ip_address_mixed_list_fail() -> None: + """Test resolving a mix of IPs and hostnames with resolve failed.""" + with patch("esphome.resolver.AsyncResolver") as MockResolver: + mock_resolver = MockResolver.return_value + mock_resolver.resolve.side_effect = EsphomeError( + "Error resolving IP address: [test.local]" + ) + + # Mix of IP and hostname - should use async resolver + result = helpers.resolve_ip_address(["192.168.1.100", "test.local"], 6053) + + assert len(result) == 1 + assert result[0][4][0] == "192.168.1.100" + MockResolver.assert_called_once_with(["test.local"], 6053) + mock_resolver.resolve.assert_called_once() + + +def test_resolve_ip_address_url() -> None: + """Test extracting hostname from URL.""" + mock_addr_info = AddrInfo( + family=socket.AF_INET, + type=socket.SOCK_STREAM, + proto=socket.IPPROTO_TCP, + sockaddr=IPv4Sockaddr(address="192.168.1.100", port=6053), + ) + + with patch("esphome.resolver.AsyncResolver") as MockResolver: + mock_resolver = MockResolver.return_value + mock_resolver.resolve.return_value = [mock_addr_info] + + result = helpers.resolve_ip_address("http://test.local", 6053) + + assert len(result) == 1 + MockResolver.assert_called_once_with(["test.local"], 6053) + mock_resolver.resolve.assert_called_once() + + +def test_resolve_ip_address_ipv6_conversion() -> None: + """Test proper IPv6 address info conversion.""" + mock_addr_info = AddrInfo( + family=socket.AF_INET6, + type=socket.SOCK_STREAM, + proto=socket.IPPROTO_TCP, + sockaddr=IPv6Sockaddr(address="2001:db8::1", port=6053, flowinfo=1, scope_id=2), + ) + + with patch("esphome.resolver.AsyncResolver") as MockResolver: + mock_resolver = MockResolver.return_value + mock_resolver.resolve.return_value = [mock_addr_info] + + result = helpers.resolve_ip_address("test.local", 6053) + + assert len(result) == 1 + assert result[0][0] == socket.AF_INET6 + assert result[0][4] == ("2001:db8::1", 6053, 1, 2) + + +def test_resolve_ip_address_error_handling() -> None: + """Test error handling from AsyncResolver.""" + with patch("esphome.resolver.AsyncResolver") as MockResolver: + mock_resolver = MockResolver.return_value + mock_resolver.resolve.side_effect = EsphomeError("Resolution failed") + + with pytest.raises(EsphomeError, match="Resolution failed"): + helpers.resolve_ip_address("test.local", 6053) + + +def test_addr_preference_ipv4() -> None: + """Test address preference for IPv4.""" + addr_info = ( + socket.AF_INET, + socket.SOCK_STREAM, + socket.IPPROTO_TCP, + "", + ("192.168.1.1", 6053), + ) + assert helpers.addr_preference_(addr_info) == 2 + + +def test_addr_preference_ipv6() -> None: + """Test address preference for regular IPv6.""" + addr_info = ( + socket.AF_INET6, + socket.SOCK_STREAM, + socket.IPPROTO_TCP, + "", + ("2001:db8::1", 6053, 0, 0), + ) + assert helpers.addr_preference_(addr_info) == 1 + + +def test_addr_preference_ipv6_link_local_no_scope() -> None: + """Test address preference for link-local IPv6 without scope.""" + addr_info = ( + socket.AF_INET6, + socket.SOCK_STREAM, + socket.IPPROTO_TCP, + "", + ("fe80::1", 6053, 0, 0), # link-local with scope_id=0 + ) + assert helpers.addr_preference_(addr_info) == 3 + + +def test_addr_preference_ipv6_link_local_with_scope() -> None: + """Test address preference for link-local IPv6 with scope.""" + addr_info = ( + socket.AF_INET6, + socket.SOCK_STREAM, + socket.IPPROTO_TCP, + "", + ("fe80::1", 6053, 0, 2), # link-local with scope_id=2 + ) + assert helpers.addr_preference_(addr_info) == 1 # Has scope, so it's usable + + +def test_mkdir_p(tmp_path: Path) -> None: + """Test mkdir_p creates directories recursively.""" + # Test creating nested directories + nested_path = tmp_path / "level1" / "level2" / "level3" + helpers.mkdir_p(nested_path) + assert nested_path.exists() + assert nested_path.is_dir() + + # Test that mkdir_p is idempotent (doesn't fail if directory exists) + helpers.mkdir_p(nested_path) + assert nested_path.exists() + + # Test with empty path (should do nothing) + helpers.mkdir_p("") + + # Test with existing directory + existing_dir = tmp_path / "existing" + existing_dir.mkdir() + helpers.mkdir_p(existing_dir) + assert existing_dir.exists() + + +def test_mkdir_p_file_exists_error(tmp_path: Path) -> None: + """Test mkdir_p raises error when path is a file.""" + # Create a file + file_path = tmp_path / "test_file.txt" + file_path.write_text("test content") + + # Try to create directory with same name as existing file + with pytest.raises(EsphomeError, match=r"Error creating directories"): + helpers.mkdir_p(file_path) + + +def test_mkdir_p_with_existing_file_raises_error(tmp_path: Path) -> None: + """Test mkdir_p raises error when trying to create dir over existing file.""" + # Create a file where we want to create a directory + file_path = tmp_path / "existing_file" + file_path.write_text("content") + + # Try to create a directory with a path that goes through the file + dir_path = file_path / "subdir" + + with pytest.raises(EsphomeError, match=r"Error creating directories"): + helpers.mkdir_p(dir_path) + + +def test_read_file(tmp_path: Path) -> None: + """Test read_file reads file content correctly.""" + # Test reading regular file + test_file = tmp_path / "test.txt" + expected_content = "Test content\nLine 2\n" + test_file.write_text(expected_content) + + content = helpers.read_file(test_file) + assert content == expected_content + + # Test reading file with UTF-8 characters + utf8_file = tmp_path / "utf8.txt" + utf8_content = "Hello 世界 🌍" + utf8_file.write_text(utf8_content, encoding="utf-8") + + content = helpers.read_file(utf8_file) + assert content == utf8_content + + +def test_read_file_not_found() -> None: + """Test read_file raises error for non-existent file.""" + with pytest.raises(EsphomeError, match=r"Error reading file"): + helpers.read_file(Path("/nonexistent/file.txt")) + + +def test_read_file_unicode_decode_error(tmp_path: Path) -> None: + """Test read_file raises error for invalid UTF-8.""" + test_file = tmp_path / "invalid.txt" + # Write invalid UTF-8 bytes + test_file.write_bytes(b"\xff\xfe") + + with pytest.raises(EsphomeError, match=r"Error reading file"): + helpers.read_file(test_file) + + +@pytest.mark.skipif(os.name == "nt", reason="Unix-specific test") +def test_write_file_unix(tmp_path: Path) -> None: + """Test write_file writes content correctly on Unix.""" + # Test writing string content + test_file = tmp_path / "test.txt" + content = "Test content\nLine 2" + helpers.write_file(test_file, content) + + assert test_file.read_text() == content + # Check file permissions + assert oct(test_file.stat().st_mode)[-3:] == "644" + + # Test overwriting existing file + new_content = "New content" + helpers.write_file(test_file, new_content) + assert test_file.read_text() == new_content + + # Test writing to nested directories (should create them) + nested_file = tmp_path / "dir1" / "dir2" / "file.txt" + helpers.write_file(nested_file, content) + assert nested_file.read_text() == content + + +@pytest.mark.skipif(os.name != "nt", reason="Windows-specific test") +def test_write_file_windows(tmp_path: Path) -> None: + """Test write_file writes content correctly on Windows.""" + # Test writing string content + test_file = tmp_path / "test.txt" + content = "Test content\nLine 2" + helpers.write_file(test_file, content) + + assert test_file.read_text() == content + # Windows doesn't have Unix-style 644 permissions + + # Test overwriting existing file + new_content = "New content" + helpers.write_file(test_file, new_content) + assert test_file.read_text() == new_content + + # Test writing to nested directories (should create them) + nested_file = tmp_path / "dir1" / "dir2" / "file.txt" + helpers.write_file(nested_file, content) + assert nested_file.read_text() == content + + +@pytest.mark.skipif(os.name == "nt", reason="Unix-specific permission test") +def test_write_file_to_non_writable_directory_unix(tmp_path: Path) -> None: + """Test write_file raises error when directory is not writable on Unix.""" + # Create a directory and make it read-only + read_only_dir = tmp_path / "readonly" + read_only_dir.mkdir() + test_file = read_only_dir / "test.txt" + + # Make directory read-only (no write permission) + read_only_dir.chmod(0o555) + + try: + with pytest.raises(EsphomeError, match=r"Could not write file"): + helpers.write_file(test_file, "content") + finally: + # Restore write permissions for cleanup + read_only_dir.chmod(0o755) + + +@pytest.mark.skipif(os.name != "nt", reason="Windows-specific test") +def test_write_file_to_non_writable_directory_windows(tmp_path: Path) -> None: + """Test write_file error handling on Windows.""" + # Windows handles permissions differently - test a different error case + # Try to write to a file path that contains an existing file as a directory component + existing_file = tmp_path / "file.txt" + existing_file.write_text("content") + + # Try to write to a path that treats the file as a directory + invalid_path = existing_file / "subdir" / "test.txt" + + with pytest.raises(EsphomeError, match=r"Could not write file"): + helpers.write_file(invalid_path, "content") + + +@pytest.mark.skipif(os.name == "nt", reason="Unix-specific permission test") +def test_write_file_with_permission_bits_unix(tmp_path: Path) -> None: + """Test that write_file sets correct permissions on Unix.""" + test_file = tmp_path / "test.txt" + helpers.write_file(test_file, "content") + + # Check that file has 644 permissions + file_mode = test_file.stat().st_mode + assert stat.S_IMODE(file_mode) == 0o644 + + +@pytest.mark.skipif(os.name == "nt", reason="Unix-specific permission test") +def test_copy_file_if_changed_permission_recovery_unix(tmp_path: Path) -> None: + """Test copy_file_if_changed handles permission errors correctly on Unix.""" + # Test with read-only destination file + src = tmp_path / "source.txt" + dst = tmp_path / "dest.txt" + src.write_text("new content") + dst.write_text("old content") + dst.chmod(0o444) # Make destination read-only + + try: + # Should handle permission error by deleting and retrying + helpers.copy_file_if_changed(src, dst) + assert dst.read_text() == "new content" + finally: + # Restore write permissions for cleanup + if dst.exists(): + dst.chmod(0o644) + + +def test_copy_file_if_changed_creates_directories(tmp_path: Path) -> None: + """Test copy_file_if_changed creates missing directories.""" + src = tmp_path / "source.txt" + dst = tmp_path / "subdir" / "nested" / "dest.txt" + src.write_text("content") + + helpers.copy_file_if_changed(src, dst) + assert dst.exists() + assert dst.read_text() == "content" + + +def test_copy_file_if_changed_nonexistent_source(tmp_path: Path) -> None: + """Test copy_file_if_changed with non-existent source.""" + src = tmp_path / "nonexistent.txt" + dst = tmp_path / "dest.txt" + + with pytest.raises(EsphomeError, match=r"Error copying file"): + helpers.copy_file_if_changed(src, dst) + + +def test_resolve_ip_address_sorting() -> None: + """Test that results are sorted by preference.""" + # Create multiple address infos with different preferences + mock_addr_infos = [ + AddrInfo( + family=socket.AF_INET6, + type=socket.SOCK_STREAM, + proto=socket.IPPROTO_TCP, + sockaddr=IPv6Sockaddr( + address="fe80::1", port=6053, flowinfo=0, scope_id=0 + ), # Preference 3 (link-local no scope) + ), + AddrInfo( + family=socket.AF_INET, + type=socket.SOCK_STREAM, + proto=socket.IPPROTO_TCP, + sockaddr=IPv4Sockaddr( + address="192.168.1.100", port=6053 + ), # Preference 2 (IPv4) + ), + AddrInfo( + family=socket.AF_INET6, + type=socket.SOCK_STREAM, + proto=socket.IPPROTO_TCP, + sockaddr=IPv6Sockaddr( + address="2001:db8::1", port=6053, flowinfo=0, scope_id=0 + ), # Preference 1 (IPv6) + ), + ] + + with patch("esphome.resolver.AsyncResolver") as MockResolver: + mock_resolver = MockResolver.return_value + mock_resolver.resolve.return_value = mock_addr_infos + + result = helpers.resolve_ip_address("test.local", 6053) + + # Should be sorted: IPv6 first, then IPv4, then link-local without scope + assert result[0][4][0] == "2001:db8::1" # IPv6 (preference 1) + assert result[1][4][0] == "192.168.1.100" # IPv4 (preference 2) + assert result[2][4][0] == "fe80::1" # Link-local no scope (preference 3) + + +def test_resolve_ip_address_with_cache() -> None: + """Test that the cache is used when provided.""" + cache = AddressCache( + mdns_cache={"test.local": ["192.168.1.100", "192.168.1.101"]}, + dns_cache={ + "example.com": ["93.184.216.34", "2606:2800:220:1:248:1893:25c8:1946"] + }, + ) + + # Test mDNS cache hit + result = helpers.resolve_ip_address("test.local", 6053, address_cache=cache) + + # Should return cached addresses without calling resolver + assert len(result) == 2 + assert result[0][4][0] == "192.168.1.100" + assert result[1][4][0] == "192.168.1.101" + + # Test DNS cache hit + result = helpers.resolve_ip_address("example.com", 6053, address_cache=cache) + + # Should return cached addresses with IPv6 first due to preference + assert len(result) == 2 + assert result[0][4][0] == "2606:2800:220:1:248:1893:25c8:1946" # IPv6 first + assert result[1][4][0] == "93.184.216.34" # IPv4 second + + +def test_resolve_ip_address_cache_miss() -> None: + """Test that resolver is called when not in cache.""" + cache = AddressCache(mdns_cache={"other.local": ["192.168.1.200"]}) + + mock_addr_info = AddrInfo( + family=socket.AF_INET, + type=socket.SOCK_STREAM, + proto=socket.IPPROTO_TCP, + sockaddr=IPv4Sockaddr(address="192.168.1.100", port=6053), + ) + + with patch("esphome.resolver.AsyncResolver") as MockResolver: + mock_resolver = MockResolver.return_value + mock_resolver.resolve.return_value = [mock_addr_info] + + result = helpers.resolve_ip_address("test.local", 6053, address_cache=cache) + + # Should call resolver since test.local is not in cache + MockResolver.assert_called_once_with(["test.local"], 6053) + assert len(result) == 1 + assert result[0][4][0] == "192.168.1.100" + + +def test_resolve_ip_address_mixed_cached_uncached() -> None: + """Test resolution with mix of cached and uncached hosts.""" + cache = AddressCache(mdns_cache={"cached.local": ["192.168.1.50"]}) + + mock_addr_info = AddrInfo( + family=socket.AF_INET, + type=socket.SOCK_STREAM, + proto=socket.IPPROTO_TCP, + sockaddr=IPv4Sockaddr(address="192.168.1.100", port=6053), + ) + + with patch("esphome.resolver.AsyncResolver") as MockResolver: + mock_resolver = MockResolver.return_value + mock_resolver.resolve.return_value = [mock_addr_info] + + # Pass a list with cached IP, cached hostname, and uncached hostname + result = helpers.resolve_ip_address( + ["192.168.1.10", "cached.local", "uncached.local"], + 6053, + address_cache=cache, + ) + + # Should only resolve uncached.local + MockResolver.assert_called_once_with(["uncached.local"], 6053) + + # Results should include all addresses + addresses = [r[4][0] for r in result] + assert "192.168.1.10" in addresses # Direct IP + assert "192.168.1.50" in addresses # From cache + assert "192.168.1.100" in addresses # From resolver diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py new file mode 100644 index 0000000000..ccbc5a1306 --- /dev/null +++ b/tests/unit_tests/test_main.py @@ -0,0 +1,2584 @@ +"""Unit tests for esphome.__main__ module.""" + +from __future__ import annotations + +from collections.abc import Generator +from dataclasses import dataclass +import logging +from pathlib import Path +import re +from typing import Any +from unittest.mock import MagicMock, Mock, patch + +import pytest +from pytest import CaptureFixture + +from esphome import platformio_api +from esphome.__main__ import ( + Purpose, + choose_upload_log_host, + command_analyze_memory, + command_clean_all, + command_rename, + command_update_all, + command_wizard, + detect_external_components, + get_port_type, + has_ip_address, + has_mqtt, + has_mqtt_ip_lookup, + has_mqtt_logging, + has_non_ip_address, + has_resolvable_address, + mqtt_get_ip, + show_logs, + upload_program, + upload_using_esptool, +) +from esphome.components.esp32.const import KEY_ESP32, KEY_VARIANT, VARIANT_ESP32 +from esphome.const import ( + CONF_API, + CONF_BROKER, + CONF_DISABLED, + CONF_ESPHOME, + CONF_LEVEL, + CONF_LOG_TOPIC, + CONF_MDNS, + CONF_MQTT, + CONF_NAME, + CONF_OTA, + CONF_PASSWORD, + CONF_PLATFORM, + CONF_PORT, + CONF_SUBSTITUTIONS, + CONF_TOPIC, + CONF_USE_ADDRESS, + CONF_WIFI, + KEY_CORE, + KEY_TARGET_PLATFORM, + PLATFORM_BK72XX, + PLATFORM_ESP32, + PLATFORM_ESP8266, + PLATFORM_RP2040, +) +from esphome.core import CORE, EsphomeError + + +def strip_ansi_codes(text: str) -> str: + """Remove ANSI escape codes from text. + + This helps make test assertions cleaner by removing color codes and other + terminal formatting that can make tests brittle. + """ + # Pattern to match ANSI escape sequences + ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") + return ansi_escape.sub("", text) + + +@dataclass +class MockSerialPort: + """Mock serial port for testing. + + Attributes: + path (str): The device path of the mock serial port (e.g., '/dev/ttyUSB0'). + description (str): A human-readable description of the mock serial port. + """ + + path: str + description: str + + +def setup_core( + config: dict[str, Any] | None = None, + address: str | None = None, + platform: str | None = None, + tmp_path: Path | None = None, + name: str = "test", +) -> None: + """ + Helper to set up CORE configuration with optional address. + + Args: + config (dict[str, Any] | None): The configuration dictionary to set for CORE. If None, an empty dict is used. + address (str | None): Optional network address to set in the configuration. If provided, it is set under the wifi config. + platform (str | None): Optional target platform to set in CORE.data. + tmp_path (Path | None): Optional temp path for setting up build paths. + name (str): The name of the device (defaults to "test"). + """ + if config is None: + config = {} + + if address is not None: + # Set address via wifi config (could also use ethernet) + config[CONF_WIFI] = {CONF_USE_ADDRESS: address} + + CORE.config = config + + if platform is not None: + CORE.data[KEY_CORE] = {} + CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = platform + + if tmp_path is not None: + CORE.config_path = str(tmp_path / f"{name}.yaml") + CORE.name = name + CORE.build_path = str(tmp_path / ".esphome" / "build" / name) + + +@pytest.fixture +def mock_no_serial_ports() -> Generator[Mock]: + """Mock get_serial_ports to return no ports.""" + with patch("esphome.__main__.get_serial_ports", return_value=[]) as mock: + yield mock + + +@pytest.fixture +def mock_get_port_type() -> Generator[Mock]: + """Mock get_port_type for testing.""" + with patch("esphome.__main__.get_port_type") as mock: + yield mock + + +@pytest.fixture +def mock_check_permissions() -> Generator[Mock]: + """Mock check_permissions for testing.""" + with patch("esphome.__main__.check_permissions") as mock: + yield mock + + +@pytest.fixture +def mock_run_miniterm() -> Generator[Mock]: + """Mock run_miniterm for testing.""" + with patch("esphome.__main__.run_miniterm") as mock: + yield mock + + +@pytest.fixture +def mock_upload_using_esptool() -> Generator[Mock]: + """Mock upload_using_esptool for testing.""" + with patch("esphome.__main__.upload_using_esptool") as mock: + yield mock + + +@pytest.fixture +def mock_upload_using_platformio() -> Generator[Mock]: + """Mock upload_using_platformio for testing.""" + with patch("esphome.__main__.upload_using_platformio") as mock: + yield mock + + +@pytest.fixture +def mock_run_ota() -> Generator[Mock]: + """Mock espota2.run_ota for testing.""" + with patch("esphome.espota2.run_ota") as mock: + yield mock + + +@pytest.fixture +def mock_is_ip_address() -> Generator[Mock]: + """Mock is_ip_address for testing.""" + with patch("esphome.__main__.is_ip_address") as mock: + yield mock + + +@pytest.fixture +def mock_mqtt_get_ip() -> Generator[Mock]: + """Mock mqtt_get_ip for testing.""" + with patch("esphome.__main__.mqtt_get_ip") as mock: + yield mock + + +@pytest.fixture +def mock_serial_ports() -> Generator[Mock]: + """Mock get_serial_ports to return test ports.""" + mock_ports = [ + MockSerialPort("/dev/ttyUSB0", "USB Serial"), + MockSerialPort("/dev/ttyUSB1", "Another USB Serial"), + ] + with patch("esphome.__main__.get_serial_ports", return_value=mock_ports) as mock: + yield mock + + +@pytest.fixture +def mock_choose_prompt() -> Generator[Mock]: + """Mock choose_prompt to return default selection.""" + with patch("esphome.__main__.choose_prompt", return_value="/dev/ttyUSB0") as mock: + yield mock + + +@pytest.fixture +def mock_no_mqtt_logging() -> Generator[Mock]: + """Mock has_mqtt_logging to return False.""" + with patch("esphome.__main__.has_mqtt_logging", return_value=False) as mock: + yield mock + + +@pytest.fixture +def mock_has_mqtt_logging() -> Generator[Mock]: + """Mock has_mqtt_logging to return True.""" + with patch("esphome.__main__.has_mqtt_logging", return_value=True) as mock: + yield mock + + +@pytest.fixture +def mock_run_external_process() -> Generator[Mock]: + """Mock run_external_process for testing.""" + with patch("esphome.__main__.run_external_process") as mock: + mock.return_value = 0 # Default to success + yield mock + + +@pytest.fixture +def mock_run_external_command_main() -> Generator[Mock]: + """Mock run_external_command in __main__ module (different from platformio_api).""" + with patch("esphome.__main__.run_external_command") as mock: + mock.return_value = 0 # Default to success + yield mock + + +@pytest.fixture +def mock_write_cpp() -> Generator[Mock]: + """Mock write_cpp for testing.""" + with patch("esphome.__main__.write_cpp") as mock: + mock.return_value = 0 # Default to success + yield mock + + +@pytest.fixture +def mock_compile_program() -> Generator[Mock]: + """Mock compile_program for testing.""" + with patch("esphome.__main__.compile_program") as mock: + mock.return_value = 0 # Default to success + yield mock + + +@pytest.fixture +def mock_get_esphome_components() -> Generator[Mock]: + """Mock get_esphome_components for testing.""" + with patch("esphome.analyze_memory.helpers.get_esphome_components") as mock: + mock.return_value = {"logger", "api", "ota"} + yield mock + + +@pytest.fixture +def mock_memory_analyzer_cli() -> Generator[Mock]: + """Mock MemoryAnalyzerCLI for testing.""" + with patch("esphome.analyze_memory.cli.MemoryAnalyzerCLI") as mock_class: + mock_analyzer = MagicMock() + mock_analyzer.generate_report.return_value = "Mock Memory Report" + mock_class.return_value = mock_analyzer + yield mock_class + + +def test_choose_upload_log_host_with_string_default() -> None: + """Test with a single string default device.""" + setup_core() + result = choose_upload_log_host( + default="192.168.1.100", + check_default=None, + purpose=Purpose.UPLOADING, + ) + assert result == ["192.168.1.100"] + + +def test_choose_upload_log_host_with_list_default() -> None: + """Test with a list of default devices.""" + setup_core() + result = choose_upload_log_host( + default=["192.168.1.100", "192.168.1.101"], + check_default=None, + purpose=Purpose.UPLOADING, + ) + assert result == ["192.168.1.100", "192.168.1.101"] + + +def test_choose_upload_log_host_with_multiple_ip_addresses() -> None: + """Test with multiple IP addresses as defaults.""" + setup_core() + result = choose_upload_log_host( + default=["1.2.3.4", "4.5.5.6"], + check_default=None, + purpose=Purpose.LOGGING, + ) + assert result == ["1.2.3.4", "4.5.5.6"] + + +def test_choose_upload_log_host_with_mixed_hostnames_and_ips() -> None: + """Test with a mix of hostnames and IP addresses.""" + setup_core() + result = choose_upload_log_host( + default=["host.one", "host.one.local", "1.2.3.4"], + check_default=None, + purpose=Purpose.UPLOADING, + ) + assert result == ["host.one", "host.one.local", "1.2.3.4"] + + +def test_choose_upload_log_host_with_ota_list() -> None: + """Test with OTA as the only item in the list.""" + setup_core(config={CONF_OTA: {}}, address="192.168.1.100") + + result = choose_upload_log_host( + default=["OTA"], + check_default=None, + purpose=Purpose.UPLOADING, + ) + assert result == ["192.168.1.100"] + + +@pytest.mark.usefixtures("mock_has_mqtt_logging") +def test_choose_upload_log_host_with_ota_list_mqtt_fallback() -> None: + """Test with OTA list falling back to MQTT when no address.""" + setup_core(config={CONF_OTA: {}, "mqtt": {}}) + + result = choose_upload_log_host( + default=["OTA"], + check_default=None, + purpose=Purpose.UPLOADING, + ) + assert result == ["MQTTIP"] + + +@pytest.mark.usefixtures("mock_has_mqtt_logging") +def test_choose_upload_log_host_with_ota_list_mqtt_fallback_logging() -> None: + """Test with OTA list with API and MQTT when no address.""" + setup_core(config={CONF_API: {}, "mqtt": {}}) + + result = choose_upload_log_host( + default=["OTA"], + check_default=None, + purpose=Purpose.LOGGING, + ) + assert result == ["MQTTIP", "MQTT"] + + +@pytest.mark.usefixtures("mock_no_serial_ports") +def test_choose_upload_log_host_with_serial_device_no_ports( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test SERIAL device when no serial ports are found.""" + setup_core() + with pytest.raises( + EsphomeError, match="All specified devices .* could not be resolved" + ): + choose_upload_log_host( + default="SERIAL", + check_default=None, + purpose=Purpose.UPLOADING, + ) + assert "No serial ports found, skipping SERIAL device" in caplog.text + + +@pytest.mark.usefixtures("mock_serial_ports") +def test_choose_upload_log_host_with_serial_device_with_ports( + mock_choose_prompt: Mock, +) -> None: + """Test SERIAL device when serial ports are available.""" + setup_core() + result = choose_upload_log_host( + default="SERIAL", + check_default=None, + purpose=Purpose.UPLOADING, + ) + assert result == ["/dev/ttyUSB0"] + mock_choose_prompt.assert_called_once_with( + [ + ("/dev/ttyUSB0 (USB Serial)", "/dev/ttyUSB0"), + ("/dev/ttyUSB1 (Another USB Serial)", "/dev/ttyUSB1"), + ], + purpose=Purpose.UPLOADING, + ) + + +def test_choose_upload_log_host_with_ota_device_with_ota_config() -> None: + """Test OTA device when OTA is configured.""" + setup_core(config={CONF_OTA: {}}, address="192.168.1.100") + + result = choose_upload_log_host( + default="OTA", + check_default=None, + purpose=Purpose.UPLOADING, + ) + assert result == ["192.168.1.100"] + + +def test_choose_upload_log_host_with_ota_device_with_api_config() -> None: + """Test OTA device when API is configured (no upload without OTA in config).""" + setup_core(config={CONF_API: {}}, address="192.168.1.100") + + with pytest.raises( + EsphomeError, match="All specified devices .* could not be resolved" + ): + choose_upload_log_host( + default="OTA", + check_default=None, + purpose=Purpose.UPLOADING, + ) + + +def test_choose_upload_log_host_with_ota_device_with_api_config_logging() -> None: + """Test OTA device when API is configured.""" + setup_core(config={CONF_API: {}}, address="192.168.1.100") + + result = choose_upload_log_host( + default="OTA", + check_default=None, + purpose=Purpose.LOGGING, + ) + assert result == ["192.168.1.100"] + + +@pytest.mark.usefixtures("mock_has_mqtt_logging") +def test_choose_upload_log_host_with_ota_device_fallback_to_mqtt() -> None: + """Test OTA device fallback to MQTT when no OTA/API config.""" + setup_core(config={"mqtt": {}}) + + result = choose_upload_log_host( + default="OTA", + check_default=None, + purpose=Purpose.LOGGING, + ) + assert result == ["MQTT"] + + +@pytest.mark.usefixtures("mock_no_mqtt_logging") +def test_choose_upload_log_host_with_ota_device_no_fallback() -> None: + """Test OTA device with no valid fallback options.""" + setup_core() + + with pytest.raises( + EsphomeError, match="All specified devices .* could not be resolved" + ): + choose_upload_log_host( + default="OTA", + check_default=None, + purpose=Purpose.UPLOADING, + ) + + +@pytest.mark.usefixtures("mock_choose_prompt") +def test_choose_upload_log_host_multiple_devices() -> None: + """Test with multiple devices including special identifiers.""" + setup_core(config={CONF_OTA: {}}, address="192.168.1.100") + + mock_ports = [MockSerialPort("/dev/ttyUSB0", "USB Serial")] + + with patch("esphome.__main__.get_serial_ports", return_value=mock_ports): + result = choose_upload_log_host( + default=["192.168.1.50", "OTA", "SERIAL"], + check_default=None, + purpose=Purpose.UPLOADING, + ) + assert result == ["192.168.1.50", "192.168.1.100", "/dev/ttyUSB0"] + + +def test_choose_upload_log_host_no_defaults_with_serial_ports( + mock_choose_prompt: Mock, +) -> None: + """Test interactive mode with serial ports available.""" + mock_ports = [ + MockSerialPort("/dev/ttyUSB0", "USB Serial"), + ] + + setup_core() + + with patch("esphome.__main__.get_serial_ports", return_value=mock_ports): + result = choose_upload_log_host( + default=None, + check_default=None, + purpose=Purpose.UPLOADING, + ) + assert result == ["/dev/ttyUSB0"] + mock_choose_prompt.assert_called_once_with( + [("/dev/ttyUSB0 (USB Serial)", "/dev/ttyUSB0")], + purpose=Purpose.UPLOADING, + ) + + +@pytest.mark.usefixtures("mock_no_serial_ports") +def test_choose_upload_log_host_no_defaults_with_ota() -> None: + """Test interactive mode with OTA option.""" + setup_core(config={CONF_OTA: {}}, address="192.168.1.100") + + with patch( + "esphome.__main__.choose_prompt", return_value="192.168.1.100" + ) as mock_prompt: + result = choose_upload_log_host( + default=None, + check_default=None, + purpose=Purpose.UPLOADING, + ) + assert result == ["192.168.1.100"] + mock_prompt.assert_called_once_with( + [("Over The Air (192.168.1.100)", "192.168.1.100")], + purpose=Purpose.UPLOADING, + ) + + +@pytest.mark.usefixtures("mock_no_serial_ports") +def test_choose_upload_log_host_no_defaults_with_api() -> None: + """Test interactive mode with API option.""" + setup_core(config={CONF_API: {}}, address="192.168.1.100") + + with patch( + "esphome.__main__.choose_prompt", return_value="192.168.1.100" + ) as mock_prompt: + result = choose_upload_log_host( + default=None, + check_default=None, + purpose=Purpose.LOGGING, + ) + assert result == ["192.168.1.100"] + mock_prompt.assert_called_once_with( + [("Over The Air (192.168.1.100)", "192.168.1.100")], + purpose=Purpose.LOGGING, + ) + + +@pytest.mark.usefixtures("mock_no_serial_ports", "mock_has_mqtt_logging") +def test_choose_upload_log_host_no_defaults_with_mqtt() -> None: + """Test interactive mode with MQTT option.""" + setup_core(config={CONF_MQTT: {CONF_BROKER: "mqtt.local"}}) + + with patch("esphome.__main__.choose_prompt", return_value="MQTT") as mock_prompt: + result = choose_upload_log_host( + default=None, + check_default=None, + purpose=Purpose.LOGGING, + ) + assert result == ["MQTT"] + mock_prompt.assert_called_once_with( + [("MQTT (mqtt.local)", "MQTT")], + purpose=Purpose.LOGGING, + ) + + +@pytest.mark.usefixtures("mock_has_mqtt_logging") +def test_choose_upload_log_host_no_defaults_with_all_options( + mock_choose_prompt: Mock, +) -> None: + """Test interactive mode with all options available.""" + setup_core( + config={CONF_OTA: {}, CONF_API: {}, CONF_MQTT: {CONF_BROKER: "mqtt.local"}}, + address="192.168.1.100", + ) + + mock_ports = [MockSerialPort("/dev/ttyUSB0", "USB Serial")] + + with patch("esphome.__main__.get_serial_ports", return_value=mock_ports): + result = choose_upload_log_host( + default=None, + check_default=None, + purpose=Purpose.UPLOADING, + ) + assert result == ["/dev/ttyUSB0"] + + expected_options = [ + ("/dev/ttyUSB0 (USB Serial)", "/dev/ttyUSB0"), + ("Over The Air (192.168.1.100)", "192.168.1.100"), + ("Over The Air (MQTT IP lookup)", "MQTTIP"), + ] + mock_choose_prompt.assert_called_once_with( + expected_options, purpose=Purpose.UPLOADING + ) + + +def test_choose_upload_log_host_no_defaults_with_all_options_logging( + mock_choose_prompt: Mock, +) -> None: + """Test interactive mode with all options available.""" + setup_core( + config={CONF_OTA: {}, CONF_API: {}, CONF_MQTT: {CONF_BROKER: "mqtt.local"}}, + address="192.168.1.100", + ) + + mock_ports = [MockSerialPort("/dev/ttyUSB0", "USB Serial")] + + with patch("esphome.__main__.get_serial_ports", return_value=mock_ports): + result = choose_upload_log_host( + default=None, + check_default=None, + purpose=Purpose.LOGGING, + ) + assert result == ["/dev/ttyUSB0"] + + expected_options = [ + ("/dev/ttyUSB0 (USB Serial)", "/dev/ttyUSB0"), + ("MQTT (mqtt.local)", "MQTT"), + ("Over The Air (192.168.1.100)", "192.168.1.100"), + ("Over The Air (MQTT IP lookup)", "MQTTIP"), + ] + mock_choose_prompt.assert_called_once_with( + expected_options, purpose=Purpose.LOGGING + ) + + +@pytest.mark.usefixtures("mock_no_serial_ports") +def test_choose_upload_log_host_check_default_matches() -> None: + """Test when check_default matches an available option.""" + setup_core(config={CONF_OTA: {}}, address="192.168.1.100") + + result = choose_upload_log_host( + default=None, + check_default="192.168.1.100", + purpose=Purpose.UPLOADING, + ) + assert result == ["192.168.1.100"] + + +@pytest.mark.usefixtures("mock_no_serial_ports") +def test_choose_upload_log_host_check_default_no_match() -> None: + """Test when check_default doesn't match any available option.""" + setup_core() + + with patch( + "esphome.__main__.choose_prompt", return_value="fallback" + ) as mock_prompt: + result = choose_upload_log_host( + default=None, + check_default="192.168.1.100", + purpose=Purpose.UPLOADING, + ) + assert result == ["fallback"] + mock_prompt.assert_called_once() + + +@pytest.mark.usefixtures("mock_no_serial_ports") +def test_choose_upload_log_host_empty_defaults_list() -> None: + """Test with an empty list as default.""" + setup_core() + with patch("esphome.__main__.choose_prompt", return_value="chosen") as mock_prompt: + result = choose_upload_log_host( + default=[], + check_default=None, + purpose=Purpose.UPLOADING, + ) + assert result == ["chosen"] + mock_prompt.assert_called_once() + + +@pytest.mark.usefixtures("mock_no_serial_ports", "mock_no_mqtt_logging") +def test_choose_upload_log_host_all_devices_unresolved() -> None: + """Test when all specified devices cannot be resolved.""" + setup_core() + + with pytest.raises( + EsphomeError, + match=r"All specified devices \['SERIAL', 'OTA'\] could not be resolved", + ): + choose_upload_log_host( + default=["SERIAL", "OTA"], + check_default=None, + purpose=Purpose.UPLOADING, + ) + + +@pytest.mark.usefixtures("mock_no_serial_ports", "mock_no_mqtt_logging") +def test_choose_upload_log_host_mixed_resolved_unresolved() -> None: + """Test with a mix of resolved and unresolved devices.""" + setup_core() + + result = choose_upload_log_host( + default=["192.168.1.50", "SERIAL", "OTA"], + check_default=None, + purpose=Purpose.UPLOADING, + ) + assert result == ["192.168.1.50"] + + +def test_choose_upload_log_host_ota_both_conditions() -> None: + """Test OTA device when both OTA and API are configured and enabled.""" + setup_core(config={CONF_OTA: {}, CONF_API: {}}, address="192.168.1.100") + + result = choose_upload_log_host( + default="OTA", + check_default=None, + purpose=Purpose.UPLOADING, + ) + assert result == ["192.168.1.100"] + + +@pytest.mark.usefixtures("mock_serial_ports") +def test_choose_upload_log_host_ota_ip_all_options() -> None: + """Test OTA device when both static IP, OTA, API and MQTT are configured and enabled but MDNS not.""" + setup_core( + config={ + CONF_OTA: {}, + CONF_API: {}, + CONF_MQTT: { + CONF_BROKER: "mqtt.local", + }, + CONF_MDNS: { + CONF_DISABLED: True, + }, + }, + address="192.168.1.100", + ) + + result = choose_upload_log_host( + default="OTA", + check_default=None, + purpose=Purpose.UPLOADING, + ) + assert result == ["192.168.1.100", "MQTTIP"] + + +@pytest.mark.usefixtures("mock_serial_ports") +def test_choose_upload_log_host_ota_local_all_options() -> None: + """Test OTA device when both static IP, OTA, API and MQTT are configured and enabled but MDNS not.""" + setup_core( + config={ + CONF_OTA: {}, + CONF_API: {}, + CONF_MQTT: { + CONF_BROKER: "mqtt.local", + }, + CONF_MDNS: { + CONF_DISABLED: True, + }, + }, + address="test.local", + ) + + result = choose_upload_log_host( + default="OTA", + check_default=None, + purpose=Purpose.UPLOADING, + ) + assert result == ["MQTTIP"] + + +@pytest.mark.usefixtures("mock_serial_ports") +def test_choose_upload_log_host_ota_ip_all_options_logging() -> None: + """Test OTA device when both static IP, OTA, API and MQTT are configured and enabled but MDNS not.""" + setup_core( + config={ + CONF_OTA: {}, + CONF_API: {}, + CONF_MQTT: { + CONF_BROKER: "mqtt.local", + }, + CONF_MDNS: { + CONF_DISABLED: True, + }, + }, + address="192.168.1.100", + ) + + result = choose_upload_log_host( + default="OTA", + check_default=None, + purpose=Purpose.LOGGING, + ) + assert result == ["192.168.1.100", "MQTTIP", "MQTT"] + + +@pytest.mark.usefixtures("mock_serial_ports") +def test_choose_upload_log_host_ota_local_all_options_logging() -> None: + """Test OTA device when both static IP, OTA, API and MQTT are configured and enabled but MDNS not.""" + setup_core( + config={ + CONF_OTA: {}, + CONF_API: {}, + CONF_MQTT: { + CONF_BROKER: "mqtt.local", + }, + CONF_MDNS: { + CONF_DISABLED: True, + }, + }, + address="test.local", + ) + + result = choose_upload_log_host( + default="OTA", + check_default=None, + purpose=Purpose.LOGGING, + ) + assert result == ["MQTTIP", "MQTT"] + + +@pytest.mark.usefixtures("mock_no_mqtt_logging") +def test_choose_upload_log_host_no_address_with_ota_config() -> None: + """Test OTA device when OTA is configured but no address is set.""" + setup_core(config={CONF_OTA: {}}) + + with pytest.raises( + EsphomeError, match="All specified devices .* could not be resolved" + ): + choose_upload_log_host( + default="OTA", + check_default=None, + purpose=Purpose.UPLOADING, + ) + + +@dataclass +class MockArgs: + """Mock args for testing.""" + + file: str | None = None + upload_speed: int = 460800 + username: str | None = None + password: str | None = None + client_id: str | None = None + topic: str | None = None + configuration: str | None = None + name: str | None = None + dashboard: bool = False + + +def test_upload_program_serial_esp32( + mock_upload_using_esptool: Mock, + mock_get_port_type: Mock, + mock_check_permissions: Mock, +) -> None: + """Test upload_program with serial port for ESP32.""" + setup_core(platform=PLATFORM_ESP32) + mock_get_port_type.return_value = "SERIAL" + mock_upload_using_esptool.return_value = 0 + + config = {} + args = MockArgs() + devices = ["/dev/ttyUSB0"] + + exit_code, host = upload_program(config, args, devices) + + assert exit_code == 0 + assert host == "/dev/ttyUSB0" + mock_check_permissions.assert_called_once_with("/dev/ttyUSB0") + mock_upload_using_esptool.assert_called_once() + + +def test_upload_program_serial_esp8266_with_file( + mock_upload_using_esptool: Mock, + mock_get_port_type: Mock, + mock_check_permissions: Mock, +) -> None: + """Test upload_program with serial port for ESP8266 with custom file.""" + setup_core(platform=PLATFORM_ESP8266) + mock_get_port_type.return_value = "SERIAL" + mock_upload_using_esptool.return_value = 0 + + config = {} + args = MockArgs(file="firmware.bin") + devices = ["/dev/ttyUSB0"] + + exit_code, host = upload_program(config, args, devices) + + assert exit_code == 0 + assert host == "/dev/ttyUSB0" + mock_check_permissions.assert_called_once_with("/dev/ttyUSB0") + mock_upload_using_esptool.assert_called_once_with( + config, "/dev/ttyUSB0", "firmware.bin", 460800 + ) + + +def test_upload_using_esptool_path_conversion( + tmp_path: Path, + mock_run_external_command_main: Mock, + mock_get_idedata: Mock, +) -> None: + """Test upload_using_esptool properly converts Path objects to strings for esptool. + + This test ensures that img.path (Path object) is converted to string before + passing to esptool, preventing AttributeError. + """ + setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path, name="test") + + # Set up ESP32-specific data required by get_esp32_variant() + CORE.data[KEY_ESP32] = {KEY_VARIANT: VARIANT_ESP32} + + # Create mock IDEData with Path objects + mock_idedata = MagicMock(spec=platformio_api.IDEData) + mock_idedata.firmware_bin_path = tmp_path / "firmware.bin" + mock_idedata.extra_flash_images = [ + platformio_api.FlashImage(path=tmp_path / "bootloader.bin", offset="0x1000"), + platformio_api.FlashImage(path=tmp_path / "partitions.bin", offset="0x8000"), + ] + + mock_get_idedata.return_value = mock_idedata + + # Create the actual firmware files so they exist + (tmp_path / "firmware.bin").touch() + (tmp_path / "bootloader.bin").touch() + (tmp_path / "partitions.bin").touch() + + config = {CONF_ESPHOME: {"platformio_options": {}}} + + # Call upload_using_esptool without custom file argument + result = upload_using_esptool(config, "/dev/ttyUSB0", None, None) + + assert result == 0 + + # Verify that run_external_command was called + assert mock_run_external_command_main.call_count == 1 + + # Get the actual call arguments + call_args = mock_run_external_command_main.call_args[0] + + # The first argument should be esptool.main function, + # followed by the command arguments + assert len(call_args) > 1 + + # Find the indices of the flash image arguments + # They should come after "write-flash" and "-z" + cmd_list = list(call_args[1:]) # Skip the esptool.main function + + # Verify all paths are strings, not Path objects + # The firmware and flash images should be at specific positions + write_flash_idx = cmd_list.index("write-flash") + + # After write-flash we have: -z, --flash-size, detect, then offset/path pairs + # Check firmware at offset 0x10000 (ESP32) + firmware_offset_idx = write_flash_idx + 4 + assert cmd_list[firmware_offset_idx] == "0x10000" + firmware_path = cmd_list[firmware_offset_idx + 1] + assert isinstance(firmware_path, str) + assert firmware_path.endswith("firmware.bin") + + # Check bootloader + bootloader_offset_idx = firmware_offset_idx + 2 + assert cmd_list[bootloader_offset_idx] == "0x1000" + bootloader_path = cmd_list[bootloader_offset_idx + 1] + assert isinstance(bootloader_path, str) + assert bootloader_path.endswith("bootloader.bin") + + # Check partitions + partitions_offset_idx = bootloader_offset_idx + 2 + assert cmd_list[partitions_offset_idx] == "0x8000" + partitions_path = cmd_list[partitions_offset_idx + 1] + assert isinstance(partitions_path, str) + assert partitions_path.endswith("partitions.bin") + + +def test_upload_using_esptool_with_file_path( + tmp_path: Path, + mock_run_external_command_main: Mock, +) -> None: + """Test upload_using_esptool with a custom file that's a Path object.""" + setup_core(platform=PLATFORM_ESP8266, tmp_path=tmp_path, name="test") + + # Create a test firmware file + firmware_file = tmp_path / "custom_firmware.bin" + firmware_file.touch() + + config = {CONF_ESPHOME: {"platformio_options": {}}} + + # Call with a Path object as the file argument (though usually it's a string) + result = upload_using_esptool(config, "/dev/ttyUSB0", str(firmware_file), None) + + assert result == 0 + + # Verify that run_external_command was called + mock_run_external_command_main.assert_called_once() + + # Get the actual call arguments + call_args = mock_run_external_command_main.call_args[0] + cmd_list = list(call_args[1:]) # Skip the esptool.main function + + # Find the firmware path in the command + write_flash_idx = cmd_list.index("write-flash") + + # For custom file, it should be at offset 0x0 + firmware_offset_idx = write_flash_idx + 4 + assert cmd_list[firmware_offset_idx] == "0x0" + firmware_path = cmd_list[firmware_offset_idx + 1] + + # Verify it's a string, not a Path object + assert isinstance(firmware_path, str) + assert firmware_path.endswith("custom_firmware.bin") + + +@pytest.mark.parametrize( + "platform,device", + [ + (PLATFORM_RP2040, "/dev/ttyACM0"), + (PLATFORM_BK72XX, "/dev/ttyUSB0"), # LibreTiny platform + ], +) +def test_upload_program_serial_platformio_platforms( + mock_upload_using_platformio: Mock, + mock_get_port_type: Mock, + mock_check_permissions: Mock, + platform: str, + device: str, +) -> None: + """Test upload_program with serial port for platformio platforms (RP2040/LibreTiny).""" + setup_core(platform=platform) + mock_get_port_type.return_value = "SERIAL" + mock_upload_using_platformio.return_value = 0 + + config = {} + args = MockArgs() + devices = [device] + + exit_code, host = upload_program(config, args, devices) + + assert exit_code == 0 + assert host == device + mock_check_permissions.assert_called_once_with(device) + mock_upload_using_platformio.assert_called_once_with(config, device) + + +def test_upload_program_serial_upload_failed( + mock_upload_using_esptool: Mock, + mock_get_port_type: Mock, + mock_check_permissions: Mock, +) -> None: + """Test upload_program when serial upload fails.""" + setup_core(platform=PLATFORM_ESP32) + mock_get_port_type.return_value = "SERIAL" + mock_upload_using_esptool.return_value = 1 # Failed + + config = {} + args = MockArgs() + devices = ["/dev/ttyUSB0"] + + exit_code, host = upload_program(config, args, devices) + + assert exit_code == 1 + assert host is None + mock_check_permissions.assert_called_once_with("/dev/ttyUSB0") + mock_upload_using_esptool.assert_called_once() + + +def test_upload_program_ota_success( + mock_run_ota: Mock, + mock_get_port_type: Mock, + tmp_path: Path, +) -> None: + """Test upload_program with OTA.""" + setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path) + + mock_get_port_type.return_value = "NETWORK" + mock_run_ota.return_value = (0, "192.168.1.100") + + config = { + CONF_OTA: [ + { + CONF_PLATFORM: CONF_ESPHOME, + CONF_PORT: 3232, + CONF_PASSWORD: "secret", + } + ] + } + args = MockArgs() + devices = ["192.168.1.100"] + + exit_code, host = upload_program(config, args, devices) + + assert exit_code == 0 + assert host == "192.168.1.100" + expected_firmware = ( + tmp_path / ".esphome" / "build" / "test" / ".pioenvs" / "test" / "firmware.bin" + ) + mock_run_ota.assert_called_once_with( + ["192.168.1.100"], 3232, "secret", expected_firmware + ) + + +def test_upload_program_ota_with_file_arg( + mock_run_ota: Mock, + mock_get_port_type: Mock, + tmp_path: Path, +) -> None: + """Test upload_program with OTA and custom file.""" + setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path) + + mock_get_port_type.return_value = "NETWORK" + mock_run_ota.return_value = (0, "192.168.1.100") + + config = { + CONF_OTA: [ + { + CONF_PLATFORM: CONF_ESPHOME, + CONF_PORT: 3232, + } + ] + } + args = MockArgs(file="custom.bin") + devices = ["192.168.1.100"] + + exit_code, host = upload_program(config, args, devices) + + assert exit_code == 0 + assert host == "192.168.1.100" + mock_run_ota.assert_called_once_with( + ["192.168.1.100"], 3232, None, Path("custom.bin") + ) + + +def test_upload_program_ota_no_config( + mock_get_port_type: Mock, +) -> None: + """Test upload_program with OTA but no OTA config.""" + setup_core(platform=PLATFORM_ESP32) + mock_get_port_type.return_value = "NETWORK" + + config = {} # No OTA config + args = MockArgs() + devices = ["192.168.1.100"] + + with pytest.raises(EsphomeError, match="Cannot upload Over the Air"): + upload_program(config, args, devices) + + +def test_upload_program_ota_with_mqtt_resolution( + mock_mqtt_get_ip: Mock, + mock_is_ip_address: Mock, + mock_run_ota: Mock, + tmp_path: Path, +) -> None: + """Test upload_program with OTA using MQTT for address resolution.""" + setup_core(address="device.local", platform=PLATFORM_ESP32, tmp_path=tmp_path) + + mock_is_ip_address.return_value = False + mock_mqtt_get_ip.return_value = ["192.168.1.100"] + mock_run_ota.return_value = (0, "192.168.1.100") + + config = { + CONF_OTA: [ + { + CONF_PLATFORM: CONF_ESPHOME, + CONF_PORT: 3232, + } + ], + CONF_MQTT: { + CONF_BROKER: "mqtt.local", + }, + CONF_MDNS: { + CONF_DISABLED: True, + }, + } + args = MockArgs(username="user", password="pass", client_id="client") + devices = ["MQTT"] + + exit_code, host = upload_program(config, args, devices) + + assert exit_code == 0 + assert host == "192.168.1.100" + mock_mqtt_get_ip.assert_called_once_with(config, "user", "pass", "client") + expected_firmware = ( + tmp_path / ".esphome" / "build" / "test" / ".pioenvs" / "test" / "firmware.bin" + ) + mock_run_ota.assert_called_once_with( + ["192.168.1.100"], 3232, None, expected_firmware + ) + + +def test_upload_program_ota_with_mqtt_empty_broker( + mock_mqtt_get_ip: Mock, + mock_is_ip_address: Mock, + mock_run_ota: Mock, + tmp_path: Path, + caplog: CaptureFixture, +) -> None: + """Test upload_program with OTA when MQTT broker is empty (issue #11653).""" + setup_core(address="192.168.1.50", platform=PLATFORM_ESP32, tmp_path=tmp_path) + + mock_is_ip_address.return_value = True + mock_mqtt_get_ip.side_effect = EsphomeError( + "Cannot discover IP via MQTT as the broker is not configured" + ) + mock_run_ota.return_value = (0, "192.168.1.50") + + config = { + CONF_OTA: [ + { + CONF_PLATFORM: CONF_ESPHOME, + CONF_PORT: 3232, + } + ], + CONF_MQTT: { + CONF_BROKER: "", + }, + CONF_MDNS: { + CONF_DISABLED: True, + }, + } + args = MockArgs(username="user", password="pass", client_id="client") + devices = ["MQTTIP", "192.168.1.50"] + + exit_code, host = upload_program(config, args, devices) + + assert exit_code == 0 + assert host == "192.168.1.50" + # Verify MQTT was attempted but failed gracefully + mock_mqtt_get_ip.assert_called_once_with(config, "user", "pass", "client") + # Verify we fell back to the IP address + expected_firmware = ( + tmp_path / ".esphome" / "build" / "test" / ".pioenvs" / "test" / "firmware.bin" + ) + mock_run_ota.assert_called_once_with( + ["192.168.1.50"], 3232, None, expected_firmware + ) + # Verify warning was logged + assert "MQTT IP discovery failed" in caplog.text + + +@patch("esphome.__main__.importlib.import_module") +def test_upload_program_platform_specific_handler( + mock_import: Mock, + mock_get_port_type: Mock, +) -> None: + """Test upload_program with platform-specific upload handler.""" + setup_core(platform="custom_platform") + mock_get_port_type.return_value = "CUSTOM" + + mock_module = MagicMock() + mock_module.upload_program.return_value = True + mock_import.return_value = mock_module + + config = {} + args = MockArgs() + devices = ["custom_device"] + + exit_code, host = upload_program(config, args, devices) + + assert exit_code == 0 + assert host == "custom_device" + mock_import.assert_called_once_with("esphome.components.custom_platform") + mock_module.upload_program.assert_called_once_with(config, args, "custom_device") + + +def test_show_logs_serial( + mock_get_port_type: Mock, + mock_check_permissions: Mock, + mock_run_miniterm: Mock, +) -> None: + """Test show_logs with serial port.""" + setup_core(config={"logger": {}}, platform=PLATFORM_ESP32) + mock_get_port_type.return_value = "SERIAL" + mock_run_miniterm.return_value = 0 + + args = MockArgs() + devices = ["/dev/ttyUSB0"] + + result = show_logs(CORE.config, args, devices) + + assert result == 0 + mock_check_permissions.assert_called_once_with("/dev/ttyUSB0") + mock_run_miniterm.assert_called_once_with(CORE.config, "/dev/ttyUSB0", args) + + +def test_show_logs_no_logger() -> None: + """Test show_logs when logger is not configured.""" + setup_core(config={}, platform=PLATFORM_ESP32) # No logger config + args = MockArgs() + devices = ["/dev/ttyUSB0"] + + with pytest.raises(EsphomeError, match="Logger is not configured"): + show_logs(CORE.config, args, devices) + + +@patch("esphome.components.api.client.run_logs") +def test_show_logs_api( + mock_run_logs: Mock, +) -> None: + """Test show_logs with API.""" + setup_core( + config={ + "logger": {}, + CONF_API: {}, + CONF_MDNS: {CONF_DISABLED: False}, + }, + platform=PLATFORM_ESP32, + ) + mock_run_logs.return_value = 0 + + args = MockArgs() + devices = ["192.168.1.100", "192.168.1.101"] + + result = show_logs(CORE.config, args, devices) + + assert result == 0 + mock_run_logs.assert_called_once_with( + CORE.config, ["192.168.1.100", "192.168.1.101"] + ) + + +@patch("esphome.components.api.client.run_logs") +def test_show_logs_api_with_fqdn_mdns_disabled( + mock_run_logs: Mock, +) -> None: + """Test show_logs with API using FQDN when mDNS is disabled.""" + setup_core( + config={ + "logger": {}, + CONF_API: {}, + CONF_MDNS: {CONF_DISABLED: True}, + }, + platform=PLATFORM_ESP32, + ) + mock_run_logs.return_value = 0 + + args = MockArgs() + devices = ["device.example.com"] + + result = show_logs(CORE.config, args, devices) + + assert result == 0 + # Should use the FQDN directly, not try MQTT lookup + mock_run_logs.assert_called_once_with(CORE.config, ["device.example.com"]) + + +@patch("esphome.components.api.client.run_logs") +def test_show_logs_api_with_mqtt_fallback( + mock_run_logs: Mock, + mock_mqtt_get_ip: Mock, +) -> None: + """Test show_logs with API using MQTT for address resolution.""" + setup_core( + config={ + "logger": {}, + CONF_API: {}, + CONF_MDNS: {CONF_DISABLED: True}, + CONF_MQTT: {CONF_BROKER: "mqtt.local"}, + }, + platform=PLATFORM_ESP32, + ) + mock_run_logs.return_value = 0 + mock_mqtt_get_ip.return_value = ["192.168.1.200"] + + args = MockArgs(username="user", password="pass", client_id="client") + devices = ["MQTTIP"] + + result = show_logs(CORE.config, args, devices) + + assert result == 0 + mock_mqtt_get_ip.assert_called_once_with(CORE.config, "user", "pass", "client") + mock_run_logs.assert_called_once_with(CORE.config, ["192.168.1.200"]) + + +@patch("esphome.mqtt.show_logs") +def test_show_logs_mqtt( + mock_mqtt_show_logs: Mock, +) -> None: + """Test show_logs with MQTT.""" + setup_core( + config={ + "logger": {}, + "mqtt": {CONF_BROKER: "mqtt.local"}, + }, + platform=PLATFORM_ESP32, + ) + mock_mqtt_show_logs.return_value = 0 + + args = MockArgs( + topic="esphome/logs", + username="user", + password="pass", + client_id="client", + ) + devices = ["MQTT"] + + result = show_logs(CORE.config, args, devices) + + assert result == 0 + mock_mqtt_show_logs.assert_called_once_with( + CORE.config, "esphome/logs", "user", "pass", "client" + ) + + +@patch("esphome.mqtt.show_logs") +def test_show_logs_network_with_mqtt_only( + mock_mqtt_show_logs: Mock, +) -> None: + """Test show_logs with network port but only MQTT configured.""" + setup_core( + config={ + "logger": {}, + "mqtt": {CONF_BROKER: "mqtt.local"}, + # No API configured + }, + platform=PLATFORM_ESP32, + ) + mock_mqtt_show_logs.return_value = 0 + + args = MockArgs( + topic="esphome/logs", + username="user", + password="pass", + client_id="client", + ) + devices = ["192.168.1.100"] + + result = show_logs(CORE.config, args, devices) + + assert result == 0 + mock_mqtt_show_logs.assert_called_once_with( + CORE.config, "esphome/logs", "user", "pass", "client" + ) + + +def test_show_logs_no_method_configured() -> None: + """Test show_logs when no remote logging method is configured.""" + setup_core( + config={ + "logger": {}, + # No API or MQTT configured + }, + platform=PLATFORM_ESP32, + ) + + args = MockArgs() + devices = ["192.168.1.100"] + + with pytest.raises( + EsphomeError, match="No remote or local logging method configured" + ): + show_logs(CORE.config, args, devices) + + +@patch("esphome.__main__.importlib.import_module") +def test_show_logs_platform_specific_handler( + mock_import: Mock, +) -> None: + """Test show_logs with platform-specific logs handler.""" + setup_core(platform="custom_platform", config={"logger": {}}) + + mock_module = MagicMock() + mock_module.show_logs.return_value = True + mock_import.return_value = mock_module + + config = {"logger": {}} + args = MockArgs() + devices = ["custom_device"] + + result = show_logs(config, args, devices) + + assert result == 0 + mock_import.assert_called_once_with("esphome.components.custom_platform") + mock_module.show_logs.assert_called_once_with(config, args, devices) + + +def test_has_mqtt_logging_no_log_topic() -> None: + """Test has_mqtt_logging returns True when CONF_LOG_TOPIC is not in mqtt_config.""" + + # Setup MQTT config without CONF_LOG_TOPIC (defaults to enabled - this is the missing test case) + setup_core(config={CONF_MQTT: {CONF_BROKER: "mqtt.local"}}) + assert has_mqtt_logging() is True + + # Setup MQTT config with CONF_LOG_TOPIC set to None (explicitly disabled) + setup_core(config={CONF_MQTT: {CONF_BROKER: "mqtt.local", CONF_LOG_TOPIC: None}}) + assert has_mqtt_logging() is False + + # Setup MQTT config with CONF_LOG_TOPIC set with topic and level (explicitly enabled) + setup_core( + config={ + CONF_MQTT: { + CONF_BROKER: "mqtt.local", + CONF_LOG_TOPIC: {CONF_TOPIC: "esphome/logs", CONF_LEVEL: "DEBUG"}, + } + } + ) + assert has_mqtt_logging() is True + + # Setup MQTT config with CONF_LOG_TOPIC set but level is NONE (disabled) + setup_core( + config={ + CONF_MQTT: { + CONF_BROKER: "mqtt.local", + CONF_LOG_TOPIC: {CONF_TOPIC: "esphome/logs", CONF_LEVEL: "NONE"}, + } + } + ) + assert has_mqtt_logging() is False + + # Setup without MQTT config at all + setup_core(config={}) + assert has_mqtt_logging() is False + + # Setup MQTT config with CONF_LOG_TOPIC but no CONF_LEVEL (regression test for #10771) + # This simulates the default configuration created by validate_config in the MQTT component + setup_core( + config={ + CONF_MQTT: { + CONF_BROKER: "mqtt.local", + CONF_LOG_TOPIC: {CONF_TOPIC: "esphome/debug"}, + } + } + ) + assert has_mqtt_logging() is True + + +def test_has_mqtt() -> None: + """Test has_mqtt function.""" + + # Test with MQTT configured + setup_core(config={CONF_MQTT: {CONF_BROKER: "mqtt.local"}}) + assert has_mqtt() is True + + # Test without MQTT configured + setup_core(config={}) + assert has_mqtt() is False + + # Test with other components but no MQTT + setup_core(config={CONF_API: {}, CONF_OTA: {}}) + assert has_mqtt() is False + + +def test_get_port_type() -> None: + """Test get_port_type function.""" + + assert get_port_type("/dev/ttyUSB0") == "SERIAL" + assert get_port_type("/dev/ttyACM0") == "SERIAL" + assert get_port_type("COM1") == "SERIAL" + assert get_port_type("COM10") == "SERIAL" + + assert get_port_type("MQTT") == "MQTT" + assert get_port_type("MQTTIP") == "MQTTIP" + + assert get_port_type("192.168.1.100") == "NETWORK" + assert get_port_type("esphome-device.local") == "NETWORK" + assert get_port_type("10.0.0.1") == "NETWORK" + + +def test_has_mqtt_ip_lookup() -> None: + """Test has_mqtt_ip_lookup function.""" + + CONF_DISCOVER_IP = "discover_ip" + + setup_core(config={}) + assert has_mqtt_ip_lookup() is False + + setup_core(config={CONF_MQTT: {CONF_BROKER: "mqtt.local"}}) + assert has_mqtt_ip_lookup() is True + + setup_core(config={CONF_MQTT: {CONF_BROKER: "mqtt.local", CONF_DISCOVER_IP: True}}) + assert has_mqtt_ip_lookup() is True + + setup_core(config={CONF_MQTT: {CONF_BROKER: "mqtt.local", CONF_DISCOVER_IP: False}}) + assert has_mqtt_ip_lookup() is False + + +def test_has_non_ip_address() -> None: + """Test has_non_ip_address function.""" + + setup_core(address=None) + assert has_non_ip_address() is False + + setup_core(address="192.168.1.100") + assert has_non_ip_address() is False + + setup_core(address="10.0.0.1") + assert has_non_ip_address() is False + + setup_core(address="esphome-device.local") + assert has_non_ip_address() is True + + setup_core(address="my-device") + assert has_non_ip_address() is True + + +def test_has_ip_address() -> None: + """Test has_ip_address function.""" + + setup_core(address=None) + assert has_ip_address() is False + + setup_core(address="192.168.1.100") + assert has_ip_address() is True + + setup_core(address="10.0.0.1") + assert has_ip_address() is True + + setup_core(address="esphome-device.local") + assert has_ip_address() is False + + setup_core(address="my-device") + assert has_ip_address() is False + + +def test_mqtt_get_ip() -> None: + """Test mqtt_get_ip function.""" + config = {CONF_MQTT: {CONF_BROKER: "mqtt.local"}} + + with patch("esphome.mqtt.get_esphome_device_ip") as mock_get_ip: + mock_get_ip.return_value = ["192.168.1.100", "192.168.1.101"] + + result = mqtt_get_ip(config, "user", "pass", "client-id") + + assert result == ["192.168.1.100", "192.168.1.101"] + mock_get_ip.assert_called_once_with(config, "user", "pass", "client-id") + + +def test_has_resolvable_address() -> None: + """Test has_resolvable_address function.""" + + # Test with mDNS enabled and .local hostname address + setup_core(config={}, address="esphome-device.local") + assert has_resolvable_address() is True + + # Test with mDNS disabled and .local hostname address (still resolvable via DNS) + setup_core( + config={CONF_MDNS: {CONF_DISABLED: True}}, address="esphome-device.local" + ) + assert has_resolvable_address() is False + + # Test with mDNS disabled and regular DNS hostname (resolvable) + setup_core(config={CONF_MDNS: {CONF_DISABLED: True}}, address="device.example.com") + assert has_resolvable_address() is True + + # Test with IP address (always resolvable, mDNS doesn't matter) + setup_core(config={}, address="192.168.1.100") + assert has_resolvable_address() is True + + # Test with IP address and mDNS disabled (still resolvable) + setup_core(config={CONF_MDNS: {CONF_DISABLED: True}}, address="192.168.1.100") + assert has_resolvable_address() is True + + # Test with no address + setup_core(config={}, address=None) + assert has_resolvable_address() is False + + # Test with no address and mDNS disabled + setup_core(config={CONF_MDNS: {CONF_DISABLED: True}}, address=None) + assert has_resolvable_address() is False + + +def test_command_wizard(tmp_path: Path) -> None: + """Test command_wizard function.""" + config_file = tmp_path / "test.yaml" + + # Mock wizard.wizard to avoid interactive prompts + with patch("esphome.wizard.wizard") as mock_wizard: + mock_wizard.return_value = 0 + + args = MockArgs(configuration=str(config_file)) + result = command_wizard(args) + + assert result == 0 + mock_wizard.assert_called_once_with(config_file) + + +def test_command_rename_invalid_characters( + tmp_path: Path, capfd: CaptureFixture[str] +) -> None: + """Test command_rename with invalid characters in name.""" + setup_core(tmp_path=tmp_path) + + # Test with invalid character (space) + args = MockArgs(name="invalid name") + result = command_rename(args, {}) + + assert result == 1 + captured = capfd.readouterr() + assert "invalid character" in captured.out.lower() + + +def test_command_rename_complex_yaml( + tmp_path: Path, capfd: CaptureFixture[str] +) -> None: + """Test command_rename with complex YAML that cannot be renamed.""" + config_file = tmp_path / "test.yaml" + config_file.write_text("# Complex YAML without esphome section\nsome_key: value\n") + setup_core(tmp_path=tmp_path) + CORE.config_path = config_file + + args = MockArgs(name="newname") + result = command_rename(args, {}) + + assert result == 1 + captured = capfd.readouterr() + assert "complex yaml" in captured.out.lower() + + +def test_command_rename_success( + tmp_path: Path, + capfd: CaptureFixture[str], + mock_run_external_process: Mock, +) -> None: + """Test successful rename of a simple configuration.""" + config_file = tmp_path / "oldname.yaml" + config_file.write_text(""" +esphome: + name: oldname + +esp32: + board: nodemcu-32s + +wifi: + ssid: "test" + password: "test1234" +""") + setup_core(tmp_path=tmp_path) + CORE.config_path = config_file + + # Set up CORE.config to avoid ValueError when accessing CORE.address + CORE.config = {CONF_ESPHOME: {CONF_NAME: "oldname"}} + + args = MockArgs(name="newname", dashboard=False) + + # Simulate successful validation and upload + mock_run_external_process.return_value = 0 + + result = command_rename(args, {}) + + assert result == 0 + + # Verify new file was created + new_file = tmp_path / "newname.yaml" + assert new_file.exists() + + # Verify old file was removed + assert not config_file.exists() + + # Verify content was updated + content = new_file.read_text() + assert ( + 'name: "newname"' in content + or "name: 'newname'" in content + or "name: newname" in content + ) + + captured = capfd.readouterr() + assert "SUCCESS" in captured.out + + +def test_command_rename_with_substitutions( + tmp_path: Path, + mock_run_external_process: Mock, +) -> None: + """Test rename with substitutions in YAML.""" + config_file = tmp_path / "oldname.yaml" + config_file.write_text(""" +substitutions: + device_name: oldname + +esphome: + name: ${device_name} + +esp32: + board: nodemcu-32s +""") + setup_core(tmp_path=tmp_path) + CORE.config_path = config_file + + # Set up CORE.config to avoid ValueError when accessing CORE.address + CORE.config = { + CONF_ESPHOME: {CONF_NAME: "oldname"}, + CONF_SUBSTITUTIONS: {"device_name": "oldname"}, + } + + args = MockArgs(name="newname", dashboard=False) + + mock_run_external_process.return_value = 0 + + result = command_rename(args, {}) + + assert result == 0 + + # Verify substitution was updated + new_file = tmp_path / "newname.yaml" + content = new_file.read_text() + assert 'device_name: "newname"' in content + + +def test_command_rename_validation_failure( + tmp_path: Path, + capfd: CaptureFixture[str], + mock_run_external_process: Mock, +) -> None: + """Test rename when validation fails.""" + config_file = tmp_path / "oldname.yaml" + config_file.write_text(""" +esphome: + name: oldname + +esp32: + board: nodemcu-32s +""") + setup_core(tmp_path=tmp_path) + CORE.config_path = config_file + + args = MockArgs(name="newname", dashboard=False) + + # First call for validation fails + mock_run_external_process.return_value = 1 + + result = command_rename(args, {}) + + assert result == 1 + + # Verify new file was created but then removed due to failure + new_file = tmp_path / "newname.yaml" + assert not new_file.exists() + + # Verify old file still exists (not removed on failure) + assert config_file.exists() + + captured = capfd.readouterr() + assert "Rename failed" in captured.out + + +def test_command_update_all_path_string_conversion( + tmp_path: Path, + mock_run_external_process: Mock, + capfd: CaptureFixture[str], +) -> None: + """Test that command_update_all properly converts Path objects to strings in output.""" + yaml1 = tmp_path / "device1.yaml" + yaml1.write_text(""" +esphome: + name: device1 + +esp32: + board: nodemcu-32s +""") + + yaml2 = tmp_path / "device2.yaml" + yaml2.write_text(""" +esphome: + name: device2 + +esp8266: + board: nodemcuv2 +""") + + setup_core(tmp_path=tmp_path) + mock_run_external_process.return_value = 0 + + assert command_update_all(MockArgs(configuration=[str(tmp_path)])) == 0 + + captured = capfd.readouterr() + clean_output = strip_ansi_codes(captured.out) + + # Check that Path objects were properly converted to strings + # The output should contain file paths without causing TypeError + assert "device1.yaml" in clean_output + assert "device2.yaml" in clean_output + assert "SUCCESS" in clean_output + assert "SUMMARY" in clean_output + + # Verify run_external_process was called for each file + assert mock_run_external_process.call_count == 2 + + +def test_command_update_all_with_failures( + tmp_path: Path, + mock_run_external_process: Mock, + capfd: CaptureFixture[str], +) -> None: + """Test command_update_all handles mixed success/failure cases properly.""" + yaml1 = tmp_path / "success_device.yaml" + yaml1.write_text(""" +esphome: + name: success_device + +esp32: + board: nodemcu-32s +""") + + yaml2 = tmp_path / "failed_device.yaml" + yaml2.write_text(""" +esphome: + name: failed_device + +esp8266: + board: nodemcuv2 +""") + + setup_core(tmp_path=tmp_path) + + # Mock mixed results - first succeeds, second fails + mock_run_external_process.side_effect = [0, 1] + + # Should return 1 (failure) since one device failed + assert command_update_all(MockArgs(configuration=[str(tmp_path)])) == 1 + + captured = capfd.readouterr() + clean_output = strip_ansi_codes(captured.out) + + # Check that both success and failure are properly displayed + assert "SUCCESS" in clean_output + assert "ERROR" in clean_output or "FAILED" in clean_output + assert "SUMMARY" in clean_output + + # Files are processed in alphabetical order, so we need to check which one succeeded/failed + # The mock_run_external_process.side_effect = [0, 1] applies to files in alphabetical order + # So "failed_device.yaml" gets 0 (success) and "success_device.yaml" gets 1 (failure) + assert "failed_device.yaml: SUCCESS" in clean_output + assert "success_device.yaml: FAILED" in clean_output + + +def test_command_update_all_empty_directory( + tmp_path: Path, + mock_run_external_process: Mock, + capfd: CaptureFixture[str], +) -> None: + """Test command_update_all with an empty directory (no YAML files).""" + setup_core(tmp_path=tmp_path) + + assert command_update_all(MockArgs(configuration=[str(tmp_path)])) == 0 + mock_run_external_process.assert_not_called() + + captured = capfd.readouterr() + clean_output = strip_ansi_codes(captured.out) + + assert "SUMMARY" in clean_output + + +def test_command_update_all_single_file( + tmp_path: Path, + mock_run_external_process: Mock, + capfd: CaptureFixture[str], +) -> None: + """Test command_update_all with a single YAML file specified.""" + yaml_file = tmp_path / "single_device.yaml" + yaml_file.write_text(""" +esphome: + name: single_device + +esp32: + board: nodemcu-32s +""") + + setup_core(tmp_path=tmp_path) + mock_run_external_process.return_value = 0 + + assert command_update_all(MockArgs(configuration=[str(yaml_file)])) == 0 + + captured = capfd.readouterr() + clean_output = strip_ansi_codes(captured.out) + + assert "single_device.yaml" in clean_output + assert "SUCCESS" in clean_output + mock_run_external_process.assert_called_once() + + +def test_command_update_all_path_formatting_in_color_calls( + tmp_path: Path, + mock_run_external_process: Mock, + capfd: CaptureFixture[str], +) -> None: + """Test that Path objects are properly converted when passed to color() function.""" + yaml_file = tmp_path / "test-device_123.yaml" + yaml_file.write_text(""" +esphome: + name: test-device_123 + +esp32: + board: nodemcu-32s +""") + + setup_core(tmp_path=tmp_path) + mock_run_external_process.return_value = 0 + + assert command_update_all(MockArgs(configuration=[str(tmp_path)])) == 0 + + captured = capfd.readouterr() + clean_output = strip_ansi_codes(captured.out) + + assert "test-device_123.yaml" in clean_output + assert "Updating" in clean_output + assert "SUCCESS" in clean_output + assert "SUMMARY" in clean_output + + # Should not have any Python error messages + assert "TypeError" not in clean_output + assert "can only concatenate str" not in clean_output + + +def test_command_clean_all_success( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test command_clean_all when writer.clean_all() succeeds.""" + args = MockArgs(configuration=["/path/to/config1", "/path/to/config2"]) + + # Set logger level to capture INFO messages + with ( + caplog.at_level(logging.INFO), + patch("esphome.writer.clean_all") as mock_clean_all, + ): + result = command_clean_all(args) + + assert result == 0 + mock_clean_all.assert_called_once_with(["/path/to/config1", "/path/to/config2"]) + + # Check that success message was logged + assert "Done!" in caplog.text + + +def test_command_clean_all_oserror( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test command_clean_all when writer.clean_all() raises OSError.""" + args = MockArgs(configuration=["/path/to/config1"]) + + # Create a mock OSError with a specific message + mock_error = OSError("Permission denied: cannot delete directory") + + # Set logger level to capture ERROR and INFO messages + with ( + caplog.at_level(logging.INFO), + patch("esphome.writer.clean_all", side_effect=mock_error) as mock_clean_all, + ): + result = command_clean_all(args) + + assert result == 1 + mock_clean_all.assert_called_once_with(["/path/to/config1"]) + + # Check that error message was logged + assert ( + "Error cleaning all files: Permission denied: cannot delete directory" + in caplog.text + ) + # Should not have success message + assert "Done!" not in caplog.text + + +def test_command_clean_all_oserror_no_message( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test command_clean_all when writer.clean_all() raises OSError without message.""" + args = MockArgs(configuration=["/path/to/config1"]) + + # Create a mock OSError without a message + mock_error = OSError() + + # Set logger level to capture ERROR and INFO messages + with ( + caplog.at_level(logging.INFO), + patch("esphome.writer.clean_all", side_effect=mock_error) as mock_clean_all, + ): + result = command_clean_all(args) + + assert result == 1 + mock_clean_all.assert_called_once_with(["/path/to/config1"]) + + # Check that error message was logged (should show empty string for OSError without message) + assert "Error cleaning all files:" in caplog.text + # Should not have success message + assert "Done!" not in caplog.text + + +def test_command_clean_all_args_used() -> None: + """Test that command_clean_all uses args.configuration parameter.""" + # Test with different configuration paths + args1 = MockArgs(configuration=["/path/to/config1"]) + args2 = MockArgs(configuration=["/path/to/config2", "/path/to/config3"]) + + with patch("esphome.writer.clean_all") as mock_clean_all: + result1 = command_clean_all(args1) + result2 = command_clean_all(args2) + + assert result1 == 0 + assert result2 == 0 + assert mock_clean_all.call_count == 2 + + # Verify the correct configuration paths were passed + mock_clean_all.assert_any_call(["/path/to/config1"]) + mock_clean_all.assert_any_call(["/path/to/config2", "/path/to/config3"]) + + +def test_upload_program_ota_static_ip_with_mqttip( + mock_mqtt_get_ip: Mock, + mock_run_ota: Mock, + tmp_path: Path, +) -> None: + """Test upload_program with static IP and MQTTIP (issue #11260). + + This tests the scenario where a device has manual_ip (static IP) configured + and MQTT is also configured. The devices list contains both the static IP + and "MQTTIP" magic string. This previously failed because only the first + device was checked for MQTT resolution. + """ + setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path) + + mock_mqtt_get_ip.return_value = ["192.168.2.50"] # Different subnet + mock_run_ota.return_value = (0, "192.168.1.100") + + config = { + CONF_OTA: [ + { + CONF_PLATFORM: CONF_ESPHOME, + CONF_PORT: 3232, + } + ], + CONF_MQTT: { + CONF_BROKER: "mqtt.local", + }, + } + args = MockArgs(username="user", password="pass", client_id="client") + # Simulates choose_upload_log_host returning static IP + MQTTIP + devices = ["192.168.1.100", "MQTTIP"] + + exit_code, host = upload_program(config, args, devices) + + assert exit_code == 0 + assert host == "192.168.1.100" + + # Verify MQTT was resolved + mock_mqtt_get_ip.assert_called_once_with(config, "user", "pass", "client") + + # Verify espota2.run_ota was called with both IPs + expected_firmware = ( + tmp_path / ".esphome" / "build" / "test" / ".pioenvs" / "test" / "firmware.bin" + ) + mock_run_ota.assert_called_once_with( + ["192.168.1.100", "192.168.2.50"], 3232, None, expected_firmware + ) + + +def test_upload_program_ota_multiple_mqttip_resolves_once( + mock_mqtt_get_ip: Mock, + mock_run_ota: Mock, + tmp_path: Path, +) -> None: + """Test that MQTT resolution only happens once even with multiple MQTT magic strings.""" + setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path) + + mock_mqtt_get_ip.return_value = ["192.168.2.50", "192.168.2.51"] + mock_run_ota.return_value = (0, "192.168.2.50") + + config = { + CONF_OTA: [ + { + CONF_PLATFORM: CONF_ESPHOME, + CONF_PORT: 3232, + } + ], + CONF_MQTT: { + CONF_BROKER: "mqtt.local", + }, + } + args = MockArgs(username="user", password="pass", client_id="client") + # Multiple MQTT magic strings in the list + devices = ["MQTTIP", "MQTT", "192.168.1.100"] + + exit_code, host = upload_program(config, args, devices) + + assert exit_code == 0 + assert host == "192.168.2.50" + + # Verify MQTT was only resolved once despite multiple MQTT magic strings + mock_mqtt_get_ip.assert_called_once_with(config, "user", "pass", "client") + + # Verify espota2.run_ota was called with all unique IPs + expected_firmware = ( + tmp_path / ".esphome" / "build" / "test" / ".pioenvs" / "test" / "firmware.bin" + ) + mock_run_ota.assert_called_once_with( + ["192.168.2.50", "192.168.2.51", "192.168.1.100"], 3232, None, expected_firmware + ) + + +def test_upload_program_ota_mqttip_deduplication( + mock_mqtt_get_ip: Mock, + mock_run_ota: Mock, + tmp_path: Path, +) -> None: + """Test that duplicate IPs are filtered when MQTT returns same IP as static IP.""" + setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path) + + # MQTT returns the same IP as the static IP + mock_mqtt_get_ip.return_value = ["192.168.1.100"] + mock_run_ota.return_value = (0, "192.168.1.100") + + config = { + CONF_OTA: [ + { + CONF_PLATFORM: CONF_ESPHOME, + CONF_PORT: 3232, + } + ], + CONF_MQTT: { + CONF_BROKER: "mqtt.local", + }, + } + args = MockArgs(username="user", password="pass", client_id="client") + devices = ["192.168.1.100", "MQTTIP"] + + exit_code, host = upload_program(config, args, devices) + + assert exit_code == 0 + assert host == "192.168.1.100" + + # Verify MQTT was resolved + mock_mqtt_get_ip.assert_called_once_with(config, "user", "pass", "client") + + # Verify espota2.run_ota was called with deduplicated IPs (only one instance of 192.168.1.100) + # Note: Current implementation doesn't dedupe, so we'll get the IP twice + # This test documents current behavior - deduplication could be future enhancement + mock_run_ota.assert_called_once() + call_args = mock_run_ota.call_args[0] + # Should contain both the original IP and MQTT-resolved IP (even if duplicate) + assert "192.168.1.100" in call_args[0] + + +@patch("esphome.components.api.client.run_logs") +def test_show_logs_api_static_ip_with_mqttip( + mock_run_logs: Mock, + mock_mqtt_get_ip: Mock, +) -> None: + """Test show_logs with static IP and MQTTIP (issue #11260). + + This tests the scenario where a device has manual_ip (static IP) configured + and MQTT is also configured. The devices list contains both the static IP + and "MQTTIP" magic string. + """ + setup_core( + config={ + "logger": {}, + CONF_API: {}, + CONF_MQTT: {CONF_BROKER: "mqtt.local"}, + }, + platform=PLATFORM_ESP32, + ) + mock_run_logs.return_value = 0 + mock_mqtt_get_ip.return_value = ["192.168.2.50"] + + args = MockArgs(username="user", password="pass", client_id="client") + # Simulates choose_upload_log_host returning static IP + MQTTIP + devices = ["192.168.1.100", "MQTTIP"] + + result = show_logs(CORE.config, args, devices) + + assert result == 0 + + # Verify MQTT was resolved + mock_mqtt_get_ip.assert_called_once_with(CORE.config, "user", "pass", "client") + + # Verify run_logs was called with both IPs + mock_run_logs.assert_called_once_with( + CORE.config, ["192.168.1.100", "192.168.2.50"] + ) + + +@patch("esphome.components.api.client.run_logs") +def test_show_logs_api_multiple_mqttip_resolves_once( + mock_run_logs: Mock, + mock_mqtt_get_ip: Mock, +) -> None: + """Test that MQTT resolution only happens once for show_logs with multiple MQTT magic strings.""" + setup_core( + config={ + "logger": {}, + CONF_API: {}, + CONF_MQTT: {CONF_BROKER: "mqtt.local"}, + }, + platform=PLATFORM_ESP32, + ) + mock_run_logs.return_value = 0 + mock_mqtt_get_ip.return_value = ["192.168.2.50", "192.168.2.51"] + + args = MockArgs(username="user", password="pass", client_id="client") + # Multiple MQTT magic strings in the list + devices = ["MQTTIP", "192.168.1.100", "MQTT"] + + result = show_logs(CORE.config, args, devices) + + assert result == 0 + + # Verify MQTT was only resolved once despite multiple MQTT magic strings + mock_mqtt_get_ip.assert_called_once_with(CORE.config, "user", "pass", "client") + + # Verify run_logs was called with all unique IPs (MQTT strings replaced with IPs) + # Note: "MQTT" is a different magic string from "MQTTIP", but both trigger MQTT resolution + # The _resolve_network_devices helper filters out both after first resolution + mock_run_logs.assert_called_once_with( + CORE.config, ["192.168.2.50", "192.168.2.51", "192.168.1.100"] + ) + + +def test_upload_program_ota_mqtt_timeout_fallback( + mock_mqtt_get_ip: Mock, + mock_run_ota: Mock, + tmp_path: Path, +) -> None: + """Test upload_program falls back to other devices when MQTT times out.""" + setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path) + + # MQTT times out + mock_mqtt_get_ip.side_effect = EsphomeError("Failed to find IP via MQTT") + mock_run_ota.return_value = (0, "192.168.1.100") + + config = { + CONF_OTA: [ + { + CONF_PLATFORM: CONF_ESPHOME, + CONF_PORT: 3232, + } + ], + CONF_MQTT: { + CONF_BROKER: "mqtt.local", + }, + } + args = MockArgs(username="user", password="pass", client_id="client") + # Static IP first, MQTTIP second + devices = ["192.168.1.100", "MQTTIP"] + + exit_code, host = upload_program(config, args, devices) + + # Should succeed using the static IP even though MQTT failed + assert exit_code == 0 + assert host == "192.168.1.100" + + # Verify MQTT was attempted + mock_mqtt_get_ip.assert_called_once_with(config, "user", "pass", "client") + + # Verify espota2.run_ota was called with only the static IP (MQTT failed) + expected_firmware = ( + tmp_path / ".esphome" / "build" / "test" / ".pioenvs" / "test" / "firmware.bin" + ) + mock_run_ota.assert_called_once_with( + ["192.168.1.100"], 3232, None, expected_firmware + ) + + +@patch("esphome.components.api.client.run_logs") +def test_show_logs_api_mqtt_timeout_fallback( + mock_run_logs: Mock, + mock_mqtt_get_ip: Mock, +) -> None: + """Test show_logs falls back to other devices when MQTT times out.""" + setup_core( + config={ + "logger": {}, + CONF_API: {}, + CONF_MQTT: {CONF_BROKER: "mqtt.local"}, + }, + platform=PLATFORM_ESP32, + ) + mock_run_logs.return_value = 0 + # MQTT times out + mock_mqtt_get_ip.side_effect = EsphomeError("Failed to find IP via MQTT") + + args = MockArgs(username="user", password="pass", client_id="client") + # Static IP first, MQTTIP second + devices = ["192.168.1.100", "MQTTIP"] + + result = show_logs(CORE.config, args, devices) + + # Should succeed using the static IP even though MQTT failed + assert result == 0 + + # Verify MQTT was attempted + mock_mqtt_get_ip.assert_called_once_with(CORE.config, "user", "pass", "client") + + # Verify run_logs was called with only the static IP (MQTT failed) + mock_run_logs.assert_called_once_with(CORE.config, ["192.168.1.100"]) + + +def test_detect_external_components_no_external( + mock_get_esphome_components: Mock, +) -> None: + """Test detect_external_components with no external components.""" + config = { + CONF_ESPHOME: {CONF_NAME: "test_device"}, + "logger": {}, + "api": {}, + } + + result = detect_external_components(config) + + assert result == set() + mock_get_esphome_components.assert_called_once() + + +def test_detect_external_components_with_external( + mock_get_esphome_components: Mock, +) -> None: + """Test detect_external_components detects external components.""" + config = { + CONF_ESPHOME: {CONF_NAME: "test_device"}, + "logger": {}, # Built-in + "api": {}, # Built-in + "my_custom_sensor": {}, # External + "another_custom": {}, # External + "external_components": [], # Special key, not a component + "substitutions": {}, # Special key, not a component + } + + result = detect_external_components(config) + + assert result == {"my_custom_sensor", "another_custom"} + mock_get_esphome_components.assert_called_once() + + +def test_detect_external_components_filters_special_keys( + mock_get_esphome_components: Mock, +) -> None: + """Test detect_external_components filters out special config keys.""" + config = { + CONF_ESPHOME: {CONF_NAME: "test_device"}, + "substitutions": {"key": "value"}, + "packages": {}, + "globals": [], + "external_components": [], + "<<": {}, # YAML merge key + } + + result = detect_external_components(config) + + assert result == set() + mock_get_esphome_components.assert_called_once() + + +def test_command_analyze_memory_success( + tmp_path: Path, + capfd: CaptureFixture[str], + mock_write_cpp: Mock, + mock_compile_program: Mock, + mock_get_idedata: Mock, + mock_get_esphome_components: Mock, + mock_memory_analyzer_cli: Mock, +) -> None: + """Test command_analyze_memory with successful compilation and analysis.""" + setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path, name="test_device") + + # Create firmware.elf file + firmware_path = ( + tmp_path / ".esphome" / "build" / "test_device" / ".pioenvs" / "test_device" + ) + firmware_path.mkdir(parents=True, exist_ok=True) + firmware_elf = firmware_path / "firmware.elf" + firmware_elf.write_text("mock elf file") + + # Mock idedata + mock_idedata_obj = MagicMock(spec=platformio_api.IDEData) + mock_idedata_obj.firmware_elf_path = str(firmware_elf) + mock_idedata_obj.objdump_path = "/path/to/objdump" + mock_idedata_obj.readelf_path = "/path/to/readelf" + mock_get_idedata.return_value = mock_idedata_obj + + config = { + CONF_ESPHOME: {CONF_NAME: "test_device"}, + "logger": {}, + } + + args = MockArgs() + + result = command_analyze_memory(args, config) + + assert result == 0 + + # Verify compilation was done + mock_write_cpp.assert_called_once_with(config) + mock_compile_program.assert_called_once_with(args, config) + + # Verify analyzer was created with correct parameters + mock_memory_analyzer_cli.assert_called_once_with( + str(firmware_elf), + "/path/to/objdump", + "/path/to/readelf", + set(), # No external components + ) + + # Verify analysis was run + mock_analyzer = mock_memory_analyzer_cli.return_value + mock_analyzer.analyze.assert_called_once() + mock_analyzer.generate_report.assert_called_once() + + # Verify report was printed + captured = capfd.readouterr() + assert "Mock Memory Report" in captured.out + + +def test_command_analyze_memory_with_external_components( + tmp_path: Path, + mock_write_cpp: Mock, + mock_compile_program: Mock, + mock_get_idedata: Mock, + mock_get_esphome_components: Mock, + mock_memory_analyzer_cli: Mock, +) -> None: + """Test command_analyze_memory detects external components.""" + setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path, name="test_device") + + # Create firmware.elf file + firmware_path = ( + tmp_path / ".esphome" / "build" / "test_device" / ".pioenvs" / "test_device" + ) + firmware_path.mkdir(parents=True, exist_ok=True) + firmware_elf = firmware_path / "firmware.elf" + firmware_elf.write_text("mock elf file") + + # Mock idedata + mock_idedata_obj = MagicMock(spec=platformio_api.IDEData) + mock_idedata_obj.firmware_elf_path = str(firmware_elf) + mock_idedata_obj.objdump_path = "/path/to/objdump" + mock_idedata_obj.readelf_path = "/path/to/readelf" + mock_get_idedata.return_value = mock_idedata_obj + + config = { + CONF_ESPHOME: {CONF_NAME: "test_device"}, + "logger": {}, + "my_custom_component": {"param": "value"}, # External component + "external_components": [{"source": "github://user/repo"}], # Not a component + } + + args = MockArgs() + + result = command_analyze_memory(args, config) + + assert result == 0 + + # Verify analyzer was created with external components detected + mock_memory_analyzer_cli.assert_called_once_with( + str(firmware_elf), + "/path/to/objdump", + "/path/to/readelf", + {"my_custom_component"}, # External component detected + ) + + +def test_command_analyze_memory_write_cpp_fails( + tmp_path: Path, + mock_write_cpp: Mock, +) -> None: + """Test command_analyze_memory when write_cpp fails.""" + setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path, name="test_device") + + config = {CONF_ESPHOME: {CONF_NAME: "test_device"}} + args = MockArgs() + + mock_write_cpp.return_value = 1 # Failure + + result = command_analyze_memory(args, config) + + assert result == 1 + mock_write_cpp.assert_called_once_with(config) + + +def test_command_analyze_memory_compile_fails( + tmp_path: Path, + mock_write_cpp: Mock, + mock_compile_program: Mock, +) -> None: + """Test command_analyze_memory when compilation fails.""" + setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path, name="test_device") + + config = {CONF_ESPHOME: {CONF_NAME: "test_device"}} + args = MockArgs() + + mock_compile_program.return_value = 1 # Compilation failed + + result = command_analyze_memory(args, config) + + assert result == 1 + mock_write_cpp.assert_called_once_with(config) + mock_compile_program.assert_called_once_with(args, config) + + +def test_command_analyze_memory_no_idedata( + tmp_path: Path, + caplog: pytest.LogCaptureFixture, + mock_write_cpp: Mock, + mock_compile_program: Mock, + mock_get_idedata: Mock, +) -> None: + """Test command_analyze_memory when idedata cannot be retrieved.""" + setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path, name="test_device") + + config = {CONF_ESPHOME: {CONF_NAME: "test_device"}} + args = MockArgs() + + mock_get_idedata.return_value = None # Failed to get idedata + + with caplog.at_level(logging.ERROR): + result = command_analyze_memory(args, config) + + assert result == 1 + assert "Failed to get IDE data for memory analysis" in caplog.text diff --git a/tests/unit_tests/test_mqtt.py b/tests/unit_tests/test_mqtt.py new file mode 100644 index 0000000000..4c2c34dff1 --- /dev/null +++ b/tests/unit_tests/test_mqtt.py @@ -0,0 +1,91 @@ +"""Unit tests for esphome.mqtt module.""" + +from __future__ import annotations + +import pytest + +from esphome.const import CONF_BROKER, CONF_ESPHOME, CONF_MQTT, CONF_NAME +from esphome.core import EsphomeError +from esphome.mqtt import get_esphome_device_ip + + +def test_get_esphome_device_ip_empty_broker() -> None: + """Test that get_esphome_device_ip raises EsphomeError when broker is empty.""" + config = { + CONF_MQTT: { + CONF_BROKER: "", + }, + CONF_ESPHOME: { + CONF_NAME: "test-device", + }, + } + + with pytest.raises( + EsphomeError, + match="Cannot discover IP via MQTT as the broker is not configured", + ): + get_esphome_device_ip(config) + + +def test_get_esphome_device_ip_none_broker() -> None: + """Test that get_esphome_device_ip raises EsphomeError when broker is None.""" + config = { + CONF_MQTT: { + CONF_BROKER: None, + }, + CONF_ESPHOME: { + CONF_NAME: "test-device", + }, + } + + with pytest.raises( + EsphomeError, + match="Cannot discover IP via MQTT as the broker is not configured", + ): + get_esphome_device_ip(config) + + +def test_get_esphome_device_ip_missing_mqtt() -> None: + """Test that get_esphome_device_ip raises EsphomeError when mqtt config is missing.""" + config = { + CONF_ESPHOME: { + CONF_NAME: "test-device", + }, + } + + with pytest.raises( + EsphomeError, + match="Cannot discover IP via MQTT as the config does not include the mqtt:", + ): + get_esphome_device_ip(config) + + +def test_get_esphome_device_ip_missing_esphome() -> None: + """Test that get_esphome_device_ip raises EsphomeError when esphome config is missing.""" + config = { + CONF_MQTT: { + CONF_BROKER: "mqtt.local", + }, + } + + with pytest.raises( + EsphomeError, + match="Cannot discover IP via MQTT as the config does not include the device name:", + ): + get_esphome_device_ip(config) + + +def test_get_esphome_device_ip_missing_name() -> None: + """Test that get_esphome_device_ip raises EsphomeError when device name is missing.""" + config = { + CONF_MQTT: { + CONF_BROKER: "mqtt.local", + }, + CONF_ESPHOME: {}, + } + + with pytest.raises( + EsphomeError, + match="Cannot discover IP via MQTT as the config does not include the device name:", + ): + get_esphome_device_ip(config) diff --git a/tests/unit_tests/test_platformio_api.py b/tests/unit_tests/test_platformio_api.py new file mode 100644 index 0000000000..13ef3516e4 --- /dev/null +++ b/tests/unit_tests/test_platformio_api.py @@ -0,0 +1,672 @@ +"""Tests for platformio_api.py path functions.""" + +import json +import os +from pathlib import Path +import shutil +from types import SimpleNamespace +from unittest.mock import MagicMock, Mock, patch + +import pytest + +from esphome import platformio_api +from esphome.core import CORE, EsphomeError + + +def test_idedata_firmware_elf_path(setup_core: Path) -> None: + """Test IDEData.firmware_elf_path returns correct path.""" + CORE.build_path = setup_core / "build" / "test" + CORE.name = "test" + raw_data = {"prog_path": "/path/to/firmware.elf"} + idedata = platformio_api.IDEData(raw_data) + + assert idedata.firmware_elf_path == Path("/path/to/firmware.elf") + + +def test_idedata_firmware_bin_path(setup_core: Path) -> None: + """Test IDEData.firmware_bin_path returns Path with .bin extension.""" + CORE.build_path = setup_core / "build" / "test" + CORE.name = "test" + prog_path = str(Path("/path/to/firmware.elf")) + raw_data = {"prog_path": prog_path} + idedata = platformio_api.IDEData(raw_data) + + result = idedata.firmware_bin_path + assert isinstance(result, Path) + expected = Path("/path/to/firmware.bin") + assert result == expected + assert str(result).endswith(".bin") + + +def test_idedata_firmware_bin_path_preserves_directory(setup_core: Path) -> None: + """Test firmware_bin_path preserves the directory structure.""" + CORE.build_path = setup_core / "build" / "test" + CORE.name = "test" + prog_path = str(Path("/complex/path/to/build/firmware.elf")) + raw_data = {"prog_path": prog_path} + idedata = platformio_api.IDEData(raw_data) + + result = idedata.firmware_bin_path + expected = Path("/complex/path/to/build/firmware.bin") + assert result == expected + + +def test_idedata_extra_flash_images(setup_core: Path) -> None: + """Test IDEData.extra_flash_images returns list of FlashImage objects.""" + CORE.build_path = setup_core / "build" / "test" + CORE.name = "test" + raw_data = { + "prog_path": "/path/to/firmware.elf", + "extra": { + "flash_images": [ + {"path": "/path/to/bootloader.bin", "offset": "0x1000"}, + {"path": "/path/to/partition.bin", "offset": "0x8000"}, + ] + }, + } + idedata = platformio_api.IDEData(raw_data) + + images = idedata.extra_flash_images + assert len(images) == 2 + assert all(isinstance(img, platformio_api.FlashImage) for img in images) + assert images[0].path == Path("/path/to/bootloader.bin") + assert images[0].offset == "0x1000" + assert images[1].path == Path("/path/to/partition.bin") + assert images[1].offset == "0x8000" + + +def test_idedata_extra_flash_images_empty(setup_core: Path) -> None: + """Test extra_flash_images returns empty list when no extra images.""" + CORE.build_path = setup_core / "build" / "test" + CORE.name = "test" + raw_data = {"prog_path": "/path/to/firmware.elf", "extra": {"flash_images": []}} + idedata = platformio_api.IDEData(raw_data) + + images = idedata.extra_flash_images + assert images == [] + + +def test_idedata_cc_path(setup_core: Path) -> None: + """Test IDEData.cc_path returns compiler path.""" + CORE.build_path = setup_core / "build" / "test" + CORE.name = "test" + raw_data = { + "prog_path": "/path/to/firmware.elf", + "cc_path": "/Users/test/.platformio/packages/toolchain-xtensa32/bin/xtensa-esp32-elf-gcc", + } + idedata = platformio_api.IDEData(raw_data) + + assert ( + idedata.cc_path + == "/Users/test/.platformio/packages/toolchain-xtensa32/bin/xtensa-esp32-elf-gcc" + ) + + +def test_flash_image_dataclass() -> None: + """Test FlashImage dataclass stores path and offset correctly.""" + image = platformio_api.FlashImage(path=Path("/path/to/image.bin"), offset="0x10000") + + assert image.path == Path("/path/to/image.bin") + assert image.offset == "0x10000" + + +def test_load_idedata_returns_dict( + setup_core: Path, mock_run_platformio_cli_run +) -> None: + """Test _load_idedata returns parsed idedata dict when successful.""" + CORE.build_path = setup_core / "build" / "test" + CORE.name = "test" + + # Create required files + platformio_ini = setup_core / "build" / "test" / "platformio.ini" + platformio_ini.parent.mkdir(parents=True, exist_ok=True) + platformio_ini.touch() + + idedata_path = setup_core / ".esphome" / "idedata" / "test.json" + idedata_path.parent.mkdir(parents=True, exist_ok=True) + idedata_path.write_text('{"prog_path": "/test/firmware.elf"}') + + mock_run_platformio_cli_run.return_value = '{"prog_path": "/test/firmware.elf"}' + + config = {"name": "test"} + result = platformio_api._load_idedata(config) + + assert result is not None + assert isinstance(result, dict) + assert result["prog_path"] == "/test/firmware.elf" + + +def test_load_idedata_uses_cache_when_valid( + setup_core: Path, mock_run_platformio_cli_run: Mock +) -> None: + """Test _load_idedata uses cached data when unchanged.""" + CORE.build_path = str(setup_core / "build" / "test") + CORE.name = "test" + + # Create platformio.ini + platformio_ini = setup_core / "build" / "test" / "platformio.ini" + platformio_ini.parent.mkdir(parents=True, exist_ok=True) + platformio_ini.write_text("content") + + # Create idedata cache file that's newer + idedata_path = setup_core / ".esphome" / "idedata" / "test.json" + idedata_path.parent.mkdir(parents=True, exist_ok=True) + idedata_path.write_text('{"prog_path": "/cached/firmware.elf"}') + + # Make idedata newer than platformio.ini + platformio_ini_mtime = platformio_ini.stat().st_mtime + os.utime(idedata_path, (platformio_ini_mtime + 1, platformio_ini_mtime + 1)) + + config = {"name": "test"} + result = platformio_api._load_idedata(config) + + # Should not call _run_idedata since cache is valid + mock_run_platformio_cli_run.assert_not_called() + + assert result["prog_path"] == "/cached/firmware.elf" + + +def test_load_idedata_regenerates_when_platformio_ini_newer( + setup_core: Path, mock_run_platformio_cli_run: Mock +) -> None: + """Test _load_idedata regenerates when platformio.ini is newer.""" + CORE.build_path = str(setup_core / "build" / "test") + CORE.name = "test" + + # Create idedata cache file first + idedata_path = setup_core / ".esphome" / "idedata" / "test.json" + idedata_path.parent.mkdir(parents=True, exist_ok=True) + idedata_path.write_text('{"prog_path": "/old/firmware.elf"}') + + # Create platformio.ini that's newer + idedata_mtime = idedata_path.stat().st_mtime + platformio_ini = setup_core / "build" / "test" / "platformio.ini" + platformio_ini.parent.mkdir(parents=True, exist_ok=True) + platformio_ini.write_text("content") + # Make platformio.ini newer than idedata + os.utime(platformio_ini, (idedata_mtime + 1, idedata_mtime + 1)) + + # Mock platformio to return new data + new_data = {"prog_path": "/new/firmware.elf"} + mock_run_platformio_cli_run.return_value = json.dumps(new_data) + + config = {"name": "test"} + result = platformio_api._load_idedata(config) + + # Should call _run_idedata since platformio.ini is newer + mock_run_platformio_cli_run.assert_called_once() + + assert result["prog_path"] == "/new/firmware.elf" + + +def test_load_idedata_regenerates_on_corrupted_cache( + setup_core: Path, mock_run_platformio_cli_run: Mock +) -> None: + """Test _load_idedata regenerates when cache file is corrupted.""" + CORE.build_path = str(setup_core / "build" / "test") + CORE.name = "test" + + # Create platformio.ini + platformio_ini = setup_core / "build" / "test" / "platformio.ini" + platformio_ini.parent.mkdir(parents=True, exist_ok=True) + platformio_ini.write_text("content") + + # Create corrupted idedata cache file + idedata_path = setup_core / ".esphome" / "idedata" / "test.json" + idedata_path.parent.mkdir(parents=True, exist_ok=True) + idedata_path.write_text('{"prog_path": invalid json') + + # Make idedata newer so it would be used if valid + platformio_ini_mtime = platformio_ini.stat().st_mtime + os.utime(idedata_path, (platformio_ini_mtime + 1, platformio_ini_mtime + 1)) + + # Mock platformio to return new data + new_data = {"prog_path": "/new/firmware.elf"} + mock_run_platformio_cli_run.return_value = json.dumps(new_data) + + config = {"name": "test"} + result = platformio_api._load_idedata(config) + + # Should call _run_idedata since cache is corrupted + mock_run_platformio_cli_run.assert_called_once() + + assert result["prog_path"] == "/new/firmware.elf" + + +def test_run_idedata_parses_json_from_output( + setup_core: Path, mock_run_platformio_cli_run: Mock +) -> None: + """Test _run_idedata extracts JSON from platformio output.""" + config = {"name": "test"} + + expected_data = { + "prog_path": "/path/to/firmware.elf", + "cc_path": "/path/to/gcc", + "extra": {"flash_images": []}, + } + + # Simulate platformio output with JSON embedded + mock_run_platformio_cli_run.return_value = ( + f"Some preamble\n{json.dumps(expected_data)}\nSome postamble" + ) + + result = platformio_api._run_idedata(config) + + assert result == expected_data + + +def test_run_idedata_raises_on_no_json( + setup_core: Path, mock_run_platformio_cli_run: Mock +) -> None: + """Test _run_idedata raises EsphomeError when no JSON found.""" + config = {"name": "test"} + + mock_run_platformio_cli_run.return_value = "No JSON in this output" + + with pytest.raises(EsphomeError): + platformio_api._run_idedata(config) + + +def test_run_idedata_raises_on_invalid_json( + setup_core: Path, mock_run_platformio_cli_run: Mock +) -> None: + """Test _run_idedata raises on malformed JSON.""" + config = {"name": "test"} + mock_run_platformio_cli_run.return_value = '{"invalid": json"}' + + # The ValueError from json.loads is re-raised + with pytest.raises(ValueError): + platformio_api._run_idedata(config) + + +def test_run_platformio_cli_sets_environment_variables( + setup_core: Path, mock_run_external_command: Mock +) -> None: + """Test run_platformio_cli sets correct environment variables.""" + CORE.build_path = str(setup_core / "build" / "test") + + with patch.dict(os.environ, {}, clear=False): + mock_run_external_command.return_value = 0 + platformio_api.run_platformio_cli("test", "arg") + + # Check environment variables were set + assert os.environ["PLATFORMIO_FORCE_COLOR"] == "true" + assert ( + setup_core / "build" / "test" + in Path(os.environ["PLATFORMIO_BUILD_DIR"]).parents + or Path(os.environ["PLATFORMIO_BUILD_DIR"]) == setup_core / "build" / "test" + ) + assert "PLATFORMIO_LIBDEPS_DIR" in os.environ + assert "PYTHONWARNINGS" in os.environ + + # Check command was called correctly + mock_run_external_command.assert_called_once() + args = mock_run_external_command.call_args[0] + assert "platformio" in args + assert "test" in args + assert "arg" in args + + +def test_run_platformio_cli_run_builds_command( + setup_core: Path, mock_run_platformio_cli: Mock +) -> None: + """Test run_platformio_cli_run builds correct command.""" + CORE.build_path = str(setup_core / "build" / "test") + mock_run_platformio_cli.return_value = 0 + + config = {"name": "test"} + platformio_api.run_platformio_cli_run(config, True, "extra", "args") + + mock_run_platformio_cli.assert_called_once_with( + "run", "-d", CORE.build_path, "-v", "extra", "args" + ) + + +def test_run_compile(setup_core: Path, mock_run_platformio_cli_run: Mock) -> None: + """Test run_compile with process limit.""" + from esphome.const import CONF_COMPILE_PROCESS_LIMIT, CONF_ESPHOME + + CORE.build_path = str(setup_core / "build" / "test") + config = {CONF_ESPHOME: {CONF_COMPILE_PROCESS_LIMIT: 4}} + mock_run_platformio_cli_run.return_value = 0 + + platformio_api.run_compile(config, verbose=True) + + mock_run_platformio_cli_run.assert_called_once_with(config, True, "-j4") + + +def test_get_idedata_caches_result( + setup_core: Path, mock_run_platformio_cli_run: Mock +) -> None: + """Test get_idedata caches result in CORE.data.""" + from esphome.const import KEY_CORE + + CORE.build_path = str(setup_core / "build" / "test") + CORE.name = "test" + CORE.data[KEY_CORE] = {} + + # Create platformio.ini to avoid regeneration + platformio_ini = setup_core / "build" / "test" / "platformio.ini" + platformio_ini.parent.mkdir(parents=True, exist_ok=True) + platformio_ini.write_text("content") + + # Mock platformio to return data + idedata = {"prog_path": "/test/firmware.elf"} + mock_run_platformio_cli_run.return_value = json.dumps(idedata) + + config = {"name": "test"} + + # First call should load and cache + result1 = platformio_api.get_idedata(config) + mock_run_platformio_cli_run.assert_called_once() + + # Second call should use cache from CORE.data + result2 = platformio_api.get_idedata(config) + mock_run_platformio_cli_run.assert_called_once() # Still only called once + + assert result1 is result2 + assert isinstance(result1, platformio_api.IDEData) + assert result1.firmware_elf_path == Path("/test/firmware.elf") + + +def test_idedata_addr2line_path_windows(setup_core: Path) -> None: + """Test IDEData.addr2line_path on Windows.""" + raw_data = {"prog_path": "/path/to/firmware.elf", "cc_path": "C:\\tools\\gcc.exe"} + idedata = platformio_api.IDEData(raw_data) + + result = idedata.addr2line_path + assert result == "C:\\tools\\addr2line.exe" + + +def test_idedata_addr2line_path_unix(setup_core: Path) -> None: + """Test IDEData.addr2line_path on Unix.""" + raw_data = {"prog_path": "/path/to/firmware.elf", "cc_path": "/usr/bin/gcc"} + idedata = platformio_api.IDEData(raw_data) + + result = idedata.addr2line_path + assert result == "/usr/bin/addr2line" + + +def test_idedata_objdump_path_windows(setup_core: Path) -> None: + """Test IDEData.objdump_path on Windows.""" + raw_data = {"prog_path": "/path/to/firmware.elf", "cc_path": "C:\\tools\\gcc.exe"} + idedata = platformio_api.IDEData(raw_data) + + result = idedata.objdump_path + assert result == "C:\\tools\\objdump.exe" + + +def test_idedata_objdump_path_unix(setup_core: Path) -> None: + """Test IDEData.objdump_path on Unix.""" + raw_data = {"prog_path": "/path/to/firmware.elf", "cc_path": "/usr/bin/gcc"} + idedata = platformio_api.IDEData(raw_data) + + result = idedata.objdump_path + assert result == "/usr/bin/objdump" + + +def test_idedata_readelf_path_windows(setup_core: Path) -> None: + """Test IDEData.readelf_path on Windows.""" + raw_data = {"prog_path": "/path/to/firmware.elf", "cc_path": "C:\\tools\\gcc.exe"} + idedata = platformio_api.IDEData(raw_data) + + result = idedata.readelf_path + assert result == "C:\\tools\\readelf.exe" + + +def test_idedata_readelf_path_unix(setup_core: Path) -> None: + """Test IDEData.readelf_path on Unix.""" + raw_data = {"prog_path": "/path/to/firmware.elf", "cc_path": "/usr/bin/gcc"} + idedata = platformio_api.IDEData(raw_data) + + result = idedata.readelf_path + assert result == "/usr/bin/readelf" + + +def test_patch_structhash(setup_core: Path) -> None: + """Test patch_structhash monkey patches platformio functions.""" + # Create simple namespace objects to act as modules + mock_cli = SimpleNamespace() + mock_helpers = SimpleNamespace() + mock_run = SimpleNamespace(cli=mock_cli, helpers=mock_helpers) + + # Mock platformio modules + with patch.dict( + "sys.modules", + { + "platformio.run.cli": mock_cli, + "platformio.run.helpers": mock_helpers, + "platformio.run": mock_run, + "platformio.project.helpers": MagicMock(), + "platformio.fs": MagicMock(), + "platformio": MagicMock(), + }, + ): + # Call patch_structhash + platformio_api.patch_structhash() + + # Verify both modules had clean_build_dir patched + # Check that clean_build_dir was set on both modules + assert hasattr(mock_cli, "clean_build_dir") + assert hasattr(mock_helpers, "clean_build_dir") + + # Verify they got the same function assigned + assert mock_cli.clean_build_dir is mock_helpers.clean_build_dir + + # Verify it's a real function (not a Mock) + assert callable(mock_cli.clean_build_dir) + assert mock_cli.clean_build_dir.__name__ == "patched_clean_build_dir" + + +def test_patched_clean_build_dir_removes_outdated(setup_core: Path) -> None: + """Test patched_clean_build_dir removes build dir when platformio.ini is newer.""" + build_dir = setup_core / "build" + build_dir.mkdir() + platformio_ini = setup_core / "platformio.ini" + platformio_ini.write_text("config") + + # Make platformio.ini newer than build_dir + build_mtime = build_dir.stat().st_mtime + os.utime(platformio_ini, (build_mtime + 1, build_mtime + 1)) + + # Track if directory was removed + removed_paths: list[Path] = [] + + def track_rmtree(path: Path) -> None: + removed_paths.append(path) + shutil.rmtree(path) + + # Create mock modules that patch_structhash expects + mock_cli = SimpleNamespace() + mock_helpers = SimpleNamespace() + mock_project_helpers = MagicMock() + mock_project_helpers.get_project_dir.return_value = str(setup_core) + mock_fs = SimpleNamespace(rmtree=track_rmtree) + + with patch.dict( + "sys.modules", + { + "platformio": SimpleNamespace(fs=mock_fs), + "platformio.fs": mock_fs, + "platformio.project.helpers": mock_project_helpers, + "platformio.run": SimpleNamespace(cli=mock_cli, helpers=mock_helpers), + "platformio.run.cli": mock_cli, + "platformio.run.helpers": mock_helpers, + }, + ): + # Call patch_structhash to install the patched function + platformio_api.patch_structhash() + + # Call the patched function + mock_helpers.clean_build_dir(str(build_dir), []) + + # Verify directory was removed and recreated + assert len(removed_paths) == 1 + assert removed_paths[0] == build_dir + assert build_dir.exists() # makedirs recreated it + + +def test_patched_clean_build_dir_keeps_updated(setup_core: Path) -> None: + """Test patched_clean_build_dir keeps build dir when it's up to date.""" + build_dir = setup_core / "build" + build_dir.mkdir() + test_file = build_dir / "test.txt" + test_file.write_text("test content") + + platformio_ini = setup_core / "platformio.ini" + platformio_ini.write_text("config") + + # Make build_dir newer than platformio.ini + ini_mtime = platformio_ini.stat().st_mtime + os.utime(build_dir, (ini_mtime + 1, ini_mtime + 1)) + + # Track if rmtree is called + removed_paths: list[str] = [] + + def track_rmtree(path: str) -> None: + removed_paths.append(path) + + # Create mock modules + mock_cli = SimpleNamespace() + mock_helpers = SimpleNamespace() + mock_project_helpers = MagicMock() + mock_project_helpers.get_project_dir.return_value = str(setup_core) + mock_fs = SimpleNamespace(rmtree=track_rmtree) + + with patch.dict( + "sys.modules", + { + "platformio": SimpleNamespace(fs=mock_fs), + "platformio.fs": mock_fs, + "platformio.project.helpers": mock_project_helpers, + "platformio.run": SimpleNamespace(cli=mock_cli, helpers=mock_helpers), + "platformio.run.cli": mock_cli, + "platformio.run.helpers": mock_helpers, + }, + ): + # Call patch_structhash to install the patched function + platformio_api.patch_structhash() + + # Call the patched function + mock_helpers.clean_build_dir(str(build_dir), []) + + # Verify rmtree was NOT called + assert len(removed_paths) == 0 + + # Verify directory and file still exist + assert build_dir.exists() + assert test_file.exists() + assert test_file.read_text() == "test content" + + +def test_patched_clean_build_dir_creates_missing(setup_core: Path) -> None: + """Test patched_clean_build_dir creates build dir when it doesn't exist.""" + build_dir = setup_core / "build" + platformio_ini = setup_core / "platformio.ini" + platformio_ini.write_text("config") + + # Ensure build_dir doesn't exist + assert not build_dir.exists() + + # Track if rmtree is called + removed_paths: list[str] = [] + + def track_rmtree(path: str) -> None: + removed_paths.append(path) + + # Create mock modules + mock_cli = SimpleNamespace() + mock_helpers = SimpleNamespace() + mock_project_helpers = MagicMock() + mock_project_helpers.get_project_dir.return_value = str(setup_core) + mock_fs = SimpleNamespace(rmtree=track_rmtree) + + with patch.dict( + "sys.modules", + { + "platformio": SimpleNamespace(fs=mock_fs), + "platformio.fs": mock_fs, + "platformio.project.helpers": mock_project_helpers, + "platformio.run": SimpleNamespace(cli=mock_cli, helpers=mock_helpers), + "platformio.run.cli": mock_cli, + "platformio.run.helpers": mock_helpers, + }, + ): + # Call patch_structhash to install the patched function + platformio_api.patch_structhash() + + # Call the patched function + mock_helpers.clean_build_dir(str(build_dir), []) + + # Verify rmtree was NOT called + assert len(removed_paths) == 0 + + # Verify directory was created + assert build_dir.exists() + + +def test_process_stacktrace_esp8266_exception(setup_core: Path, caplog) -> None: + """Test process_stacktrace handles ESP8266 exceptions.""" + config = {"name": "test"} + + # Test exception type parsing + line = "Exception (28):" + backtrace_state = False + + result = platformio_api.process_stacktrace(config, line, backtrace_state) + + assert "Access to invalid address: LOAD (wild pointer?)" in caplog.text + assert result is False + + +def test_process_stacktrace_esp8266_backtrace( + setup_core: Path, mock_decode_pc: Mock +) -> None: + """Test process_stacktrace handles ESP8266 multi-line backtrace.""" + config = {"name": "test"} + + # Start of backtrace + line1 = ">>>stack>>>" + state = platformio_api.process_stacktrace(config, line1, False) + assert state is True + + # Backtrace content with addresses + line2 = "40201234 40205678" + state = platformio_api.process_stacktrace(config, line2, state) + assert state is True + assert mock_decode_pc.call_count == 2 + + # End of backtrace + line3 = "<< None: + """Test process_stacktrace handles ESP32 single-line backtrace.""" + config = {"name": "test"} + + line = "Backtrace: 0x40081234:0x3ffb1234 0x40085678:0x3ffb5678" + state = platformio_api.process_stacktrace(config, line, False) + + # Should decode both addresses + assert mock_decode_pc.call_count == 2 + mock_decode_pc.assert_any_call(config, "40081234") + mock_decode_pc.assert_any_call(config, "40085678") + assert state is False + + +def test_process_stacktrace_bad_alloc( + setup_core: Path, mock_decode_pc: Mock, caplog +) -> None: + """Test process_stacktrace handles bad alloc messages.""" + config = {"name": "test"} + + line = "last failed alloc call: 40201234(512)" + state = platformio_api.process_stacktrace(config, line, False) + + assert "Memory allocation of 512 bytes failed at 40201234" in caplog.text + mock_decode_pc.assert_called_once_with(config, "40201234") + assert state is False diff --git a/tests/unit_tests/test_resolver.py b/tests/unit_tests/test_resolver.py new file mode 100644 index 0000000000..b4cca05d9f --- /dev/null +++ b/tests/unit_tests/test_resolver.py @@ -0,0 +1,169 @@ +"""Tests for the DNS resolver module.""" + +from __future__ import annotations + +import re +import socket +from unittest.mock import patch + +from aioesphomeapi.core import ResolveAPIError, ResolveTimeoutAPIError +from aioesphomeapi.host_resolver import AddrInfo, IPv4Sockaddr, IPv6Sockaddr +import pytest + +from esphome.core import EsphomeError +from esphome.resolver import RESOLVE_TIMEOUT, AsyncResolver + + +@pytest.fixture +def mock_addr_info_ipv4() -> AddrInfo: + """Create a mock IPv4 AddrInfo.""" + return AddrInfo( + family=socket.AF_INET, + type=socket.SOCK_STREAM, + proto=socket.IPPROTO_TCP, + sockaddr=IPv4Sockaddr(address="192.168.1.100", port=6053), + ) + + +@pytest.fixture +def mock_addr_info_ipv6() -> AddrInfo: + """Create a mock IPv6 AddrInfo.""" + return AddrInfo( + family=socket.AF_INET6, + type=socket.SOCK_STREAM, + proto=socket.IPPROTO_TCP, + sockaddr=IPv6Sockaddr(address="2001:db8::1", port=6053, flowinfo=0, scope_id=0), + ) + + +def test_async_resolver_successful_resolution(mock_addr_info_ipv4: AddrInfo) -> None: + """Test successful DNS resolution.""" + with patch( + "esphome.resolver.hr.async_resolve_host", + return_value=[mock_addr_info_ipv4], + ) as mock_resolve: + resolver = AsyncResolver(["test.local"], 6053) + result = resolver.resolve() + + assert result == [mock_addr_info_ipv4] + mock_resolve.assert_called_once_with( + ["test.local"], 6053, timeout=RESOLVE_TIMEOUT + ) + + +def test_async_resolver_multiple_hosts( + mock_addr_info_ipv4: AddrInfo, mock_addr_info_ipv6: AddrInfo +) -> None: + """Test resolving multiple hosts.""" + mock_results = [mock_addr_info_ipv4, mock_addr_info_ipv6] + + with patch( + "esphome.resolver.hr.async_resolve_host", + return_value=mock_results, + ) as mock_resolve: + resolver = AsyncResolver(["test1.local", "test2.local"], 6053) + result = resolver.resolve() + + assert result == mock_results + mock_resolve.assert_called_once_with( + ["test1.local", "test2.local"], 6053, timeout=RESOLVE_TIMEOUT + ) + + +def test_async_resolver_resolve_api_error() -> None: + """Test handling of ResolveAPIError.""" + error_msg = "Failed to resolve" + with patch( + "esphome.resolver.hr.async_resolve_host", + side_effect=ResolveAPIError(error_msg), + ): + resolver = AsyncResolver(["test.local"], 6053) + with pytest.raises( + EsphomeError, match=re.escape(f"Error resolving IP address: {error_msg}") + ): + resolver.resolve() + + +def test_async_resolver_timeout_error() -> None: + """Test handling of ResolveTimeoutAPIError.""" + error_msg = "Resolution timed out" + + with patch( + "esphome.resolver.hr.async_resolve_host", + side_effect=ResolveTimeoutAPIError(error_msg), + ): + resolver = AsyncResolver(["test.local"], 6053) + # Match either "Timeout" or "Error" since ResolveTimeoutAPIError is a subclass of ResolveAPIError + # and depending on import order/test execution context, it might be caught as either + with pytest.raises( + EsphomeError, + match=f"(Timeout|Error) resolving IP address: {re.escape(error_msg)}", + ): + resolver.resolve() + + +def test_async_resolver_generic_exception() -> None: + """Test handling of generic exceptions.""" + error = RuntimeError("Unexpected error") + with patch( + "esphome.resolver.hr.async_resolve_host", + side_effect=error, + ): + resolver = AsyncResolver(["test.local"], 6053) + with pytest.raises(RuntimeError, match="Unexpected error"): + resolver.resolve() + + +def test_async_resolver_thread_timeout() -> None: + """Test timeout when thread doesn't complete in time.""" + # Mock the start method to prevent actual thread execution + with ( + patch.object(AsyncResolver, "start"), + patch("esphome.resolver.hr.async_resolve_host"), + ): + resolver = AsyncResolver(["test.local"], 6053) + # Override event.wait to simulate timeout (return False = timeout occurred) + with ( + patch.object(resolver.event, "wait", return_value=False), + pytest.raises( + EsphomeError, match=re.escape("Timeout resolving IP address") + ), + ): + resolver.resolve() + + # Verify thread start was called + resolver.start.assert_called_once() + + +def test_async_resolver_ip_addresses(mock_addr_info_ipv4: AddrInfo) -> None: + """Test resolving IP addresses.""" + with patch( + "esphome.resolver.hr.async_resolve_host", + return_value=[mock_addr_info_ipv4], + ) as mock_resolve: + resolver = AsyncResolver(["192.168.1.100"], 6053) + result = resolver.resolve() + + assert result == [mock_addr_info_ipv4] + mock_resolve.assert_called_once_with( + ["192.168.1.100"], 6053, timeout=RESOLVE_TIMEOUT + ) + + +def test_async_resolver_mixed_addresses( + mock_addr_info_ipv4: AddrInfo, mock_addr_info_ipv6: AddrInfo +) -> None: + """Test resolving mix of hostnames and IP addresses.""" + mock_results = [mock_addr_info_ipv4, mock_addr_info_ipv6] + + with patch( + "esphome.resolver.hr.async_resolve_host", + return_value=mock_results, + ) as mock_resolve: + resolver = AsyncResolver(["test.local", "192.168.1.100", "::1"], 6053) + result = resolver.resolve() + + assert result == mock_results + mock_resolve.assert_called_once_with( + ["test.local", "192.168.1.100", "::1"], 6053, timeout=RESOLVE_TIMEOUT + ) diff --git a/tests/unit_tests/test_storage_json.py b/tests/unit_tests/test_storage_json.py new file mode 100644 index 0000000000..a3a38960e7 --- /dev/null +++ b/tests/unit_tests/test_storage_json.py @@ -0,0 +1,660 @@ +"""Tests for storage_json.py path functions.""" + +from datetime import datetime +import json +from pathlib import Path +import sys +from unittest.mock import MagicMock, Mock, patch + +import pytest + +from esphome import storage_json +from esphome.const import CONF_DISABLED, CONF_MDNS +from esphome.core import CORE + + +def test_storage_path(setup_core: Path) -> None: + """Test storage_path returns correct path for current config.""" + CORE.config_path = setup_core / "my_device.yaml" + + result = storage_json.storage_path() + + data_dir = Path(CORE.data_dir) + expected = data_dir / "storage" / "my_device.yaml.json" + assert result == expected + + +def test_ext_storage_path(setup_core: Path) -> None: + """Test ext_storage_path returns correct path for given filename.""" + result = storage_json.ext_storage_path("other_device.yaml") + + data_dir = Path(CORE.data_dir) + expected = data_dir / "storage" / "other_device.yaml.json" + assert result == expected + + +def test_ext_storage_path_handles_various_extensions(setup_core: Path) -> None: + """Test ext_storage_path works with different file extensions.""" + result_yml = storage_json.ext_storage_path("device.yml") + assert str(result_yml).endswith("device.yml.json") + + result_no_ext = storage_json.ext_storage_path("device") + assert str(result_no_ext).endswith("device.json") + + result_path = storage_json.ext_storage_path("my/device.yaml") + assert str(result_path).endswith("device.yaml.json") + + +def test_esphome_storage_path(setup_core: Path) -> None: + """Test esphome_storage_path returns correct path.""" + result = storage_json.esphome_storage_path() + + data_dir = Path(CORE.data_dir) + expected = data_dir / "esphome.json" + assert result == expected + + +def test_ignored_devices_storage_path(setup_core: Path) -> None: + """Test ignored_devices_storage_path returns correct path.""" + result = storage_json.ignored_devices_storage_path() + + data_dir = Path(CORE.data_dir) + expected = data_dir / "ignored-devices.json" + assert result == expected + + +def test_trash_storage_path(setup_core: Path) -> None: + """Test trash_storage_path returns correct path.""" + CORE.config_path = setup_core / "configs" / "device.yaml" + + result = storage_json.trash_storage_path() + + expected = setup_core / "configs" / "trash" + assert result == expected + + +def test_archive_storage_path(setup_core: Path) -> None: + """Test archive_storage_path returns correct path.""" + CORE.config_path = setup_core / "configs" / "device.yaml" + + result = storage_json.archive_storage_path() + + expected = setup_core / "configs" / "archive" + assert result == expected + + +def test_storage_path_with_subdirectory(setup_core: Path) -> None: + """Test storage paths work correctly when config is in subdirectory.""" + subdir = setup_core / "configs" / "basement" + subdir.mkdir(parents=True, exist_ok=True) + CORE.config_path = subdir / "sensor.yaml" + + result = storage_json.storage_path() + + data_dir = Path(CORE.data_dir) + expected = data_dir / "storage" / "sensor.yaml.json" + assert result == expected + + +def test_storage_json_firmware_bin_path_property(setup_core: Path) -> None: + """Test StorageJSON firmware_bin_path property.""" + storage = storage_json.StorageJSON( + storage_version=1, + name="test_device", + friendly_name="Test Device", + comment=None, + esphome_version="2024.1.0", + src_version=None, + address="192.168.1.100", + web_port=80, + target_platform="ESP32", + build_path="build/test_device", + firmware_bin_path="/path/to/firmware.bin", + loaded_integrations={"wifi", "api"}, + loaded_platforms=set(), + no_mdns=False, + ) + + assert storage.firmware_bin_path == "/path/to/firmware.bin" + + +def test_storage_json_save_creates_directory( + setup_core: Path, tmp_path: Path, mock_write_file_if_changed: Mock +) -> None: + """Test StorageJSON.save creates storage directory if it doesn't exist.""" + storage_dir = tmp_path / "new_data" / "storage" + storage_file = storage_dir / "test.json" + + assert not storage_dir.exists() + + storage = storage_json.StorageJSON( + storage_version=1, + name="test", + friendly_name="Test", + comment=None, + esphome_version="2024.1.0", + src_version=None, + address="test.local", + web_port=None, + target_platform="ESP8266", + build_path=None, + firmware_bin_path=None, + loaded_integrations=set(), + loaded_platforms=set(), + no_mdns=False, + ) + + storage.save(str(storage_file)) + mock_write_file_if_changed.assert_called_once() + call_args = mock_write_file_if_changed.call_args[0] + assert call_args[0] == str(storage_file) + + +def test_storage_json_from_wizard(setup_core: Path) -> None: + """Test StorageJSON.from_wizard creates correct storage object.""" + storage = storage_json.StorageJSON.from_wizard( + name="my_device", + friendly_name="My Device", + address="my_device.local", + platform="ESP32", + ) + + assert storage.name == "my_device" + assert storage.friendly_name == "My Device" + assert storage.address == "my_device.local" + assert storage.target_platform == "ESP32" + assert storage.build_path is None + assert storage.firmware_bin_path is None + + +@pytest.mark.skipif(sys.platform == "win32", reason="HA addons don't run on Windows") +@patch("esphome.core.is_ha_addon") +def test_storage_paths_with_ha_addon(mock_is_ha_addon: bool, tmp_path: Path) -> None: + """Test storage paths when running as Home Assistant addon.""" + mock_is_ha_addon.return_value = True + + CORE.config_path = tmp_path / "test.yaml" + + result = storage_json.storage_path() + # When is_ha_addon is True, CORE.data_dir returns "/data" + # This is the standard mount point for HA addon containers + expected = Path("/data") / "storage" / "test.yaml.json" + assert result == expected + + result = storage_json.esphome_storage_path() + expected = Path("/data") / "esphome.json" + assert result == expected + + +def test_storage_json_as_dict() -> None: + """Test StorageJSON.as_dict returns correct dictionary.""" + storage = storage_json.StorageJSON( + storage_version=1, + name="test_device", + friendly_name="Test Device", + comment="Test comment", + esphome_version="2024.1.0", + src_version=1, + address="192.168.1.100", + web_port=80, + target_platform="ESP32", + build_path="/path/to/build", + firmware_bin_path="/path/to/firmware.bin", + loaded_integrations={"wifi", "api", "ota"}, + loaded_platforms={"sensor", "binary_sensor"}, + no_mdns=True, + framework="arduino", + core_platform="esp32", + ) + + result = storage.as_dict() + + assert result["storage_version"] == 1 + assert result["name"] == "test_device" + assert result["friendly_name"] == "Test Device" + assert result["comment"] == "Test comment" + assert result["esphome_version"] == "2024.1.0" + assert result["src_version"] == 1 + assert result["address"] == "192.168.1.100" + assert result["web_port"] == 80 + assert result["esp_platform"] == "ESP32" + assert result["build_path"] == "/path/to/build" + assert result["firmware_bin_path"] == "/path/to/firmware.bin" + assert "api" in result["loaded_integrations"] + assert "wifi" in result["loaded_integrations"] + assert "ota" in result["loaded_integrations"] + assert result["loaded_integrations"] == sorted( + ["wifi", "api", "ota"] + ) # Should be sorted + assert "sensor" in result["loaded_platforms"] + assert result["loaded_platforms"] == sorted( + ["sensor", "binary_sensor"] + ) # Should be sorted + assert result["no_mdns"] is True + assert result["framework"] == "arduino" + assert result["core_platform"] == "esp32" + + +def test_storage_json_to_json() -> None: + """Test StorageJSON.to_json returns valid JSON string.""" + storage = storage_json.StorageJSON( + storage_version=1, + name="test", + friendly_name="Test", + comment=None, + esphome_version="2024.1.0", + src_version=None, + address="test.local", + web_port=None, + target_platform="ESP8266", + build_path=None, + firmware_bin_path=None, + loaded_integrations=set(), + loaded_platforms=set(), + no_mdns=False, + ) + + json_str = storage.to_json() + + # Should be valid JSON + parsed = json.loads(json_str) + assert parsed["name"] == "test" + assert parsed["storage_version"] == 1 + + # Should end with newline + assert json_str.endswith("\n") + + +def test_storage_json_save(tmp_path: Path) -> None: + """Test StorageJSON.save writes file correctly.""" + storage = storage_json.StorageJSON( + storage_version=1, + name="test", + friendly_name="Test", + comment=None, + esphome_version="2024.1.0", + src_version=None, + address="test.local", + web_port=None, + target_platform="ESP32", + build_path=None, + firmware_bin_path=None, + loaded_integrations=set(), + loaded_platforms=set(), + no_mdns=False, + ) + + save_path = tmp_path / "test.json" + + with patch("esphome.storage_json.write_file_if_changed") as mock_write: + storage.save(str(save_path)) + mock_write.assert_called_once_with(str(save_path), storage.to_json()) + + +def test_storage_json_from_esphome_core(setup_core: Path) -> None: + """Test StorageJSON.from_esphome_core creates correct storage object.""" + # Mock CORE object + mock_core = MagicMock() + mock_core.name = "my_device" + mock_core.friendly_name = "My Device" + mock_core.comment = "A test device" + mock_core.address = "192.168.1.50" + mock_core.web_port = 8080 + mock_core.target_platform = "esp32" + mock_core.is_esp32 = True + mock_core.build_path = "/build/my_device" + mock_core.firmware_bin = "/build/my_device/firmware.bin" + mock_core.loaded_integrations = {"wifi", "api"} + mock_core.loaded_platforms = {"sensor"} + mock_core.config = {CONF_MDNS: {CONF_DISABLED: True}} + mock_core.target_framework = "esp-idf" + + with patch("esphome.components.esp32.get_esp32_variant") as mock_variant: + mock_variant.return_value = "ESP32-C3" + + result = storage_json.StorageJSON.from_esphome_core(mock_core, old=None) + + assert result.name == "my_device" + assert result.friendly_name == "My Device" + assert result.comment == "A test device" + assert result.address == "192.168.1.50" + assert result.web_port == 8080 + assert result.target_platform == "ESP32-C3" + assert result.build_path == "/build/my_device" + assert result.firmware_bin_path == "/build/my_device/firmware.bin" + assert result.loaded_integrations == {"wifi", "api"} + assert result.loaded_platforms == {"sensor"} + assert result.no_mdns is True + assert result.framework == "esp-idf" + assert result.core_platform == "esp32" + + +def test_storage_json_from_esphome_core_mdns_enabled(setup_core: Path) -> None: + """Test from_esphome_core with mDNS enabled.""" + mock_core = MagicMock() + mock_core.name = "test" + mock_core.friendly_name = "Test" + mock_core.comment = None + mock_core.address = "test.local" + mock_core.web_port = None + mock_core.target_platform = "esp8266" + mock_core.is_esp32 = False + mock_core.build_path = "/build" + mock_core.firmware_bin = "/build/firmware.bin" + mock_core.loaded_integrations = set() + mock_core.loaded_platforms = set() + mock_core.config = {} # No MDNS config means enabled + mock_core.target_framework = "arduino" + + result = storage_json.StorageJSON.from_esphome_core(mock_core, old=None) + + assert result.no_mdns is False + + +def test_storage_json_load_valid_file(tmp_path: Path) -> None: + """Test StorageJSON.load with valid JSON file.""" + storage_data = { + "storage_version": 1, + "name": "loaded_device", + "friendly_name": "Loaded Device", + "comment": "Loaded from file", + "esphome_version": "2024.1.0", + "src_version": 2, + "address": "10.0.0.1", + "web_port": 8080, + "esp_platform": "ESP32", + "build_path": "/loaded/build", + "firmware_bin_path": "/loaded/firmware.bin", + "loaded_integrations": ["wifi", "api"], + "loaded_platforms": ["sensor"], + "no_mdns": True, + "framework": "arduino", + "core_platform": "esp32", + } + + file_path = tmp_path / "storage.json" + file_path.write_text(json.dumps(storage_data)) + + result = storage_json.StorageJSON.load(file_path) + + assert result is not None + assert result.name == "loaded_device" + assert result.friendly_name == "Loaded Device" + assert result.comment == "Loaded from file" + assert result.esphome_version == "2024.1.0" + assert result.src_version == 2 + assert result.address == "10.0.0.1" + assert result.web_port == 8080 + assert result.target_platform == "ESP32" + assert result.build_path == Path("/loaded/build") + assert result.firmware_bin_path == Path("/loaded/firmware.bin") + assert result.loaded_integrations == {"wifi", "api"} + assert result.loaded_platforms == {"sensor"} + assert result.no_mdns is True + assert result.framework == "arduino" + assert result.core_platform == "esp32" + + +def test_storage_json_load_invalid_file(tmp_path: Path) -> None: + """Test StorageJSON.load with invalid JSON file.""" + file_path = tmp_path / "invalid.json" + file_path.write_text("not valid json{") + + result = storage_json.StorageJSON.load(file_path) + + assert result is None + + +def test_storage_json_load_nonexistent_file() -> None: + """Test StorageJSON.load with non-existent file.""" + result = storage_json.StorageJSON.load("/nonexistent/file.json") + + assert result is None + + +def test_storage_json_equality() -> None: + """Test StorageJSON equality comparison.""" + storage1 = storage_json.StorageJSON( + storage_version=1, + name="test", + friendly_name="Test", + comment=None, + esphome_version="2024.1.0", + src_version=1, + address="test.local", + web_port=80, + target_platform="ESP32", + build_path="/build", + firmware_bin_path="/firmware.bin", + loaded_integrations={"wifi"}, + loaded_platforms=set(), + no_mdns=False, + ) + + storage2 = storage_json.StorageJSON( + storage_version=1, + name="test", + friendly_name="Test", + comment=None, + esphome_version="2024.1.0", + src_version=1, + address="test.local", + web_port=80, + target_platform="ESP32", + build_path="/build", + firmware_bin_path="/firmware.bin", + loaded_integrations={"wifi"}, + loaded_platforms=set(), + no_mdns=False, + ) + + storage3 = storage_json.StorageJSON( + storage_version=1, + name="different", # Different name + friendly_name="Test", + comment=None, + esphome_version="2024.1.0", + src_version=1, + address="test.local", + web_port=80, + target_platform="ESP32", + build_path="/build", + firmware_bin_path="/firmware.bin", + loaded_integrations={"wifi"}, + loaded_platforms=set(), + no_mdns=False, + ) + + assert storage1 == storage2 + assert storage1 != storage3 + assert storage1 != "not a storage object" + + +def test_esphome_storage_json_as_dict() -> None: + """Test EsphomeStorageJSON.as_dict returns correct dictionary.""" + storage = storage_json.EsphomeStorageJSON( + storage_version=1, + cookie_secret="secret123", + last_update_check="2024-01-15T10:30:00", + remote_version="2024.1.1", + ) + + result = storage.as_dict() + + assert result["storage_version"] == 1 + assert result["cookie_secret"] == "secret123" + assert result["last_update_check"] == "2024-01-15T10:30:00" + assert result["remote_version"] == "2024.1.1" + + +def test_esphome_storage_json_last_update_check_property() -> None: + """Test EsphomeStorageJSON.last_update_check property.""" + storage = storage_json.EsphomeStorageJSON( + storage_version=1, + cookie_secret="secret", + last_update_check="2024-01-15T10:30:00", + remote_version=None, + ) + + # Test getter + result = storage.last_update_check + assert isinstance(result, datetime) + assert result.year == 2024 + assert result.month == 1 + assert result.day == 15 + assert result.hour == 10 + assert result.minute == 30 + + # Test setter + new_date = datetime(2024, 2, 20, 15, 45, 30) + storage.last_update_check = new_date + assert storage.last_update_check_str == "2024-02-20T15:45:30" + + +def test_esphome_storage_json_last_update_check_invalid() -> None: + """Test EsphomeStorageJSON.last_update_check with invalid date.""" + storage = storage_json.EsphomeStorageJSON( + storage_version=1, + cookie_secret="secret", + last_update_check="invalid date", + remote_version=None, + ) + + result = storage.last_update_check + assert result is None + + +def test_esphome_storage_json_to_json() -> None: + """Test EsphomeStorageJSON.to_json returns valid JSON string.""" + storage = storage_json.EsphomeStorageJSON( + storage_version=1, + cookie_secret="mysecret", + last_update_check="2024-01-15T10:30:00", + remote_version="2024.1.1", + ) + + json_str = storage.to_json() + + # Should be valid JSON + parsed = json.loads(json_str) + assert parsed["cookie_secret"] == "mysecret" + assert parsed["storage_version"] == 1 + + # Should end with newline + assert json_str.endswith("\n") + + +def test_esphome_storage_json_save(tmp_path: Path) -> None: + """Test EsphomeStorageJSON.save writes file correctly.""" + storage = storage_json.EsphomeStorageJSON( + storage_version=1, + cookie_secret="secret", + last_update_check=None, + remote_version=None, + ) + + save_path = tmp_path / "esphome.json" + + with patch("esphome.storage_json.write_file_if_changed") as mock_write: + storage.save(str(save_path)) + mock_write.assert_called_once_with(str(save_path), storage.to_json()) + + +def test_esphome_storage_json_load_valid_file(tmp_path: Path) -> None: + """Test EsphomeStorageJSON.load with valid JSON file.""" + storage_data = { + "storage_version": 1, + "cookie_secret": "loaded_secret", + "last_update_check": "2024-01-20T14:30:00", + "remote_version": "2024.1.2", + } + + file_path = tmp_path / "esphome.json" + file_path.write_text(json.dumps(storage_data)) + + result = storage_json.EsphomeStorageJSON.load(str(file_path)) + + assert result is not None + assert result.storage_version == 1 + assert result.cookie_secret == "loaded_secret" + assert result.last_update_check_str == "2024-01-20T14:30:00" + assert result.remote_version == "2024.1.2" + + +def test_esphome_storage_json_load_invalid_file(tmp_path: Path) -> None: + """Test EsphomeStorageJSON.load with invalid JSON file.""" + file_path = tmp_path / "invalid.json" + file_path.write_text("not valid json{") + + result = storage_json.EsphomeStorageJSON.load(str(file_path)) + + assert result is None + + +def test_esphome_storage_json_load_nonexistent_file() -> None: + """Test EsphomeStorageJSON.load with non-existent file.""" + result = storage_json.EsphomeStorageJSON.load("/nonexistent/file.json") + + assert result is None + + +def test_esphome_storage_json_get_default() -> None: + """Test EsphomeStorageJSON.get_default creates default storage.""" + with patch("esphome.storage_json.os.urandom") as mock_urandom: + # Mock urandom to return predictable bytes + mock_urandom.return_value = b"test" * 16 # 64 bytes + + result = storage_json.EsphomeStorageJSON.get_default() + + assert result.storage_version == 1 + assert len(result.cookie_secret) == 128 # 64 bytes hex = 128 chars + assert result.last_update_check is None + assert result.remote_version is None + + +def test_esphome_storage_json_equality() -> None: + """Test EsphomeStorageJSON equality comparison.""" + storage1 = storage_json.EsphomeStorageJSON( + storage_version=1, + cookie_secret="secret", + last_update_check="2024-01-15T10:30:00", + remote_version="2024.1.1", + ) + + storage2 = storage_json.EsphomeStorageJSON( + storage_version=1, + cookie_secret="secret", + last_update_check="2024-01-15T10:30:00", + remote_version="2024.1.1", + ) + + storage3 = storage_json.EsphomeStorageJSON( + storage_version=1, + cookie_secret="different", # Different secret + last_update_check="2024-01-15T10:30:00", + remote_version="2024.1.1", + ) + + assert storage1 == storage2 + assert storage1 != storage3 + assert storage1 != "not a storage object" + + +def test_storage_json_load_legacy_esphomeyaml_version(tmp_path: Path) -> None: + """Test loading storage with legacy esphomeyaml_version field.""" + storage_data = { + "storage_version": 1, + "name": "legacy_device", + "friendly_name": "Legacy Device", + "esphomeyaml_version": "1.14.0", # Legacy field name + "address": "legacy.local", + "esp_platform": "ESP8266", + } + + file_path = tmp_path / "legacy.json" + file_path.write_text(json.dumps(storage_data)) + + result = storage_json.StorageJSON.load(file_path) + + assert result is not None + assert result.esphome_version == "1.14.0" # Should map to esphome_version diff --git a/tests/unit_tests/test_substitutions.py b/tests/unit_tests/test_substitutions.py index b2b7cb1ea4..7d50b44506 100644 --- a/tests/unit_tests/test_substitutions.py +++ b/tests/unit_tests/test_substitutions.py @@ -1,10 +1,15 @@ import glob import logging -import os +from pathlib import Path +from typing import Any -from esphome import yaml_util +from esphome import config as config_module, yaml_util from esphome.components import substitutions -from esphome.const import CONF_PACKAGES +from esphome.config import resolve_extend_remove +from esphome.config_helpers import merge_config +from esphome.const import CONF_PACKAGES, CONF_SUBSTITUTIONS +from esphome.core import CORE +from esphome.util import OrderedDict _LOGGER = logging.getLogger(__name__) @@ -52,9 +57,31 @@ def dict_diff(a, b, path=""): return diffs -def write_yaml(path, data): - with open(path, "w", encoding="utf-8") as f: - f.write(yaml_util.dump(data)) +def write_yaml(path: Path, data: dict) -> None: + path.write_text(yaml_util.dump(data), encoding="utf-8") + + +def verify_database(value: Any, path: str = "") -> str | None: + if isinstance(value, list): + for i, v in enumerate(value): + result = verify_database(v, f"{path}[{i}]") + if result is not None: + return result + return None + if isinstance(value, dict): + for k, v in value.items(): + key_result = verify_database(k, f"{path}/{k}") + if key_result is not None: + return key_result + value_result = verify_database(v, f"{path}/{k}") + if value_result is not None: + return value_result + return None + if isinstance(value, str): + if not isinstance(value, yaml_util.ESPHomeDataBase): + return f"{path}: {value!r} is not ESPHomeDataBase" + return None + return None def test_substitutions_fixtures(fixture_path): @@ -64,11 +91,10 @@ def test_substitutions_fixtures(fixture_path): failures = [] for source_path in sources: + source_path = Path(source_path) try: - expected_path = source_path.replace(".input.yaml", ".approved.yaml") - test_case = os.path.splitext(os.path.basename(source_path))[0].replace( - ".input", "" - ) + expected_path = source_path.with_suffix("").with_suffix(".approved.yaml") + test_case = source_path.with_suffix("").stem # Load using ESPHome's YAML loader config = yaml_util.load_yaml(source_path) @@ -80,13 +106,18 @@ def test_substitutions_fixtures(fixture_path): substitutions.do_substitution_pass(config, None) + resolve_extend_remove(config) + verify_database_result = verify_database(config) + if verify_database_result is not None: + raise AssertionError(verify_database_result) + # Also load expected using ESPHome's loader, or use {} if missing and DEV_MODE - if os.path.isfile(expected_path): + if expected_path.is_file(): expected = yaml_util.load_yaml(expected_path) elif DEV_MODE: expected = {} else: - assert os.path.isfile(expected_path), ( + assert expected_path.is_file(), ( f"Expected file missing: {expected_path}" ) @@ -97,16 +128,14 @@ def test_substitutions_fixtures(fixture_path): if got_sorted != expected_sorted: diff = "\n".join(dict_diff(got_sorted, expected_sorted)) msg = ( - f"Substitution result mismatch for {os.path.basename(source_path)}\n" + f"Substitution result mismatch for {source_path.name}\n" f"Diff:\n{diff}\n\n" f"Got: {got_sorted}\n" f"Expected: {expected_sorted}" ) # Write out the received file when test fails if DEV_MODE: - received_path = os.path.join( - os.path.dirname(source_path), f"{test_case}.received.yaml" - ) + received_path = source_path.with_name(f"{test_case}.received.yaml") write_yaml(received_path, config) print(msg) failures.append(msg) @@ -122,3 +151,200 @@ def test_substitutions_fixtures(fixture_path): if DEV_MODE: _LOGGER.error("Tests passed, but Dev mode is enabled.") assert not DEV_MODE # make sure DEV_MODE is disabled after you are finished. + + +def test_substitutions_with_command_line_maintains_ordered_dict() -> None: + """Test that substitutions remain an OrderedDict when command line substitutions are provided, + and that move_to_end() can be called successfully. + + This is a regression test for https://github.com/esphome/esphome/issues/11182 + where the config would become a regular dict and fail when move_to_end() was called. + """ + # Create an OrderedDict config with substitutions + config = OrderedDict() + config["esphome"] = {"name": "test"} + config[CONF_SUBSTITUTIONS] = {"var1": "value1", "var2": "value2"} + config["other_key"] = "other_value" + + # Command line substitutions that should override + command_line_subs = {"var2": "override", "var3": "new_value"} + + # Call do_substitution_pass with command line substitutions + substitutions.do_substitution_pass(config, command_line_subs) + + # Verify that config is still an OrderedDict + assert isinstance(config, OrderedDict), "Config should remain an OrderedDict" + + # Verify substitutions are at the beginning (move_to_end with last=False) + keys = list(config.keys()) + assert keys[0] == CONF_SUBSTITUTIONS, "Substitutions should be first key" + + # Verify substitutions were properly merged + assert config[CONF_SUBSTITUTIONS]["var1"] == "value1" + assert config[CONF_SUBSTITUTIONS]["var2"] == "override" + assert config[CONF_SUBSTITUTIONS]["var3"] == "new_value" + + # Verify config[CONF_SUBSTITUTIONS] is also an OrderedDict + assert isinstance(config[CONF_SUBSTITUTIONS], OrderedDict), ( + "Substitutions should be an OrderedDict" + ) + + +def test_substitutions_without_command_line_maintains_ordered_dict() -> None: + """Test that substitutions work correctly without command line substitutions.""" + config = OrderedDict() + config["esphome"] = {"name": "test"} + config[CONF_SUBSTITUTIONS] = {"var1": "value1"} + config["other_key"] = "other_value" + + # Call without command line substitutions + substitutions.do_substitution_pass(config, None) + + # Verify that config is still an OrderedDict + assert isinstance(config, OrderedDict), "Config should remain an OrderedDict" + + # Verify substitutions are at the beginning + keys = list(config.keys()) + assert keys[0] == CONF_SUBSTITUTIONS, "Substitutions should be first key" + + +def test_substitutions_after_merge_config_maintains_ordered_dict() -> None: + """Test that substitutions work after merge_config (packages scenario). + + This is a regression test for https://github.com/esphome/esphome/issues/11182 + where using packages would cause config to become a regular dict, breaking move_to_end(). + """ + # Simulate what happens with packages - merge two OrderedDict configs + base_config = OrderedDict() + base_config["esphome"] = {"name": "base"} + base_config[CONF_SUBSTITUTIONS] = {"var1": "value1"} + + package_config = OrderedDict() + package_config["sensor"] = [{"platform": "template"}] + package_config[CONF_SUBSTITUTIONS] = {"var2": "value2"} + + # Merge configs (simulating package merge) + merged_config = merge_config(base_config, package_config) + + # Verify merged config is still an OrderedDict + assert isinstance(merged_config, OrderedDict), ( + "Merged config should be an OrderedDict" + ) + + # Now try to run substitution pass on the merged config + substitutions.do_substitution_pass(merged_config, None) + + # Should not raise AttributeError + assert isinstance(merged_config, OrderedDict), ( + "Config should still be OrderedDict after substitution pass" + ) + keys = list(merged_config.keys()) + assert keys[0] == CONF_SUBSTITUTIONS, "Substitutions should be first key" + + +def test_validate_config_with_command_line_substitutions_maintains_ordered_dict( + tmp_path, +) -> None: + """Test that validate_config preserves OrderedDict when merging command-line substitutions. + + This tests the code path in config.py where result[CONF_SUBSTITUTIONS] is set + using merge_dicts_ordered() with command-line substitutions provided. + """ + # Create a minimal valid config + test_config = OrderedDict() + test_config["esphome"] = {"name": "test_device", "platform": "ESP32"} + test_config[CONF_SUBSTITUTIONS] = OrderedDict({"var1": "value1", "var2": "value2"}) + test_config["esp32"] = {"board": "esp32dev"} + + # Command line substitutions that should override + command_line_subs = {"var2": "override", "var3": "new_value"} + + # Set up CORE for the test with a proper Path object + test_yaml = tmp_path / "test.yaml" + test_yaml.write_text("# test config") + CORE.config_path = test_yaml + + # Call validate_config with command line substitutions + result = config_module.validate_config(test_config, command_line_subs) + + # Verify that result[CONF_SUBSTITUTIONS] is an OrderedDict + assert isinstance(result.get(CONF_SUBSTITUTIONS), OrderedDict), ( + "Result substitutions should be an OrderedDict" + ) + + # Verify substitutions were properly merged + assert result[CONF_SUBSTITUTIONS]["var1"] == "value1" + assert result[CONF_SUBSTITUTIONS]["var2"] == "override" + assert result[CONF_SUBSTITUTIONS]["var3"] == "new_value" + + +def test_validate_config_without_command_line_substitutions_maintains_ordered_dict( + tmp_path, +) -> None: + """Test that validate_config preserves OrderedDict without command-line substitutions. + + This tests the code path in config.py where result[CONF_SUBSTITUTIONS] is set + using merge_dicts_ordered() when command_line_substitutions is None. + """ + # Create a minimal valid config + test_config = OrderedDict() + test_config["esphome"] = {"name": "test_device", "platform": "ESP32"} + test_config[CONF_SUBSTITUTIONS] = OrderedDict({"var1": "value1", "var2": "value2"}) + test_config["esp32"] = {"board": "esp32dev"} + + # Set up CORE for the test with a proper Path object + test_yaml = tmp_path / "test.yaml" + test_yaml.write_text("# test config") + CORE.config_path = test_yaml + + # Call validate_config without command line substitutions + result = config_module.validate_config(test_config, None) + + # Verify that result[CONF_SUBSTITUTIONS] is an OrderedDict + assert isinstance(result.get(CONF_SUBSTITUTIONS), OrderedDict), ( + "Result substitutions should be an OrderedDict" + ) + + # Verify substitutions are unchanged + assert result[CONF_SUBSTITUTIONS]["var1"] == "value1" + assert result[CONF_SUBSTITUTIONS]["var2"] == "value2" + + +def test_merge_config_preserves_ordered_dict() -> None: + """Test that merge_config preserves OrderedDict type. + + This is a regression test to ensure merge_config doesn't lose OrderedDict type + when merging configs, which causes AttributeError on move_to_end(). + """ + # Test OrderedDict + dict = OrderedDict + od = OrderedDict([("a", 1), ("b", 2)]) + d = {"b": 20, "c": 3} + result = merge_config(od, d) + assert isinstance(result, OrderedDict), ( + "OrderedDict + dict should return OrderedDict" + ) + + # Test dict + OrderedDict = OrderedDict + d = {"a": 1, "b": 2} + od = OrderedDict([("b", 20), ("c", 3)]) + result = merge_config(d, od) + assert isinstance(result, OrderedDict), ( + "dict + OrderedDict should return OrderedDict" + ) + + # Test OrderedDict + OrderedDict = OrderedDict + od1 = OrderedDict([("a", 1), ("b", 2)]) + od2 = OrderedDict([("b", 20), ("c", 3)]) + result = merge_config(od1, od2) + assert isinstance(result, OrderedDict), ( + "OrderedDict + OrderedDict should return OrderedDict" + ) + + # Test that dict + dict still returns regular dict (no unnecessary conversion) + d1 = {"a": 1, "b": 2} + d2 = {"b": 20, "c": 3} + result = merge_config(d1, d2) + assert isinstance(result, dict), "dict + dict should return dict" + assert not isinstance(result, OrderedDict), ( + "dict + dict should not return OrderedDict" + ) diff --git a/tests/unit_tests/test_util.py b/tests/unit_tests/test_util.py new file mode 100644 index 0000000000..85873caea8 --- /dev/null +++ b/tests/unit_tests/test_util.py @@ -0,0 +1,404 @@ +"""Tests for esphome.util module.""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from esphome import util + + +def test_list_yaml_files_with_files_and_directories(tmp_path: Path) -> None: + """Test that list_yaml_files handles both files and directories.""" + # Create directory structure + dir1 = tmp_path / "configs" + dir1.mkdir() + dir2 = tmp_path / "more_configs" + dir2.mkdir() + + # Create YAML files in directories + (dir1 / "config1.yaml").write_text("test: 1") + (dir1 / "config2.yml").write_text("test: 2") + (dir1 / "not_yaml.txt").write_text("not yaml") + + (dir2 / "config3.yaml").write_text("test: 3") + + # Create standalone YAML files + standalone1 = tmp_path / "standalone.yaml" + standalone1.write_text("test: 4") + standalone2 = tmp_path / "another.yml" + standalone2.write_text("test: 5") + + # Test with mixed input (directories and files) + configs = [ + dir1, + standalone1, + dir2, + standalone2, + ] + + result = util.list_yaml_files(configs) + + # Should include all YAML files but not the .txt file + assert set(result) == { + dir1 / "config1.yaml", + dir1 / "config2.yml", + dir2 / "config3.yaml", + standalone1, + standalone2, + } + # Check that results are sorted + assert result == sorted(result) + + +def test_list_yaml_files_only_directories(tmp_path: Path) -> None: + """Test list_yaml_files with only directories.""" + dir1 = tmp_path / "dir1" + dir1.mkdir() + dir2 = tmp_path / "dir2" + dir2.mkdir() + + (dir1 / "a.yaml").write_text("test: a") + (dir1 / "b.yml").write_text("test: b") + (dir2 / "c.yaml").write_text("test: c") + + result = util.list_yaml_files([dir1, dir2]) + + assert set(result) == { + dir1 / "a.yaml", + dir1 / "b.yml", + dir2 / "c.yaml", + } + assert result == sorted(result) + + +def test_list_yaml_files_only_files(tmp_path: Path) -> None: + """Test list_yaml_files with only files.""" + file1 = tmp_path / "file1.yaml" + file2 = tmp_path / "file2.yml" + file3 = tmp_path / "file3.yaml" + non_yaml = tmp_path / "not_yaml.json" + + file1.write_text("test: 1") + file2.write_text("test: 2") + file3.write_text("test: 3") + non_yaml.write_text("{}") + + # Include a non-YAML file to test filtering + result = util.list_yaml_files( + [ + file1, + file2, + file3, + non_yaml, + ] + ) + + assert set(result) == { + file1, + file2, + file3, + } + assert result == sorted(result) + + +def test_list_yaml_files_empty_directory(tmp_path: Path) -> None: + """Test list_yaml_files with an empty directory.""" + empty_dir = tmp_path / "empty" + empty_dir.mkdir() + + result = util.list_yaml_files([empty_dir]) + + assert result == [] + + +def test_list_yaml_files_nonexistent_path(tmp_path: Path) -> None: + """Test list_yaml_files with a nonexistent path raises an error.""" + nonexistent = tmp_path / "nonexistent" + existing = tmp_path / "existing.yaml" + existing.write_text("test: 1") + + # Should raise an error for non-existent directory + with pytest.raises(FileNotFoundError): + util.list_yaml_files([nonexistent, existing]) + + +def test_list_yaml_files_mixed_extensions(tmp_path: Path) -> None: + """Test that both .yaml and .yml extensions are recognized.""" + dir1 = tmp_path / "configs" + dir1.mkdir() + + yaml_file = dir1 / "config.yaml" + yml_file = dir1 / "config.yml" + other_file = dir1 / "config.txt" + + yaml_file.write_text("test: yaml") + yml_file.write_text("test: yml") + other_file.write_text("test: txt") + + result = util.list_yaml_files([dir1]) + + assert set(result) == { + yaml_file, + yml_file, + } + + +def test_list_yaml_files_does_not_recurse_into_subdirectories(tmp_path: Path) -> None: + """Test that list_yaml_files only finds files in specified directory, not subdirectories.""" + # Create directory structure with YAML files at different depths + root = tmp_path / "configs" + root.mkdir() + + # Create YAML files in the root directory + (root / "config1.yaml").write_text("test: 1") + (root / "config2.yml").write_text("test: 2") + (root / "device.yaml").write_text("test: device") + + # Create subdirectory with YAML files (should NOT be found) + subdir = root / "subdir" + subdir.mkdir() + (subdir / "nested1.yaml").write_text("test: nested1") + (subdir / "nested2.yml").write_text("test: nested2") + + # Create deeper subdirectory (should NOT be found) + deep_subdir = subdir / "deeper" + deep_subdir.mkdir() + (deep_subdir / "very_nested.yaml").write_text("test: very_nested") + + # Test listing files from the root directory + result = util.list_yaml_files([str(root)]) + + # Should only find the 3 files in root, not the 3 in subdirectories + assert len(result) == 3 + + # Check that only root-level files are found + assert root / "config1.yaml" in result + assert root / "config2.yml" in result + assert root / "device.yaml" in result + + # Ensure nested files are NOT found + for r in result: + r_str = str(r) + assert "subdir" not in r_str + assert "deeper" not in r_str + assert "nested1.yaml" not in r_str + assert "nested2.yml" not in r_str + assert "very_nested.yaml" not in r_str + + +def test_list_yaml_files_excludes_secrets(tmp_path: Path) -> None: + """Test that secrets.yaml and secrets.yml are excluded.""" + root = tmp_path / "configs" + root.mkdir() + + # Create various YAML files including secrets + (root / "config.yaml").write_text("test: config") + (root / "secrets.yaml").write_text("wifi_password: secret123") + (root / "secrets.yml").write_text("api_key: secret456") + (root / "device.yaml").write_text("test: device") + + result = util.list_yaml_files([str(root)]) + + # Should find 2 files (config.yaml and device.yaml), not secrets + assert len(result) == 2 + assert root / "config.yaml" in result + assert root / "device.yaml" in result + assert root / "secrets.yaml" not in result + assert root / "secrets.yml" not in result + + +def test_list_yaml_files_excludes_hidden_files(tmp_path: Path) -> None: + """Test that hidden files (starting with .) are excluded.""" + root = tmp_path / "configs" + root.mkdir() + + # Create regular and hidden YAML files + (root / "config.yaml").write_text("test: config") + (root / ".hidden.yaml").write_text("test: hidden") + (root / ".backup.yml").write_text("test: backup") + (root / "device.yaml").write_text("test: device") + + result = util.list_yaml_files([str(root)]) + + # Should find only non-hidden files + assert len(result) == 2 + assert root / "config.yaml" in result + assert root / "device.yaml" in result + assert root / ".hidden.yaml" not in result + assert root / ".backup.yml" not in result + + +def test_filter_yaml_files_basic() -> None: + """Test filter_yaml_files function.""" + files = [ + Path("/path/to/config.yaml"), + Path("/path/to/device.yml"), + Path("/path/to/readme.txt"), + Path("/path/to/script.py"), + Path("/path/to/data.json"), + Path("/path/to/another.yaml"), + ] + + result = util.filter_yaml_files(files) + + assert len(result) == 3 + assert Path("/path/to/config.yaml") in result + assert Path("/path/to/device.yml") in result + assert Path("/path/to/another.yaml") in result + assert Path("/path/to/readme.txt") not in result + assert Path("/path/to/script.py") not in result + assert Path("/path/to/data.json") not in result + + +def test_filter_yaml_files_excludes_secrets() -> None: + """Test that filter_yaml_files excludes secrets files.""" + files = [ + Path("/path/to/config.yaml"), + Path("/path/to/secrets.yaml"), + Path("/path/to/secrets.yml"), + Path("/path/to/device.yaml"), + Path("/some/dir/secrets.yaml"), + ] + + result = util.filter_yaml_files(files) + + assert len(result) == 2 + assert Path("/path/to/config.yaml") in result + assert Path("/path/to/device.yaml") in result + assert Path("/path/to/secrets.yaml") not in result + assert Path("/path/to/secrets.yml") not in result + assert Path("/some/dir/secrets.yaml") not in result + + +def test_filter_yaml_files_excludes_hidden() -> None: + """Test that filter_yaml_files excludes hidden files.""" + files = [ + Path("/path/to/config.yaml"), + Path("/path/to/.hidden.yaml"), + Path("/path/to/.backup.yml"), + Path("/path/to/device.yaml"), + Path("/some/dir/.config.yaml"), + ] + + result = util.filter_yaml_files(files) + + assert len(result) == 2 + assert Path("/path/to/config.yaml") in result + assert Path("/path/to/device.yaml") in result + assert Path("/path/to/.hidden.yaml") not in result + assert Path("/path/to/.backup.yml") not in result + assert Path("/some/dir/.config.yaml") not in result + + +def test_filter_yaml_files_case_sensitive() -> None: + """Test that filter_yaml_files is case-sensitive for extensions.""" + files = [ + Path("/path/to/config.yaml"), + Path("/path/to/config.YAML"), + Path("/path/to/config.YML"), + Path("/path/to/config.Yaml"), + Path("/path/to/config.yml"), + ] + + result = util.filter_yaml_files(files) + + # Should only match lowercase .yaml and .yml + assert len(result) == 2 + + # Check the actual suffixes to ensure case-sensitive filtering + result_suffixes = [p.suffix for p in result] + assert ".yaml" in result_suffixes + assert ".yml" in result_suffixes + + # Verify the filtered files have the expected names + result_names = [p.name for p in result] + assert "config.yaml" in result_names + assert "config.yml" in result_names + # Ensure uppercase extensions are NOT included + assert "config.YAML" not in result_names + assert "config.YML" not in result_names + assert "config.Yaml" not in result_names + + +@pytest.mark.parametrize( + ("input_str", "expected"), + [ + # Empty string + ("", "''"), + # Simple strings that don't need quoting + ("hello", "hello"), + ("test123", "test123"), + ("file.txt", "file.txt"), + ("/path/to/file", "/path/to/file"), + ("user@host", "user@host"), + ("value:123", "value:123"), + ("item,list", "item,list"), + ("path-with-dash", "path-with-dash"), + # Strings that need quoting + ("hello world", "'hello world'"), + ("test\ttab", "'test\ttab'"), + ("line\nbreak", "'line\nbreak'"), + ("semicolon;here", "'semicolon;here'"), + ("pipe|symbol", "'pipe|symbol'"), + ("redirect>file", "'redirect>file'"), + ("redirect None: + """Test shlex_quote properly escapes shell arguments.""" + assert util.shlex_quote(input_str) == expected + + +def test_shlex_quote_safe_characters() -> None: + """Test that safe characters are not quoted.""" + # These characters are considered safe and shouldn't be quoted + safe_chars = ( + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@%+=:,./-_" + ) + for char in safe_chars: + assert util.shlex_quote(char) == char + assert util.shlex_quote(f"test{char}test") == f"test{char}test" + + +def test_shlex_quote_unsafe_characters() -> None: + """Test that unsafe characters trigger quoting.""" + # These characters should trigger quoting + unsafe_chars = ' \t\n;|>&<$`"\\?*[](){}!#~^' + for char in unsafe_chars: + result = util.shlex_quote(f"test{char}test") + assert result.startswith("'") + assert result.endswith("'") + + +def test_shlex_quote_edge_cases() -> None: + """Test edge cases for shlex_quote.""" + # Multiple single quotes + assert util.shlex_quote("'''") == "''\"'\"''\"'\"''\"'\"''" + + # Mixed quotes + assert util.shlex_quote('"\'"') == "'\"'\"'\"'\"'" + + # Only whitespace + assert util.shlex_quote(" ") == "' '" + assert util.shlex_quote("\t") == "'\t'" + assert util.shlex_quote("\n") == "'\n'" + assert util.shlex_quote(" ") == "' '" diff --git a/tests/unit_tests/test_vscode.py b/tests/unit_tests/test_vscode.py index 4b28a2215b..63bdf3e255 100644 --- a/tests/unit_tests/test_vscode.py +++ b/tests/unit_tests/test_vscode.py @@ -1,5 +1,5 @@ import json -import os +from pathlib import Path from unittest.mock import Mock, patch from esphome import vscode @@ -45,7 +45,7 @@ RESULT_NO_ERROR = '{"type": "result", "yaml_errors": [], "validation_errors": [] def test_multi_file(): - source_path = os.path.join("dir_path", "x.yaml") + source_path = str(Path("dir_path", "x.yaml")) output_lines = _run_repl_test( [ _validate(source_path), @@ -62,7 +62,7 @@ esp8266: expected_lines = [ _read_file(source_path), - _read_file(os.path.join("dir_path", "secrets.yaml")), + _read_file(str(Path("dir_path", "secrets.yaml"))), RESULT_NO_ERROR, ] @@ -70,7 +70,7 @@ esp8266: def test_shows_correct_range_error(): - source_path = os.path.join("dir_path", "x.yaml") + source_path = str(Path("dir_path", "x.yaml")) output_lines = _run_repl_test( [ _validate(source_path), @@ -98,7 +98,7 @@ esp8266: def test_shows_correct_loaded_file_error(): - source_path = os.path.join("dir_path", "x.yaml") + source_path = str(Path("dir_path", "x.yaml")) output_lines = _run_repl_test( [ _validate(source_path), @@ -121,7 +121,7 @@ packages: validation_error = error["validation_errors"][0] assert validation_error["message"].startswith("[broad] is an invalid option for") range = validation_error["range"] - assert range["document"] == os.path.join("dir_path", ".pkg.esp8266.yaml") + assert range["document"] == str(Path("dir_path", ".pkg.esp8266.yaml")) assert range["start_line"] == 1 assert range["start_col"] == 2 assert range["end_line"] == 1 diff --git a/tests/unit_tests/test_wizard.py b/tests/unit_tests/test_wizard.py index ab20b2abb5..fd53a0b0b7 100644 --- a/tests/unit_tests/test_wizard.py +++ b/tests/unit_tests/test_wizard.py @@ -1,9 +1,11 @@ """Tests for the wizard.py file.""" -import os +from pathlib import Path +from typing import Any from unittest.mock import MagicMock import pytest +from pytest import MonkeyPatch from esphome.components.bk72xx.boards import BK72XX_BOARD_PINS from esphome.components.esp32.boards import ESP32_BOARD_PINS @@ -15,8 +17,9 @@ import esphome.wizard as wz @pytest.fixture -def default_config(): +def default_config() -> dict[str, Any]: return { + "type": "basic", "name": "test-name", "platform": "ESP8266", "board": "esp01_1m", @@ -27,7 +30,7 @@ def default_config(): @pytest.fixture -def wizard_answers(): +def wizard_answers() -> list[str]: return [ "test-node", # Name of the node "ESP8266", # platform @@ -52,7 +55,9 @@ def test_sanitize_quotes_replaces_with_escaped_char(): assert output_str == '\\"key\\": \\"value\\"' -def test_config_file_fallback_ap_includes_descriptive_name(default_config): +def test_config_file_fallback_ap_includes_descriptive_name( + default_config: dict[str, Any], +): """ The fallback AP should include the node and a descriptive name """ @@ -66,7 +71,9 @@ def test_config_file_fallback_ap_includes_descriptive_name(default_config): assert 'ssid: "Test Node Fallback Hotspot"' in config -def test_config_file_fallback_ap_name_less_than_32_chars(default_config): +def test_config_file_fallback_ap_name_less_than_32_chars( + default_config: dict[str, Any], +): """ The fallback AP name must be less than 32 chars. Since it is composed of the node name and "Fallback Hotspot" this can be too long and needs truncating @@ -81,7 +88,7 @@ def test_config_file_fallback_ap_name_less_than_32_chars(default_config): assert 'ssid: "A Very Long Name For This Node"' in config -def test_config_file_should_include_ota(default_config): +def test_config_file_should_include_ota(default_config: dict[str, Any]): """ The Over-The-Air update should be enabled by default """ @@ -94,7 +101,9 @@ def test_config_file_should_include_ota(default_config): assert "ota:" in config -def test_config_file_should_include_ota_when_password_set(default_config): +def test_config_file_should_include_ota_when_password_set( + default_config: dict[str, Any], +): """ The Over-The-Air update should be enabled when a password is set """ @@ -108,14 +117,16 @@ def test_config_file_should_include_ota_when_password_set(default_config): assert "ota:" in config -def test_wizard_write_sets_platform(default_config, tmp_path, monkeypatch): +def test_wizard_write_sets_platform( + default_config: dict[str, Any], tmp_path: Path, monkeypatch: MonkeyPatch +): """ If the platform is not explicitly set, use "ESP8266" if the board is one of the ESP8266 boards """ # Given del default_config["platform"] monkeypatch.setattr(wz, "write_file", MagicMock()) - monkeypatch.setattr(CORE, "config_path", os.path.dirname(tmp_path)) + monkeypatch.setattr(CORE, "config_path", tmp_path.parent) # When wz.wizard_write(tmp_path, **default_config) @@ -125,8 +136,49 @@ def test_wizard_write_sets_platform(default_config, tmp_path, monkeypatch): assert "esp8266:" in generated_config +def test_wizard_empty_config(tmp_path: Path, monkeypatch: MonkeyPatch): + """ + The wizard should be able to create an empty configuration + """ + # Given + empty_config = { + "type": "empty", + "name": "test-empty", + } + monkeypatch.setattr(wz, "write_file", MagicMock()) + monkeypatch.setattr(CORE, "config_path", tmp_path.parent) + + # When + wz.wizard_write(tmp_path, **empty_config) + + # Then + generated_config = wz.write_file.call_args.args[1] + assert generated_config == "" + + +def test_wizard_upload_config(tmp_path: Path, monkeypatch: MonkeyPatch): + """ + The wizard should be able to import an base64 encoded configuration + """ + # Given + empty_config = { + "type": "upload", + "name": "test-upload", + "file_text": "# imported file 📁\n\n", + } + monkeypatch.setattr(wz, "write_file", MagicMock()) + monkeypatch.setattr(CORE, "config_path", tmp_path.parent) + + # When + wz.wizard_write(tmp_path, **empty_config) + + # Then + generated_config = wz.write_file.call_args.args[1] + assert generated_config == "# imported file 📁\n\n" + + def test_wizard_write_defaults_platform_from_board_esp8266( - default_config, tmp_path, monkeypatch + default_config: dict[str, Any], tmp_path: Path, monkeypatch: MonkeyPatch ): """ If the platform is not explicitly set, use "ESP8266" if the board is one of the ESP8266 boards @@ -136,7 +188,7 @@ def test_wizard_write_defaults_platform_from_board_esp8266( default_config["board"] = [*ESP8266_BOARD_PINS][0] monkeypatch.setattr(wz, "write_file", MagicMock()) - monkeypatch.setattr(CORE, "config_path", os.path.dirname(tmp_path)) + monkeypatch.setattr(CORE, "config_path", tmp_path.parent) # When wz.wizard_write(tmp_path, **default_config) @@ -147,7 +199,7 @@ def test_wizard_write_defaults_platform_from_board_esp8266( def test_wizard_write_defaults_platform_from_board_esp32( - default_config, tmp_path, monkeypatch + default_config: dict[str, Any], tmp_path: Path, monkeypatch: MonkeyPatch ): """ If the platform is not explicitly set, use "ESP32" if the board is one of the ESP32 boards @@ -157,7 +209,7 @@ def test_wizard_write_defaults_platform_from_board_esp32( default_config["board"] = [*ESP32_BOARD_PINS][0] monkeypatch.setattr(wz, "write_file", MagicMock()) - monkeypatch.setattr(CORE, "config_path", os.path.dirname(tmp_path)) + monkeypatch.setattr(CORE, "config_path", tmp_path.parent) # When wz.wizard_write(tmp_path, **default_config) @@ -168,7 +220,7 @@ def test_wizard_write_defaults_platform_from_board_esp32( def test_wizard_write_defaults_platform_from_board_bk72xx( - default_config, tmp_path, monkeypatch + default_config: dict[str, Any], tmp_path: Path, monkeypatch: MonkeyPatch ): """ If the platform is not explicitly set, use "BK72XX" if the board is one of BK72XX boards @@ -178,7 +230,7 @@ def test_wizard_write_defaults_platform_from_board_bk72xx( default_config["board"] = [*BK72XX_BOARD_PINS][0] monkeypatch.setattr(wz, "write_file", MagicMock()) - monkeypatch.setattr(CORE, "config_path", os.path.dirname(tmp_path)) + monkeypatch.setattr(CORE, "config_path", tmp_path.parent) # When wz.wizard_write(tmp_path, **default_config) @@ -189,7 +241,7 @@ def test_wizard_write_defaults_platform_from_board_bk72xx( def test_wizard_write_defaults_platform_from_board_ln882x( - default_config, tmp_path, monkeypatch + default_config: dict[str, Any], tmp_path: Path, monkeypatch: MonkeyPatch ): """ If the platform is not explicitly set, use "LN882X" if the board is one of LN882X boards @@ -199,7 +251,7 @@ def test_wizard_write_defaults_platform_from_board_ln882x( default_config["board"] = [*LN882X_BOARD_PINS][0] monkeypatch.setattr(wz, "write_file", MagicMock()) - monkeypatch.setattr(CORE, "config_path", os.path.dirname(tmp_path)) + monkeypatch.setattr(CORE, "config_path", tmp_path.parent) # When wz.wizard_write(tmp_path, **default_config) @@ -210,7 +262,7 @@ def test_wizard_write_defaults_platform_from_board_ln882x( def test_wizard_write_defaults_platform_from_board_rtl87xx( - default_config, tmp_path, monkeypatch + default_config: dict[str, Any], tmp_path: Path, monkeypatch: MonkeyPatch ): """ If the platform is not explicitly set, use "RTL87XX" if the board is one of RTL87XX boards @@ -220,7 +272,7 @@ def test_wizard_write_defaults_platform_from_board_rtl87xx( default_config["board"] = [*RTL87XX_BOARD_PINS][0] monkeypatch.setattr(wz, "write_file", MagicMock()) - monkeypatch.setattr(CORE, "config_path", os.path.dirname(tmp_path)) + monkeypatch.setattr(CORE, "config_path", tmp_path.parent) # When wz.wizard_write(tmp_path, **default_config) @@ -230,7 +282,7 @@ def test_wizard_write_defaults_platform_from_board_rtl87xx( assert "rtl87xx:" in generated_config -def test_safe_print_step_prints_step_number_and_description(monkeypatch): +def test_safe_print_step_prints_step_number_and_description(monkeypatch: MonkeyPatch): """ The safe_print_step function prints the step number and the passed description """ @@ -254,7 +306,7 @@ def test_safe_print_step_prints_step_number_and_description(monkeypatch): assert any(f"STEP {step_num}" in arg for arg in all_args) -def test_default_input_uses_default_if_no_input_supplied(monkeypatch): +def test_default_input_uses_default_if_no_input_supplied(monkeypatch: MonkeyPatch): """ The default_input() function should return the supplied default value if the user doesn't enter anything """ @@ -270,7 +322,7 @@ def test_default_input_uses_default_if_no_input_supplied(monkeypatch): assert retval == default_string -def test_default_input_uses_user_supplied_value(monkeypatch): +def test_default_input_uses_user_supplied_value(monkeypatch: MonkeyPatch): """ The default_input() function should return the value that the user enters """ @@ -309,7 +361,7 @@ def test_wizard_rejects_path_with_invalid_extension(): """ # Given - config_file = "test.json" + config_file = Path("test.json") # When retval = wz.wizard(config_file) @@ -318,29 +370,31 @@ def test_wizard_rejects_path_with_invalid_extension(): assert retval == 1 -def test_wizard_rejects_existing_files(tmpdir): +def test_wizard_rejects_existing_files(tmp_path): """ The wizard should reject any configuration file that already exists """ # Given - config_file = tmpdir.join("test.yaml") - config_file.write("") + config_file = tmp_path / "test.yaml" + config_file.write_text("") # When - retval = wz.wizard(str(config_file)) + retval = wz.wizard(config_file) # Then assert retval == 2 -def test_wizard_accepts_default_answers_esp8266(tmpdir, monkeypatch, wizard_answers): +def test_wizard_accepts_default_answers_esp8266( + tmp_path: Path, monkeypatch: MonkeyPatch, wizard_answers: list[str] +): """ The wizard should accept the given default answers for esp8266 """ # Given - config_file = tmpdir.join("test.yaml") + config_file = tmp_path / "test.yaml" input_mock = MagicMock(side_effect=wizard_answers) monkeypatch.setattr("builtins.input", input_mock) monkeypatch.setattr(wz, "safe_print", lambda t=None, end=None: 0) @@ -348,13 +402,15 @@ def test_wizard_accepts_default_answers_esp8266(tmpdir, monkeypatch, wizard_answ monkeypatch.setattr(wz, "wizard_write", MagicMock()) # When - retval = wz.wizard(str(config_file)) + retval = wz.wizard(config_file) # Then assert retval == 0 -def test_wizard_accepts_default_answers_esp32(tmpdir, monkeypatch, wizard_answers): +def test_wizard_accepts_default_answers_esp32( + tmp_path: Path, monkeypatch: MonkeyPatch, wizard_answers: list[str] +): """ The wizard should accept the given default answers for esp32 """ @@ -362,7 +418,7 @@ def test_wizard_accepts_default_answers_esp32(tmpdir, monkeypatch, wizard_answer # Given wizard_answers[1] = "ESP32" wizard_answers[2] = "nodemcu-32s" - config_file = tmpdir.join("test.yaml") + config_file = tmp_path / "test.yaml" input_mock = MagicMock(side_effect=wizard_answers) monkeypatch.setattr("builtins.input", input_mock) monkeypatch.setattr(wz, "safe_print", lambda t=None, end=None: 0) @@ -370,13 +426,15 @@ def test_wizard_accepts_default_answers_esp32(tmpdir, monkeypatch, wizard_answer monkeypatch.setattr(wz, "wizard_write", MagicMock()) # When - retval = wz.wizard(str(config_file)) + retval = wz.wizard(config_file) # Then assert retval == 0 -def test_wizard_offers_better_node_name(tmpdir, monkeypatch, wizard_answers): +def test_wizard_offers_better_node_name( + tmp_path: Path, monkeypatch: MonkeyPatch, wizard_answers: list[str] +): """ When the node name does not conform, a better alternative is offered * Removes special chars @@ -392,7 +450,7 @@ def test_wizard_offers_better_node_name(tmpdir, monkeypatch, wizard_answers): wz, "default_input", MagicMock(side_effect=lambda _, default: default) ) - config_file = tmpdir.join("test.yaml") + config_file = tmp_path / "test.yaml" input_mock = MagicMock(side_effect=wizard_answers) monkeypatch.setattr("builtins.input", input_mock) monkeypatch.setattr(wz, "safe_print", lambda t=None, end=None: 0) @@ -400,14 +458,16 @@ def test_wizard_offers_better_node_name(tmpdir, monkeypatch, wizard_answers): monkeypatch.setattr(wz, "wizard_write", MagicMock()) # When - retval = wz.wizard(str(config_file)) + retval = wz.wizard(config_file) # Then assert retval == 0 assert wz.default_input.call_args.args[1] == expected_name -def test_wizard_requires_correct_platform(tmpdir, monkeypatch, wizard_answers): +def test_wizard_requires_correct_platform( + tmp_path: Path, monkeypatch: MonkeyPatch, wizard_answers: list[str] +): """ When the platform is not either esp32 or esp8266, the wizard should reject it """ @@ -415,7 +475,7 @@ def test_wizard_requires_correct_platform(tmpdir, monkeypatch, wizard_answers): # Given wizard_answers.insert(1, "foobar") # add invalid entry for platform - config_file = tmpdir.join("test.yaml") + config_file = tmp_path / "test.yaml" input_mock = MagicMock(side_effect=wizard_answers) monkeypatch.setattr("builtins.input", input_mock) monkeypatch.setattr(wz, "safe_print", lambda t=None, end=None: 0) @@ -423,13 +483,15 @@ def test_wizard_requires_correct_platform(tmpdir, monkeypatch, wizard_answers): monkeypatch.setattr(wz, "wizard_write", MagicMock()) # When - retval = wz.wizard(str(config_file)) + retval = wz.wizard(config_file) # Then assert retval == 0 -def test_wizard_requires_correct_board(tmpdir, monkeypatch, wizard_answers): +def test_wizard_requires_correct_board( + tmp_path: Path, monkeypatch: MonkeyPatch, wizard_answers: list[str] +): """ When the board is not a valid esp8266 board, the wizard should reject it """ @@ -437,7 +499,7 @@ def test_wizard_requires_correct_board(tmpdir, monkeypatch, wizard_answers): # Given wizard_answers.insert(2, "foobar") # add an invalid entry for board - config_file = tmpdir.join("test.yaml") + config_file = tmp_path / "test.yaml" input_mock = MagicMock(side_effect=wizard_answers) monkeypatch.setattr("builtins.input", input_mock) monkeypatch.setattr(wz, "safe_print", lambda t=None, end=None: 0) @@ -445,13 +507,15 @@ def test_wizard_requires_correct_board(tmpdir, monkeypatch, wizard_answers): monkeypatch.setattr(wz, "wizard_write", MagicMock()) # When - retval = wz.wizard(str(config_file)) + retval = wz.wizard(config_file) # Then assert retval == 0 -def test_wizard_requires_valid_ssid(tmpdir, monkeypatch, wizard_answers): +def test_wizard_requires_valid_ssid( + tmp_path: Path, monkeypatch: MonkeyPatch, wizard_answers: list[str] +): """ When the board is not a valid esp8266 board, the wizard should reject it """ @@ -459,7 +523,7 @@ def test_wizard_requires_valid_ssid(tmpdir, monkeypatch, wizard_answers): # Given wizard_answers.insert(3, "") # add an invalid entry for ssid - config_file = tmpdir.join("test.yaml") + config_file = tmp_path / "test.yaml" input_mock = MagicMock(side_effect=wizard_answers) monkeypatch.setattr("builtins.input", input_mock) monkeypatch.setattr(wz, "safe_print", lambda t=None, end=None: 0) @@ -467,7 +531,28 @@ def test_wizard_requires_valid_ssid(tmpdir, monkeypatch, wizard_answers): monkeypatch.setattr(wz, "wizard_write", MagicMock()) # When - retval = wz.wizard(str(config_file)) + retval = wz.wizard(config_file) # Then assert retval == 0 + + +def test_wizard_write_protects_existing_config( + tmp_path: Path, default_config: dict[str, Any], monkeypatch: MonkeyPatch +): + """ + The wizard_write function should not overwrite existing config files and return False + """ + # Given + config_file = tmp_path / "test.yaml" + original_content = "# Original config content\n" + config_file.write_text(original_content) + + monkeypatch.setattr(CORE, "config_path", tmp_path.parent) + + # When + result = wz.wizard_write(config_file, **default_config) + + # Then + assert result is False # Should return False when file exists + assert config_file.read_text() == original_content diff --git a/tests/unit_tests/test_writer.py b/tests/unit_tests/test_writer.py new file mode 100644 index 0000000000..a2a358f4d3 --- /dev/null +++ b/tests/unit_tests/test_writer.py @@ -0,0 +1,1064 @@ +"""Test writer module functionality.""" + +from collections.abc import Callable +from pathlib import Path +from typing import Any +from unittest.mock import MagicMock, patch + +import pytest + +from esphome.core import EsphomeError +from esphome.storage_json import StorageJSON +from esphome.writer import ( + CPP_AUTO_GENERATE_BEGIN, + CPP_AUTO_GENERATE_END, + CPP_INCLUDE_BEGIN, + CPP_INCLUDE_END, + GITIGNORE_CONTENT, + clean_build, + clean_cmake_cache, + storage_should_clean, + update_storage_json, + write_cpp, + write_gitignore, +) + + +@pytest.fixture +def mock_copy_src_tree(): + """Mock copy_src_tree to avoid side effects during tests.""" + with patch("esphome.writer.copy_src_tree"): + yield + + +@pytest.fixture +def create_storage() -> Callable[..., StorageJSON]: + """Factory fixture to create StorageJSON instances.""" + + def _create( + loaded_integrations: list[str] | None = None, **kwargs: Any + ) -> StorageJSON: + return StorageJSON( + storage_version=kwargs.get("storage_version", 1), + name=kwargs.get("name", "test"), + friendly_name=kwargs.get("friendly_name", "Test Device"), + comment=kwargs.get("comment"), + esphome_version=kwargs.get("esphome_version", "2025.1.0"), + src_version=kwargs.get("src_version", 1), + address=kwargs.get("address", "test.local"), + web_port=kwargs.get("web_port", 80), + target_platform=kwargs.get("target_platform", "ESP32"), + build_path=kwargs.get("build_path", "/build"), + firmware_bin_path=kwargs.get("firmware_bin_path", "/firmware.bin"), + loaded_integrations=set(loaded_integrations or []), + loaded_platforms=kwargs.get("loaded_platforms", set()), + no_mdns=kwargs.get("no_mdns", False), + framework=kwargs.get("framework", "arduino"), + core_platform=kwargs.get("core_platform", "esp32"), + ) + + return _create + + +def test_storage_should_clean_when_old_is_none( + create_storage: Callable[..., StorageJSON], +) -> None: + """Test that clean is triggered when old storage is None.""" + new = create_storage(loaded_integrations=["api", "wifi"]) + assert storage_should_clean(None, new) is True + + +def test_storage_should_clean_when_src_version_changes( + create_storage: Callable[..., StorageJSON], +) -> None: + """Test that clean is triggered when src_version changes.""" + old = create_storage(loaded_integrations=["api", "wifi"], src_version=1) + new = create_storage(loaded_integrations=["api", "wifi"], src_version=2) + assert storage_should_clean(old, new) is True + + +def test_storage_should_clean_when_build_path_changes( + create_storage: Callable[..., StorageJSON], +) -> None: + """Test that clean is triggered when build_path changes.""" + old = create_storage(loaded_integrations=["api", "wifi"], build_path="/build1") + new = create_storage(loaded_integrations=["api", "wifi"], build_path="/build2") + assert storage_should_clean(old, new) is True + + +def test_storage_should_clean_when_component_removed( + create_storage: Callable[..., StorageJSON], +) -> None: + """Test that clean is triggered when a component is removed.""" + old = create_storage( + loaded_integrations=["api", "wifi", "bluetooth_proxy", "esp32_ble_tracker"] + ) + new = create_storage(loaded_integrations=["api", "wifi", "esp32_ble_tracker"]) + assert storage_should_clean(old, new) is True + + +def test_storage_should_clean_when_multiple_components_removed( + create_storage: Callable[..., StorageJSON], +) -> None: + """Test that clean is triggered when multiple components are removed.""" + old = create_storage( + loaded_integrations=["api", "wifi", "ota", "web_server", "logger"] + ) + new = create_storage(loaded_integrations=["api", "wifi", "logger"]) + assert storage_should_clean(old, new) is True + + +def test_storage_should_not_clean_when_nothing_changes( + create_storage: Callable[..., StorageJSON], +) -> None: + """Test that clean is not triggered when nothing changes.""" + old = create_storage(loaded_integrations=["api", "wifi", "logger"]) + new = create_storage(loaded_integrations=["api", "wifi", "logger"]) + assert storage_should_clean(old, new) is False + + +def test_storage_should_not_clean_when_component_added( + create_storage: Callable[..., StorageJSON], +) -> None: + """Test that clean is not triggered when a component is only added.""" + old = create_storage(loaded_integrations=["api", "wifi"]) + new = create_storage(loaded_integrations=["api", "wifi", "ota"]) + assert storage_should_clean(old, new) is False + + +def test_storage_should_not_clean_when_other_fields_change( + create_storage: Callable[..., StorageJSON], +) -> None: + """Test that clean is not triggered when non-relevant fields change.""" + old = create_storage( + loaded_integrations=["api", "wifi"], + friendly_name="Old Name", + esphome_version="2024.12.0", + ) + new = create_storage( + loaded_integrations=["api", "wifi"], + friendly_name="New Name", + esphome_version="2025.1.0", + ) + assert storage_should_clean(old, new) is False + + +def test_storage_edge_case_empty_integrations( + create_storage: Callable[..., StorageJSON], +) -> None: + """Test edge case when old has integrations but new has none.""" + old = create_storage(loaded_integrations=["api", "wifi"]) + new = create_storage(loaded_integrations=[]) + assert storage_should_clean(old, new) is True + + +def test_storage_edge_case_from_empty_integrations( + create_storage: Callable[..., StorageJSON], +) -> None: + """Test edge case when old has no integrations but new has some.""" + old = create_storage(loaded_integrations=[]) + new = create_storage(loaded_integrations=["api", "wifi"]) + assert storage_should_clean(old, new) is False + + +@patch("esphome.writer.clean_build") +@patch("esphome.writer.StorageJSON") +@patch("esphome.writer.storage_path") +@patch("esphome.writer.CORE") +def test_update_storage_json_logging_when_old_is_none( + mock_core: MagicMock, + mock_storage_path: MagicMock, + mock_storage_json_class: MagicMock, + mock_clean_build: MagicMock, + create_storage: Callable[..., StorageJSON], + caplog: pytest.LogCaptureFixture, +) -> None: + """Test that update_storage_json doesn't crash when old storage is None. + + This is a regression test for the AttributeError that occurred when + old was None and we tried to access old.loaded_integrations. + """ + # Setup mocks + mock_storage_path.return_value = "/test/path" + mock_storage_json_class.load.return_value = None # Old storage is None + + new_storage = create_storage(loaded_integrations=["api", "wifi"]) + new_storage.save = MagicMock() # Mock the save method + mock_storage_json_class.from_esphome_core.return_value = new_storage + + # Call the function - should not raise AttributeError + with caplog.at_level("INFO"): + update_storage_json() + + # Verify clean_build was called + mock_clean_build.assert_called_once() + + # Verify the correct log message was used (not the component removal message) + assert "Core config or version changed, cleaning build files..." in caplog.text + assert "Components removed" not in caplog.text + + # Verify save was called + new_storage.save.assert_called_once_with("/test/path") + + +@patch("esphome.writer.clean_build") +@patch("esphome.writer.StorageJSON") +@patch("esphome.writer.storage_path") +@patch("esphome.writer.CORE") +def test_update_storage_json_logging_components_removed( + mock_core: MagicMock, + mock_storage_path: MagicMock, + mock_storage_json_class: MagicMock, + mock_clean_build: MagicMock, + create_storage: Callable[..., StorageJSON], + caplog: pytest.LogCaptureFixture, +) -> None: + """Test that update_storage_json logs removed components correctly.""" + # Setup mocks + mock_storage_path.return_value = "/test/path" + + old_storage = create_storage(loaded_integrations=["api", "wifi", "bluetooth_proxy"]) + new_storage = create_storage(loaded_integrations=["api", "wifi"]) + new_storage.save = MagicMock() # Mock the save method + + mock_storage_json_class.load.return_value = old_storage + mock_storage_json_class.from_esphome_core.return_value = new_storage + + # Call the function + with caplog.at_level("INFO"): + update_storage_json() + + # Verify clean_build was called + mock_clean_build.assert_called_once() + + # Verify the correct log message was used with component names + assert ( + "Components removed (bluetooth_proxy), cleaning build files..." in caplog.text + ) + assert "Core config or version changed" not in caplog.text + + # Verify save was called + new_storage.save.assert_called_once_with("/test/path") + + +@patch("esphome.writer.CORE") +def test_clean_cmake_cache( + mock_core: MagicMock, + tmp_path: Path, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test clean_cmake_cache removes CMakeCache.txt file.""" + # Create directory structure + pioenvs_dir = tmp_path / ".pioenvs" + pioenvs_dir.mkdir() + device_dir = pioenvs_dir / "test_device" + device_dir.mkdir() + cmake_cache_file = device_dir / "CMakeCache.txt" + cmake_cache_file.write_text("# CMake cache file") + + # Setup mocks + mock_core.relative_pioenvs_path.return_value = pioenvs_dir + mock_core.name = "test_device" + + # Verify file exists before + assert cmake_cache_file.exists() + + # Call the function + with caplog.at_level("INFO"): + clean_cmake_cache() + + # Verify file was removed + assert not cmake_cache_file.exists() + + # Verify logging + assert "Deleting" in caplog.text + assert "CMakeCache.txt" in caplog.text + + +@patch("esphome.writer.CORE") +def test_clean_cmake_cache_no_pioenvs_dir( + mock_core: MagicMock, + tmp_path: Path, +) -> None: + """Test clean_cmake_cache when pioenvs directory doesn't exist.""" + # Setup non-existent directory path + pioenvs_dir = tmp_path / ".pioenvs" + + # Setup mocks + mock_core.relative_pioenvs_path.return_value = pioenvs_dir + + # Verify directory doesn't exist + assert not pioenvs_dir.exists() + + # Call the function - should not crash + clean_cmake_cache() + + # Verify directory still doesn't exist + assert not pioenvs_dir.exists() + + +@patch("esphome.writer.CORE") +def test_clean_cmake_cache_no_cmake_file( + mock_core: MagicMock, + tmp_path: Path, +) -> None: + """Test clean_cmake_cache when CMakeCache.txt doesn't exist.""" + # Create directory structure without CMakeCache.txt + pioenvs_dir = tmp_path / ".pioenvs" + pioenvs_dir.mkdir() + device_dir = pioenvs_dir / "test_device" + device_dir.mkdir() + cmake_cache_file = device_dir / "CMakeCache.txt" + + # Setup mocks + mock_core.relative_pioenvs_path.return_value = pioenvs_dir + mock_core.name = "test_device" + + # Verify file doesn't exist + assert not cmake_cache_file.exists() + + # Call the function - should not crash + clean_cmake_cache() + + # Verify file still doesn't exist + assert not cmake_cache_file.exists() + + +@patch("esphome.writer.CORE") +def test_clean_build( + mock_core: MagicMock, + tmp_path: Path, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test clean_build removes all build artifacts.""" + # Create directory structure and files + pioenvs_dir = tmp_path / ".pioenvs" + pioenvs_dir.mkdir() + (pioenvs_dir / "test_file.o").write_text("object file") + + piolibdeps_dir = tmp_path / ".piolibdeps" + piolibdeps_dir.mkdir() + (piolibdeps_dir / "library").mkdir() + + dependencies_lock = tmp_path / "dependencies.lock" + dependencies_lock.write_text("lock file") + + # Create PlatformIO cache directory + platformio_cache_dir = tmp_path / ".platformio" / ".cache" + platformio_cache_dir.mkdir(parents=True) + (platformio_cache_dir / "downloads").mkdir() + (platformio_cache_dir / "http").mkdir() + (platformio_cache_dir / "tmp").mkdir() + (platformio_cache_dir / "downloads" / "package.tar.gz").write_text("package") + + # Setup mocks + mock_core.relative_pioenvs_path.return_value = pioenvs_dir + mock_core.relative_piolibdeps_path.return_value = piolibdeps_dir + mock_core.relative_build_path.return_value = dependencies_lock + + # Verify all exist before + assert pioenvs_dir.exists() + assert piolibdeps_dir.exists() + assert dependencies_lock.exists() + assert platformio_cache_dir.exists() + + # Mock PlatformIO's ProjectConfig cache_dir + with patch( + "platformio.project.config.ProjectConfig.get_instance" + ) as mock_get_instance: + mock_config = MagicMock() + mock_get_instance.return_value = mock_config + mock_config.get.side_effect = ( + lambda section, option: str(platformio_cache_dir) + if (section, option) == ("platformio", "cache_dir") + else "" + ) + + # Call the function + with caplog.at_level("INFO"): + clean_build() + + # Verify all were removed + assert not pioenvs_dir.exists() + assert not piolibdeps_dir.exists() + assert not dependencies_lock.exists() + assert not platformio_cache_dir.exists() + + # Verify logging + assert "Deleting" in caplog.text + assert ".pioenvs" in caplog.text + assert ".piolibdeps" in caplog.text + assert "dependencies.lock" in caplog.text + assert "PlatformIO cache" in caplog.text + + +@patch("esphome.writer.CORE") +def test_clean_build_partial_exists( + mock_core: MagicMock, + tmp_path: Path, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test clean_build when only some paths exist.""" + # Create only pioenvs directory + pioenvs_dir = tmp_path / ".pioenvs" + pioenvs_dir.mkdir() + (pioenvs_dir / "test_file.o").write_text("object file") + + piolibdeps_dir = tmp_path / ".piolibdeps" + dependencies_lock = tmp_path / "dependencies.lock" + + # Setup mocks + mock_core.relative_pioenvs_path.return_value = pioenvs_dir + mock_core.relative_piolibdeps_path.return_value = piolibdeps_dir + mock_core.relative_build_path.return_value = dependencies_lock + + # Verify only pioenvs exists + assert pioenvs_dir.exists() + assert not piolibdeps_dir.exists() + assert not dependencies_lock.exists() + + # Call the function + with caplog.at_level("INFO"): + clean_build() + + # Verify only existing path was removed + assert not pioenvs_dir.exists() + assert not piolibdeps_dir.exists() + assert not dependencies_lock.exists() + + # Verify logging - only pioenvs should be logged + assert "Deleting" in caplog.text + assert ".pioenvs" in caplog.text + assert ".piolibdeps" not in caplog.text + assert "dependencies.lock" not in caplog.text + + +@patch("esphome.writer.CORE") +def test_clean_build_nothing_exists( + mock_core: MagicMock, + tmp_path: Path, +) -> None: + """Test clean_build when no build artifacts exist.""" + # Setup paths that don't exist + pioenvs_dir = tmp_path / ".pioenvs" + piolibdeps_dir = tmp_path / ".piolibdeps" + dependencies_lock = tmp_path / "dependencies.lock" + + # Setup mocks + mock_core.relative_pioenvs_path.return_value = pioenvs_dir + mock_core.relative_piolibdeps_path.return_value = piolibdeps_dir + mock_core.relative_build_path.return_value = dependencies_lock + + # Verify nothing exists + assert not pioenvs_dir.exists() + assert not piolibdeps_dir.exists() + assert not dependencies_lock.exists() + + # Call the function - should not crash + clean_build() + + # Verify nothing was created + assert not pioenvs_dir.exists() + assert not piolibdeps_dir.exists() + assert not dependencies_lock.exists() + + +@patch("esphome.writer.CORE") +def test_clean_build_platformio_not_available( + mock_core: MagicMock, + tmp_path: Path, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test clean_build when PlatformIO is not available.""" + # Create directory structure and files + pioenvs_dir = tmp_path / ".pioenvs" + pioenvs_dir.mkdir() + + piolibdeps_dir = tmp_path / ".piolibdeps" + piolibdeps_dir.mkdir() + + dependencies_lock = tmp_path / "dependencies.lock" + dependencies_lock.write_text("lock file") + + # Setup mocks + mock_core.relative_pioenvs_path.return_value = pioenvs_dir + mock_core.relative_piolibdeps_path.return_value = piolibdeps_dir + mock_core.relative_build_path.return_value = dependencies_lock + + # Verify all exist before + assert pioenvs_dir.exists() + assert piolibdeps_dir.exists() + assert dependencies_lock.exists() + + # Mock import error for platformio + with ( + patch.dict("sys.modules", {"platformio.project.config": None}), + caplog.at_level("INFO"), + ): + # Call the function + clean_build() + + # Verify standard paths were removed but no cache cleaning attempted + assert not pioenvs_dir.exists() + assert not piolibdeps_dir.exists() + assert not dependencies_lock.exists() + + # Verify no cache logging + assert "PlatformIO cache" not in caplog.text + + +@patch("esphome.writer.CORE") +def test_clean_build_empty_cache_dir( + mock_core: MagicMock, + tmp_path: Path, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test clean_build when get_project_cache_dir returns empty/whitespace.""" + # Create directory structure and files + pioenvs_dir = tmp_path / ".pioenvs" + pioenvs_dir.mkdir() + + # Setup mocks + mock_core.relative_pioenvs_path.return_value = pioenvs_dir + mock_core.relative_piolibdeps_path.return_value = tmp_path / ".piolibdeps" + mock_core.relative_build_path.return_value = tmp_path / "dependencies.lock" + + # Verify pioenvs exists before + assert pioenvs_dir.exists() + + # Mock PlatformIO's ProjectConfig cache_dir to return whitespace + with patch( + "platformio.project.config.ProjectConfig.get_instance" + ) as mock_get_instance: + mock_config = MagicMock() + mock_get_instance.return_value = mock_config + mock_config.get.side_effect = ( + lambda section, option: " " # Whitespace only + if (section, option) == ("platformio", "cache_dir") + else "" + ) + + # Call the function + with caplog.at_level("INFO"): + clean_build() + + # Verify pioenvs was removed + assert not pioenvs_dir.exists() + + # Verify no cache cleaning was attempted due to empty string + assert "PlatformIO cache" not in caplog.text + + +@patch("esphome.writer.CORE") +def test_write_gitignore_creates_new_file( + mock_core: MagicMock, + tmp_path: Path, +) -> None: + """Test write_gitignore creates a new .gitignore file when it doesn't exist.""" + gitignore_path = tmp_path / ".gitignore" + + # Setup mocks + mock_core.relative_config_path.return_value = gitignore_path + + # Verify file doesn't exist + assert not gitignore_path.exists() + + # Call the function + write_gitignore() + + # Verify file was created with correct content + assert gitignore_path.exists() + assert gitignore_path.read_text() == GITIGNORE_CONTENT + + +@patch("esphome.writer.CORE") +def test_write_gitignore_skips_existing_file( + mock_core: MagicMock, + tmp_path: Path, +) -> None: + """Test write_gitignore doesn't overwrite existing .gitignore file.""" + gitignore_path = tmp_path / ".gitignore" + existing_content = "# Custom gitignore\n/custom_dir/\n" + gitignore_path.write_text(existing_content) + + # Setup mocks + mock_core.relative_config_path.return_value = gitignore_path + + # Verify file exists with custom content + assert gitignore_path.exists() + assert gitignore_path.read_text() == existing_content + + # Call the function + write_gitignore() + + # Verify file was not modified + assert gitignore_path.exists() + assert gitignore_path.read_text() == existing_content + + +@patch("esphome.writer.write_file_if_changed") # Mock to capture output +@patch("esphome.writer.copy_src_tree") # Keep this mock as it's complex +@patch("esphome.writer.CORE") +def test_write_cpp_with_existing_file( + mock_core: MagicMock, + mock_copy_src_tree: MagicMock, + mock_write_file: MagicMock, + tmp_path: Path, +) -> None: + """Test write_cpp when main.cpp already exists.""" + # Create a real file with markers + main_cpp = tmp_path / "main.cpp" + existing_content = f"""#include "esphome.h" +{CPP_INCLUDE_BEGIN} +// Old includes +{CPP_INCLUDE_END} +void setup() {{ +{CPP_AUTO_GENERATE_BEGIN} +// Old code +{CPP_AUTO_GENERATE_END} +}} +void loop() {{}}""" + main_cpp.write_text(existing_content) + + # Setup mocks + mock_core.relative_src_path.return_value = main_cpp + mock_core.cpp_global_section = "// Global section" + + # Call the function + test_code = " // New generated code" + write_cpp(test_code) + + # Verify copy_src_tree was called + mock_copy_src_tree.assert_called_once() + + # Get the content that would be written + mock_write_file.assert_called_once() + written_path, written_content = mock_write_file.call_args[0] + + # Check that markers are preserved and content is updated + assert CPP_INCLUDE_BEGIN in written_content + assert CPP_INCLUDE_END in written_content + assert CPP_AUTO_GENERATE_BEGIN in written_content + assert CPP_AUTO_GENERATE_END in written_content + assert test_code in written_content + assert "// Global section" in written_content + + +@patch("esphome.writer.write_file_if_changed") # Mock to capture output +@patch("esphome.writer.copy_src_tree") # Keep this mock as it's complex +@patch("esphome.writer.CORE") +def test_write_cpp_creates_new_file( + mock_core: MagicMock, + mock_copy_src_tree: MagicMock, + mock_write_file: MagicMock, + tmp_path: Path, +) -> None: + """Test write_cpp when main.cpp doesn't exist.""" + # Setup path for new file + main_cpp = tmp_path / "main.cpp" + + # Setup mocks + mock_core.relative_src_path.return_value = main_cpp + mock_core.cpp_global_section = "// Global section" + + # Verify file doesn't exist + assert not main_cpp.exists() + + # Call the function + test_code = " // Generated code" + write_cpp(test_code) + + # Verify copy_src_tree was called + mock_copy_src_tree.assert_called_once() + + # Get the content that would be written + mock_write_file.assert_called_once() + written_path, written_content = mock_write_file.call_args[0] + assert written_path == main_cpp + + # Check that all necessary parts are in the new file + assert '#include "esphome.h"' in written_content + assert CPP_INCLUDE_BEGIN in written_content + assert CPP_INCLUDE_END in written_content + assert CPP_AUTO_GENERATE_BEGIN in written_content + assert CPP_AUTO_GENERATE_END in written_content + assert test_code in written_content + assert "void setup()" in written_content + assert "void loop()" in written_content + assert "App.setup();" in written_content + assert "App.loop();" in written_content + + +@pytest.mark.usefixtures("mock_copy_src_tree") +@patch("esphome.writer.CORE") +def test_write_cpp_with_missing_end_marker( + mock_core: MagicMock, + tmp_path: Path, +) -> None: + """Test write_cpp raises error when end marker is missing.""" + # Create a file with begin marker but no end marker + main_cpp = tmp_path / "main.cpp" + existing_content = f"""#include "esphome.h" +{CPP_AUTO_GENERATE_BEGIN} +// Code without end marker""" + main_cpp.write_text(existing_content) + + # Setup mocks + mock_core.relative_src_path.return_value = main_cpp + + # Call should raise an error + with pytest.raises(EsphomeError, match="Could not find auto generated code end"): + write_cpp("// New code") + + +@pytest.mark.usefixtures("mock_copy_src_tree") +@patch("esphome.writer.CORE") +def test_write_cpp_with_duplicate_markers( + mock_core: MagicMock, + tmp_path: Path, +) -> None: + """Test write_cpp raises error when duplicate markers exist.""" + # Create a file with duplicate begin markers + main_cpp = tmp_path / "main.cpp" + existing_content = f"""#include "esphome.h" +{CPP_AUTO_GENERATE_BEGIN} +// First section +{CPP_AUTO_GENERATE_END} +{CPP_AUTO_GENERATE_BEGIN} +// Duplicate section +{CPP_AUTO_GENERATE_END}""" + main_cpp.write_text(existing_content) + + # Setup mocks + mock_core.relative_src_path.return_value = main_cpp + + # Call should raise an error + with pytest.raises(EsphomeError, match="Found multiple auto generate code begins"): + write_cpp("// New code") + + +@patch("esphome.writer.CORE") +def test_clean_all_with_yaml_file( + mock_core: MagicMock, + tmp_path: Path, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test clean_all with a .yaml file uses parent directory.""" + # Create config directory with yaml file + config_dir = tmp_path / "config" + config_dir.mkdir() + yaml_file = config_dir / "test.yaml" + yaml_file.write_text("esphome:\n name: test\n") + + build_dir = config_dir / ".esphome" + build_dir.mkdir() + (build_dir / "dummy.txt").write_text("x") + + from esphome.writer import clean_all + + with caplog.at_level("INFO"): + clean_all([str(yaml_file)]) + + # Verify .esphome directory still exists but contents cleaned + assert build_dir.exists() + assert not (build_dir / "dummy.txt").exists() + + # Verify logging mentions the build dir + assert "Cleaning" in caplog.text + assert str(build_dir) in caplog.text + + +@patch("esphome.writer.CORE") +def test_clean_all( + mock_core: MagicMock, + tmp_path: Path, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test clean_all removes build and PlatformIO dirs.""" + # Create build directories for multiple configurations + config1_dir = tmp_path / "config1" + config2_dir = tmp_path / "config2" + config1_dir.mkdir() + config2_dir.mkdir() + + build_dir1 = config1_dir / ".esphome" + build_dir2 = config2_dir / ".esphome" + build_dir1.mkdir() + build_dir2.mkdir() + (build_dir1 / "dummy.txt").write_text("x") + (build_dir2 / "dummy.txt").write_text("x") + + # Create PlatformIO directories + pio_cache = tmp_path / "pio_cache" + pio_packages = tmp_path / "pio_packages" + pio_platforms = tmp_path / "pio_platforms" + pio_core = tmp_path / "pio_core" + for d in (pio_cache, pio_packages, pio_platforms, pio_core): + d.mkdir() + (d / "keep").write_text("x") + + # Mock ProjectConfig + with patch( + "platformio.project.config.ProjectConfig.get_instance" + ) as mock_get_instance: + mock_config = MagicMock() + mock_get_instance.return_value = mock_config + + def cfg_get(section: str, option: str) -> str: + mapping = { + ("platformio", "cache_dir"): str(pio_cache), + ("platformio", "packages_dir"): str(pio_packages), + ("platformio", "platforms_dir"): str(pio_platforms), + ("platformio", "core_dir"): str(pio_core), + } + return mapping.get((section, option), "") + + mock_config.get.side_effect = cfg_get + + # Call + from esphome.writer import clean_all + + with caplog.at_level("INFO"): + clean_all([str(config1_dir), str(config2_dir)]) + + # Verify deletions - .esphome directories remain but contents are cleaned + # The .esphome directory itself is not removed because it may contain storage + assert build_dir1.exists() + assert build_dir2.exists() + + # Verify that files in .esphome were removed + assert not (build_dir1 / "dummy.txt").exists() + assert not (build_dir2 / "dummy.txt").exists() + assert not pio_cache.exists() + assert not pio_packages.exists() + assert not pio_platforms.exists() + assert not pio_core.exists() + + # Verify logging mentions each + assert "Cleaning" in caplog.text + assert str(build_dir1) in caplog.text + assert str(build_dir2) in caplog.text + assert "PlatformIO cache" in caplog.text + assert "PlatformIO packages" in caplog.text + assert "PlatformIO platforms" in caplog.text + assert "PlatformIO core" in caplog.text + + +@patch("esphome.writer.CORE") +def test_clean_all_preserves_storage( + mock_core: MagicMock, + tmp_path: Path, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test clean_all preserves storage directory.""" + # Create build directory with storage subdirectory + config_dir = tmp_path / "config" + config_dir.mkdir() + + build_dir = config_dir / ".esphome" + build_dir.mkdir() + (build_dir / "dummy.txt").write_text("x") + (build_dir / "other_file.txt").write_text("y") + + # Create storage directory with content + storage_dir = build_dir / "storage" + storage_dir.mkdir() + (storage_dir / "storage.json").write_text('{"test": "data"}') + (storage_dir / "other_storage.txt").write_text("storage content") + + # Call clean_all + from esphome.writer import clean_all + + with caplog.at_level("INFO"): + clean_all([str(config_dir)]) + + # Verify .esphome directory still exists + assert build_dir.exists() + + # Verify storage directory still exists with its contents + assert storage_dir.exists() + assert (storage_dir / "storage.json").exists() + assert (storage_dir / "other_storage.txt").exists() + + # Verify storage contents are intact + assert (storage_dir / "storage.json").read_text() == '{"test": "data"}' + assert (storage_dir / "other_storage.txt").read_text() == "storage content" + + # Verify other files were removed + assert not (build_dir / "dummy.txt").exists() + assert not (build_dir / "other_file.txt").exists() + + # Verify logging mentions deletion + assert "Cleaning" in caplog.text + assert str(build_dir) in caplog.text + + +@patch("esphome.writer.CORE") +def test_clean_all_platformio_not_available( + mock_core: MagicMock, + tmp_path: Path, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test clean_all when PlatformIO is not available.""" + # Build dirs + config_dir = tmp_path / "config" + config_dir.mkdir() + build_dir = config_dir / ".esphome" + build_dir.mkdir() + + # PlatformIO dirs that should remain untouched + pio_cache = tmp_path / "pio_cache" + pio_cache.mkdir() + + from esphome.writer import clean_all + + with ( + patch.dict("sys.modules", {"platformio.project.config": None}), + caplog.at_level("INFO"), + ): + clean_all([str(config_dir)]) + + # Build dir contents cleaned, PlatformIO dirs remain + assert build_dir.exists() + assert pio_cache.exists() + + # No PlatformIO-specific logs + assert "PlatformIO" not in caplog.text + + +@patch("esphome.writer.CORE") +def test_clean_all_partial_exists( + mock_core: MagicMock, + tmp_path: Path, +) -> None: + """Test clean_all when only some build dirs exist.""" + config_dir = tmp_path / "config" + config_dir.mkdir() + build_dir = config_dir / ".esphome" + build_dir.mkdir() + + with patch( + "platformio.project.config.ProjectConfig.get_instance" + ) as mock_get_instance: + mock_config = MagicMock() + mock_get_instance.return_value = mock_config + # Return non-existent dirs + mock_config.get.side_effect = lambda *_args, **_kw: str( + tmp_path / "does_not_exist" + ) + + from esphome.writer import clean_all + + clean_all([str(config_dir)]) + + assert build_dir.exists() + + +@patch("esphome.writer.CORE") +def test_clean_all_removes_non_storage_directories( + mock_core: MagicMock, + tmp_path: Path, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test clean_all removes directories other than storage.""" + # Create build directory with various subdirectories + config_dir = tmp_path / "config" + config_dir.mkdir() + + build_dir = config_dir / ".esphome" + build_dir.mkdir() + + # Create files + (build_dir / "file1.txt").write_text("content1") + (build_dir / "file2.txt").write_text("content2") + + # Create storage directory (should be preserved) + storage_dir = build_dir / "storage" + storage_dir.mkdir() + (storage_dir / "storage.json").write_text('{"test": "data"}') + + # Create other directories (should be removed) + cache_dir = build_dir / "cache" + cache_dir.mkdir() + (cache_dir / "cache_file.txt").write_text("cache content") + + logs_dir = build_dir / "logs" + logs_dir.mkdir() + (logs_dir / "log1.txt").write_text("log content") + + temp_dir = build_dir / "temp" + temp_dir.mkdir() + (temp_dir / "temp_file.txt").write_text("temp content") + + # Call clean_all + from esphome.writer import clean_all + + with caplog.at_level("INFO"): + clean_all([str(config_dir)]) + + # Verify .esphome directory still exists + assert build_dir.exists() + + # Verify storage directory and its contents are preserved + assert storage_dir.exists() + assert (storage_dir / "storage.json").exists() + assert (storage_dir / "storage.json").read_text() == '{"test": "data"}' + + # Verify files were removed + assert not (build_dir / "file1.txt").exists() + assert not (build_dir / "file2.txt").exists() + + # Verify non-storage directories were removed + assert not cache_dir.exists() + assert not logs_dir.exists() + assert not temp_dir.exists() + + # Verify logging mentions cleaning + assert "Cleaning" in caplog.text + assert str(build_dir) in caplog.text + + +@patch("esphome.writer.CORE") +def test_clean_all_preserves_json_files( + mock_core: MagicMock, + tmp_path: Path, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test clean_all preserves .json files.""" + # Create build directory with various files + config_dir = tmp_path / "config" + config_dir.mkdir() + + build_dir = config_dir / ".esphome" + build_dir.mkdir() + + # Create .json files (should be preserved) + (build_dir / "config.json").write_text('{"config": "data"}') + (build_dir / "metadata.json").write_text('{"metadata": "info"}') + + # Create non-.json files (should be removed) + (build_dir / "dummy.txt").write_text("x") + (build_dir / "other.log").write_text("log content") + + # Call clean_all + from esphome.writer import clean_all + + with caplog.at_level("INFO"): + clean_all([str(config_dir)]) + + # Verify .esphome directory still exists + assert build_dir.exists() + + # Verify .json files are preserved + assert (build_dir / "config.json").exists() + assert (build_dir / "config.json").read_text() == '{"config": "data"}' + assert (build_dir / "metadata.json").exists() + assert (build_dir / "metadata.json").read_text() == '{"metadata": "info"}' + + # Verify non-.json files were removed + assert not (build_dir / "dummy.txt").exists() + assert not (build_dir / "other.log").exists() + + # Verify logging mentions cleaning + assert "Cleaning" in caplog.text + assert str(build_dir) in caplog.text diff --git a/tests/unit_tests/test_yaml_util.py b/tests/unit_tests/test_yaml_util.py index f31e9554dc..eac0ceabb8 100644 --- a/tests/unit_tests/test_yaml_util.py +++ b/tests/unit_tests/test_yaml_util.py @@ -1,9 +1,26 @@ -from esphome import yaml_util +from pathlib import Path +import shutil +from unittest.mock import patch + +import pytest + +from esphome import core, yaml_util from esphome.components import substitutions from esphome.core import EsphomeError +from esphome.util import OrderedDict -def test_include_with_vars(fixture_path): +@pytest.fixture(autouse=True) +def clear_secrets_cache() -> None: + """Clear the secrets cache before each test.""" + yaml_util._SECRET_VALUES.clear() + yaml_util._SECRET_CACHE.clear() + yield + yaml_util._SECRET_VALUES.clear() + yaml_util._SECRET_CACHE.clear() + + +def test_include_with_vars(fixture_path: Path) -> None: yaml_file = fixture_path / "yaml_util" / "includetest.yaml" actual = yaml_util.load_yaml(yaml_file) @@ -50,15 +67,214 @@ def test_parsing_with_custom_loader(fixture_path): """ yaml_file = fixture_path / "yaml_util" / "includetest.yaml" - loader_calls = [] + loader_calls: list[Path] = [] - def custom_loader(fname): + def custom_loader(fname: Path): loader_calls.append(fname) - with open(yaml_file, encoding="utf-8") as f_handle: + with yaml_file.open(encoding="utf-8") as f_handle: yaml_util.parse_yaml(yaml_file, f_handle, custom_loader) assert len(loader_calls) == 3 - assert loader_calls[0].endswith("includes/included.yaml") - assert loader_calls[1].endswith("includes/list.yaml") - assert loader_calls[2].endswith("includes/scalar.yaml") + assert loader_calls[0].parts[-2:] == ("includes", "included.yaml") + assert loader_calls[1].parts[-2:] == ("includes", "list.yaml") + assert loader_calls[2].parts[-2:] == ("includes", "scalar.yaml") + + +def test_construct_secret_simple(fixture_path: Path) -> None: + """Test loading a YAML file with !secret tags.""" + yaml_file = fixture_path / "yaml_util" / "test_secret.yaml" + + actual = yaml_util.load_yaml(yaml_file) + + # Check that secrets were properly loaded + assert actual["wifi"]["password"] == "super_secret_wifi" + assert actual["api"]["encryption"]["key"] == "0123456789abcdef" + assert actual["sensor"][0]["id"] == "my_secret_value" + + +def test_construct_secret_missing(fixture_path: Path, tmp_path: Path) -> None: + """Test that missing secrets raise proper errors.""" + # Create a YAML file with a secret that doesn't exist + test_yaml = tmp_path / "test.yaml" + test_yaml.write_text(""" +esphome: + name: test + +wifi: + password: !secret nonexistent_secret +""") + + # Create an empty secrets file + secrets_yaml = tmp_path / "secrets.yaml" + secrets_yaml.write_text("some_other_secret: value") + + with pytest.raises(EsphomeError, match="Secret 'nonexistent_secret' not defined"): + yaml_util.load_yaml(test_yaml) + + +def test_construct_secret_no_secrets_file(tmp_path: Path) -> None: + """Test that missing secrets.yaml file raises proper error.""" + # Create a YAML file with a secret but no secrets.yaml + test_yaml = tmp_path / "test.yaml" + test_yaml.write_text(""" +wifi: + password: !secret some_secret +""") + + # Mock CORE.config_path to avoid NoneType error + with ( + patch.object(core.CORE, "config_path", tmp_path / "main.yaml"), + pytest.raises(EsphomeError, match="secrets.yaml"), + ): + yaml_util.load_yaml(test_yaml) + + +def test_construct_secret_fallback_to_main_config_dir( + fixture_path: Path, tmp_path: Path +) -> None: + """Test fallback to main config directory for secrets.""" + # Create a subdirectory with a YAML file that uses secrets + subdir = tmp_path / "subdir" + subdir.mkdir() + + test_yaml = subdir / "test.yaml" + test_yaml.write_text(""" +wifi: + password: !secret test_secret +""") + + # Create secrets.yaml in the main directory + main_secrets = tmp_path / "secrets.yaml" + main_secrets.write_text("test_secret: main_secret_value") + + # Mock CORE.config_path to point to main directory + with patch.object(core.CORE, "config_path", tmp_path / "main.yaml"): + actual = yaml_util.load_yaml(test_yaml) + assert actual["wifi"]["password"] == "main_secret_value" + + +def test_construct_include_dir_named(fixture_path: Path, tmp_path: Path) -> None: + """Test !include_dir_named directive.""" + # Copy fixture directory to temporary location + src_dir = fixture_path / "yaml_util" + dst_dir = tmp_path / "yaml_util" + shutil.copytree(src_dir, dst_dir) + + # Create test YAML that uses include_dir_named + test_yaml = dst_dir / "test_include_named.yaml" + test_yaml.write_text(""" +sensor: !include_dir_named named_dir +""") + + actual = yaml_util.load_yaml(test_yaml) + actual_sensor = actual["sensor"] + + # Check that files were loaded with their names as keys + assert isinstance(actual_sensor, OrderedDict) + assert "sensor1" in actual_sensor + assert "sensor2" in actual_sensor + assert "sensor3" in actual_sensor # Files from subdirs are included with basename + + # Check content of loaded files + assert actual_sensor["sensor1"]["platform"] == "template" + assert actual_sensor["sensor1"]["name"] == "Sensor 1" + assert actual_sensor["sensor2"]["platform"] == "template" + assert actual_sensor["sensor2"]["name"] == "Sensor 2" + + # Check that subdirectory files are included with their basename + assert actual_sensor["sensor3"]["platform"] == "template" + assert actual_sensor["sensor3"]["name"] == "Sensor 3 in subdir" + + # Check that hidden files and non-YAML files are not included + assert ".hidden" not in actual_sensor + assert "not_yaml" not in actual_sensor + + +def test_construct_include_dir_named_empty_dir(tmp_path: Path) -> None: + """Test !include_dir_named with empty directory.""" + # Create empty directory + empty_dir = tmp_path / "empty_dir" + empty_dir.mkdir() + + test_yaml = tmp_path / "test.yaml" + test_yaml.write_text(""" +sensor: !include_dir_named empty_dir +""") + + actual = yaml_util.load_yaml(test_yaml) + + # Should return empty OrderedDict + assert isinstance(actual["sensor"], OrderedDict) + assert len(actual["sensor"]) == 0 + + +def test_construct_include_dir_named_with_dots(tmp_path: Path) -> None: + """Test that include_dir_named ignores files starting with dots.""" + # Create directory with various files + test_dir = tmp_path / "test_dir" + test_dir.mkdir() + + # Create visible file + visible_file = test_dir / "visible.yaml" + visible_file.write_text("key: visible_value") + + # Create hidden file + hidden_file = test_dir / ".hidden.yaml" + hidden_file.write_text("key: hidden_value") + + # Create hidden directory with files + hidden_dir = test_dir / ".hidden_dir" + hidden_dir.mkdir() + hidden_subfile = hidden_dir / "subfile.yaml" + hidden_subfile.write_text("key: hidden_subfile_value") + + test_yaml = tmp_path / "test.yaml" + test_yaml.write_text(""" +test: !include_dir_named test_dir +""") + + actual = yaml_util.load_yaml(test_yaml) + + # Should only include visible file + assert "visible" in actual["test"] + assert actual["test"]["visible"]["key"] == "visible_value" + + # Should not include hidden files or directories + assert ".hidden" not in actual["test"] + assert ".hidden_dir" not in actual["test"] + + +def test_find_files_recursive(fixture_path: Path, tmp_path: Path) -> None: + """Test that _find_files works recursively through include_dir_named.""" + # Copy fixture directory to temporary location + src_dir = fixture_path / "yaml_util" + dst_dir = tmp_path / "yaml_util" + shutil.copytree(src_dir, dst_dir) + + # This indirectly tests _find_files by using include_dir_named + test_yaml = dst_dir / "test_include_recursive.yaml" + test_yaml.write_text(""" +all_sensors: !include_dir_named named_dir +""") + + actual = yaml_util.load_yaml(test_yaml) + + # Should find sensor1.yaml, sensor2.yaml, and subdir/sensor3.yaml (all flattened) + assert len(actual["all_sensors"]) == 3 + assert "sensor1" in actual["all_sensors"] + assert "sensor2" in actual["all_sensors"] + assert "sensor3" in actual["all_sensors"] + + +def test_secret_values_tracking(fixture_path: Path) -> None: + """Test that secret values are properly tracked for dumping.""" + yaml_file = fixture_path / "yaml_util" / "test_secret.yaml" + + yaml_util.load_yaml(yaml_file) + + # Check that secret values are tracked + assert "super_secret_wifi" in yaml_util._SECRET_VALUES + assert yaml_util._SECRET_VALUES["super_secret_wifi"] == "wifi_password" + assert "0123456789abcdef" in yaml_util._SECRET_VALUES + assert yaml_util._SECRET_VALUES["0123456789abcdef"] == "api_key"